<?xml version="1.0" encoding="utf-8"?>
<!-- GENERATED by scripts/build-blog-index.mjs. Do not edit: run `npm run build:blog`. -->
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Tamperlens — measurements from the parser</title>
  <subtitle>Evidence from building a PDF fraud-signal engine. Full text, because truncating a feed to drive clicks is not this brand's register.</subtitle>
  <link rel="self" type="application/atom+xml" href="https://tamperlens.com/blog/feed.xml" />
  <link rel="alternate" type="text/html" href="https://tamperlens.com/blog/" />
  <id>https://tamperlens.com/blog/</id>
  <updated>2026-08-20T00:00:00Z</updated>
  <author>
    <name>Tamperlens</name>
    <uri>https://tamperlens.com/</uri>
  </author>
  <rights>© Tamperlens. Full text is republishable with attribution and the canonical link.</rights>
  <entry>
    <title>Everything Tamperlens detects, and exactly what defeats each one</title>
    <link rel="alternate" type="text/html" href="https://tamperlens.com/blog/what-defeats-every-signal" />
    <id>https://tamperlens.com/blog/what-defeats-every-signal</id>
    <published>2026-08-20T00:00:00Z</published>
    <updated>2026-08-20T00:00:00Z</updated>
    <summary type="text">A row for every signal family, a column for every evasion I know how to describe, and the eight cells I could not fill in honestly.</summary>
    <content type="html" xml:lang="en">&lt;p class=&quot;post-meta&quot;&gt;
        Douglas Haruo · &lt;time datetime=&quot;2026-08-20&quot;&gt;20 August 2026&lt;/time&gt; · cells derived from
        &lt;code&gt;src/engine/signals/&lt;/code&gt;, not from a marketing sheet
      &lt;/p&gt;
      &lt;p class=&quot;post-reference&quot;&gt;
        Reference: &lt;a href=&quot;https://tamperlens.com/pdf-fraud-signals&quot;&gt;PDF fraud signals — the field guide&lt;/a&gt;
      &lt;/p&gt;

      &lt;p class=&quot;lede&quot;&gt;
        Every vendor in this space publishes what it catches. None of them publish what
        makes it stop working. This is the second list, for my own engine, in full.
      &lt;/p&gt;

      &lt;nav class=&quot;toc&quot; aria-label=&quot;On this page&quot;&gt;
        &lt;h2&gt;On this page&lt;/h2&gt;
        &lt;ol&gt;
          &lt;li&gt;&lt;a href=&quot;#why&quot;&gt;Why publish the manual&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#attacks&quot;&gt;The five columns&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#matrix&quot;&gt;The matrix&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#surprises&quot;&gt;Three cells worth reading twice&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#survives&quot;&gt;What survives everything&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#untested&quot;&gt;The cells I could not fill in&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#recall&quot;&gt;The number I will not give you&lt;/a&gt;&lt;/li&gt;
        &lt;/ol&gt;
      &lt;/nav&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;why&quot;&gt;Why publish the manual&lt;/h2&gt;
        &lt;p&gt;
          The argument against a page like this is that it is a how-to guide for the
          people I am trying to catch. I do not think that survives contact with reality.
        &lt;/p&gt;
        &lt;p&gt;
          Every command below is on the first page of results for &quot;how to remove pdf
          metadata&quot;. Someone building fake statements for money already owns the tools and
          already knows which one leaves the cleanest file — that knowledge is what they
          sell. The people who do not know are the ones on the receiving end: the analyst,
          the letting agent, the accountant who reads a clean report and hears
          &lt;em&gt;this document is genuine&lt;/em&gt;.
        &lt;/p&gt;
        &lt;p&gt;
          A clean report from a byte-level parser means the bytes are clean. Those are
          different claims, and the entire risk lives in the gap between them. So here is
          the gap, drawn to scale.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;attacks&quot;&gt;The five columns&lt;/h2&gt;
        &lt;p&gt;
          Four of these are the ones people actually run. The fifth is in the table
          because my own code puts it there.
        &lt;/p&gt;

        &lt;h3&gt;1. Metadata strip&lt;/h3&gt;
        &lt;pre class=&quot;code&quot;&gt;&lt;code&gt;exiftool -all= statement.pdf&lt;/code&gt;&lt;/pre&gt;
        &lt;p&gt;
          What most people think this does is delete the metadata. What it does to a PDF
          is append a new revision in which the metadata is blank. The original bytes stay
          exactly where they were.
        &lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 690 214&quot; width=&quot;100%&quot; role=&quot;img&quot; aria-labelledby=&quot;strip-title&quot;&gt;
            &lt;title id=&quot;strip-title&quot;&gt;Byte-layout diagram: before the strip the file is one revision; after the strip the original bytes are unchanged and a second revision is appended carrying a blanked Info dictionary.&lt;/title&gt;
            &lt;text x=&quot;10&quot; y=&quot;22&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;before&lt;/text&gt;
            &lt;rect x=&quot;10&quot; y=&quot;32&quot; width=&quot;200&quot; height=&quot;26&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;210&quot; y=&quot;32&quot; width=&quot;80&quot; height=&quot;26&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;290&quot; y=&quot;32&quot; width=&quot;80&quot; height=&quot;26&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;370&quot; y=&quot;32&quot; width=&quot;50&quot; height=&quot;26&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;110&quot; y=&quot;49&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text)&quot;&gt;objects + /Info&lt;/text&gt;
            &lt;text x=&quot;250&quot; y=&quot;49&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text)&quot;&gt;xref&lt;/text&gt;
            &lt;text x=&quot;330&quot; y=&quot;49&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text)&quot;&gt;trailer /ID&lt;/text&gt;
            &lt;text x=&quot;395&quot; y=&quot;49&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text)&quot;&gt;%%EOF&lt;/text&gt;

            &lt;text x=&quot;10&quot; y=&quot;102&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;after&lt;/text&gt;
            &lt;rect x=&quot;10&quot; y=&quot;112&quot; width=&quot;200&quot; height=&quot;26&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;210&quot; y=&quot;112&quot; width=&quot;80&quot; height=&quot;26&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;290&quot; y=&quot;112&quot; width=&quot;80&quot; height=&quot;26&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;370&quot; y=&quot;112&quot; width=&quot;50&quot; height=&quot;26&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;110&quot; y=&quot;129&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text-faint)&quot;&gt;objects + /Info&lt;/text&gt;
            &lt;text x=&quot;250&quot; y=&quot;129&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text-faint)&quot;&gt;xref&lt;/text&gt;
            &lt;text x=&quot;330&quot; y=&quot;129&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text-faint)&quot;&gt;trailer /ID&lt;/text&gt;
            &lt;text x=&quot;395&quot; y=&quot;129&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text-faint)&quot;&gt;%%EOF&lt;/text&gt;
            &lt;rect x=&quot;424&quot; y=&quot;112&quot; width=&quot;110&quot; height=&quot;26&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.55&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;534&quot; y=&quot;112&quot; width=&quot;76&quot; height=&quot;26&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.55&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;610&quot; y=&quot;112&quot; width=&quot;66&quot; height=&quot;26&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.55&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;479&quot; y=&quot;129&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text)&quot;&gt;blank /Info&lt;/text&gt;
            &lt;text x=&quot;572&quot; y=&quot;129&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text)&quot;&gt;xref /Prev&lt;/text&gt;
            &lt;text x=&quot;643&quot; y=&quot;129&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--text)&quot;&gt;trailer, %%EOF&lt;/text&gt;

            &lt;line x1=&quot;422&quot; y1=&quot;100&quot; x2=&quot;422&quot; y2=&quot;176&quot; stroke=&quot;var(--border-strong)&quot; stroke-dasharray=&quot;4 3&quot; /&gt;
            &lt;text x=&quot;215&quot; y=&quot;164&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;revision 1 — byte-identical to the original&lt;/text&gt;
            &lt;text x=&quot;550&quot; y=&quot;164&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;revision 2 — the strip&lt;/text&gt;
            &lt;text x=&quot;10&quot; y=&quot;196&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-muted)&quot;&gt;Any signature signed only what is left of the dashed line. Everything right of it is now uncovered.&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            A metadata strip on a PDF is an append, not a delete. The revision count goes
            up by one, and on a signed document the appended bytes fall outside the signed
            &lt;code&gt;/ByteRange&lt;/code&gt;. This is why the tool's own documentation tells you to
            run a rewriter afterwards.
          &lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;h3&gt;2. Whole-file rewrite&lt;/h3&gt;
        &lt;pre class=&quot;code&quot;&gt;&lt;code&gt;qpdf --linearize in.pdf out.pdf
