Taking a PDF apart with PDFLang
On screen a PDF looks like one flat thing, a picture of a page. It is not. It is a box of parts, and most of the questions people ask about a file — is this going to print correctly? is anything missing? — are really questions about one of those parts.
PDFLang gives you five ways to reach inside. Four of them are already there when your script starts. The fifth you draw yourself.
The document
Section titled “The document”Every script begins with a variable called doc. Nothing to open, nothing to
load: doc is the file being checked.
check "What am I looking at" { print("file:", doc.filename) print("pages:", doc.page_count) print("title:", doc.title)}doc.title and doc.author come from the file’s metadata — the information a
PDF carries about itself, which you never see on the page. It is often empty,
because whoever exported the file never filled it in. That is worth checking on
its own:
check "The file says what it is" { assert doc.title != "", "the PDF has no title in its metadata"}doc also holds three lists: doc.pages, doc.fonts and doc.images. They
are ordinary lists, so anything you can do to a list you can do to them.
The pages
Section titled “The pages”doc.pages gives you the pages one at a time. Each one knows its own size, how
much ink it carries, and which of its boxes are defined.
Sizes are in points, the unit PDFs think in: 72 points to the inch, so an A4
page is 595 × 842. You do not have to think in points, though — write 210mm or
8.5in and the comparison works:
check "Everything is A4" { doc.pages.each { |page| assert page.width > 200mm, "page #{page.number} is too narrow: #{page.width}pt" }}Note page.number. Pages count from 1, the way they do in conversation, so the
number in a failure message is the page a person will turn to.
Then there are the boxes. A print-ready page carries several rectangles that say what to do with it: the TrimBox is where the guillotine cuts, the BleedBox is the slightly larger area where colour must run past the cut so a millimetre of drift does not leave a white edge. A file without them can look perfect on screen and still be unusable at the printer:
check "Ready for the guillotine" { doc.pages.each { |page| assert page.has_trim_box, "page #{page.number} has no TrimBox" assert page.has_bleed_box, "page #{page.number} has no BleedBox" }}A page also reports page.tac: how much ink lands on its heaviest spot, as a
percentage. Four inks at full strength would be 400%, which no press will take;
300% is a common ceiling.
The fonts
Section titled “The fonts”doc.fonts lists the typefaces the file uses, and each one answers a single
important question: is it embedded?
An embedded font travels inside the PDF. A font that is not embedded is only named, and the machine opening the file substitutes something it has. The text does not disappear — it changes shape. Lines re-break, a heading that fitted on one line now takes two, and nobody notices until it is printed.
check "Every font travels with the file" { doc.fonts.each { |font| assert font.is_embedded, "font '#{font.name}' is not embedded" }}The images
Section titled “The images”doc.images gives you every image in the file, and page.images the ones on a
particular page. The property worth understanding is dpi.
DPI is dots per inch — how much detail there is in each inch of print. PDFLang does not report the number stored in the image’s metadata. It reports the effective resolution: the pixels the image actually has, divided by the size it is printed at. Stretch a 1000-pixel photo across 20 centimetres and it is a low-resolution image, whatever it claims about itself. That is the number that decides whether the print looks sharp.
check "Sharp enough to print" { doc.images.each { |img| assert img.dpi >= 300, "image on page #{img.page_number}: #{img.dpi} DPI, needs 300" }}img.color_space is the other one people reach for. Offset presses work in
CMYK; an image still in RGB has to be converted, and the conversion shifts
colours. Better to find it before the plates are made.
The areas you draw yourself
Section titled “The areas you draw yourself”The four above come with the file. The fifth does not: a region is a rectangle you define, so you can ask about one part of a page instead of the whole thing.
footer = region(0, 0, 595, 60, "footer")band = region(20mm, 250mm, 60mm, 15mm, "warning band")The one thing to remember is where the origin sits. In a PDF, 0, 0 is the
bottom-left corner of the page, not the top-left. Y grows upwards.
Regions are what let you write the rules that actually matter to a job. Not “does this file contain the legal text” but “is the legal text in the band reserved for it”:
check "Warning band" { band = region(0, 700, 595, 142, "band") content = text::extract_from_region(1, band) assert content.contains("PRESCRIPTION ONLY"), "the band is missing the mandatory text"}Regions can be compared, too. footer.intersects(header) tells you whether two
areas overlap, which is how you catch an element that has wandered into space
reserved for something else. footer.expand(5mm) and footer.inset(3mm) give
you a looser or tighter copy, for tolerance.
Where to look next
Section titled “Where to look next”The document types chapter lists every property and method of all five, with examples for each. It is reference material — read it when you need a specific thing, not front to back.
If you want to see the five used together on real jobs, the recipes and the examples are the place.