PaperPony

Images missing from a generated PDF

The advice for this is a list of settings to change: turn off lazy loading, move the image above the fold, wait longer, use an img instead of a background. Measured on the renderer, every one of those drew the image already.

Chromium 151.0.7922.34, against a server that counted what it was asked for, reading the number of images that reached the file.

What was measured drawing the image anyway

Measured
every setting usually blamed for this

                                      images in the pdf  requests

  no image in the document at all            0              0
  an <img> with an empty src                 0              0

  a plain <img>                              1              1
  loading="lazy"                             1              1
  lazy, 600mm below the fold                 1              1
  eager, 600mm below the fold                1              1
  a CSS background-image                     1              1
  a response that takes 1500ms               1              1
  srcset offering only a 2x source           1              1
  served as text/html, not an image          1              1

The first two lines are the baseline and they are what make the rest readable: a document with no image counts zero, so a count above zero means something was drawn.

loading="lazy" is the usual first suspect and it is innocent, including on an image six hundred millimetres below the top of the document. Printing lays the whole document out, so there is no viewport to be outside of.

A response taking a second and a half also arrived, and a file served with the wrong content type was sniffed and drawn. Changing any of these changes nothing, which is the time this page is meant to save.

The two that did it, and they look different

Measured
the two that did it

                                      images in the pdf  requests

  a CSS background, printBackground off      0              1
  the image answers 404                      2              1
  the image answers 403                      2              1
  the connection is dropped                  2              1
  a host nothing is listening on             2              0

A CSS background with backgrounds turned off is fetched and thrown away. The server was asked for it and the file has nothing. That is a deliberate print setting rather than a failure, and it applies to colours and gradients as much as to images: background colours missing from a PDF measures the same switch from the other side.

A fetch that fails does not leave an empty space. A 404, a 403, a dropped connection and a host with nothing listening all put two rasters in the file rather than none, because Chromium draws its broken-image placeholder. It is small and grey and easy to overlook at print size, and it is not nothing.

So the empty space tells you which half you are in

This is the part worth carrying away. A space where an image should be, with nothing at all in it, is not a failed download: a failed download draws the placeholder. Something decided not to paint it, and the print settings are where to look.

A small broken icon is the opposite. The document is fine and the fetch is not: the URL, the status, the certificate or whatever stands between the renderer and the host.

The last row of the second table is the one to notice for a hosted renderer. Nothing was requested at all, because there was nothing to reach. A renderer that cannot reach your image produces exactly this, and no amount of adjusting the markup will change it.

Checking it on your own file

Counting the rasters in the file separates all three cases without opening it:

Counts embedded rasters
node -e "const s=require('fs').readFileSync('report.pdf','latin1'); console.log(((s.match(/\/Subtype *\/Image/g)||[]).length) + ' embedded rasters')"
One image, drawn
1 embedded rasters

Zero with an image in the markup means it was not painted. A count higher than the number of images you expect means placeholders are being drawn for some of them.

How we handle this

PaperPony renders every document behind an egress proxy, so an image on a private address, on the loopback interface or on a host the proxy refuses is not fetched. In the file that shows up as the broken-image placeholder described above rather than as an error on the job, so a document full of internal URLs prints and looks wrong rather than failing.

print_background is on by default with us, so the first row of the second table is not something you meet unless you turn it off. Images reachable on the public internet, and data URIs, are the two shapes that always work.