gs -sDEVICE=pdfwrite -o out.pdf in.pdf&lt;/code&gt;&lt;/pre&gt;
        &lt;p&gt;
          One revision comes out the other side, with a rebuilt cross-reference table and
          no history. The two tools are not equivalent, though, and the difference decides
          five cells in the table below.
        &lt;/p&gt;
        &lt;p&gt;
          A structural rewriter copies through what it does not need to understand — the
          Info dictionary, the XMP packet, the font objects. A re-renderer regenerates all
          of it from what it drew. Same column, opposite outcome, and I have not measured
          which tools sit where.
        &lt;/p&gt;

        &lt;h3&gt;3. Print-to-PDF&lt;/h3&gt;
        &lt;p&gt;
          Chromium's Skia backend, macOS Quartz, Microsoft's print driver. The document is
          rendered and a new file is written from the rendering. Nothing structural
          crosses over.
        &lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 690 250&quot; width=&quot;100%&quot; role=&quot;img&quot; aria-labelledby=&quot;print-title&quot;&gt;
            &lt;title id=&quot;print-title&quot;&gt;Diagram: printing to PDF discards the old file's account of itself — metadata, identifiers, revision history, signature, annotations — and writes a fresh structure from the rendered page. Only the appearance of the page crosses over.&lt;/title&gt;
            &lt;rect x=&quot;4&quot; y=&quot;10&quot; width=&quot;250&quot; height=&quot;176&quot; rx=&quot;6&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;18&quot; y=&quot;32&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;the old file's account of itself&lt;/text&gt;
            &lt;text x=&quot;18&quot; y=&quot;56&quot; font-size=&quot;11&quot; fill=&quot;var(--text-faint)&quot;&gt;Info dictionary · XMP packet&lt;/text&gt;
            &lt;text x=&quot;18&quot; y=&quot;76&quot; font-size=&quot;11&quot; fill=&quot;var(--text-faint)&quot;&gt;trailer /ID pair&lt;/text&gt;
            &lt;text x=&quot;18&quot; y=&quot;96&quot; font-size=&quot;11&quot; fill=&quot;var(--text-faint)&quot;&gt;revision chain (/Prev)&lt;/text&gt;
            &lt;text x=&quot;18&quot; y=&quot;116&quot; font-size=&quot;11&quot; fill=&quot;var(--text-faint)&quot;&gt;font subset prefixes&lt;/text&gt;
            &lt;text x=&quot;18&quot; y=&quot;136&quot; font-size=&quot;11&quot; fill=&quot;var(--text-faint)&quot;&gt;signature /ByteRange&lt;/text&gt;
            &lt;text x=&quot;18&quot; y=&quot;156&quot; font-size=&quot;11&quot; fill=&quot;var(--text-faint)&quot;&gt;annotations · JavaScript&lt;/text&gt;
            &lt;text x=&quot;18&quot; y=&quot;176&quot; font-size=&quot;11&quot; fill=&quot;var(--high)&quot;&gt;all discarded&lt;/text&gt;

            &lt;path d=&quot;M266 96 L 418 96&quot; stroke=&quot;var(--accent)&quot; stroke-width=&quot;2&quot; /&gt;
            &lt;path d=&quot;M410 90 L 420 96 L 410 102 Z&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;342&quot; y=&quot;84&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-muted)&quot;&gt;render&lt;/text&gt;
            &lt;text x=&quot;342&quot; y=&quot;116&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-muted)&quot;&gt;then write anew&lt;/text&gt;

            &lt;rect x=&quot;432&quot; y=&quot;10&quot; width=&quot;252&quot; height=&quot;176&quot; rx=&quot;6&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.08&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;446&quot; y=&quot;32&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;what the new file says&lt;/text&gt;
            &lt;text x=&quot;446&quot; y=&quot;56&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;one revision, no /Prev&lt;/text&gt;
            &lt;text x=&quot;446&quot; y=&quot;76&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;fresh /ID, both halves equal&lt;/text&gt;
            &lt;text x=&quot;446&quot; y=&quot;96&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;producer = the printer&lt;/text&gt;
            &lt;text x=&quot;446&quot; y=&quot;116&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;one font subset per face&lt;/text&gt;
            &lt;text x=&quot;446&quot; y=&quot;136&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;no signature, no annotations&lt;/text&gt;
            &lt;text x=&quot;446&quot; y=&quot;156&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;coherent, recent timestamps&lt;/text&gt;
            &lt;text x=&quot;446&quot; y=&quot;176&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;a pristine document&lt;/text&gt;

            &lt;rect x=&quot;4&quot; y=&quot;198&quot; width=&quot;680&quot; height=&quot;42&quot; rx=&quot;6&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;18&quot; y=&quot;216&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;What does cross over: the page as it looks.&lt;/text&gt;
            &lt;text x=&quot;18&quot; y=&quot;233&quot; font-size=&quot;11&quot; fill=&quot;var(--text-faint)&quot;&gt;Including, in paint order, the text that somebody hid under a black rectangle.&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            The reason print-to-PDF beats most of this table is not subtlety. It throws the
            evidence away and writes a new file that has never been edited, because it
            genuinely has not been. What it cannot throw away is the picture — which is
            where the two families in the last column of the matrix survive.
          &lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;h3&gt;4. Flatten to raster&lt;/h3&gt;
        &lt;p&gt;
          Every page becomes one image. No text, no fonts, no structure worth reading. The
          heaviest hammer available and the only one in this list with a real cost, which I
          come back to at the end.
        &lt;/p&gt;

        &lt;h3&gt;5. Encrypt with an owner password&lt;/h3&gt;
        &lt;p&gt;
          Not a bypass anybody markets, and the one column here that is derived purely from
          my own source. An encrypted PDF has its strings and streams as ciphertext, so
          five families are skipped by the engine rather than run on noise. The list is a
          constant called &lt;code&gt;ENCRYPTED_SUPPRESSED_FAMILIES&lt;/code&gt;, and it is five names
          long.
        &lt;/p&gt;
        &lt;p&gt;
          An owner password does not stop anyone opening the file. It still turns off a
          third of the engine.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;matrix&quot;&gt;The matrix&lt;/h2&gt;
        &lt;p&gt;
          Rows are the 12 registered families, in registry order. Eleven of them run on
          every document; &lt;code&gt;issuer-mismatch&lt;/code&gt; only runs when the caller supplies a
          baseline, and it is the last row for that reason.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;strong&gt;gone&lt;/strong&gt; means the family cannot fire. &lt;strong&gt;weaker&lt;/strong&gt; means
          it fires with less to say. &lt;strong&gt;louder&lt;/strong&gt; means the evasion itself
          creates the finding. &lt;strong&gt;untested&lt;/strong&gt; means I could not decide it from
          the code and have not run the experiment.
        &lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 682 422&quot; width=&quot;100%&quot; role=&quot;img&quot; aria-labelledby=&quot;grid-title&quot;&gt;
            &lt;title id=&quot;grid-title&quot;&gt;Heat grid: every signal family against every evasion. Most cells go quiet; the issuer-mismatch row gets louder in all five columns.&lt;/title&gt;
            &lt;text x=&quot;232&quot; y=&quot;38&quot; text-anchor=&quot;middle&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;strip&lt;/text&gt;
            &lt;text x=&quot;332&quot; y=&quot;38&quot; text-anchor=&quot;middle&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;rewrite&lt;/text&gt;
            &lt;text x=&quot;432&quot; y=&quot;38&quot; text-anchor=&quot;middle&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;print&lt;/text&gt;
            &lt;text x=&quot;532&quot; y=&quot;38&quot; text-anchor=&quot;middle&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;raster&lt;/text&gt;
            &lt;text x=&quot;632&quot; y=&quot;38&quot; text-anchor=&quot;middle&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;encrypt&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;69&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;incremental-updates&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;54&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;69&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--on-accent)&quot;&gt;louder&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;54&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;69&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;54&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;69&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;54&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;69&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;54&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;69&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;95&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;metadata-mismatch&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;80&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;95&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;80&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.7&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; stroke-dasharray=&quot;3 2&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;95&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;untested&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;80&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;95&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;80&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;95&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;80&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;95&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;121&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;date-anomalies&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;106&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;121&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;106&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.7&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; stroke-dasharray=&quot;3 2&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;121&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;untested&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;106&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;121&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;106&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;121&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;106&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;121&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;147&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;producer-fingerprint&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;132&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.22&quot; stroke=&quot;var(--accent)&quot; stroke-width=&quot;1&quot; stroke-opacity=&quot;0.55&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;weaker&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;132&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.7&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; stroke-dasharray=&quot;3 2&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;untested&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;132&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.22&quot; stroke=&quot;var(--accent)&quot; stroke-width=&quot;1&quot; stroke-opacity=&quot;0.55&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;weaker&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;132&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--on-accent)&quot;&gt;louder&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;132&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;147&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;173&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;id-inconsistency&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;158&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.7&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; stroke-dasharray=&quot;3 2&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;173&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;untested&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;158&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;173&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;158&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;173&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;158&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;173&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;158&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;173&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;199&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;font-anomalies&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;184&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;199&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;184&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.7&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; stroke-dasharray=&quot;3 2&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;199&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;untested&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;184&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;199&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;184&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;199&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;184&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;199&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;225&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;hybrid-page&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;210&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;225&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;210&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;225&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;210&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.7&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; stroke-dasharray=&quot;3 2&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;225&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;untested&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;210&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;225&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;210&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;225&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;251&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;active-content&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;236&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;251&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;236&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.7&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; stroke-dasharray=&quot;3 2&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;251&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;untested&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;236&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;251&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;236&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;251&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;236&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;251&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;277&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;signature-coverage&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;262&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;277&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--on-accent)&quot;&gt;louder&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;262&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;277&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;262&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;277&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;262&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;277&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;262&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;277&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;303&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;structure-warnings&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;288&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;303&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;288&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;303&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;288&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;303&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;288&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;303&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;288&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;303&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--on-accent)&quot;&gt;louder&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;329&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;redaction-exposure&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;314&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;329&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;314&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;329&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;fires&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;314&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.7&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; stroke-dasharray=&quot;3 2&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;329&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;untested&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;314&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;329&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;314&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.22&quot; stroke=&quot;var(--accent)&quot; stroke-width=&quot;1&quot; stroke-opacity=&quot;0.55&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;329&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text)&quot;&gt;weaker&lt;/text&gt;
            &lt;text x=&quot;178&quot; y=&quot;355&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;var(--text)&quot;&gt;issuer-mismatch&lt;/text&gt;
            &lt;rect x=&quot;186&quot; y=&quot;340&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;&lt;text x=&quot;232&quot; y=&quot;355&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--on-accent)&quot;&gt;louder&lt;/text&gt;
            &lt;rect x=&quot;286&quot; y=&quot;340&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;&lt;text x=&quot;332&quot; y=&quot;355&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--on-accent)&quot;&gt;louder&lt;/text&gt;
            &lt;rect x=&quot;386&quot; y=&quot;340&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;&lt;text x=&quot;432&quot; y=&quot;355&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--on-accent)&quot;&gt;louder&lt;/text&gt;
            &lt;rect x=&quot;486&quot; y=&quot;340&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;&lt;text x=&quot;532&quot; y=&quot;355&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--on-accent)&quot;&gt;louder&lt;/text&gt;
            &lt;rect x=&quot;586&quot; y=&quot;340&quot; width=&quot;92&quot; height=&quot;22&quot; rx=&quot;3&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;&lt;text x=&quot;632&quot; y=&quot;355&quot; text-anchor=&quot;middle&quot; font-size=&quot;10.5&quot; fill=&quot;var(--on-accent)&quot;&gt;louder&lt;/text&gt;
            &lt;rect x=&quot;0&quot; y=&quot;382&quot; width=&quot;13&quot; height=&quot;13&quot; rx=&quot;2&quot; fill=&quot;none&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;19&quot; y=&quot;392&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;gone&lt;/text&gt;
            &lt;rect x=&quot;86&quot; y=&quot;382&quot; width=&quot;13&quot; height=&quot;13&quot; rx=&quot;2&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.22&quot; stroke=&quot;var(--accent)&quot; stroke-opacity=&quot;0.55&quot; /&gt;
            &lt;text x=&quot;105&quot; y=&quot;392&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;weaker&lt;/text&gt;
            &lt;rect x=&quot;190&quot; y=&quot;382&quot; width=&quot;13&quot; height=&quot;13&quot; rx=&quot;2&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.62&quot; /&gt;
            &lt;text x=&quot;209&quot; y=&quot;392&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;fires&lt;/text&gt;
            &lt;rect x=&quot;280&quot; y=&quot;382&quot; width=&quot;13&quot; height=&quot;13&quot; rx=&quot;2&quot; fill=&quot;var(--high)&quot; fill-opacity=&quot;0.78&quot; /&gt;
            &lt;text x=&quot;299&quot; y=&quot;392&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;louder&lt;/text&gt;
            &lt;rect x=&quot;372&quot; y=&quot;382&quot; width=&quot;13&quot; height=&quot;13&quot; rx=&quot;2&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.7&quot; stroke=&quot;var(--border-strong)&quot; stroke-dasharray=&quot;3 2&quot; /&gt;
            &lt;text x=&quot;391&quot; y=&quot;392&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;untested&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            The shape, without the reading. Two columns are nearly solid: raster silences
            almost everything, and the bottom row survives everything. The detail — and the
            reasons — are in the table below.
          &lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;div class=&quot;post-table&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Every signal family against every evasion, with reasons&quot;&gt;
          &lt;table&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;Family&lt;/th&gt;
                &lt;th&gt;Metadata strip&lt;/th&gt;
                &lt;th&gt;Whole-file rewrite&lt;/th&gt;
                &lt;th&gt;Print-to-PDF&lt;/th&gt;
                &lt;th&gt;Flatten to raster&lt;/th&gt;
                &lt;th&gt;Encrypt&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;incremental-updates&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;louder — the strip is the second revision&lt;/td&gt;
                &lt;td&gt;gone — one revision out&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;fires — the xref chain is not encrypted&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;metadata-mismatch&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;gone — needs both stores present&lt;/td&gt;
                &lt;td&gt;untested — copied through, or regenerated?&lt;/td&gt;
                &lt;td&gt;gone — one writer fills both&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;gone — suppressed by the engine&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;date-anomalies&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;gone — no timestamps left&lt;/td&gt;
                &lt;td&gt;untested — same question&lt;/td&gt;
                &lt;td&gt;gone — fresh, coherent dates&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;gone — suppressed&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;producer-fingerprint&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;weaker — drops to &quot;declares no producing tool&quot;, severity low&lt;/td&gt;
                &lt;td&gt;untested — qpdf keeps the string, a re-render replaces it&lt;/td&gt;
                &lt;td&gt;weaker — Quartz and Microsoft are on the list, Skia is not&lt;/td&gt;
                &lt;td&gt;louder — a full-page image escalates the finding to high&lt;/td&gt;
                &lt;td&gt;gone — suppressed&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;id-inconsistency&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;untested — does the appended trailer touch /ID[1]?&lt;/td&gt;
                &lt;td&gt;gone — one save, both halves equal&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;fires — /ID lives in the trailer&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;font-anomalies&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;fires — fonts are not metadata&lt;/td&gt;
                &lt;td&gt;untested — a re-subset would merge the duplicates&lt;/td&gt;
                &lt;td&gt;gone — one subset per face&lt;/td&gt;
                &lt;td&gt;gone — no fonts at all&lt;/td&gt;
                &lt;td&gt;gone — suppressed&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;hybrid-page&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;fires&lt;/td&gt;
                &lt;td&gt;fires — the image and the text both copy through&lt;/td&gt;
                &lt;td&gt;untested — depends whether the printer keeps one image per page&lt;/td&gt;
                &lt;td&gt;gone — no text layer left to be suspicious about&lt;/td&gt;
                &lt;td&gt;gone — suppressed&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;active-content&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;fires — a strip touches metadata only&lt;/td&gt;
                &lt;td&gt;untested — structural copy keeps it, a re-render drops it&lt;/td&gt;
                &lt;td&gt;gone — actions are not printed&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;fires — read from dictionary keys, and the finding says so&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;signature-coverage&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;louder — appended bytes land outside the signed range&lt;/td&gt;
                &lt;td&gt;gone — signature destroyed, and nothing reports its absence&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;fires — /ByteRange is plain&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;structure-warnings&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;fires — an append repairs nothing&lt;/td&gt;
                &lt;td&gt;gone — repairing is what a rewriter is for&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;gone&lt;/td&gt;
                &lt;td&gt;louder — carries the disclosure and names what was skipped&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;redaction-exposure&lt;/code&gt;&lt;/td&gt;
                &lt;td&gt;fires — the words under the box are content&lt;/td&gt;
                &lt;td&gt;fires — paint order copies through&lt;/td&gt;
                &lt;td&gt;untested — the marks half is gone, the covered-text half probably survives&lt;/td&gt;
                &lt;td&gt;gone — and here the evasion is also the correct fix&lt;/td&gt;
                &lt;td&gt;weaker — unapplied marks still counted, covered text unreadable&lt;/td&gt;
              &lt;/tr&gt;
              &lt;tr&gt;
                &lt;td&gt;&lt;code&gt;issuer-mismatch&lt;/code&gt; &lt;em&gt;(needs a baseline)&lt;/em&gt;&lt;/td&gt;
                &lt;td&gt;louder — an absent producer mismatches too&lt;/td&gt;
                &lt;td&gt;louder&lt;/td&gt;
                &lt;td&gt;louder — two dimensions move, which is what reaches high&lt;/td&gt;
                &lt;td&gt;louder — producer and font set both move&lt;/td&gt;
                &lt;td&gt;louder — suppressed fields read as absent&lt;/td&gt;
              &lt;/tr&gt;
            &lt;/tbody&gt;
          &lt;/table&gt;
        &lt;/div&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;surprises&quot;&gt;Three cells worth reading twice&lt;/h2&gt;

        &lt;h3&gt;A metadata strip makes two families louder&lt;/h3&gt;
        &lt;p&gt;
          Because it is an append. The revision count goes from one to two, which is
          exactly what &lt;code&gt;incremental-updates&lt;/code&gt; looks for. And on a signed
          document the appended region sits past the end of the signed
          &lt;code&gt;/ByteRange&lt;/code&gt;, which is the highest-severity finding the engine has.
        &lt;/p&gt;
        &lt;p&gt;
          Someone tidying a PDF before sending it to me makes the report worse, not better.
          That is not clever detection on my part. It is the file format.
        &lt;/p&gt;

        &lt;h3&gt;Removing the producer string does not silence the producer family&lt;/h3&gt;
        &lt;p&gt;
          With no producing tool declared anywhere, &lt;code&gt;producer-fingerprint&lt;/code&gt; still
          returns a finding: &lt;em&gt;document declares no producing tool&lt;/em&gt;, at severity
          &lt;code&gt;low&lt;/code&gt;.
        &lt;/p&gt;
        &lt;p&gt;
          Low is the right place for it, and our own corpus is the reason. Six genuine
          Huntington account-analysis statements carry no producer and no creator string at
          all — high-volume composition engines often emit none. An absent toolchain is
          weakly suspicious and weakly normal at the same time, so it sits at the bottom of
          the ladder and stays there.
        &lt;/p&gt;

        &lt;h3&gt;Chromium's print-to-PDF trips nothing, on purpose&lt;/h3&gt;
        &lt;p&gt;
          The consumer-editor list has around fifty entries. &lt;code&gt;Quartz PDFContext&lt;/code&gt;
          is on it. &lt;code&gt;Microsoft Print to PDF&lt;/code&gt; is on it. &lt;code&gt;Skia/PDF&lt;/code&gt;,
          which is what Chrome writes when you print a page, is not.
        &lt;/p&gt;
        &lt;p&gt;
          I know how to close that. I am not going to. When we parsed 213 PDFs published by
          US banks, two Bank of America online-banking agreements came back as
          &lt;code&gt;Skia/PDF m130&lt;/code&gt; and &lt;code&gt;Skia/PDF m135&lt;/code&gt; — printed straight out
          of a browser by whoever maintained the page. Adding Skia to that list turns a real
          bank's real document into a finding, on a page that is genuinely from the bank.
        &lt;/p&gt;
        &lt;p&gt;
          So printing from Chrome is the cheapest evasion in this table. It costs nothing,
          takes one keystroke, and my engine says nothing about the tool that did it.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;survives&quot;&gt;What survives everything&lt;/h2&gt;
        &lt;p&gt;
          Read the matrix down the columns and the answer is uncomfortable:
          &lt;strong&gt;nothing in the unconditional set survives all five.&lt;/strong&gt;
        &lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 690 236&quot; width=&quot;100%&quot; role=&quot;img&quot; aria-labelledby=&quot;cost-title&quot;&gt;
            &lt;title id=&quot;cost-title&quot;&gt;Bar chart: how many of the eleven unconditional signal families each evasion silences, and what the evasion costs the person sending the document.&lt;/title&gt;
            &lt;text x=&quot;176&quot; y=&quot;26&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;families silenced, of 11&lt;/text&gt;
            &lt;text x=&quot;430&quot; y=&quot;26&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;what it costs the sender&lt;/text&gt;
            &lt;text x=&quot;168&quot; y=&quot;57&quot; text-anchor=&quot;end&quot; font-size=&quot;11.5&quot; fill=&quot;var(--text)&quot;&gt;metadata strip&lt;/text&gt;
            &lt;rect x=&quot;176&quot; y=&quot;44&quot; width=&quot;232&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.55&quot; /&gt;
            &lt;rect x=&quot;176&quot; y=&quot;44&quot; width=&quot;42&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.72&quot; /&gt;
            &lt;text x=&quot;225&quot; y=&quot;57&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-muted)&quot;&gt;2&lt;/text&gt;
            &lt;text x=&quot;430&quot; y=&quot;57&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;costs nothing — and appends a revision&lt;/text&gt;
            &lt;text x=&quot;168&quot; y=&quot;91&quot; text-anchor=&quot;end&quot; font-size=&quot;11.5&quot; fill=&quot;var(--text)&quot;&gt;whole-file rewrite&lt;/text&gt;
            &lt;rect x=&quot;176&quot; y=&quot;78&quot; width=&quot;232&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.55&quot; /&gt;
            &lt;rect x=&quot;176&quot; y=&quot;78&quot; width=&quot;84&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.72&quot; /&gt;
            &lt;text x=&quot;267&quot; y=&quot;91&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-muted)&quot;&gt;4&lt;/text&gt;
            &lt;text x=&quot;430&quot; y=&quot;91&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;costs any signature&lt;/text&gt;
            &lt;text x=&quot;168&quot; y=&quot;125&quot; text-anchor=&quot;end&quot; font-size=&quot;11.5&quot; fill=&quot;var(--text)&quot;&gt;encrypt (owner password)&lt;/text&gt;
            &lt;rect x=&quot;176&quot; y=&quot;112&quot; width=&quot;232&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.55&quot; /&gt;
            &lt;rect x=&quot;176&quot; y=&quot;112&quot; width=&quot;105&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.72&quot; /&gt;
            &lt;text x=&quot;288&quot; y=&quot;125&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-muted)&quot;&gt;5&lt;/text&gt;
            &lt;text x=&quot;430&quot; y=&quot;125&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;costs nothing — the report says so out loud&lt;/text&gt;
            &lt;text x=&quot;168&quot; y=&quot;159&quot; text-anchor=&quot;end&quot; font-size=&quot;11.5&quot; fill=&quot;var(--text)&quot;&gt;print-to-PDF&lt;/text&gt;
            &lt;rect x=&quot;176&quot; y=&quot;146&quot; width=&quot;232&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.55&quot; /&gt;
            &lt;rect x=&quot;176&quot; y=&quot;146&quot; width=&quot;169&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.72&quot; /&gt;
            &lt;text x=&quot;352&quot; y=&quot;159&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-muted)&quot;&gt;8&lt;/text&gt;
            &lt;text x=&quot;430&quot; y=&quot;159&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;costs the whole revision history&lt;/text&gt;
            &lt;text x=&quot;168&quot; y=&quot;193&quot; text-anchor=&quot;end&quot; font-size=&quot;11.5&quot; fill=&quot;var(--text)&quot;&gt;flatten to raster&lt;/text&gt;
            &lt;rect x=&quot;176&quot; y=&quot;180&quot; width=&quot;232&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--border)&quot; fill-opacity=&quot;0.55&quot; /&gt;
            &lt;rect x=&quot;176&quot; y=&quot;180&quot; width=&quot;211&quot; height=&quot;18&quot; rx=&quot;3&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.72&quot; /&gt;
            &lt;text x=&quot;394&quot; y=&quot;193&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-muted)&quot;&gt;10&lt;/text&gt;
            &lt;text x=&quot;430&quot; y=&quot;193&quot; font-size=&quot;10.5&quot; fill=&quot;var(--text-faint)&quot;&gt;costs the text layer&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            Counting only cells marked &lt;em&gt;gone&lt;/em&gt;; the untested ones are left out, so
            three of these bars are floors rather than measurements. Cost is what the
            evasion takes away from the person sending the document, which is the column
            that decides whether anyone actually does it.
          &lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;p&gt;
          The one row that survives is &lt;code&gt;issuer-mismatch&lt;/code&gt;, and it is the row that
          needs something the document cannot supply. Every other family reads the file's
          own account of itself, so rewriting the file rewrites the evidence. That one
          compares the file to documents from the institution it claims to come from — and
          all five evasions replace the toolchain a real issuer used.
        &lt;/p&gt;
        &lt;p&gt;
          That is the whole asymmetry, in one sentence: &lt;strong&gt;self-description is
          defeated by regeneration, and comparison to an outside reference is not.&lt;/strong&gt;
          The price is that you have to hold the reference. Public specimens mostly do not
          work for this; documents you already know are genuine do.
        &lt;/p&gt;

        &lt;h3&gt;The raster trade-off, honestly&lt;/h3&gt;
        &lt;p&gt;
          Flattening every page to an image silences ten of the eleven. It is the strongest
          move on the board and I have nothing that beats it.
        &lt;/p&gt;
        &lt;p&gt;
          What it costs is the text layer. No selection, no copy-paste, no extraction, no
          screen reader. If the document is going into any pipeline that reads the text —
          an income parser, an accounting import, a reconciliation step — a raster fails on
          arrival, and it fails before anyone gets to the question of whether it is real.
        &lt;/p&gt;
        &lt;p&gt;
          But that cost lands on the workflow, not on my parser. If a human is going to
          open the file and look at it, a flattened page sails past everything on this
          site.
        &lt;/p&gt;
        &lt;div class=&quot;post-callout&quot;&gt;
          &lt;span class=&quot;label&quot;&gt;The concession&lt;/span&gt;
          &lt;p&gt;
            A full-page raster is not itself reported as a finding, because scanners exist
            and a scanned document looks exactly like that. On a flattened file the engine
            is mostly silent, and silence here means &lt;em&gt;I could not look&lt;/em&gt;, not
            &lt;em&gt;I looked and it was fine&lt;/em&gt;. If your process accepts raster PDFs, assume
            this engine is close to blind on them and put the control somewhere else.
          &lt;/p&gt;
        &lt;/div&gt;
        &lt;p&gt;
          One more thing the columns hide: they compose. The strip tool's own documentation
          tells you to run a rewriter afterwards, precisely because the strip alone leaves
          the original bytes on disk. Run both and you have removed the metadata and the
          history in one go. That pair is the realistic attack, and it is cheaper than
          either of the last two columns.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;untested&quot;&gt;The cells I could not fill in&lt;/h2&gt;
        &lt;p&gt;
          Eight cells say &lt;em&gt;untested&lt;/em&gt;. They are not hedging — they are the cells where
          the answer depends on which tool ran, and I have derived this table from source
          rather than from a bench full of files.
        &lt;/p&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;code&gt;id-inconsistency&lt;/code&gt; against a metadata strip — whether the appended trailer rewrites the second half of &lt;code&gt;/ID&lt;/code&gt;.&lt;/li&gt;
          &lt;li&gt;&lt;code&gt;metadata-mismatch&lt;/code&gt;, &lt;code&gt;date-anomalies&lt;/code&gt;, &lt;code&gt;producer-fingerprint&lt;/code&gt;, &lt;code&gt;font-anomalies&lt;/code&gt; and &lt;code&gt;active-content&lt;/code&gt; against a whole-file rewrite — all five turn on the same unmeasured question, whether the rewriter copies those objects through or regenerates them.&lt;/li&gt;
          &lt;li&gt;&lt;code&gt;hybrid-page&lt;/code&gt; against print-to-PDF — whether the print path emits one image per page or tiles it, which decides whether the page still reads as a full-page image at all.&lt;/li&gt;
          &lt;li&gt;&lt;code&gt;redaction-exposure&lt;/code&gt; against print-to-PDF — the unapplied-marks half is certainly gone; whether the covered text is re-emitted underneath the box is the part I have not run.&lt;/li&gt;
        &lt;/ul&gt;
        &lt;p&gt;
          Each of those is a morning's work with a fixture set, and when I run it this post
          gets superseded by a dated one rather than quietly corrected.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;recall&quot;&gt;The number I will not give you&lt;/h2&gt;
        &lt;p&gt;
          The obvious next question is what fraction of real document fraud this catches.
          I do not know, and neither does anyone else selling this.
        &lt;/p&gt;
        &lt;p&gt;
          Every academic benchmark I could find is pixel-level image forensics; there is no
          PDF-structure equivalent. No vendor publishes a false-positive rate on legitimate
          documents. The one public figure I have seen is an unmethodologised 80% on a
          competitor's site.
        &lt;/p&gt;
        &lt;p&gt;
          So the claim is &lt;strong&gt;high precision, unknown recall&lt;/strong&gt;. When a family
          fires, the report shows the bytes it fired on and you can check them yourself.
          When nothing fires, you have learned that this particular set of tests came back
          quiet — which, after reading the table above, you now know how little that can
          mean.
        &lt;/p&gt;
        &lt;p&gt;
          A finding is a statement about bytes. &quot;This document is fraudulent&quot; is a statement
          about a person, and a parser does not get to make it.
        &lt;/p&gt;

        &lt;div class=&quot;post-cta&quot;&gt;
          &lt;h2&gt;Run it on a file of your own&lt;/h2&gt;
          &lt;p&gt;
            The &lt;a href=&quot;https://tamperlens.com/&quot;&gt;free checker&lt;/a&gt; takes a PDF and gives back the same report this
            table describes, with the raw evidence under every finding. No account, nothing
            stored.
          &lt;/p&gt;
          &lt;p&gt;
            If you want to see the table hold up, run a document twice: once as you received
            it, once after printing it to PDF. The difference is the point of this post.
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/section&gt;</content>
  </entry>
  <entry>
    <title>The black box in your PDF is a shape, not a delete key</title>
    <link rel="alternate" type="text/html" href="https://tamperlens.com/blog/black-box-is-a-shape-not-a-delete-key" />
    <id>https://tamperlens.com/blog/black-box-is-a-shape-not-a-delete-key</id>
    <published>2026-08-13T00:00:00Z</published>
    <updated>2026-08-13T00:00:00Z</updated>
    <summary type="text">Two ways to black out a name look identical on screen and are entirely different files. Here is how to tell which one you have.</summary>
    <content type="html" xml:lang="en">&lt;p class=&quot;post-meta&quot;&gt;
        Douglas Haruo · 13 August 2026
      &lt;/p&gt;
      &lt;p class=&quot;post-reference&quot;&gt;
        Reference: &lt;a href=&quot;https://tamperlens.com/redaction-check&quot;&gt;Check a PDF redaction&lt;/a&gt;
      &lt;/p&gt;

      &lt;p class=&quot;lede&quot;&gt;
        There are two ways to black out a name in a PDF. The first deletes the text and
        then draws a black rectangle where it used to be. The second just draws the black
        rectangle.
      &lt;/p&gt;

      &lt;p&gt;On screen they are indistinguishable.&lt;/p&gt;

      &lt;p&gt;
        In the file they are entirely different documents. In the second one every
        character of the name is still there — selectable, copyable, and extractable by
        any PDF library in about one line of code.
      &lt;/p&gt;

      &lt;p&gt;
        This mistake keeps reaching production in court filings, disclosure releases and
        regulatory submissions, from organisations that employ lawyers and document teams.
        It survives because there is no feedback. The person doing the redacting sees a
        black box either way, and nothing tells them which one they made until somebody
        else selects the text.
      &lt;/p&gt;

      &lt;figure class=&quot;post-figure&quot;&gt;
        &lt;svg viewBox=&quot;0 0 620 356&quot; width=&quot;100%&quot; role=&quot;img&quot;&gt;
          &lt;title&gt;Two PDF files that render identically. In one the text object was deleted before the rectangle was drawn; in the other the text object is still in the content stream underneath the rectangle.&lt;/title&gt;
          &lt;text x=&quot;20&quot; y=&quot;16&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;File A — text deleted, then box drawn&lt;/text&gt;
          &lt;text x=&quot;330&quot; y=&quot;16&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;File B — only the box drawn&lt;/text&gt;

          &lt;rect x=&quot;20&quot; y=&quot;26&quot; width=&quot;270&quot; height=&quot;104&quot; rx=&quot;6&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--border)&quot; /&gt;
          &lt;rect x=&quot;38&quot; y=&quot;44&quot; width=&quot;184&quot; height=&quot;7&quot; rx=&quot;3&quot; fill=&quot;var(--border-strong)&quot; /&gt;
          &lt;text x=&quot;38&quot; y=&quot;72&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;Name:&lt;/text&gt;
          &lt;rect x=&quot;84&quot; y=&quot;59&quot; width=&quot;130&quot; height=&quot;17&quot; rx=&quot;2&quot; fill=&quot;var(--redaction-bar)&quot; stroke=&quot;var(--redaction-bar-edge)&quot; /&gt;
          &lt;rect x=&quot;38&quot; y=&quot;92&quot; width=&quot;214&quot; height=&quot;7&quot; rx=&quot;3&quot; fill=&quot;var(--border-strong)&quot; /&gt;
          &lt;rect x=&quot;38&quot; y=&quot;108&quot; width=&quot;160&quot; height=&quot;7&quot; rx=&quot;3&quot; fill=&quot;var(--border-strong)&quot; /&gt;

          &lt;rect x=&quot;330&quot; y=&quot;26&quot; width=&quot;270&quot; height=&quot;104&quot; rx=&quot;6&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--border)&quot; /&gt;
          &lt;rect x=&quot;348&quot; y=&quot;44&quot; width=&quot;184&quot; height=&quot;7&quot; rx=&quot;3&quot; fill=&quot;var(--border-strong)&quot; /&gt;
          &lt;text x=&quot;348&quot; y=&quot;72&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;Name:&lt;/text&gt;
          &lt;rect x=&quot;394&quot; y=&quot;59&quot; width=&quot;130&quot; height=&quot;17&quot; rx=&quot;2&quot; fill=&quot;var(--redaction-bar)&quot; stroke=&quot;var(--redaction-bar-edge)&quot; /&gt;
          &lt;rect x=&quot;348&quot; y=&quot;92&quot; width=&quot;214&quot; height=&quot;7&quot; rx=&quot;3&quot; fill=&quot;var(--border-strong)&quot; /&gt;
          &lt;rect x=&quot;348&quot; y=&quot;108&quot; width=&quot;160&quot; height=&quot;7&quot; rx=&quot;3&quot; fill=&quot;var(--border-strong)&quot; /&gt;

          &lt;text x=&quot;310&quot; y=&quot;150&quot; font-size=&quot;12&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--text-muted)&quot;&gt;identical on screen · different content streams&lt;/text&gt;

          &lt;rect x=&quot;20&quot; y=&quot;164&quot; width=&quot;270&quot; height=&quot;150&quot; rx=&quot;6&quot; fill=&quot;var(--bg-code)&quot; stroke=&quot;var(--border)&quot; /&gt;
          &lt;text x=&quot;36&quot; y=&quot;190&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;0 0 0 rg&lt;/text&gt;
          &lt;text x=&quot;36&quot; y=&quot;208&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;74 656 130 17 re f&lt;/text&gt;
          &lt;text x=&quot;36&quot; y=&quot;244&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;No text object anywhere on&lt;/text&gt;
          &lt;text x=&quot;36&quot; y=&quot;260&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;the page. Nothing to recover.&lt;/text&gt;

          &lt;rect x=&quot;330&quot; y=&quot;164&quot; width=&quot;270&quot; height=&quot;150&quot; rx=&quot;6&quot; fill=&quot;var(--bg-code)&quot; stroke=&quot;var(--border)&quot; /&gt;
          &lt;text x=&quot;346&quot; y=&quot;190&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;BT /F1 12 Tf 76 660 Td&lt;/text&gt;
          &lt;text x=&quot;346&quot; y=&quot;208&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;(Dana Whitfield) Tj ET&lt;/text&gt;
          &lt;text x=&quot;346&quot; y=&quot;226&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;0 0 0 rg&lt;/text&gt;
          &lt;text x=&quot;346&quot; y=&quot;244&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;74 656 130 17 re f&lt;/text&gt;
          &lt;text x=&quot;346&quot; y=&quot;280&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;The name is still in the file.&lt;/text&gt;

          &lt;text x=&quot;20&quot; y=&quot;338&quot; font-size=&quot;14&quot; font-weight=&quot;600&quot; fill=&quot;var(--low)&quot;&gt;Redacted.&lt;/text&gt;
          &lt;text x=&quot;330&quot; y=&quot;338&quot; font-size=&quot;14&quot; font-weight=&quot;600&quot; fill=&quot;var(--high)&quot;&gt;Not redacted.&lt;/text&gt;
        &lt;/svg&gt;
        &lt;figcaption&gt;
          A constructed pair. Same page box, same coordinates, same rendered result — and
          in File B the string is sitting in the content stream underneath the rectangle.
          The name is invented.
        &lt;/figcaption&gt;
      &lt;/figure&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;page-is-a-program&quot;&gt;A page is a program&lt;/h2&gt;

        &lt;p&gt;
          The reason the two operations look the same is worth understanding, because it
          is also the reason you can tell them apart.
        &lt;/p&gt;

        &lt;p&gt;
          A page's content stream is a sequence of operators executed in order onto a
          blank canvas. A very small one looks like this.
        &lt;/p&gt;

        &lt;p class=&quot;code-label&quot;&gt;Text, then a rectangle over it&lt;/p&gt;
        &lt;pre class=&quot;code&quot;&gt;&lt;code&gt;BT /F1 12 Tf 76 660 Td (Dana Whitfield) Tj ET
