Table of Contents

PDF snapshot format

A PDF snapshot is UTF-8 text with LF line endings and no byte-order mark, saved as {name}.verified.pdfua.txt.

# pdfua-snapshot v1
document
  marked=yes lang=en-US display-doc-title=yes pages=3
...
structure
  Document lang=en-US
    H1 p1 "Trail Table"
    Figure p1 alt="Park logo" images=[3f9c0a1b2d4e5f60]
    Table p2-3 summary="Trail details"
      TR
        TH1 p2 scope=Column "Trail"
        TH2 p2 scope=Column "Length"
      TR
        TD p2 headers=[TH1] "Ridgeline"
        TD p2-3 headers=[TH2] "Cedars"
untagged
  p1 "Stray text"
  p1 images=1 paths=2
fonts
  "Arial" TrueType embedded=yes to-unicode=yes glyphs=3f9c0a1b2d4e5f60
layout
  p1 text tagged at=72,720 width=85 font="Arial" size=18 color=gray:0 "Trail Table"
  p1 text untagged at=72,90 width=38 font="Arial" size=9 color=gray:0 mode=invisible "Stray text"
  p1 image tagged at=72,600 size=120x40 pixels=600x200 hash=3f9c0a1b2d4e5f60
  p1 path artifact at=72,60 size=468x1 stroke=cmyk:0,0,0,1 line-width=0.5

Sections

Sections always appear in this order. Each can be left out with PdfSection.

