Files
craft-libs/crates/plate-test
aiongg 24a472717a prepress: transparency flattener (flatten feature) (#194)
* hayro-interpret: ink colors, overprint and font bytes for the flattener

Local patches for craft-prepress's PDF-in adapter, labeled Craft patch
(flatten) and listed in vendor/README.craft-reader.md:

- Device::set_overprint and Device::set_isolated, called where the
  blend mode is set and after an isolated group is pushed.
- Color::ink_paint, ColorSpace::ink_space and ink_values: colors and
  image samples as the inks they print, without converting to RGB.
  RasterImage::with_inks gives native samples with Decode and Matte.
- OutlineFontData::embedded and index, and the CFF bytes of Type1C
  fonts, so text can be embedded again.
- The sh operator ignores /Background (ISO 32000-2 8.7.4.5.1).

* plate-test: flattened-page checks, image alpha and scenes F01, F02

- report::run_flattened separates each scene's page after a caller's
  flattener as well as the original, and structure::transparency lists
  what transparency a page still has. plate-test doesn't depend on the
  flattener.
- Scene images take an alpha channel, written as /SMask.
- F01: a partly transparent image over a flat fill; where its alpha is
  0 the fill keeps its exact inks (vectorcraft#2160).
- F02: art hidden under opaque fills leaves no trace after flattening
  (vectorcraft#2161).

* prepress: transparency flattener (flatten feature)

craft_prepress::flatten turns PDF pages with transparency into pages
without it, keeping inks, overprint and live text, for PDF/X-1a and
other output that can't carry transparency.

- pdf: reads a page into a display list in inks through hayro-interpret:
  paths, outlined strokes, clips, groups, soft masks, overprint, spot
  and DeviceN colors, images in their own color space, gradients,
  rasterized mesh shadings, tiling patterns and glyph runs.
- font: prepares the source fonts for embedding again (TrueType,
  OpenType, CFF wrapped as OpenType); others fall back to outlines with
  ActualText.
- geom: planar regions with the shapes that cover each one (linesweeper).
- compose: composites each region per ink (separable and non-separable
  blend modes, isolated and knockout groups, overprint per plate as ISO
  32000 defines it); regions under images, gradients and soft masks are
  marked complex.
- raster: complex regions as DeviceN images through plate mode, grid
  snapped with a one-pixel outset; flatten_page.
- write: writes the result with the vendored krilla; flatten_document
  writes every page into one PDF with the output intent, page boxes and
  fonts shared across pages when their bytes match.

The regions and the compositor are ported from VectorCraft (commit
c2b15982, crates/pathops and crates/engine/src/cmd/flatten.rs), credited
in the crate README. scripts/plate-check.sh --flatten separates every
scene's flattened page with Ghostscript (local only). CI runs
craft-prepress with all features.

* krilla: DeviceN /Colorants from PDF 1.3, NChannel attributes from 1.6 (patch 11)

* prepress: flatten_document output PDF version and PDF/X version

* prepress: format the PDF/X-1a flatten test

* krilla, craft-prepress: write GTS_PDFXConformance next to GTS_PDFXVersion

PDF/X-1a:2003 files carry GTS_PDFXVersion (PDF/X-1:2003) and
GTS_PDFXConformance (PDF/X-1a:2003). Krilla patch 12 adds
SerializeSettings::pdfx_conformance, and FlattenDocumentOptions passes it
through.

* krilla, craft-pdf, craft-prepress: carry the modification date over

Krilla patch 13 adds Metadata::modification_date, written as /ModDate and
xmp:ModifyDate in place of the creation date. craft-pdf's Metadata gains
modified, and flatten_document takes it from the source's ModDate.

* craft-prepress: count the print annotations a read leaves out

ReadReport::dropped_annotations counts the annotations a printer would
paint (Print flag, not Hidden, with a normal appearance) when
ReadOptions::annotations is false; FlattenTotals sums it.

* craft-prepress: degenerate groups rasterize without a soft mask

A group the region sweep fails on is clipped to its members' boxes, which
no other group's items reach, so the paper is its only backdrop. Its
image is written opaque, as the plates of the items over paper, and the
page keeps no transparency for PDF/X-1a.

* craft-prepress: write untouched JPEG images as they are

A DCT-encoded DeviceGray or DeviceCMYK image that krilla's JPEG
passthrough writes with the same meaning (8 bits, no decode parameters,
the default gray Decode or the inverted Adobe CMYK one) keeps its data
in ImageItem::jpeg, and the writer embeds it unchanged when the image
has no soft mask. Other images are still 8-bit Flate samples.

* craft-prepress: keep group members with no transparency above them

Groups are transitive by bounding box, so a background frame put all the
body text on it under a transparent object far away, and the text came
back as outlined fills. A member is now drawn as it is when it is opaque
and, in every region its shapes are in, only members kept this way are
above it. The region fills under it are the composite of the other
members, and it is drawn over them in paint order.

* prepress: keep the backdrop under elements of a knockout group

* prepress: rasterize complex regions thinner than a point

* prepress: mark flatten errors, reasons and reports non_exhaustive

* prepress: bound the items a tiling pattern fill expands into

Each of up to MAX_TILING_CELLS cells copied the whole cell content, and a
cell can hold many items or an expanded pattern of its own. A fill whose
cells would hold more than MAX_TILING_ITEMS items in all, counting groups,
masks and nested patterns, is read as an image.

* prepress: stroke solid past the dash budget

A dash pattern that cuts a stroke into more than 250,000 pieces is outlined
solid, with the estimate hayro's renderer uses.

* prepress: bound the sampling and memory of rasterized tiling patterns

Each pixel sampled every cell over it, which for a BBox far larger than
its step is millions of cells per pixel; past MAX_TILING_CELLS cells per
pixel the fill paints nothing, and below it the image resolution drops to
keep the samples within MAX_TILING_SAMPLES. The canvases also hold a value
per page ink, so their pixel limits shrink with the ink count to keep the
memory of CMYK canvases, and the canvas size uses checked arithmetic.

* prepress: mark the page reader's error and report non_exhaustive

* prepress: format the tiling tests

* prepress: CodeRabbit fixes: no nonzero overprint mode for images and shadings, count dropped tilings, widen the hhea ascender

* prepress: refuse DeviceN spaces past the output's colorant limit and regions past the separated spot inks
2026-10-24 16:15:41 -04:00
..

Craft plate test

craft-plate-test holds print-color test scenes for the Craft apps and the tools to check PDFs against them. A scene is an app-neutral JSON description of one page (spot inks, CMYK fills, transparency, overprint, soft masks, placed PDFs), the ink each plate must carry at sample points, and the PDF structure a correct print export has.

  • scene: serde types for the scene files in scenes/, and Scene::validate for the checks a schema can't make (declared inks, geometry fields per object type, plates named by points).
  • reference: writes a scene to PDF with krilla through craft-pdf. It tests the shared writer on its own. A feature the writer can't express yet returns Error::Unsupported.
  • structure: summarizes a PDF with hayro-syntax (page and form transparency groups with /S /CS /I /K; graphics states with OP op OPM BM SMask; color space families; DeviceRGB use; output intents) and checks the summary against a scene's structure block.
  • gs, tiff, judge, report: the local plate check (below).

tests/scenes.rs parses every scene, writes every scene without pending, and checks the structure. It runs in CI in seconds, with the unit tests of the TIFF reader and the judge.

Scenes

The file names say what each scene tests; title and source say more.

IDs Cases
S01–S17 Print-color research cases: overprint, spots, blending spaces, knockout and isolated groups, masks, placed PDFs, gradients
D01–D10 DesignCraft print cases (its DC list) and a GWG idea (D04)
M1, M2, N1 Luminosity mask backdrop and transfer; DeviceN overprint
F01 Flattener: a semi-transparent image over a flat fill; where the image's alpha is 0 the fill prints unchanged (vectorcraft#2160)
F02 Flattener: art hidden under opaque fills, with transparency over them, leaves no trace (vectorcraft#2161)

Scene format

Coordinates are points from the page's top-left corner; rectangles are [x, y, width, height]. Ink values are percentages (0 = no ink, 100 = solid). Plates use separation names: Cyan, Magenta, Yellow, Black and the spot names.

Field Meaning
id, title, source, note Identity and origin (research case, app regression)
page [width, height] in pt
inks Spot inks, each {"name", "cmyk": [c, m, y, k]} or "lab": [L, a, b]; their order is the DeviceN colorant order after the process inks
settings group_cs ("rgb" or "cmyk": the transparency blending space), output_intent ("fogra39": writes the scene with craft-pdf write::print settings and craft-cms's COATED_FOGRA39_ICC; tests/scenes.rs also writes every non-pending scene this way)
objects Drawn in order (below)
expect plates: every plate the separation must produce, process plates included, and no others. points: {at, kind: "area" or "text", region (text), plate (text, default Black), inks: {plate: value or [lo, hi]}, tol, info, note}; unlisted plates expect 0, info points are measured but not judged
structure Expected PDF structure (below)
pending The task that makes the scene writable, e.g. "T4"; pending scenes are parsed but not written

Objects have a type: rect and ellipse (rect), text (at = baseline start, size, text), image (rect, image), pdf (rect, scene = id of a scene placed as a page), group (objects, isolated, knockout). Any object can carry opacity (whole object, as a transparency group), blend (multiply, color, … in kebab case), overprint ({fill, stroke, mode}, mode = OPM, default 1) and mask ({kind: "luminosity" or "alpha", content: [objects], backdrop, transfer}; backdrop is a process or gray paint, transfer a list of samples 0–1). Shapes and text take fill and stroke ({paint, width}).

Paints:

Paint Example
Process {"cmyk": [100, 0, 0, 0]}
Gray (black ink %, written as DeviceGray) {"gray": 50}
RGB (0–255) {"rgb": [255, 0, 0]}
Spot {"spot": "Gold", "tint": 50} (tint defaults to 100)
Several inks (DeviceN) {"inks": {"Cyan": 60, "Black": 40}}
Registration (/All) "registration"
Linear gradient {"linear": {"from": [x, y], "to": [x, y], "stops": [{"at": 0, "paint": …}, …]}}
Mesh gradient (one Coons patch with straight edges) {"mesh": {"rect": [x, y, w, h], "corners": [paint, paint, paint, paint]}} (top left, top right, bottom right, bottom left)

Images are synthetic: {"px": [w, h], "ramp": [paint, paint]} (left to right), "split": [paint, paint] (left and right halves) or "feather": pt (a gray soft-mask image, white inside and fading to black at the edges). Shadows and feathers are luminosity masks over such an image. A ramp or split image can carry "alpha": {"rect": [x, y, w, h], "opacity": 0.6}, written as its /SMask like a PNG with transparency: pixels whose centers lie in rect (page pt) have that opacity, the rest 0.

Tolerances: ±2 for device, spot and overprint values; ±6 for blends and ICC; ±10 for shadows and gradients.

The structure block

Each field is optional; only the fields present are checked.

Field Check
page_group The page group's /CS family; "none" = no page group, "no-cs" = a group without /CS
group_cs The set of /CS values of form and soft-mask groups ("none" = no /CS)
knockout, isolated Some group has /K true / /I true (page groups excluded for isolated)
overprint, smask Some graphics state sets OP or op true / a soft mask
blend_modes The set of blend modes other than Normal
ext_g_states Each entry (keys OP op OPM BM SMask BC TR) matches some graphics state
color_spaces, color_spaces_include The exact set / a subset of color space families, e.g. DeviceCMYK, Separation(Gold, alt DeviceCMYK)
device_rgb Some painting color space, alternates included, is DeviceRGB
output_intents The /S of each output intent

A failing check prints the full summary as JSON, which is also the quickest way to write a new block.

Adding a scene

  1. Add scenes/<ID>-<slug>.json. Reuse the 360 × 216 pt page and the Gold spot unless the case needs otherwise. Use fonts and images that the harness makes; never commit fonts, GWG files or other third-party test files.
  2. Add pending when the writer can't express the scene yet, and remove it in the change that adds the feature.
  3. Run cargo test -p craft-plate-test.

Text uses Inter-Regular.ttf from CRAFT_PDF_TEST_FONT_DIR (python scripts/fetch-pdf-test-fonts.py <dir outside the checkout>). Without it, text objects are skipped with a printed note.

Use from an app

Add the crate as a dev-dependency and write an adapter in the app's tests: build each scene as an app document, export it through the app's own print path, and check the result with structure::summarize and the scene's structure block. Scenes the app can't build yet are skipped by the adapter, not edited.

let scenes = craft_plate_test::scene::load_dir(&craft_plate_test::scene::scenes_dir())?;
for scene in scenes.iter().filter(|s| s.pending.is_none()) {
    let pdf = my_app_export(scene)?; // the app's adapter
    let summary = craft_plate_test::structure::summarize(&pdf)?;
    assert!(scene.structure.check(&summary).is_empty());
}

Plate check (local only)

scripts/plate-check.sh

The check writes every scene, separates it with Ghostscript's tiffsep device at 72 dpi with overprint simulated, and judges the plates:

  • Area points: the median of the 5 × 5 pixels around the point, per plate.
  • Text points: the median over the glyph's pixels, those in region with at least half the darkest ink on the point's plate.
  • Each point's plates within tol of the expected value (or inside the [lo, hi] range).
  • The separation produces exactly the scene's plates.

Pending scenes are written without the features they wait for (reference::write_partial) and are expected to fail; the report shows what the writer prints today. A pending scene that passes is reported as such: its plates don't show the missing feature under Ghostscript, so its structure block is the check that does. The test fails when a scene without pending fails or the harness errors.

scripts/plate-check.sh --flatten runs the same check on each scene's page flattened by craft-prepress (flatten_page at 300 dpi; the test is flatten_plates in craft-prepress, since craft-prepress dev-depends on this crate). The flattened page must also have no transparency left (structure::transparency: transparency groups, soft masks in graphics states, blend modes other than Normal, ca/CA below 1). Its report goes to plate-test/flattened/ and shows both pages side by side, with the flattener's time per scene.

The results go to plate-test/report.md and results.json in Cargo's target directory (CARGO_TARGET_DIR is respected). The script sets CRAFT_PDF_TEST_FONT_DIR to ~/.cache/craft-pdf-test-fonts when the font is there; without the font, text points are skipped.

Ghostscript is AGPL. It runs only as an external program on developer machines (gs on PATH, or CRAFT_PLATE_TEST_GS), never in CI. Don't link it, copy its code, or commit its output. Without Ghostscript the test returns with a note.