0 0 0 rg
74 656 130 17 re f&lt;/code&gt;&lt;/pre&gt;

        &lt;p&gt;
          Reading it out: begin text, select font &lt;code&gt;F1&lt;/code&gt; at 12pt, move to
          (76, 660), show the string, end text. Then set the non-stroking colour to black
          (&lt;code&gt;rg&lt;/code&gt;), build a rectangle at (74, 656) 130 wide and 17 high
          (&lt;code&gt;re&lt;/code&gt;), and fill it (&lt;code&gt;f&lt;/code&gt;).
        &lt;/p&gt;

        &lt;p&gt;
          There is no z-index here, and no concept of one object being above another.
          There is only order. Later paints over earlier. The rectangle covers the name
          for the same reason a second coat of paint covers the first.
        &lt;/p&gt;

        &lt;p&gt;Now swap the two halves.&lt;/p&gt;

        &lt;p class=&quot;code-label&quot;&gt;The same two objects, opposite order&lt;/p&gt;
        &lt;pre class=&quot;code&quot;&gt;&lt;code&gt;0 0 0 rg
74 656 130 17 re f
BT /F1 12 Tf 76 660 Td (Dana Whitfield) Tj ET&lt;/code&gt;&lt;/pre&gt;

        &lt;p&gt;
          Same objects, same coordinates — and now the name is drawn on top of the black
          box and is perfectly legible. Which is exactly what a table's shaded header row
          is: a filled rectangle, painted first, with text on it.
        &lt;/p&gt;

        &lt;p&gt;That single fact is the whole of what follows.&lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 620 300&quot; width=&quot;100%&quot; role=&quot;img&quot;&gt;
            &lt;title&gt;Paint order as a stack. A shape painted before text becomes a background; the same shape painted after the text covers it.&lt;/title&gt;
            &lt;text x=&quot;20&quot; y=&quot;16&quot; font-size=&quot;14&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;Shape first, then text&lt;/text&gt;
            &lt;rect x=&quot;20&quot; y=&quot;30&quot; width=&quot;210&quot; height=&quot;32&quot; rx=&quot;5&quot; fill=&quot;var(--bg-inset)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;34&quot; y=&quot;51&quot; font-size=&quot;13&quot; fill=&quot;var(--text)&quot;&gt;1 · fill a grey band&lt;/text&gt;
            &lt;path d=&quot;M125 66 v10&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M120 76 h10 l-5 8 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;20&quot; y=&quot;86&quot; width=&quot;210&quot; height=&quot;32&quot; rx=&quot;5&quot; fill=&quot;var(--bg-inset)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;34&quot; y=&quot;107&quot; font-size=&quot;13&quot; fill=&quot;var(--text)&quot;&gt;2 · show the words&lt;/text&gt;
            &lt;path d=&quot;M125 122 v10&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M120 132 h10 l-5 8 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;20&quot; y=&quot;158&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;renders as&lt;/text&gt;
            &lt;rect x=&quot;20&quot; y=&quot;166&quot; width=&quot;250&quot; height=&quot;34&quot; rx=&quot;5&quot; fill=&quot;var(--bg-inset)&quot; stroke=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;34&quot; y=&quot;188&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;13&quot; fill=&quot;var(--text)&quot;&gt;ACCOUNT SUMMARY&lt;/text&gt;
            &lt;text x=&quot;20&quot; y=&quot;226&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;Ordinary design. The band had to come&lt;/text&gt;
            &lt;text x=&quot;20&quot; y=&quot;242&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;first, or the words would not be readable.&lt;/text&gt;

            &lt;text x=&quot;330&quot; y=&quot;16&quot; font-size=&quot;14&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;Text first, then shape&lt;/text&gt;
            &lt;rect x=&quot;330&quot; y=&quot;30&quot; width=&quot;210&quot; height=&quot;32&quot; rx=&quot;5&quot; fill=&quot;var(--bg-inset)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;344&quot; y=&quot;51&quot; font-size=&quot;13&quot; fill=&quot;var(--text)&quot;&gt;1 · show the words&lt;/text&gt;
            &lt;path d=&quot;M435 66 v10&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M430 76 h10 l-5 8 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;
            &lt;rect x=&quot;330&quot; y=&quot;86&quot; width=&quot;210&quot; height=&quot;32&quot; rx=&quot;5&quot; fill=&quot;var(--bg-inset)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;344&quot; y=&quot;107&quot; font-size=&quot;13&quot; fill=&quot;var(--text)&quot;&gt;2 · fill a black box&lt;/text&gt;
            &lt;path d=&quot;M435 122 v10&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M430 132 h10 l-5 8 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;330&quot; y=&quot;158&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;renders as&lt;/text&gt;
            &lt;rect x=&quot;330&quot; y=&quot;166&quot; width=&quot;250&quot; height=&quot;34&quot; rx=&quot;5&quot; fill=&quot;var(--redaction-bar)&quot; stroke=&quot;var(--redaction-bar-edge)&quot; /&gt;
            &lt;text x=&quot;330&quot; y=&quot;226&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;A cover. The words are under the paint&lt;/text&gt;
            &lt;text x=&quot;330&quot; y=&quot;242&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;and still in the content stream.&lt;/text&gt;

            &lt;text x=&quot;310&quot; y=&quot;284&quot; font-size=&quot;13&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--text-muted)&quot;&gt;There is no z-index in a PDF page. Later paints over earlier.&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            The same two operators in the two possible orders. Everything a checker can
            say about a black box follows from which one it found.
          &lt;/figcaption&gt;
        &lt;/figure&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;check-it-yourself&quot;&gt;Check it yourself in one line&lt;/h2&gt;

        &lt;p&gt;If you have a PDF with a black box in it, start here.&lt;/p&gt;

        &lt;p class=&quot;code-label&quot;&gt;Does the text come back out?&lt;/p&gt;
        &lt;pre class=&quot;code&quot;&gt;&lt;code&gt;pdftotext -layout suspect.pdf - | less&lt;/code&gt;&lt;/pre&gt;

        &lt;p&gt;
          If the redacted words appear in that output, they were never removed. That is
          the entire test, and it is worth running on anything you are about to send.
        &lt;/p&gt;

        &lt;p&gt;
          To look at the operators rather than the text, decompress the streams first.
          Content streams are Flate-compressed, so &lt;code&gt;strings&lt;/code&gt; and
          &lt;code&gt;grep&lt;/code&gt; see nothing useful without this step.
        &lt;/p&gt;

        &lt;p class=&quot;code-label&quot;&gt;Reading the page as the program it is&lt;/p&gt;
        &lt;pre class=&quot;code&quot;&gt;&lt;code&gt;qpdf --qdf --object-streams=disable suspect.pdf decompressed.pdf
