unipdf/extractor/image.go
2019-05-16 20:44:51 +00:00

253 lines
6.5 KiB
Go

/*
* This file is subject to the terms and conditions defined in
* file 'LICENSE.md', which is part of this source code package.
*/
package extractor
import (
"github.com/unidoc/unipdf/v3/common"
"github.com/unidoc/unipdf/v3/contentstream"
"github.com/unidoc/unipdf/v3/core"
"github.com/unidoc/unipdf/v3/model"
)
// ImageExtractOptions contains options for controlling image extraction from
// PDF pages.
type ImageExtractOptions struct {
IncludeInlineStencilMasks bool
}
// ExtractPageImages returns the image contents of the page extractor, including data
// and position, size information for each image.
// A set of options to control page image extraction can be passed in. The options
// parameter can be nil for the default options. By default, inline stencil masks
// are not extracted.
func (e *Extractor) ExtractPageImages(options *ImageExtractOptions) (*PageImages, error) {
ctx := &imageExtractContext{
options: options,
}
err := ctx.extractContentStreamImages(e.contents, e.resources)
if err != nil {
return nil, err
}
return &PageImages{
Images: ctx.extractedImages,
}, nil
}
// PageImages represents extracted images on a PDF page with spatial information:
// display position and size.
type PageImages struct {
Images []ImageMark
}
// ImageMark represents an image drawn on a page and its position in device coordinates.
// All coordinates are in device coordinates.
type ImageMark struct {
Image *model.Image
// Dimensions of the image as displayed in the PDF.
Width float64
Height float64
// Position of the image in PDF coordinates (lower left corner).
X float64
Y float64
// Angle in degrees, if rotated.
Angle float64
}
// Provide context for image extraction content stream processing.
type imageExtractContext struct {
extractedImages []ImageMark
inlineImages int
xObjectImages int
xObjectForms int
// Cache to avoid processing same image many times.
cacheXObjectImages map[*core.PdfObjectStream]*cachedImage
// Extract options.
options *ImageExtractOptions
}
type cachedImage struct {
image *model.Image
cs model.PdfColorspace
}
func (ctx *imageExtractContext) extractContentStreamImages(contents string, resources *model.PdfPageResources) error {
cstreamParser := contentstream.NewContentStreamParser(contents)
operations, err := cstreamParser.Parse()
if err != nil {
return err
}
if ctx.cacheXObjectImages == nil {
ctx.cacheXObjectImages = map[*core.PdfObjectStream]*cachedImage{}
}
if ctx.options == nil {
ctx.options = &ImageExtractOptions{}
}
processor := contentstream.NewContentStreamProcessor(*operations)
processor.AddHandler(contentstream.HandlerConditionEnumAllOperands, "",
func(op *contentstream.ContentStreamOperation, gs contentstream.GraphicsState, resources *model.PdfPageResources) error {
return ctx.processOperand(op, gs, resources)
})
return processor.Process(resources)
}
// Process individual content stream operands for image extraction.
func (ctx *imageExtractContext) processOperand(op *contentstream.ContentStreamOperation, gs contentstream.GraphicsState, resources *model.PdfPageResources) error {
if op.Operand == "BI" && len(op.Params) == 1 {
// BI: Inline image.
iimg, ok := op.Params[0].(*contentstream.ContentStreamInlineImage)
if !ok {
return nil
}
if isImageMask, ok := core.GetBoolVal(iimg.ImageMask); ok {
if isImageMask && !ctx.options.IncludeInlineStencilMasks {
return nil
}
}
return ctx.extractInlineImage(iimg, gs, resources)
} else if op.Operand == "Do" && len(op.Params) == 1 {
// Do: XObject.
name, ok := core.GetName(op.Params[0])
if !ok {
common.Log.Debug("ERROR: Type")
return errTypeCheck
}
_, xtype := resources.GetXObjectByName(*name)
switch xtype {
case model.XObjectTypeImage:
return ctx.extractXObjectImage(name, gs, resources)
case model.XObjectTypeForm:
return ctx.extractFormImages(name, gs, resources)
}
}
return nil
}
func (ctx *imageExtractContext) extractInlineImage(iimg *contentstream.ContentStreamInlineImage, gs contentstream.GraphicsState, resources *model.PdfPageResources) error {
img, err := iimg.ToImage(resources)
if err != nil {
return err
}
cs, err := iimg.GetColorSpace(resources)
if err != nil {
return err
}
if cs == nil {
// Default if not specified?
cs = model.NewPdfColorspaceDeviceGray()
}
rgbImg, err := cs.ImageToRGB(*img)
if err != nil {
return err
}
imgMark := ImageMark{
Image: &rgbImg,
Width: gs.CTM.ScalingFactorX(),
Height: gs.CTM.ScalingFactorY(),
Angle: gs.CTM.Angle(),
}
imgMark.X, imgMark.Y = gs.CTM.Translation()
ctx.extractedImages = append(ctx.extractedImages, imgMark)
ctx.inlineImages++
return nil
}
func (ctx *imageExtractContext) extractXObjectImage(name *core.PdfObjectName, gs contentstream.GraphicsState, resources *model.PdfPageResources) error {
stream, _ := resources.GetXObjectByName(*name)
if stream == nil {
return nil
}
// Cache on stream pointer so can ensure that it is the same object (better than using name).
cimg, cached := ctx.cacheXObjectImages[stream]
if !cached {
ximg, err := resources.GetXObjectImageByName(*name)
if err != nil {
return err
}
if ximg == nil {
return nil
}
img, err := ximg.ToImage()
if err != nil {
return err
}
cimg = &cachedImage{
image: img,
cs: ximg.ColorSpace,
}
ctx.cacheXObjectImages[stream] = cimg
}
img := cimg.image
cs := cimg.cs
rgbImg, err := cs.ImageToRGB(*img)
if err != nil {
return err
}
common.Log.Debug("@Do CTM: %s", gs.CTM.String())
imgMark := ImageMark{
Image: &rgbImg,
Width: gs.CTM.ScalingFactorX(),
Height: gs.CTM.ScalingFactorY(),
Angle: gs.CTM.Angle(),
}
imgMark.X, imgMark.Y = gs.CTM.Translation()
ctx.extractedImages = append(ctx.extractedImages, imgMark)
ctx.xObjectImages++
return nil
}
// Go through the XObject Form content stream (recursive processing).
func (ctx *imageExtractContext) extractFormImages(name *core.PdfObjectName, gs contentstream.GraphicsState, resources *model.PdfPageResources) error {
xform, err := resources.GetXObjectFormByName(*name)
if err != nil {
return err
}
if xform == nil {
return nil
}
formContent, err := xform.GetContentStream()
if err != nil {
return err
}
// Process the content stream in the Form object too:
formResources := xform.Resources
if formResources == nil {
formResources = resources
}
// Process the content stream in the Form object too:
err = ctx.extractContentStreamImages(string(formContent), formResources)
if err != nil {
return err
}
ctx.xObjectForms++
return nil
}