Section Contents
document Whether the PDF is tagged, suspects=yes when it says its tags may be unreliable, and parent-tree=missing when elements own content but there's no parent tree, or parent-tree-problems=[duplicate-keys,unsorted-keys,next-key-too-low] listing what's wrong with the parent tree's keys; its language, whether the viewer shows the title, page count, and how many objects it refers to that couldn't be read, such as one missing from the file (unreadable-objects=2), when any
settings PDF version, page mode and layout, what happens when the file opens, whether it has JavaScript (in document-level scripts, the open action, document actions, form fields, links and other annotations, page actions or bookmarks) or document actions, encryption and what it permits; viewer preferences, optional-content layers and their configurations (layer-config), embedded files (both-names=no when one isn't named both as F and as UF, and page= for one attached through a comment), and how many reference XObjects the pages' resources and annotation appearances hold (reference-xobjects), when any do
metadata Title, author, subject, keywords; whether dates, producer and creator are present; PDF/UA identifier and revision (pdfuaid-part=2 pdfuaid-rev=2024); XMP title, creator, description, keywords, subject, language and PDF/A conformance, or xmp=unreadable; each PDF Declaration the XMP makes, such as Well-Tagged PDF, with whether it gives claim data (declaration "http://pdfa.org/declarations/wtpdf#accessibility1.0" claim-data=yes), recorded but not checked; every other Info entry (such as a company name or classification label)
role-map Each custom tag type and the standard type it maps to, then the custom types each PDF 2.0 namespace maps, with their namespaces (Tip -> Aside ns="https://example.com/tags" mapped-ns=pdf2)
pages Size (named when standard, such as letter or a4), rotation, tab order; crop, bleed, trim and art boxes, user unit, transparency group and a hash of the page's default color spaces and those of the forms it draws (default-colors=), when any are set; how many artifacts sit inside tagged content (artifacts-in-tagged) and how many hold tagged content (artifacts-holding-tagged), when any do; how many marked-content sequences reuse an MCID from earlier in the same content stream, as when a form with MCIDs is drawn twice (repeated-mcids), when any do; pages that couldn't be read, with a hash of what they hold (p3 unreadable hash=3f9c0a1b2d4e5f60); page label ranges
bookmarks The outline tree, with each bookmark's target: a page and the view it opens at, a web address, another file, the address a form is submitted to, or the type of another action it runs, with what it acts on (action-target="Print": a Named action's name, the fields a ResetForm or Hide action names, or a hash of a JavaScript action's script). Also whether it opens with its children hidden (closed=yes), and its title's color and style (color=#ff0000, style=bold). Or unreadable when it nests more than 256 deep, loops back on itself or has an item without a title
structure One line per structure element, in reading order: type, mapped type, pages, the PDF 2.0 namespace its type is in when it has one other than PDF 1.7's (ns=pdf2, ns=mathml, or any other namespace's URI in quotes) and the mapped type's when that differs (mapped-ns=pdf2), language, alt text, actual text, expansion, title, table attributes (scope, headers, spans, summary), list numbering, a form field's role (field-role=checkbox), whether set on the element or through the class map, the files PDF 2.0 lets it carry, such as a formula's MathML (associated-files=[Supplement:3f9c0a1b2d4e5f60:"formula.mml"]: how each relates, a hash of its contents and its name), any of those table, list and form attributes under the wrong attribute owner (attribute-owners=[Scope:Layout]), a hash of the attributes from /A and the class map that PDiffy doesn't read (other-attributes=), links, the hashes of the images it draws (images=[3f9c0a1b2d4e5f60], the same hash as its layout line), the language, replacement text, alt text and expansion that marked content inside it sets (an empty one as ""), each with the text it covers unless that's all of the element's text (span-lang=[fr-FR:"bonjour"], span-expansion=["Mount":"Mt."]), how many of its marked-content references name no page, or name something that isn't one (mcids-without-page, whose text can't be found), how many point into a form XObject (mcids-in-xobject), how many child elements are nested more than 256 deep and left out (unread-children), how many of its own words come before each child when a child comes before some of them (children-after-words=[2], so a link moved within its sentence shows; left out of a page-limited snapshot that drops one of the element's children), parent=missing when it has no parent entry, parent=wrong when it names an element other than the one that lists it, or parent=unlisted when its parent doesn't list it, so only the parent tree reaches it (such an element follows the rest of the tree, at the top level), id=duplicate when another element has the same ID or id=missing for a Note without one, how many of the children it lists that the tree already reached elsewhere (shared-children; each such child shows only where it was reached first), the marked content and annotations it owns that the parent tree gives to another element or to none (parent-tree=[p2:7->P], or p2:xobject:0->none for marked content inside a form XObject), and the element's text
artifacts Text marked as an artifact (headers, footers, page numbers), per page, with its kind when it gives one (type=Pagination subtype=Footer)
untagged Text that is neither tagged nor marked as an artifact, one line per line of text (and tagged text that no structure element points to, one line per marked-content ID), and a count of such images and paths per page, and of shadings when there are any (shadings=1)
fonts Each font the pages draw with, and each font a text, combo box, list box or signature field's appearance, or a FreeText annotation's, draws with: name, type, whether it's embedded, whether it has a Unicode map, a hash of the outlines its embedded program draws for the text's codes (glyphs=), the character codes its text uses that don't map to Unicode (that text can't be copied, searched or read aloud), those that show the missing-glyph box, and what PDF/UA forbids in its dictionaries and embedded program
annotations Every annotation (links, comments, form widgets and so on), page by page in the order each page lists them: type, tagged or untagged, rectangle, flags (such as print or hidden), how it looks (its border style and width, color, highlight mode, a widget's caption, border color and background, and a hash of its appearance: border=solid:0 color=#0000ff appearance=3f9c0a1b2d4e5f60), where a link goes (a web address, or a page and the view it opens at, or another file, or the address a form is submitted to, or another action's type and what it acts on: action=Named action-target="NextPage"), contents and accessible name, and for a Screen annotation each media clip it can play: its content type and alternate text (media-type=video/mp4 media-alt="Trail tour", then media-type-2= and media-alt-2= for a second clip), also on the structure line of the element that tags it
forms A form line for settings that apply to the whole form, when it sets any: the order calculate scripts run in (calculation-order=["trip.tax","trip.total"]), whether the viewer must draw the fields itself (need-appearances=yes) and an XFA form (xfa=static or xfa=dynamic). Then every form field: type, full name (address.city), page, accessible name, value, options, maximum length, flags (such as required or read-only), default value, the value each check box or radio button widget sends (exports=["Yes"]), alignment when it isn't left (align=center), the font, size and color of typed text (text-font="Helvetica" text-size=auto text-color=gray:0, from the widget, the field or the form) and the field's JavaScript by when it runs, as a hash (scripts=format:1a2b3c4d5e6f7081). A signed signature field prints signed=yes, the signature's format (subfilter=), whether the signed bytes run to the end of the file (covers-file=no after an update saved since signing), whether a reason, location and signing time are given (reason=(present)), and what a certifying signature still allows (certification=form-filling). The signature's bytes and signing time aren't recorded, so signing the same file again gives the same snapshot
layout Every text run, image, painted vector path and sh shading, page by page, each labeled tagged, artifact or untagged. Text runs: position, width along the baseline (width=, which character and word spacing and horizontal scaling change), font, size, color (and the outline's color, stroke=, for text that's both filled and outlined), render mode, angle. A part of a line that's raised or lowered by half a point or more, such as a superscript, is a run of its own. Images: position, size, pixel size, a hash of the pixels and of what decides how they show (a palette or color profile, bit depth, Decode array, masks and smoothing), and for an image mask (an image that's only a shape), the color it paints with (fill=). Paths: bounding box, a hash of their outline measured from the box's corner and, for a filled path, its fill rule (shape=, left out for one horizontal or vertical line, or one rectangle drawn with re with its sides parallel to the page's edges, which the box describes), fill, stroke and line width, and, when they aren't PDF's defaults, the outline's dash pattern (dash=3,2, with dash-offset= when the line starts partway into the pattern), ends (cap=round or cap=square), corners (join=round or join=bevel) and, for mitered corners, how far a sharp corner may reach (miter-limit=4). Shadings painted with the sh operator (a gradient or mesh that fills the clipping area without a path): the area they fill (the clipping area's bounding box, cut to the shading's own box when it has one, or the visible page when nothing clips) and a hash of what they draw. Any of them that a graphics state makes see-through, blends or masks also prints an opacity below 1 for each part it paints (opacity=0.1 for the fill, stroke-opacity= for the outline), a blend mode other than normal (blend=multiply), a rendering intent other than PDF's default, relative colorimetric (intent=perceptual), and a hash of the soft masks that fade it, including what they draw (soft-mask=3f9c0a1b2d4e5f60). Any of them inside a transparency group (a form with /Group) that is itself drawn see-through also gets the group's opacity on top of its own, the group's blend mode when it doesn't set its own (or group-blend=multiply beside its own when it does, listing nested groups' modes innermost first), and the group's soft mask along with its own, so a watermark faded as a group prints opacity=. Any of them that a clipping path, or the box of the form it's drawn in, leaves out entirely prints clipped=yes. A clipping path counts in its real shape, with curves traced in small straight steps. Text drawn in a clipping mode (4 to 7) clips to its letters' shapes, or to their boxes when PDiffy can't read the font's outlines, as with a standard font that isn't embedded. Any of them entirely covered by something opaque drawn later prints covered=yes. Only a shape filled with a plain color or an image without a mask counts as covering, and not when it's in a layer. It covers only what lies wholly inside its real shape. Any of them drawn inside optional-content marked content (/OC ... BDC, a layer a viewer can hide), or by an image or form that puts itself in a layer with its own /OC entry, also lists its layers by name, outermost first (layers=["Draft"])
checks Only with Check(PdfCheckSet): what the checks find, one line per finding with its rule, page and path (FigureAltText p2 structure > Document > Figure[2]), sorted, or (none)

