List Images
Inventorying the images in a document means parsing each page’s content stream and looking at what it draws, because an image can appear in more than one way. This is also how you find the XObject name of a particular image, which is what remove an image watermark needs.
If you want the pixel data plus where each image lands on the page, use
image extraction instead. The
extractor.ExtractPageImages route returns decoded images with position, size and
rotation. Parsing the content stream yourself is for when you want the raw
properties: which filter, which colorspace, how many bits per component.
The two forms an image takes
| Form | In the content stream | How to reach it |
|---|---|---|
| XObject image | /Name Do | resources.GetXObjectImageByName(name) |
| Inline image | BI ... ID ... EI | the *contentstream.ContentStreamInlineImage parameter of the BI operand |
Both need handling. Inline images are meant for small bitmaps and carry their data directly in the stream, so they are invisible to anything that only looks at the resource dictionary.
A third case is not an image at all but contains them: Do can also invoke a form
XObject, whose content stream draws its own images against its own resource
dictionary. Recursing into those is the part most naive scans miss.
Doing it
parser := contentstream.NewContentStreamParser(contents)
operations, err := parser.Parse()
if err != nil {
return err
}
for _, op := range *operations {
if op.Operand != "Do" || len(op.Params) != 1 {
continue
}
name := op.Params[0].(*core.PdfObjectName)
if _, xtype := resources.GetXObjectByName(*name); xtype == model.XObjectTypeImage {
ximg, err := resources.GetXObjectImageByName(*name)
if err != nil {
return err
}
fmt.Printf("%s: %dx%d, %s\n", *name, *ximg.Width, *ximg.Height,
ximg.ColorSpace.String())
}
}Get the page’s content with page.GetAllContentStreams(), which concatenates a
/Contents array into one string so the parse sees the page as a viewer does.
GetXObjectByName returns the stream and an XObjectType, one of
XObjectTypeImage, XObjectTypeForm, XObjectTypePS, XObjectTypeUnknown or
XObjectTypeUndefined. Undefined covers a missing entry and a malformed one alike,
so a check for == XObjectTypeImage is the safe test rather than a check against
form.
Recursing into a form XObject means switching resource dictionaries. Names are
scoped to a resource dictionary, so /Im0 inside a form is not the page’s /Im0.
Use the form’s own Resources when it has one and fall back to the page’s when it
does not.
Limitations
ximg.Width, ximg.Height and ximg.BitsPerComponent are pointers and are
dereferenced directly in the example. They are required entries for an image
XObject, but a corrupt file that omits them will panic rather than return an error.
Colorspace names come back from ColorSpace.String(). For an ICCBased
colorspace that is the wrapper name, not the underlying device space, so a JPEG
embedded with its ICC profile intact reports differently from the same JPEG
without one.
The walk covers page content streams and form XObjects. Images reachable only from somewhere else, an annotation appearance stream, a tiling pattern, a soft mask, a Type 3 font glyph procedure, are not visited.
An XObject drawn several times on one page is reported once. That is deliberate, since the resource entry is one image, but it means the count is of distinct images rather than of draws.
Run the example
The example prints a block per page listing each image with its filter, dimensions,
color component count, colorspace and bit depth, then a summary of how many times
each filter and colorspace appeared across all the files given. listImagesInContentStream
does the work and is the function that recurses into form XObjects. It accepts
several input paths and accumulates the summary over all of them.
git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/image
go run pdf_list_images.go input.pdfIf this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.