grep -n --text -E ' (re|f|Tj|TJ)$' decompressed.pdf | head -40&lt;/code&gt;&lt;/pre&gt;

        &lt;p&gt;
          Now you can see for yourself whether the &lt;code&gt;Tj&lt;/code&gt; comes before the
          &lt;code&gt;re f&lt;/code&gt;.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;naive-version&quot;&gt;The naive version fires on everything&lt;/h2&gt;

        &lt;p&gt;
          I built the obvious detector first: find dark filled rectangles, find text
          underneath them, report. It fired on an enormous number of completely honest
          documents. Every table with a shaded header. Every highlighted paragraph. Every
          coloured callout box.
        &lt;/p&gt;

        &lt;p&gt;
          Paint order fixes that, and it fixes it by construction rather than by tuning. A
          background painted after its text would have hidden that text, so it would not
          be a background. If you can read the words, the shape came first. If the shape
          came second, it is hiding something.
        &lt;/p&gt;

        &lt;p&gt;
          Four more guards sit on top of paint order in the shipped signal. Each one is
          there because the version without it produced a false positive on a real
          document.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;Opacity.&lt;/strong&gt; A highlighter is a filled rectangle painted over text,
          and it hides nothing. The graphics state carries a non-stroking alpha
          (&lt;code&gt;ca&lt;/code&gt;, set through a named &lt;code&gt;/ExtGState&lt;/code&gt;), and a paint has
          to be at least 90% opaque before it counts as a cover at all.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;Size.&lt;/strong&gt; A covering paint larger than 40% of the page area is a
          watermark, a stamp or a page background. Without that bound, one document with a
          full-page &lt;code&gt;DRAFT&lt;/code&gt; stamp rendered as an opaque image reported every
          single line on the page as exposed.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;Invisible text.&lt;/strong&gt; Text rendering modes 3 and 7 paint no glyphs.
          That is the OCR layer underneath a scanned page — text designed to be invisible
          on screen and extractable by machine. Those runs are never collected as
          candidates, because reporting them would fire on every scanned document ever
          produced.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;Image versus drawn shape.&lt;/strong&gt; A filled rectangle over a line of
          text is conclusive. An image over a line of text is not: a signature stamp, a
          logo or a pasted-in scan legitimately lands on top of a word. So a page whose
          covers are all images still gets reported — one severity band lower, with a note
          saying why.
        &lt;/p&gt;

        &lt;p&gt;
          There is a size floor as well, because a 2-point fill cannot be hiding a word: a
          cover has to be at least 6 points wide and 3 tall. And the run has to be
          genuinely underneath the paint, not merely adjacent — at least 70% of the run's
          box has to fall inside the cover.
        &lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 620 490&quot; width=&quot;100%&quot; role=&quot;img&quot;&gt;
            &lt;title&gt;The four guards as a decision flow, each labelled with the false positive it prevents.&lt;/title&gt;
            &lt;rect x=&quot;20&quot; y=&quot;18&quot; width=&quot;330&quot; height=&quot;44&quot; rx=&quot;6&quot; fill=&quot;var(--accent-wash)&quot; stroke=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;36&quot; y=&quot;45&quot; font-size=&quot;13&quot; fill=&quot;var(--text)&quot;&gt;a paint overlapping text drawn before it&lt;/text&gt;
            &lt;path d=&quot;M100 62 v14&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M95 76 h10 l-5 8 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;

            &lt;rect x=&quot;20&quot; y=&quot;84&quot; width=&quot;330&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--bg-inset)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;36&quot; y=&quot;108&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;1 · is the paint opaque?&lt;/text&gt;
            &lt;text x=&quot;36&quot; y=&quot;128&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;non-stroking alpha of 0.9 or more&lt;/text&gt;
            &lt;path d=&quot;M350 112 h30&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M380 107 v10 l8 -5 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;365&quot; y=&quot;104&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--text-muted)&quot;&gt;no&lt;/text&gt;
            &lt;rect x=&quot;392&quot; y=&quot;84&quot; width=&quot;208&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;406&quot; y=&quot;110&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;a highlighter. It hides&lt;/text&gt;
            &lt;text x=&quot;406&quot; y=&quot;126&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;nothing.&lt;/text&gt;
            &lt;path d=&quot;M100 140 v14&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M95 154 h10 l-5 8 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;

            &lt;rect x=&quot;20&quot; y=&quot;170&quot; width=&quot;330&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--bg-inset)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;36&quot; y=&quot;194&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;2 · is it small enough to be a box?&lt;/text&gt;
            &lt;text x=&quot;36&quot; y=&quot;214&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;at most 40% of the page area&lt;/text&gt;
            &lt;path d=&quot;M350 198 h30&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M380 193 v10 l8 -5 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;365&quot; y=&quot;190&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--text-muted)&quot;&gt;no&lt;/text&gt;
            &lt;rect x=&quot;392&quot; y=&quot;170&quot; width=&quot;208&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;406&quot; y=&quot;196&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;a watermark, a stamp or a&lt;/text&gt;
            &lt;text x=&quot;406&quot; y=&quot;212&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;page background.&lt;/text&gt;
            &lt;path d=&quot;M100 226 v14&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M95 240 h10 l-5 8 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;

            &lt;rect x=&quot;20&quot; y=&quot;256&quot; width=&quot;330&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--bg-inset)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;36&quot; y=&quot;280&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;3 · was the text actually drawn?&lt;/text&gt;
            &lt;text x=&quot;36&quot; y=&quot;300&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;render modes 3 and 7 paint no glyphs&lt;/text&gt;
            &lt;path d=&quot;M350 284 h30&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M380 279 v10 l8 -5 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;365&quot; y=&quot;276&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--text-muted)&quot;&gt;no&lt;/text&gt;
            &lt;rect x=&quot;392&quot; y=&quot;256&quot; width=&quot;208&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;406&quot; y=&quot;282&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;the OCR layer under a&lt;/text&gt;
            &lt;text x=&quot;406&quot; y=&quot;298&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;scanned page.&lt;/text&gt;
            &lt;path d=&quot;M100 312 v14&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M95 326 h10 l-5 8 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;

            &lt;rect x=&quot;20&quot; y=&quot;342&quot; width=&quot;330&quot; height=&quot;56&quot; rx=&quot;6&quot; fill=&quot;var(--bg-inset)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;36&quot; y=&quot;366&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;4 · a drawn shape, or an image?&lt;/text&gt;
            &lt;text x=&quot;36&quot; y=&quot;386&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;a filled shape is the conclusive case&lt;/text&gt;
            &lt;path d=&quot;M350 370 h30&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M380 365 v10 l8 -5 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;
            &lt;text x=&quot;362&quot; y=&quot;362&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--text-muted)&quot;&gt;image&lt;/text&gt;
            &lt;rect x=&quot;392&quot; y=&quot;336&quot; width=&quot;208&quot; height=&quot;68&quot; rx=&quot;6&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;406&quot; y=&quot;360&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;could be a signature stamp&lt;/text&gt;
            &lt;text x=&quot;406&quot; y=&quot;376&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;or a logo — still reported,&lt;/text&gt;
            &lt;text x=&quot;406&quot; y=&quot;392&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;one severity band lower.&lt;/text&gt;
            &lt;path d=&quot;M100 398 v14&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M95 412 h10 l-5 8 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;

            &lt;rect x=&quot;20&quot; y=&quot;428&quot; width=&quot;330&quot; height=&quot;44&quot; rx=&quot;6&quot; fill=&quot;var(--high-bg)&quot; stroke=&quot;var(--high-border)&quot; /&gt;
            &lt;text x=&quot;36&quot; y=&quot;455&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--high-text)&quot;&gt;reported: text readable under a cover&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            Each branch on the right is a document class the naive detector reported and
            should not have. The guards are subtraction, not scoring.
          &lt;/figcaption&gt;
        &lt;/figure&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;getting-the-words-out&quot;&gt;Getting the words back out is a separate problem&lt;/h2&gt;

        &lt;p&gt;
          Finding that a run of text sits under a box tells you there is a leak. Saying
          what leaked needs one more step, because the bytes inside a &lt;code&gt;Tj&lt;/code&gt;
          string are character codes, not characters, and what they mean depends on the
          font.
        &lt;/p&gt;

        &lt;p&gt;A font can carry a &lt;code&gt;/ToUnicode&lt;/code&gt; CMap that maps them.&lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 620 200&quot; width=&quot;100%&quot; role=&quot;img&quot;&gt;
            &lt;title&gt;Character codes from a Tj string resolved through a ToUnicode CMap into readable text.&lt;/title&gt;
            &lt;text x=&quot;20&quot; y=&quot;36&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;bytes inside the Tj string&lt;/text&gt;
            &lt;rect x=&quot;20&quot; y=&quot;48&quot; width=&quot;170&quot; height=&quot;64&quot; rx=&quot;6&quot; fill=&quot;var(--bg-code)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;34&quot; y=&quot;87&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;14&quot; fill=&quot;var(--text-code)&quot;&gt;\001\002\003\004\005&lt;/text&gt;
            &lt;path d=&quot;M198 80 h22&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M220 75 v10 l8 -5 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;

            &lt;text x=&quot;238&quot; y=&quot;20&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;/ToUnicode CMap&lt;/text&gt;
            &lt;rect x=&quot;238&quot; y=&quot;30&quot; width=&quot;164&quot; height=&quot;118&quot; rx=&quot;6&quot; fill=&quot;var(--bg-code)&quot; stroke=&quot;var(--border)&quot; /&gt;
            &lt;text x=&quot;252&quot; y=&quot;54&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;&amp;lt;01&amp;gt; &amp;lt;0041&amp;gt;&lt;/text&gt;
            &lt;text x=&quot;252&quot; y=&quot;72&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;&amp;lt;02&amp;gt; &amp;lt;0044&amp;gt;&lt;/text&gt;
            &lt;text x=&quot;252&quot; y=&quot;90&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;&amp;lt;03&amp;gt; &amp;lt;004D&amp;gt;&lt;/text&gt;
            &lt;text x=&quot;252&quot; y=&quot;108&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;&amp;lt;04&amp;gt; &amp;lt;0049&amp;gt;&lt;/text&gt;
            &lt;text x=&quot;252&quot; y=&quot;126&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;12&quot; fill=&quot;var(--text-code)&quot;&gt;&amp;lt;05&amp;gt; &amp;lt;004E&amp;gt;&lt;/text&gt;
            &lt;path d=&quot;M410 80 h22&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; fill=&quot;none&quot; /&gt;
            &lt;path d=&quot;M432 75 v10 l8 -5 z&quot; fill=&quot;var(--border-strong)&quot; /&gt;

            &lt;text x=&quot;450&quot; y=&quot;36&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;recovered text&lt;/text&gt;
            &lt;rect x=&quot;450&quot; y=&quot;48&quot; width=&quot;150&quot; height=&quot;64&quot; rx=&quot;6&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;472&quot; y=&quot;92&quot; font-family=&quot;var(--mono)&quot; font-size=&quot;22&quot; fill=&quot;var(--text)&quot;&gt;ADMIN&lt;/text&gt;

            &lt;text x=&quot;20&quot; y=&quot;180&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;Without a map, the coverage is still reported and the words are not guessed.&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            The content-stream bytes are not text in any useful sense until the font's map
            resolves them. Synthetic example.
          &lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;p&gt;
          Fonts without a &lt;code&gt;/ToUnicode&lt;/code&gt; map are common. There the honest answer
          is that the coverage is real and the words could not be decoded, rather than a
          guess. Simple fonts usually turn out to be WinAnsi-encoded, where the byte is
          the character, which covers most of the practical cases.
        &lt;/p&gt;

        &lt;p&gt;
          The recovered words go back in the response and nowhere else. Files are parsed
          in memory and discarded with the reply — the evidence lives exactly as long as
          the HTTP response does.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;not-redaction&quot;&gt;Things that are not redaction&lt;/h2&gt;

        &lt;p&gt;All of these leave every character in the file.&lt;/p&gt;

        &lt;ul&gt;
          &lt;li&gt;Drawing a filled rectangle over the text.&lt;/li&gt;
          &lt;li&gt;Using a black highlighter annotation.&lt;/li&gt;
          &lt;li&gt;Setting the text colour to match the background.&lt;/li&gt;
          &lt;li&gt;Placing an image over the area.&lt;/li&gt;
          &lt;li&gt;
            Adding a &lt;code&gt;/Redact&lt;/code&gt; annotation and never applying it.
          &lt;/li&gt;
        &lt;/ul&gt;

        &lt;p&gt;
          The last one is the cruellest, because it is the right tool used incompletely.
          The annotation is a mark requesting redaction. The content only leaves when the
          tool applies the marks, and most viewers draw the pending marks as solid black
          boxes. The half-finished state looks exactly like the finished one, so it is
          checked first and reported on its own.
        &lt;/p&gt;

        &lt;p&gt;
          What does work: your tool's redaction feature, followed by actually applying it.
          Or exporting the page to a flattened image, which removes the text layer along
          with your searchable text.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;cannot-see&quot;&gt;What this kind of check cannot see&lt;/h2&gt;

        &lt;p&gt;
          Worth being explicit, because a checker that implies more coverage than it has
          is worse than none.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;Form XObjects.&lt;/strong&gt; Reusable content blocks are their own little
          content streams. They are not recursed into, so a covering box inside one is
          invisible here. Any count is a floor rather than a total, and the report says so
          on pages that use them.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;Already-flattened pages.&lt;/strong&gt; No text layer, nothing to find — which
          also means the redaction worked.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;Everything outside the page content.&lt;/strong&gt; A name taken off the
          visible page can still be sitting in the document metadata, an attachment, a
          form field, or an earlier revision of the file. Different problem, different
          checks.
        &lt;/p&gt;

        &lt;p&gt;
          There is one more approximation worth naming. No font width array is loaded, so
          glyph advance is assumed to be half an em. A run whose width is misjudged by a
          fifth still passes or fails the 70% test the same way, because redaction boxes
          are drawn with generous margins around what they hide.
        &lt;/p&gt;

        &lt;div class=&quot;post-callout&quot;&gt;
          &lt;p&gt;
            Nothing on this page describes a real document, organisation or person. The
            examples are constructed and the names are invented. This post is about a
            class of mistake, not about anybody's filing.
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;try-it&quot;&gt;Try it on a file whose history you know&lt;/h2&gt;

        &lt;p&gt;
          I built the redaction check because I got tired of running the
          &lt;code&gt;qpdf&lt;/code&gt;/&lt;code&gt;pdftotext&lt;/code&gt; dance on documents people sent me and
          wondering whether I had missed one. It walks each page's content stream tracking
          the transformation matrix, fill colour, alpha and paint order, computes boxes
          for the text runs, and reports the runs that ended up under something opaque —
          with the recovered text, because if a checker can read it so can anyone who
          received the document.
        &lt;/p&gt;

        &lt;p&gt;
          The most useful four minutes you can spend with it: take a document of your own,
          draw a black rectangle over a line in whatever editor you have, export it, and
          check that. Watching it recover words from a file whose history you personally
          know is worth more than this post.
        &lt;/p&gt;

        &lt;p class=&quot;post-cta&quot;&gt;
          &lt;a href=&quot;https://tamperlens.com/redaction-check&quot;&gt;Check a PDF redaction&lt;/a&gt; — free, nothing stored, and
          a fictional sample opens automatically so you can see the output before deciding
          whether to trust it with anything of your own.
        &lt;/p&gt;
      &lt;/section&gt;</content>
  </entry>
  <entry>
    <title>What 190 published US bank PDFs actually look like inside</title>
    <link rel="alternate" type="text/html" href="https://tamperlens.com/blog/what-190-bank-pdfs-look-like-inside" />
    <id>https://tamperlens.com/blog/what-190-bank-pdfs-look-like-inside</id>
    <published>2026-08-06T00:00:00Z</published>
    <updated>2026-08-06T00:00:00Z</updated>
    <summary type="text">I downloaded 213 PDFs from 24 US banks to see whether an issuer leaves a structural fingerprint. Across 17 eligible profiles, zero agreed on the producer.</summary>
    <content type="html" xml:lang="en">&lt;p class=&quot;post-meta&quot;&gt;
        Douglas Haruo · 6 August 2026 · corpus collected 26 July 2026
      &lt;/p&gt;
      &lt;p class=&quot;post-reference&quot;&gt;
        Reference: &lt;a href=&quot;https://tamperlens.com/pdf-fraud-signals&quot;&gt;PDF fraud signals — the field guide&lt;/a&gt;
      &lt;/p&gt;

      &lt;p class=&quot;lede&quot;&gt;
        Every serious document-fraud product has a model of what a real document from a
        given institution looks like. Resistant AI calls it &lt;code&gt;MODEL_MATCHED&lt;/code&gt;.
        Inscribe has &lt;code&gt;TEMPLATES&lt;/code&gt; and &lt;code&gt;FINGERPRINT&lt;/code&gt;. Snappt claims
        2,000-plus institutions.
      &lt;/p&gt;

      &lt;p&gt;
        The idea is simple enough to be irritating. If you know a Chase statement is always
        produced by a particular composition engine, at a particular PDF version, with a
        particular font set, then a file claiming to be a Chase statement and produced by
        Microsoft Word is worth a second look.
      &lt;/p&gt;

      &lt;p&gt;
        I wanted that, and I did not want to build it on a guess. So I downloaded 213 PDFs
        from 24 US financial institutions' own domains and parsed every one.
      &lt;/p&gt;

      &lt;p&gt;The result is split, and the half that failed is the more useful half.&lt;/p&gt;

      &lt;div class=&quot;post-callout&quot;&gt;
        &lt;p&gt;
          &lt;strong&gt;For documents banks publish on their websites — fee schedules, account
          agreements, privacy notices, brochures — there is no usable issuer
          fingerprint.&lt;/strong&gt; Across the 17 profiles with at least five samples,
          &lt;strong&gt;zero&lt;/strong&gt; reached 80% agreement on the producer string. One reached it
          on the font set. Bank of America's nine documents carry seven distinct producer
          strings. Marcus's twelve carry ten.
        &lt;/p&gt;
        &lt;p&gt;
          &lt;strong&gt;For the one genuine statement pipeline in the corpus, the fingerprint is
          sharp.&lt;/strong&gt; Six Huntington account-analysis statements agree 6/6 on PDF
          version, cross-reference style, page size, revision count and linearisation, 5/6
          on the exact font set, and 6/6 carry no producer or creator string at all.
        &lt;/p&gt;
      &lt;/div&gt;

      &lt;p&gt;
        So the premise holds — but only for statement output, which is exactly the class of
        document you cannot get from a bank's website. I obtained a real statement pipeline
        for &lt;strong&gt;one&lt;/strong&gt; of the 27 institutions I tried.
      &lt;/p&gt;

      &lt;p&gt;
        None of what follows is a claim about US banking. It is a description of 190
        specific files. I am publishing it mostly because the negative result is the
        actionable one: it tells you which document classes a fraud baseline must
        &lt;em&gt;not&lt;/em&gt; be built from.
      &lt;/p&gt;

      &lt;nav class=&quot;toc&quot; aria-label=&quot;On this page&quot;&gt;
        &lt;h2&gt;On this page&lt;/h2&gt;
        &lt;ol&gt;
          &lt;li&gt;&lt;a href=&quot;#caveat&quot;&gt;The caveat that governs everything else&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#corpus&quot;&gt;What the corpus is&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#producers&quot;&gt;Finding 1 — producer strings are not an issuer tell&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#fonts&quot;&gt;Finding 2 — font sets are near-unique per document&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#huntington&quot;&gt;Finding 3 — the one real statement pipeline&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#ccm&quot;&gt;Finding 4 — composition platforms are visible&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#uniform&quot;&gt;Finding 5 — what is uniform is uniform everywhere&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#support&quot;&gt;What this data can and cannot support&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#shipped&quot;&gt;What we shipped, given all that&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#limits&quot;&gt;The limits you should hold this against&lt;/a&gt;&lt;/li&gt;
        &lt;/ol&gt;
      &lt;/nav&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;caveat&quot;&gt;The caveat that governs everything else&lt;/h2&gt;

        &lt;p&gt;&lt;strong&gt;A marketing or disclosure PDF is not evidence about a statement pipeline.&lt;/strong&gt;&lt;/p&gt;

        &lt;p&gt;
          Banks compose customer statements on dedicated output-management systems that run
          unattended at volume. They compose fee schedules and brochures in desktop
          publishing tools, over fifteen years, using whatever the marketing team had.
        &lt;/p&gt;

        &lt;p&gt;These toolchains have nothing in common but a logo.&lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 640 292&quot; width=&quot;100%&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-pipelines-title&quot;&gt;
            &lt;title id=&quot;fig-pipelines-title&quot;&gt;Two unrelated toolchains: customer statements come off an unattended output-management system and carry no producer or creator string, identical structure across all six files; website documents come from InDesign, Word, Chrome print, Quartz, Nitro and a Ricoh copier, yielding seven producer strings in nine Bank of America files and 167 font sets across 190 files. Building a baseline from the second and testing the first against it flags the genuine document.&lt;/title&gt;

            &lt;rect x=&quot;8&quot; y=&quot;34&quot; width=&quot;275&quot; height=&quot;170&quot; rx=&quot;8&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--border)&quot; stroke-width=&quot;1&quot; /&gt;
            &lt;text x=&quot;24&quot; y=&quot;60&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;Customer statements&lt;/text&gt;
            &lt;text x=&quot;24&quot; y=&quot;82&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;output-management system,&lt;/text&gt;
            &lt;text x=&quot;24&quot; y=&quot;99&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;unattended, at volume&lt;/text&gt;
            &lt;line x1=&quot;24&quot; y1=&quot;112&quot; x2=&quot;267&quot; y2=&quot;112&quot; stroke=&quot;var(--border)&quot; stroke-width=&quot;1&quot; /&gt;
            &lt;text x=&quot;24&quot; y=&quot;134&quot; font-size=&quot;12&quot; fill=&quot;var(--accent)&quot;&gt;no producer, no creator (6/6)&lt;/text&gt;
            &lt;text x=&quot;24&quot; y=&quot;153&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;EX_CFF_ font-name prefixes&lt;/text&gt;
            &lt;text x=&quot;24&quot; y=&quot;172&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;PDF 1.6, xref stream, 612×792&lt;/text&gt;
            &lt;text x=&quot;24&quot; y=&quot;191&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;identical across all six files&lt;/text&gt;

            &lt;rect x=&quot;357&quot; y=&quot;34&quot; width=&quot;275&quot; height=&quot;170&quot; rx=&quot;8&quot; fill=&quot;var(--bg-raised)&quot; stroke=&quot;var(--border)&quot; stroke-width=&quot;1&quot; /&gt;
            &lt;text x=&quot;373&quot; y=&quot;60&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;Website documents&lt;/text&gt;
            &lt;text x=&quot;373&quot; y=&quot;82&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;InDesign, Word, Chrome print,&lt;/text&gt;
            &lt;text x=&quot;373&quot; y=&quot;99&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;Quartz, Nitro, a Ricoh copier&lt;/text&gt;
            &lt;line x1=&quot;373&quot; y1=&quot;112&quot; x2=&quot;616&quot; y2=&quot;112&quot; stroke=&quot;var(--border)&quot; stroke-width=&quot;1&quot; /&gt;
            &lt;text x=&quot;373&quot; y=&quot;134&quot; font-size=&quot;12&quot; fill=&quot;var(--accent)&quot;&gt;7 producer strings in 9 files&lt;/text&gt;
            &lt;text x=&quot;373&quot; y=&quot;153&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;167 font sets in 190 files&lt;/text&gt;
            &lt;text x=&quot;373&quot; y=&quot;172&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;0 of 17 profiles agree on&lt;/text&gt;
            &lt;text x=&quot;373&quot; y=&quot;191&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot;&gt;the producer string&lt;/text&gt;

            &lt;text x=&quot;320&quot; y=&quot;108&quot; font-size=&quot;10&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;middle&quot;&gt;shared:&lt;/text&gt;
            &lt;line x1=&quot;291&quot; y1=&quot;119&quot; x2=&quot;349&quot; y2=&quot;119&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; stroke-dasharray=&quot;4 4&quot; /&gt;
            &lt;text x=&quot;320&quot; y=&quot;136&quot; font-size=&quot;10&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;middle&quot;&gt;a logo&lt;/text&gt;

            &lt;line x1=&quot;145&quot; y1=&quot;204&quot; x2=&quot;145&quot; y2=&quot;226&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;
            &lt;line x1=&quot;494&quot; y1=&quot;204&quot; x2=&quot;494&quot; y2=&quot;226&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1&quot; /&gt;

            &lt;rect x=&quot;8&quot; y=&quot;226&quot; width=&quot;624&quot; height=&quot;54&quot; rx=&quot;8&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.08&quot; /&gt;
            &lt;text x=&quot;26&quot; y=&quot;252&quot; font-size=&quot;12&quot; fill=&quot;var(--text)&quot;&gt;Build a baseline from the right-hand column, test a left-hand document&lt;/text&gt;
            &lt;text x=&quot;26&quot; y=&quot;270&quot; font-size=&quot;12&quot; fill=&quot;var(--text)&quot;&gt;against it, and the file you flag is the genuine one.&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            The two toolchains a bank runs. A baseline drawn from the right-hand column
            cannot describe the left-hand one, and this corpus is mostly made of the
            right-hand column.
          &lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;p&gt;
          Which means: build a &quot;Chase baseline&quot; out of Chase fee schedules, test a genuine
          Chase statement against it, and you flag the genuine document. That is not
          hypothetical — it is what this corpus is mostly made of.
        &lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 640 236&quot; width=&quot;100%&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-kinds-title&quot;&gt;
            &lt;title id=&quot;fig-kinds-title&quot;&gt;Document kinds across 190 US samples: disclosure 108, other forms and worksheets and policy 42, statement-guide 19, marketing 14, statement-specimen 6, investor 1.&lt;/title&gt;
            &lt;text x=&quot;0&quot; y=&quot;14&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;What kind of document — 190 US samples&lt;/text&gt;
            &lt;line x1=&quot;220&quot; y1=&quot;26&quot; x2=&quot;220&quot; y2=&quot;205&quot; stroke=&quot;var(--border)&quot; stroke-width=&quot;1&quot; /&gt;

            &lt;text x=&quot;212&quot; y=&quot;47&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;disclosure&lt;/text&gt;
            &lt;rect x=&quot;220&quot; y=&quot;34&quot; width=&quot;367&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.22&quot; /&gt;
            &lt;text x=&quot;594&quot; y=&quot;47&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;108&lt;/text&gt;

            &lt;text x=&quot;212&quot; y=&quot;77&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;other (forms, worksheets, policy)&lt;/text&gt;
            &lt;rect x=&quot;220&quot; y=&quot;64&quot; width=&quot;143&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.22&quot; /&gt;
            &lt;text x=&quot;370&quot; y=&quot;77&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;42&lt;/text&gt;

            &lt;text x=&quot;212&quot; y=&quot;107&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;statement-guide&lt;/text&gt;
            &lt;rect x=&quot;220&quot; y=&quot;94&quot; width=&quot;65&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.22&quot; /&gt;
            &lt;text x=&quot;292&quot; y=&quot;107&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;19&lt;/text&gt;

            &lt;text x=&quot;212&quot; y=&quot;137&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;marketing&lt;/text&gt;
            &lt;rect x=&quot;220&quot; y=&quot;124&quot; width=&quot;48&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.22&quot; /&gt;
            &lt;text x=&quot;275&quot; y=&quot;137&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;14&lt;/text&gt;

            &lt;text x=&quot;212&quot; y=&quot;167&quot; font-size=&quot;12&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot; text-anchor=&quot;end&quot;&gt;statement-specimen&lt;/text&gt;
            &lt;rect x=&quot;220&quot; y=&quot;154&quot; width=&quot;20&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;247&quot; y=&quot;167&quot; font-size=&quot;11&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;6&lt;/text&gt;

            &lt;text x=&quot;212&quot; y=&quot;197&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;investor&lt;/text&gt;
            &lt;rect x=&quot;220&quot; y=&quot;184&quot; width=&quot;4&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.22&quot; /&gt;
            &lt;text x=&quot;231&quot; y=&quot;197&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;1&lt;/text&gt;

            &lt;text x=&quot;220&quot; y=&quot;224&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;Six statement specimens, every one of them from a single institution.&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            The corpus by document kind. The class the whole technique depends on is the
            second-rarest thing in it.
          &lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;p&gt;
          That is why a profile's identity is &lt;code&gt;&amp;lt;issuer&amp;gt;-&amp;lt;documentKind&amp;gt;&lt;/code&gt;
          and kinds are never merged. A profile named &lt;code&gt;chase-disclosure&lt;/code&gt; announces
          in its own name that it is a baseline for Chase disclosure PDFs and not for Chase
          statements.
        &lt;/p&gt;

        &lt;p&gt;
          If that seems like an excess of caution, consider the alternative: a confident
          mismatch finding on a real customer's real statement.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;corpus&quot;&gt;What the corpus is&lt;/h2&gt;

        &lt;ul&gt;
          &lt;li&gt;
            27 institutions attempted; PDFs obtained from 24 domains; 23 issuers with at
            least one US-entity sample.
          &lt;/li&gt;
          &lt;li&gt;
            213 files, all from the institution's own official domain. 23 from non-US arms
            (TD Canada, Amex Australia and Austria, Citi EMEA, BNY EMEA, USAA EU and UK) are
            excluded from every profile, which is what leaves 190.
          &lt;/li&gt;
          &lt;li&gt;
            Nothing from a template site or a document-sharing site. &lt;strong&gt;No real
            person's statement was obtained&lt;/strong&gt;, and none is needed — everything here is
            a published specimen or a public disclosure.
          &lt;/li&gt;
          &lt;li&gt;
            Every sample's source URL, size and SHA-256 is recorded, and a
            &lt;code&gt;--check&lt;/code&gt; mode re-derives the whole data set from the files and fails
            if it has drifted.
          &lt;/li&gt;
        &lt;/ul&gt;

        &lt;p&gt;
          Institutions I could not get usable data for: &lt;strong&gt;Wells Fargo statements&lt;/strong&gt;
          (only generic site PDFs), &lt;strong&gt;Charles Schwab&lt;/strong&gt;, &lt;strong&gt;SoFi&lt;/strong&gt;,
          &lt;strong&gt;Chime&lt;/strong&gt;, &lt;strong&gt;Goldman Sachs'&lt;/strong&gt; institutional site, and
          &lt;strong&gt;TD Bank US&lt;/strong&gt; — every TD PDF I found belonged to TD Canada.
        &lt;/p&gt;

        &lt;p&gt;
          Citi and American Express are represented by two and one US samples, which is not
          enough to say anything, and I am not going to.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;producers&quot;&gt;Finding 1 — producer strings are not an issuer tell for published documents&lt;/h2&gt;

        &lt;p&gt;
          The &lt;code&gt;/Producer&lt;/code&gt; string names whatever wrote the PDF bytes. It is the
          first thing anyone reaches for, and across 190 files it looks like this.
        &lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 640 290&quot; width=&quot;100%&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-producers-title&quot;&gt;
            &lt;title id=&quot;fig-producers-title&quot;&gt;Producer families across 190 US bank PDFs: Adobe PDF Library or Distiller 86, Microsoft Office 39, no producer string at all 16, Skia/PDF from Chromium print 9, macOS Quartz 4, Apache FOP 3, iText 3, everything else 30.&lt;/title&gt;
            &lt;text x=&quot;0&quot; y=&quot;14&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;Producer family — 190 US samples&lt;/text&gt;
            &lt;line x1=&quot;240&quot; y1=&quot;26&quot; x2=&quot;240&quot; y2=&quot;272&quot; stroke=&quot;var(--border)&quot; stroke-width=&quot;1&quot; /&gt;

            &lt;text x=&quot;232&quot; y=&quot;49&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;Adobe (PDF Library / Distiller)&lt;/text&gt;
            &lt;rect x=&quot;240&quot; y=&quot;36&quot; width=&quot;344&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.75&quot; /&gt;
            &lt;text x=&quot;591&quot; y=&quot;49&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;86&lt;/text&gt;

            &lt;text x=&quot;232&quot; y=&quot;80&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;Microsoft Office&lt;/text&gt;
            &lt;rect x=&quot;240&quot; y=&quot;67&quot; width=&quot;156&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.75&quot; /&gt;
            &lt;text x=&quot;403&quot; y=&quot;80&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;39&lt;/text&gt;

            &lt;text x=&quot;232&quot; y=&quot;111&quot; font-size=&quot;12&quot; font-style=&quot;italic&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;no producer string at all&lt;/text&gt;
            &lt;rect x=&quot;240&quot; y=&quot;98&quot; width=&quot;64&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.75&quot; /&gt;
            &lt;text x=&quot;311&quot; y=&quot;111&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;16&lt;/text&gt;

            &lt;text x=&quot;232&quot; y=&quot;142&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;Skia/PDF (Chromium print)&lt;/text&gt;
            &lt;rect x=&quot;240&quot; y=&quot;129&quot; width=&quot;36&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.75&quot; /&gt;
            &lt;text x=&quot;283&quot; y=&quot;142&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;9&lt;/text&gt;

            &lt;text x=&quot;232&quot; y=&quot;173&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;macOS Quartz&lt;/text&gt;
            &lt;rect x=&quot;240&quot; y=&quot;160&quot; width=&quot;16&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.75&quot; /&gt;
            &lt;text x=&quot;263&quot; y=&quot;173&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;4&lt;/text&gt;

            &lt;text x=&quot;232&quot; y=&quot;204&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;Apache FOP&lt;/text&gt;
            &lt;rect x=&quot;240&quot; y=&quot;191&quot; width=&quot;12&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.75&quot; /&gt;
            &lt;text x=&quot;259&quot; y=&quot;204&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;3&lt;/text&gt;

            &lt;text x=&quot;232&quot; y=&quot;235&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;iText&lt;/text&gt;
            &lt;rect x=&quot;240&quot; y=&quot;222&quot; width=&quot;12&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.75&quot; /&gt;
            &lt;text x=&quot;259&quot; y=&quot;235&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;3&lt;/text&gt;

            &lt;text x=&quot;232&quot; y=&quot;266&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;everything else&lt;/text&gt;
            &lt;rect x=&quot;240&quot; y=&quot;253&quot; width=&quot;120&quot; height=&quot;17&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.75&quot; /&gt;
            &lt;text x=&quot;367&quot; y=&quot;266&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;30&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            &quot;Everything else&quot; is Nitro, PDF-XChange, QuarkXPress, PDFCreator and, in one
            file, a Ricoh copier. These are 190 documents published by banks.
          &lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;p&gt;Per institution, the spread is the finding.&lt;/p&gt;

        &lt;figure class=&quot;post-figure&quot;&gt;
          &lt;svg viewBox=&quot;0 0 640 275&quot; width=&quot;100%&quot; role=&quot;img&quot; aria-labelledby=&quot;fig-issuers-title&quot;&gt;
            &lt;title id=&quot;fig-issuers-title&quot;&gt;Distinct producer strings per institution: Bank of America 7 of 9 samples, Marcus 10 of 12, M&amp;amp;T Bank 7 of 18, KeyBank 8 of 8, Huntington 8 of 15, Chase 5 of 9, PNC 3 of 8.&lt;/title&gt;
            &lt;text x=&quot;0&quot; y=&quot;14&quot; font-size=&quot;13&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;Distinct producer strings per institution&lt;/text&gt;
            &lt;rect x=&quot;0&quot; y=&quot;22&quot; width=&quot;12&quot; height=&quot;10&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; /&gt;
            &lt;text x=&quot;18&quot; y=&quot;31&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;samples&lt;/text&gt;
            &lt;rect x=&quot;86&quot; y=&quot;22&quot; width=&quot;12&quot; height=&quot;10&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;104&quot; y=&quot;31&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;distinct producer strings&lt;/text&gt;

            &lt;text x=&quot;152&quot; y=&quot;62&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;bank-of-america&lt;/text&gt;
            &lt;rect x=&quot;160&quot; y=&quot;48&quot; width=&quot;180&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; /&gt;
            &lt;rect x=&quot;160&quot; y=&quot;48&quot; width=&quot;140&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;348&quot; y=&quot;62&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;7 of 9&lt;/text&gt;

            &lt;text x=&quot;152&quot; y=&quot;94&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;marcus&lt;/text&gt;
            &lt;rect x=&quot;160&quot; y=&quot;80&quot; width=&quot;240&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; /&gt;
            &lt;rect x=&quot;160&quot; y=&quot;80&quot; width=&quot;200&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;408&quot; y=&quot;94&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;10 of 12&lt;/text&gt;

            &lt;text x=&quot;152&quot; y=&quot;126&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;m-and-t-bank&lt;/text&gt;
            &lt;rect x=&quot;160&quot; y=&quot;112&quot; width=&quot;360&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; /&gt;
            &lt;rect x=&quot;160&quot; y=&quot;112&quot; width=&quot;140&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;528&quot; y=&quot;126&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;7 of 18&lt;/text&gt;

            &lt;text x=&quot;152&quot; y=&quot;158&quot; font-size=&quot;12&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot; text-anchor=&quot;end&quot;&gt;keybank&lt;/text&gt;
            &lt;rect x=&quot;160&quot; y=&quot;144&quot; width=&quot;160&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; /&gt;
            &lt;rect x=&quot;160&quot; y=&quot;144&quot; width=&quot;160&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;328&quot; y=&quot;158&quot; font-size=&quot;11&quot; font-weight=&quot;600&quot; fill=&quot;var(--text)&quot;&gt;8 of 8&lt;/text&gt;

            &lt;text x=&quot;152&quot; y=&quot;190&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;huntington&lt;/text&gt;
            &lt;rect x=&quot;160&quot; y=&quot;176&quot; width=&quot;300&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; /&gt;
            &lt;rect x=&quot;160&quot; y=&quot;176&quot; width=&quot;160&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;468&quot; y=&quot;190&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;8 of 15&lt;/text&gt;

            &lt;text x=&quot;152&quot; y=&quot;222&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;chase&lt;/text&gt;
            &lt;rect x=&quot;160&quot; y=&quot;208&quot; width=&quot;180&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; /&gt;
            &lt;rect x=&quot;160&quot; y=&quot;208&quot; width=&quot;100&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;348&quot; y=&quot;222&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;5 of 9&lt;/text&gt;

            &lt;text x=&quot;152&quot; y=&quot;254&quot; font-size=&quot;12&quot; fill=&quot;var(--text-muted)&quot; text-anchor=&quot;end&quot;&gt;pnc&lt;/text&gt;
            &lt;rect x=&quot;160&quot; y=&quot;240&quot; width=&quot;160&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; fill-opacity=&quot;0.18&quot; /&gt;
            &lt;rect x=&quot;160&quot; y=&quot;240&quot; width=&quot;60&quot; height=&quot;19&quot; fill=&quot;var(--accent)&quot; /&gt;
            &lt;text x=&quot;328&quot; y=&quot;254&quot; font-size=&quot;11&quot; fill=&quot;var(--text-muted)&quot;&gt;3 of 8&lt;/text&gt;
          &lt;/svg&gt;
          &lt;figcaption&gt;
            A full bar means every document had a different producer. KeyBank: eight
            documents, eight producers, not one repeat.
          &lt;/figcaption&gt;
        &lt;/figure&gt;

        &lt;p&gt;
          The font sets behave the same way. Across those same seven institutions the counts
          are 9, 11, 16, 8, 11, 9 and 6 distinct font sets — in three cases one per file.
        &lt;/p&gt;

        &lt;p&gt;
          Two Bank of America online-banking agreements were printed straight out of a
          browser — &lt;code&gt;Skia/PDF m130&lt;/code&gt; and &lt;code&gt;Skia/PDF m135&lt;/code&gt;, with creator
          strings that are literally Chrome user-agent strings.
        &lt;/p&gt;

        &lt;p&gt;
          Chase serves card rate-and-fee tables produced by &lt;strong&gt;Apache FOP&lt;/strong&gt;, a
          server-side XSL-FO renderer — a real automated pipeline — sitting alongside
          brochures out of Adobe InDesign. Two document classes, one issuer, nothing in
          common.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;The implication is uncomfortable and worth stating plainly: a document
          whose producer is &quot;Microsoft Word&quot; or &quot;Chromium&quot; is not, on that basis,
          anomalous.&lt;/strong&gt; Real banks publish such files constantly. If you have seen a
          fraud vendor imply otherwise, that is the claim to press on.
        &lt;/p&gt;

        &lt;p&gt;
          Producer strings become evidence only when the baseline is drawn from the same
          document class — and, as Finding 3 shows, then they become good evidence. The
          &lt;a href=&quot;https://tamperlens.com/pdf-metadata-forensics&quot;&gt;metadata forensics guide&lt;/a&gt; covers what
          &lt;code&gt;/Producer&lt;/code&gt; and &lt;code&gt;/Creator&lt;/code&gt; are specified to mean, and where
          each one is written twice.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;fonts&quot;&gt;Finding 2 — font sets are near-unique per document, not per issuer&lt;/h2&gt;

        &lt;p&gt;
          I expected the font set to be one of the strongest tells, on the theory that fonts
          follow from a template.
        &lt;/p&gt;

        &lt;p&gt;
          In this corpus they mostly follow from the &lt;em&gt;document&lt;/em&gt;. 190 samples yield
          &lt;strong&gt;167 distinct font sets&lt;/strong&gt;, of which only 12 occur more than once. The
          top-ranked font set reaches 80% of samples in exactly one profile out of 17 — the
          Huntington statements.
        &lt;/p&gt;

        &lt;p&gt;
          Brand fonts do show up, and they are genuinely distinctive when they do:
          &lt;code&gt;PNCBrand&lt;/code&gt;, &lt;code&gt;DiscoverSans&lt;/code&gt;, &lt;code&gt;CitiSans&lt;/code&gt;,
          &lt;code&gt;Connections&lt;/code&gt; (Bank of America), &lt;code&gt;Optimist&lt;/code&gt; (Capital One),
          &lt;code&gt;M&amp;amp;TBalto&lt;/code&gt;, &lt;code&gt;Huntington_Apex&lt;/code&gt;. A file claiming to be from
          Capital One with no trace of Optimist is a reasonable question to ask.
        &lt;/p&gt;

        &lt;p&gt;
          But the brand font appears alongside a different supporting cast in almost every
          file — whatever the author's machine had — so the &lt;em&gt;joined set&lt;/em&gt; almost never
          repeats. And it is the joined set you would have to match on.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;huntington&quot;&gt;Finding 3 — the one real statement pipeline is sharply consistent&lt;/h2&gt;

        &lt;p&gt;
          Huntington publishes six sample account-analysis statements — commercial treasury
          statements, not consumer checking — as raw pipeline output rather than a redrawn
          mock-up. Somebody exported them from the real system and put them on the website.
        &lt;/p&gt;

        &lt;p class=&quot;code-label&quot;&gt;The huntington-statement-specimen profile&lt;/p&gt;
        &lt;pre class=&quot;code&quot;&gt;&lt;code&gt;producer        (none — 6/6 carry no producer string)