Rules

  • The first line is the format version. See Format changes for when it changes.
  • When anything is ignored, the next lines list it: # ignored sections: ... and # ignored fields: .... Other options add a line after them: Tolerate adds # tolerances: ... (such as # tolerances: positions 1pt), OnlyPages adds # only pages: ..., and Check adds # checks: ... with the rule set and how many rules it has (such as # checks: latest, 42 rules). Comparing two snapshots applies the tolerances their header lists.

Lines and keys

  • An empty section prints (none) and an ignored one prints (ignored), so something disappearing shows up as a change.
  • Two spaces of indent per nesting level. Keys appear in a fixed order and are left out when they have no value.
  • Text is quoted and normalized to Unicode NFC, so a letter typed with a separate accent prints as one letter. " and \ are escaped, a tab and a line break print as \t and \n, and characters a reader can't see or tell apart print as \uXXXX: control and format characters (such as the soft hyphen and zero-width and direction marks), private-use characters, line and paragraph separators, spaces other than U+0020 (such as a no-break space), U+FFFD to U+FFFF and variation selectors.
  • Pages print as p1, or p2-3 for an element that spans pages.
  • Table header cells that other cells refer to get names numbered within each table (TH1, TH2) instead of the PDF's internal IDs. A cell that names a header in another table also names that table, with tables numbered in reading order: headers=[table1:TH1], or no-table:TH1 for a header outside every table.

