mirror of
https://github.com/unidoc/unipdf.git
synced 2025-04-27 13:48:51 +08:00
253 lines
6.5 KiB
Go
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
|
|
}
|