creator         (none — 6/6)
pdfVersion      1.6                      6/6
xrefStyle       stream                   6/6
pageSize        612x792                  6/6
revisionCount   1                        6/6
linearized      true                     6/6
signed          false                    6/6
fontSet         EX_CFF_Arial|EX_CFF_Calibri|EX_CFF_CalibriBold|
                EX_CFF_Huntington_Apex_Medium|Helvetica|ZapfDingbats    5/6&lt;/code&gt;&lt;/pre&gt;

        &lt;p&gt;Two things stand out.&lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;The complete absence of producer and creator metadata is itself the
          signature.&lt;/strong&gt; High-volume composition engines frequently emit none; nearly
          every desktop tool announces itself by name and version. An empty
          &lt;code&gt;/Producer&lt;/code&gt; is not a null result here, it is a positive one.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;The &lt;code&gt;EX_CFF_&lt;/code&gt; font-name prefix is an artefact of the composition
          engine's font handling&lt;/strong&gt;, not of any font a human chose. Neither would be
          reproduced by accident by someone building a fake statement in Word or a browser.
        &lt;/p&gt;

        &lt;p&gt;
          That is what a usable baseline dimension looks like: stable within the issuer, and
          not reproducible without the issuer's toolchain.
        &lt;/p&gt;

        &lt;p&gt;
          One institution, one statement product. It does not generalise. But it is a
          concrete existence proof that the signal this feature depends on is real where it
          matters.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;ccm&quot;&gt;Finding 4 — customer-communication platforms are visible, and distinguishable&lt;/h2&gt;

        &lt;p&gt;
          A handful of samples were produced not by desktop software but by
          customer-communications management platforms — the same category of tooling that
          generates statements.
        &lt;/p&gt;

        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;PNC&lt;/strong&gt;: &lt;code&gt;Smart Communications - SC33&lt;/code&gt; / &lt;code&gt;SC31&lt;/code&gt;, on four consumer documents.&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;USAA&lt;/strong&gt;: &lt;code&gt;xPression 4.6SP1&lt;/code&gt;, an OpenText composition engine.&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;Wells Fargo&lt;/strong&gt;: &lt;code&gt;Crawford Technologies&lt;/code&gt;, &lt;code&gt;OAT&lt;/code&gt;.&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;Navy Federal&lt;/strong&gt;: &lt;code&gt;Accessibil-IT Inc.&lt;/code&gt;, an accessibility-remediation post-processor.&lt;/li&gt;
        &lt;/ul&gt;

        &lt;p&gt;
          These strings are rare in general-purpose PDFs and hard to produce accidentally.
          PNC is also the tightest of the seven institutions above — three producer strings
          across eight samples, against KeyBank's eight across eight.
        &lt;/p&gt;

        &lt;p&gt;
          Where a bank routes even its disclosures through this tooling, its published
          documents sit structurally closer to its statements than a typical brochure does. I
          have no statement from any of those institutions to confirm it against, so treat
          that as a hypothesis with a clear test rather than a result.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;uniform&quot;&gt;Finding 5 — what is uniform is uniform because it is uniform everywhere&lt;/h2&gt;

        &lt;p&gt;
          Among the 17 profiles with five or more samples, this is how many reach 80%
          agreement on each dimension.
        &lt;/p&gt;

        &lt;div class=&quot;table-scroll&quot;&gt;
          &lt;table class=&quot;post-table&quot;&gt;
            &lt;thead&gt;
              &lt;tr&gt;
                &lt;th&gt;dimension&lt;/th&gt;
                &lt;th&gt;profiles reaching ≥0.8&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              &lt;tr&gt;&lt;td&gt;signed&lt;/td&gt;&lt;td&gt;17 / 17&lt;/td&gt;&lt;/tr&gt;
              &lt;tr&gt;&lt;td&gt;pageSize&lt;/td&gt;&lt;td&gt;11 / 17&lt;/td&gt;&lt;/tr&gt;
              &lt;tr&gt;&lt;td&gt;linearized&lt;/td&gt;&lt;td&gt;10 / 17&lt;/td&gt;&lt;/tr&gt;
              &lt;tr&gt;&lt;td&gt;revisionCount&lt;/td&gt;&lt;td&gt;9 / 17&lt;/td&gt;&lt;/tr&gt;
              &lt;tr&gt;&lt;td&gt;xrefStyle&lt;/td&gt;&lt;td&gt;5 / 17&lt;/td&gt;&lt;/tr&gt;
              &lt;tr&gt;&lt;td&gt;pdfVersion&lt;/td&gt;&lt;td&gt;3 / 17&lt;/td&gt;&lt;/tr&gt;
              &lt;tr&gt;&lt;td&gt;fontSet&lt;/td&gt;&lt;td&gt;1 / 17&lt;/td&gt;&lt;/tr&gt;
              &lt;tr&gt;&lt;td&gt;producer&lt;/td&gt;&lt;td&gt;0 / 17&lt;/td&gt;&lt;/tr&gt;
              &lt;tr&gt;&lt;td&gt;creator&lt;/td&gt;&lt;td&gt;0 / 17&lt;/td&gt;&lt;/tr&gt;
            &lt;/tbody&gt;
          &lt;/table&gt;
        &lt;/div&gt;

        &lt;p&gt;
          &lt;strong&gt;Not one of the 190 documents is digitally signed.&lt;/strong&gt;
          &lt;code&gt;signed&lt;/code&gt; is perfectly consistent and therefore worthless as an issuer
          discriminator: everyone matches it, forgeries included. &lt;code&gt;pageSize&lt;/code&gt; is
          mostly US Letter — 612×792 in 152 of 190 — for the same reason.
        &lt;/p&gt;

        &lt;p&gt;
          This is the trap in the whole approach, and it is easy to fall into if you grade
          yourself on agreement scores. A dimension stable &lt;em&gt;across all issuers&lt;/em&gt; carries
          no issuer information.
        &lt;/p&gt;

        &lt;p&gt;
          The useful ones are stable &lt;em&gt;within&lt;/em&gt; an issuer and varying &lt;em&gt;between&lt;/em&gt;
          them. In this corpus only the Huntington statement set demonstrates that pattern.
        &lt;/p&gt;

        &lt;p&gt;
          One more number, because it contradicts something people repeat: &lt;strong&gt;142 of 190
          documents are single-revision, but 48 are not, and one carries 24
          revisions.&lt;/strong&gt; Multiple revisions in a genuine, published, bank-originated PDF
          are ordinary.
        &lt;/p&gt;

        &lt;p&gt;
          If you are counting &lt;code&gt;%%EOF&lt;/code&gt; markers and treating two as suspicious, this
          corpus is your counterexample set. The
          &lt;a href=&quot;https://tamperlens.com/detect-edited-pdf&quot;&gt;edited-PDF guide&lt;/a&gt; explains the two directions that
          count is wrong in, and what to walk instead.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;support&quot;&gt;What this data can and cannot support&lt;/h2&gt;

        &lt;p&gt;&lt;strong&gt;It can support:&lt;/strong&gt;&lt;/p&gt;

        &lt;ul&gt;
          &lt;li&gt;That issuer structural baselines are viable &lt;em&gt;for statement output&lt;/em&gt;.&lt;/li&gt;
          &lt;li&gt;
            Calibration of what normal looks like for published bank PDFs, useful mainly for
            &lt;em&gt;suppressing&lt;/em&gt; false positives on documents that are not statements at all.
          &lt;/li&gt;
          &lt;li&gt;
            A concrete argument that baselines must be built per document class, and named
            that way.
          &lt;/li&gt;
        &lt;/ul&gt;

        &lt;p&gt;&lt;strong&gt;It cannot support:&lt;/strong&gt;&lt;/p&gt;

        &lt;ul&gt;
          &lt;li&gt;
            Any claim of the form &quot;Bank X's statements use producer Y&quot; for any bank other
            than Huntington's account-analysis product — and not for Huntington's consumer
            statements either.
          &lt;/li&gt;
          &lt;li&gt;
            Percentage claims about US banking. 23 non-randomly sampled issuers is not a
            population.
          &lt;/li&gt;
          &lt;li&gt;
            Detection thresholds. With 16 of 17 eligible profiles failing the consistency
            gate on producer, the honest engine behaviour is to produce &lt;strong&gt;no
            expectation at all&lt;/strong&gt;, which is what it does.
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;shipped&quot;&gt;What we shipped, given all that&lt;/h2&gt;

        &lt;p&gt;
          The issuer-baseline feature is live and &lt;strong&gt;labelled experimental, disabled as
          a production default&lt;/strong&gt;.
        &lt;/p&gt;

        &lt;p&gt;
          Of 56 profiles, 39 sit below the minimum-samples threshold and can only ever report
          &lt;code&gt;unknown&lt;/code&gt;. Of the 17 that clear it, 16 are built from disclosure,
          marketing and form documents whose internal consistency is too low to produce a
          mismatch on the dimensions that matter — so they will also, correctly, stay quiet.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;The operational value of this data set today is close to exactly one
          profile.&lt;/strong&gt;
        &lt;/p&gt;

        &lt;p&gt;
          That is a strange thing to ship. The reason it shipped anyway is that the parser,
          the build script and the provenance format are the scaffolding for the data set
          that will actually work: &lt;strong&gt;tenant-supplied known-good statements.&lt;/strong&gt;
        &lt;/p&gt;

        &lt;p&gt;
          If you hold originals — a lender with a year of real statements from the banks you
          actually see — those are the documents this was designed for, and a baseline built
          from them has none of the problems above. Structurally it is the same operation as
          &lt;a href=&quot;https://tamperlens.com/compare-pdf-files&quot;&gt;comparing two files you both trust&lt;/a&gt;, run across a
          set instead of a pair.
        &lt;/p&gt;

        &lt;p&gt;
          That path is on the paid plans, and it is the only version of this feature I would
          defend.
        &lt;/p&gt;
      &lt;/section&gt;

      &lt;section&gt;
        &lt;h2 id=&quot;limits&quot;&gt;The limits you should hold this against&lt;/h2&gt;

        &lt;p&gt;
          &lt;strong&gt;A cleanly re-generated PDF launders every signal described here.&lt;/strong&gt;
          Print-to-PDF or save-as rewrites the byte structure; the revision chain, the
          &lt;code&gt;/ID&lt;/code&gt; pair, the producer string and the font-subset tags all come out
          coherent and fresh. There is no version of this technique that survives that, and
          anyone telling you their version does is selling you something.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;Nobody has measured what fraction of real document fraud structural
          analysis catches.&lt;/strong&gt; The only public figure I know of is one vendor's
          unmethodologised &quot;80%&quot;. Every academic benchmark I could find is pixel-level image
          forensics; there is no PDF-structure equivalent. The defensible claim is high
          precision, unknown recall, and I am not going to put a number on the second one.
        &lt;/p&gt;

        &lt;p&gt;
          &lt;strong&gt;This reports signals, never a verdict.&lt;/strong&gt; &quot;The producer string does
          not match this issuer's baseline&quot; is a statement about bytes. &quot;This document is
          fraudulent&quot; is a statement about a person, and it is not one a parser gets to make.
        &lt;/p&gt;

        &lt;div class=&quot;post-cta&quot;&gt;
          &lt;h2&gt;Run the same parser on a file of your own&lt;/h2&gt;
          &lt;p&gt;
            Every number above came out of the parser that runs on the
            &lt;a href=&quot;https://tamperlens.com/&quot;&gt;free checker&lt;/a&gt;. Drop in a PDF and you get the same report this
            corpus was built from — producer and creator strings, the revision chain, the
            font sets, page geometry. No account, nothing stored.
          &lt;/p&gt;
          &lt;p&gt;
            Everything in this post traces to a specific file with its source URL and
            SHA-256. If you hold a corpus of genuine statements from an institution and want
            to know whether it produces a usable baseline, that is the same measurement, run
            against documents you supply.
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/section&gt;</content>
  </entry>
</feed>