Layout

  • Positions and sizes are whole points from the page's bottom-left corner, and a text run's width is in whole points. Font sizes and line widths are rounded to 0.1 pt.
  • Colors name their color space: gray:0.5, #ff0000 (RGB), cmyk:0,0,0,1, or pattern:shading:3f9c0a1b2d4e5f60 / pattern:tiling:... for gradients and tiled fills, with a hash of what the pattern draws. A path, text or image mask painted with an uncolored tile, which has no color of its own, adds the color it's painted in (pattern:tiling:3f9c0a1b2d4e5f60:#ff0000). A color given in a color space other than a device or pattern one, such as a spot color or an ICC-based one, starts with that space's family and a hash of its definition (separation:3f9c0a1b2d4e5f60:cmyk:0.8,0,1,0), so renaming a spot color or changing a profile shows even when the device color stays the same. So does the color of an uncolored tile painted in such a space through a pattern space like [/Pattern [/Separation ...]] (separation:3f9c0a1b2d4e5f60:pattern:tiling:...:gray:0).
  • intent= appears only for content whose rendering intent isn't PDF's default, relative colorimetric (intent=perceptual).
  • mode= appears only for text that isn't drawn the usual way (invisible, stroke, fill-stroke, ...).
  • angle= appears only for text that doesn't run across the page: the angle in degrees, counterclockwise, such as angle=90 for text that reads upward. (A page's rotate= is the PDF's own setting, which turns clockwise.)
  • fill=, stroke= and a path's line-width= appear only for what the path actually paints.
  • An image's hash is taken from its decoded pixels where PDiffy can decode them, so changing only how it's compressed, such as Flate-compressing an uncompressed image, doesn't change it. A JPEG or JPEG 2000 image is hashed as stored, since PDiffy doesn't decode either format. Re-encoding one almost always changes its pixels anyway, but a re-encoding that keeps them, such as one that only strips metadata, still changes the hash.
  • Annotation rectangles use the same convention: the bottom-left corner and size, in whole points.
  • A link that goes to a page prints dest=p3 (p? when its page reference isn't a page), the view it opens the page at (view=XYZ, Fit, FitH and so on) and the view's positions in whole points (view-at=72,720, with - for a position the viewer keeps as it is), and an XYZ view's zoom when it sets one (zoom=2). The positions belong to PdfDetail.Positions, so they can be tolerated or ignored.
  • A named destination is looked up, so it prints like any other. One the PDF doesn't define, or one in another file, prints dest-name="...".
  • A link to another file prints file="maps.pdf", with the page it opens there for action=GoToR and action=GoToE. For an embedded file (action=GoToE), file= is its name, or a path such as data.pdf/inner.pdf for a file inside another embedded file, with .. standing for the file that holds this one. A file an attachment comment holds prints as the comment's page and either its place among the page's annotations or its name: p1#0 or p1#chart.
  • A bookmark prints -> p2 and the view it opens at, or uri="...", or file="..." with dest= for the page it opens in that file, or dest-name="..." like a link. A bookmark that runs another kind of action prints its type (action=Named) and, for a Named, ResetForm, Hide or JavaScript action, what it acts on (action-target="Print", after except: or show: when a ResetForm or Hide action is reversed); a SubmitForm action prints the address it sends the form to as file=. One with no target prints just its title. A bookmark whose children are hidden until it's opened prints closed=yes, one whose title isn't black prints color=, and one whose title isn't plain prints style=bold, italic or bold-italic.
  • The open action reads its destination the same way, and prints it in the PDF's order inside open-action="...": the page, the view, its positions and an XYZ view's zoom (GoTo p1 XYZ 72 720 2). A name the PDF doesn't define prints as named intro. Another action prints its type, then what it acts on, as on a bookmark: Named NextPage. The positions aren't left out with PdfDetail.Positions, since the whole value belongs to PdfDetail.OpenAction.

Fonts

  • glyphs=3f9c0a1b2d4e5f60 is a hash of the outlines an embedded TrueType, Type 1, CFF or OpenType program draws for the codes of the font's drawn text, each hashed with its code. A font swapped for one with the same name and widths that draws other shapes changes it; a program subset again with its glyphs renumbered but drawing the same shapes doesn't. A CID font whose encoding isn't Identity-H or Identity-V hashes every glyph its program has, each with its CID, since PDiffy doesn't match its codes to CIDs through any other CMap. It's left out for a font that isn't embedded (its shapes depend on where the PDF is opened), for a font whose text is all invisible, and for a program PDiffy can't read or whose outlines it can't draw.
  • unmapped=[0041-0043,0050] lists hex character codes, with runs shortened to first-last.
  • A code counts as mapped when the font's ToUnicode map gives it text other than U+0000, U+FEFF or U+FFFE. When the map leaves the code out, a simple font also maps it when its encoding gives it a standard glyph name, and a CID font when it uses a predefined CMap other than Identity-H or Identity-V over one of Adobe's character collections (Japanese, Chinese or Korean).
  • missing-glyphs=[01] lists the codes that show the font's .notdef glyph, the empty box drawn for a character the font lacks: a simple font's encoding names the code .notdef, or a CID font with an Identity encoding picks CID 0 or maps the code to glyph 0. An embedded TrueType, Type 1, CFF or OpenType program adds the codes whose glyph it lacks.
  • problems=[cid-system-info] lists what PDF/UA forbids in a Type 0 or TrueType font's dictionaries, one PdfFontProblem each: cid-system-info, cid-to-gid-map, cmap-not-embedded, cmap-writing-mode, cmap-reference, true-type-encoding and symbolic-encoding, and from an embedded TrueType, Type 1, CFF or OpenType program, glyph-widths, cid-set, char-set, true-type-cmap, symbolic-cmap and unreadable-program.
  • Text inside form XObjects counts too, under each font it's drawn with.

Text

Text is normalized to Unicode NFC, with runs of spaces, tabs and line breaks collapsed to one space and trimmed. Other spaces inside the text, such as a no-break space, are kept. A space is added between two pieces of text when the baseline changes or the gap is wider than 0.2 em beyond the line's letter spacing, which is read from its narrowest gaps. The same rule joins the pieces of an element's text, so a word a PDF splits across two tagged pieces stays one word. Pieces on different pages, and a piece that starts well to the left of where the last one ended on the same line, are always separated by a space.

What's left out on purpose

These change without changing what anyone reading the document gets:

  • Object numbers, compression, object order, stream lengths and cross-reference layout.
  • MCIDs and structure element IDs; references are resolved to names.
  • Font subset prefixes: ABCDEF+Arial is recorded as Arial.
  • Dates, document IDs and the producer string: only whether they're present. ShowProducerAndCreator() prints the producer and creator.

Damaged files

A page that can't be loaded (for example, one with a damaged font) comes back marked unreadable, and its snapshot line says so, instead of the whole read failing. The same goes for an attachment whose contents can't be decoded, for XMP metadata that isn't well-formed XML or has a DTD (xmp=unreadable), and for bookmarks where an item has no title. An unreadable page's line also prints a hash of what decides how the page draws, as the file stores it, including its content streams, its resources and what they reach, so a change to the page still fails the test (p3 unreadable hash=3f9c0a1b2d4e5f60). An image that can't be decoded is hashed as stored, and a reference to an object the file doesn't have, or can't find, reads as nothing, as the PDF standard says. The document line counts such objects (unreadable-objects=2), so one going missing still shows. A reference to object 0, a number no object can have, isn't counted: when qpdf linearizes a file, it writes one in place of a reference to an object it drops. A file that can't be opened at all throws InvalidDataException.

PDiffy also reads structure elements and bookmarks only 256 levels deep, so a file built to overflow the stack can't crash the test run. An element whose children are deeper gets unread-children=N, and their content counts as untagged. Bookmarks nested deeper, or that loop back on themselves, print unreadable. A Flate-compressed stream that would decompress to more than 256 MB isn't decompressed. If it holds a page's content, the page comes back unreadable; if it's an image, the image is hashed as stored; if it's XMP metadata, the snapshot prints xmp=unreadable. See Limitations for what PDiffy can't guard against.