Put a five-page PDF through an image extractor and it is tempting to expect five images, or one per picture you can see on screen. My test file shows pictures in eight places and yields three files. Both of the naive expectations are counting something the PDF doesn't store, and once you know what it does store, most "the extractor missed an image" reports turn into bookkeeping.
What a page actually holds
A PDF page has no equivalent of an <img> tag carrying its own pixels. Its content stream says "paint object /Im1 here, scaled like this", and the pixels live once, somewhere else in the file, as an image XObject. Painting the same object a second time costs a few bytes of operators, not another copy of the photo. Many generators go a step further with logos and letterheads: the image sits inside a Form XObject, a small reusable drawing, and each page invokes the form, which puts the image two hops away from the page. Transparency adds a third kind of object. An RGBA PNG usually becomes an RGB image plus a separate grayscale image that the first one references as its /SMask, so a single "picture" with alpha is two image XObjects on disk.
I generated the test file with reportlab so I'd have ground truth. Page 1 has a 1200×800 JPEG photo and a 400×160 JPEG logo placed through a form. Page 2 paints the same photo twice, large and small, plus the form. Page 3 is a 600×600 RGBA PNG. Page 4 is vector only, a flowchart and a bar chart with no bitmaps at all. Page 5 has the form and the photo once more. PyMuPDF lists all of it if you pass full=True, which makes get_images look inside forms as well. The two calls that matter are these (excerpt; the rest of my script just tallies and prints):
# full=True also looks inside Form XObjects; the last field names the referencer
for xref, smask, w, h, bpc, cs, alt, name, filt, referencer in page.get_images(full=True):
n = len(page.get_image_rects(xref)) # times this object is painted on the page
get_images gives you each image object once per page, with its soft mask and, in the last field, the xref of the form that references it (0 when the page paints it directly). get_image_rects gives you one rectangle per time the object is painted, so its length is the placement count. On the test file, the photo is xref 5, painted once on page 1, twice on page 2 and once on page 5, straight from the page. The logo is xref 3, painted once on pages 1, 2 and 5, each time via Form xref 4. The PNG is xref 8, painted once on page 3, with smask=9. The totals come out as 5 pages, 8 placements, 3 image objects and 1 soft mask.
Page 4 never shows up. Eight placements, three image objects, and one soft mask (xref 9) that exists only to give xref 8 its alpha. If someone asks "how many images are in this PDF", any of 8, 4 or 3 is a defensible answer depending on whether they mean placements, XObjects, or pictures a person would want as files.
Three is the number worth returning
For getting assets back out, the useful count is distinct pictures: one file per image object, masks not counted as pictures of their own. ImgIng's Extract PDF images is a tool I help build, though my part is the codecs and model loading; the PDF engine is someone else's code, so what follows is behaviour I measured, not code I wrote. On this sample it returned three files. The photo and the logo came out once each, and each card lists every page the object appears on, "Page 1, Page 2, Page 5". That list is per page, not per placement, so the photo painted twice on page 2 still reads "Page 2" once. No card mentions page 4.
The mask went where I hoped. It did not come out as a stray grayscale image; it was merged into the alpha channel of image-003.png, and that file is pixel-identical to the RGBA PNG I started from, alpha running the full 0 to 255. The bytes are not identical, because it was written out as a fresh PNG, which is why I compare decoded pixels for anything that went through Flate.
When the stored bytes are the answer
JPEG is the opposite case. A /DCTDecode stream is already a complete JPEG file, so the most faithful extraction is to write those bytes out and not decode anything. I checked three samples: an RGB JPEG saved at quality 85 (297,323 bytes), a CMYK JPEG and a grayscale JPEG. All three extracted files had the same SHA-256 as the JPEGs I embedded, and the CMYK one stayed a CMYK JPEG instead of being converted to PNG. The colours came out right next to the rendered page. My first guess was that the PDF's /Decode [1 0 1 0 1 0 1 0] and the JPEG's Adobe APP14 marker were cancelling each other out. Stripping the APP14 segment and re-testing proved that wrong: Pillow 11.3 and Chromium 149 decoded exactly the same pixels, because both read any 4-channel JPEG as Adobe-style inverted CMYK regardless of the marker. The colours matched because this sample happens to be stored that way. A CMYK JPEG stored non-inverted with no /Decode would probably come out as a negative, but I have not built one to test.
Passing bytes through has a price, and I only found it because I built the combination deliberately: a JPEG that also has an /SMask. On the rendered page that picture fades into an oval over a yellow background. Extraction handed back the original 75,299-byte JPEG, hash-identical, with the mask simply dropped, so you get the full rectangle. The Flate path merges masks; in this sample the JPEG pass-through path doesn't. I have one such file and I attached its mask with pypdf myself, so I can't say how often real exports from design tools produce it. It is the first thing I'd look at when a supposedly transparent product shot comes back with a box around it.
Things I haven't measured yet: forms nested more than one level, inline images written straight into the content stream with BI/ID/EI, and two objects with byte-identical streams but different object numbers, which some generators emit and which may or may not be collapsed into one file. My habit before arguing with any extractor's count is now that two-call listing. When placements and objects differ, the images that seem to be missing were never separate images in the first place.
If you want to repeat the three-file result on your own PDFs, the extractor is at https://imging.ai/ and it reads the file in the browser tab.

Top comments (0)