A PDF that looks like a single document is really a container of separate objects, and the images inside it are stored as XObject streams referenced by the page’s content stream. Unlike simple text extraction, PDF images are stored as XObject streams that require dedicated parsing (see our guide to what data parsing entails) to avoid re-rendering.
The compression filter on each stream tells you how the image was encoded, and that filter determines whether you can pull the original bytes or not. A photo placed from a JPEG file usually arrives wrapped in a DCTDecode stream, which is just the original JPEG with its markers intact. Extract those bytes directly and you get the exact file the author inserted, at full resolution, with no generational loss. Scanned pages and screenshots usually land in FlateDecode streams of raw pixel data (sometimes with a /BitsPerComponent or /DecodeParms twist), and CMYK scans from print production pipelines often show up as JPXDecode (JPEG 2000), which naive readers skip entirely because the filter name isn’t on their whitelist.
Here is where most extraction scripts quietly fail. The easy path is to render the page to an image and crop the region, but rendering rasterizes the whole page at a fixed DPI, then re-encodes the result to PNG. A 300 DPI embedded JPEG comes back blurred, upscaled, or downsampled depending on the DPI you chose, and the re-compression strips the original quantization tables. The distinction shows up directly in PyMuPDF: page.get_pixmap() renders (and degrades), while page.get_images() followed by doc.extract_image(xref) returns the raw stream bytes, complete with the correct file extension and the original color space. pdfimages -list gives you the same inventory from the command line, so you can see filter type, width, height, and colorspace per xref before writing a line of extraction code.
Resolution loss is only one hidden cost. The other is duplication: a single logo or header image reused across 40 pages appears as one XObject referenced 40 times, and a renderer that dumps pages as PNGs will write 40 slightly different copies of it. Extract by xref instead, dedupe on the hash of the extracted bytes, and you cut both storage and downstream matching time. The same applies to scanned bitmaps: identical blank margins and stamped page furniture repeat across the file, and naive per-page rendering treats each as unique.
Manual export earns its keep in a narrow case. When a PDF is a flattened scan with no embedded XObjects at all, there is nothing to extract, and pulling individual regions from a rendered page at 600 DPI with careful cropping outperforms any stream-level script. Know which situation you are in before you build the pipeline.
Want this done for you?
Send us the URLs. We'll quote it in 24 hours.
Paste the URL(s) you want scraped. We'll reply within 24 hours with a feasibility check and a ballpark quote.


