Skip to content

fix(preflight): stop sparse bleed marks passing content-bleed (#120) - #161

Open
mberrys wants to merge 12 commits into
devfrom
cc/issue-120-sparse-bleed-raster
Open

mberrys wants to merge 12 commits into
devfrom
cc/issue-120-sparse-bleed-raster

Conversation

@mberrys

@mberrys mberrys commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Closes #120 (L01-15). Lands backlog row bleed-raster-strip-depth.

What was wrong

content-bleed accepted a bleed strip as populated as soon as the union of artwork bounds touched it, so a single stray mark per edge passed clean. Raster confirmation never re-checked an edge the bounds pass had accepted, so enabling it did not help, and the 10% ink floor only applied to upgrading empty edges.

Change

  • A strip counts as populated only above a 10% coverage floor (PDFBleedMarginProbeSettings::minEdgeCoverage). The fast pass measures bounds coverage per strip in 8 bands across its depth.
  • Bounds prove a strip populated only through images and filled geometry. The solid share is the painted area of each filled piece intersected with the strip and its clip (04b2b684, 1b1bf0be); stroke-only paths never count as solid (79d44ad2). Anything else that touches enough of the strip (hairlines, strokes) is unconfirmed (PDFBleedMarginProbeEdgeResult::confirmed).
  • With raster_confirm the strip raster decides every unconfirmed edge, including demoting a sparse one to bleed-margin-empty. Without it, or when the strip raster cannot be measured (over max_raster_pixels or unusable page boxes), the page reports informational check-incomplete (so pass is false) with a reason naming which applies, instead of passing clean.
  • Overlay row bleed-raster-strip-depth is landed; content-bleed limitations rewritten; generated catalogs regenerated; README documents the floor.

Proof

Fixtures committed first (6ea3891b) and shown clean on the unchanged engine:

fixture profile before after
content-bleed-sparse-marks (1 pt dot per edge) tiered-bleed pass, 0 findings content-bleed warning, all four edges
content-bleed-hairline-margin (0.1 pt diagonal per edge) tiered-bleed-raster pass, 0 findings 4 x bleed-margin-empty
content-bleed-hairline-margin tiered-bleed (no raster) pass check-incomplete (pass false), covered by an engine test
  • Golden corpus unchanged: no existing snapshot differs (git diff origin/dev --stat shows only the two new snapshots).
  • Local lanes (exit code 0): UnitTestsPreflightEngine, UnitTestsPreflightCorpus, UnitTestsBleedMarginProbe, UnitTestsPreflightChecks, UnitTestsPreflightVerdict, UnitTestsEvidenceGraph, UnitTestsContentProcessorLimits. Windows MSVC Release build.
  • generate-architecture-catalogs.py --check, check_source_integrity.py and check-change.py --dry-run pass. clang-format 18 clean on every touched source file (the probe .cpp/.h already violated on dev; formatted in 0559ee62).
  • Source SHA at handoff: 04b2b684 (adds the UnitTestsBleedMarginProbe proof lane in architecture/proof-lanes.yaml; check-architecture.py passes locally).
  • Not run locally: the full check-change build/ctest/clang-tidy, the Linux lanes and the other mapped unit targets. Hosted CI covers them.

Remaining risk

  • The 10% floor is a policy choice. A legitimately sparse bleed texture (stars on white, say) is now flagged.
  • A white fill still counts as artwork. This stays open and is listed in the overlay limitations.
  • With raster_confirm on, pages whose artwork is not rectangles or images now render strips that were previously skipped.
  • Profiles without raster_confirm that relied on a stroked or text-only margin will now see check-incomplete.

Anti-slop review

The change is one coverage rule applied in the existing probe, with one new finding type reused from other checks. No new profile keys, no evidence added to existing findings, and no snapshot churn beyond the two new fixtures.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Comment thread LoopLibCore/sources/pdfbleedmarginprobe.cpp Outdated
if (rasterEdge.totalPixels > 0)

const qreal inkCoverage = static_cast<qreal>(rasterEdge.inkPixels) / static_cast<qreal>(rasterEdge.totalPixels);
const bool populated = inkCoverage >= settings.minEdgeCoverage;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pale-tint vector bleed is demoted to empty. Non-rectangular vector bleed art now always goes to the raster pass, and pixelIsInk treats near-white pixels as background. A light tint (e.g. a 3% yellow curved background) that really fills the bleed measures under 10% ink, so it is demoted and reported as bleed-margin-empty. Before this PR it passed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Policy, not a bug: ink is any non-near-white pixel (probe_threshold, default 16), and the 10% coverage floor is deliberate. A tint under that floor on a sparse area is flagged, and this is recorded as a residual limit in the evidence manifest. Left open for your call.

Comment thread LoopLibCore/sources/pdfbleedmarginprobe.cpp
Comment thread LoopLibCore/sources/pdfbleedmarginprobe.cpp Outdated

// An edge that artwork bounds call populated but that is not provably solid, and that no
// raster measurement confirmed, must not pass as clean bleed.
if (!result.allEdgesConfirmed())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete on every vector-bleed page by default. raster_confirm is off by default, so every page whose bleed is a curve, a logo or text, rather than an image or a filled rectangle, gets a check-incomplete finding. That marks ordinary, correct documents as not fully inspected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intended fail-closed: without raster_confirm, bounds cannot show a hairline/stroke/text-only strip is inked, so the page reports informational check-incomplete (severity info) rather than passing. Fixture content-bleed-hairline-margin asserts it; profiles that want a verdict enable raster_confirm. Left open for your call.

}
}

const QString reason = check.rasterConfirm

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason always blames the pixel budget. Edges also stay unconfirmed when the reference or target box is invalid or the strip maps to an empty pixel rect. In those cases the message tells the operator to raise max_raster_pixels, which changes nothing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly addressed in 1b1bf0b: the reason now distinguishes raster-off from "raster could not be measured (over pixel budget, or unusable page boxes)". Separate reasons for an invalid reference vs target box are not split; they share the 'unusable page boxes' wording. Left open.

Comment thread LoopLibCore/sources/pdfbleedmarginprobe.cpp Outdated
PDFBleedMarginProbeResult result = probeFast(page, pageIndex, settings);

if (settings.fastOnly || result.allEdgesCovered() || !m_session)
if (settings.fastOnly || (result.allEdgesCovered() && result.allEdgesConfirmed()) || !m_session)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raster pass now runs for most pages. Any unconfirmed edge (any bleed art that isn't an image, a shading or a filled rectangle) triggers it, and probeRaster re-renders the whole page once per strip. A 300-page document with vector bleed art goes from zero renders to 1200 full content-stream renders.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped, not removed: the raster only runs when raster_confirm is on and some edge is unconfirmed or uncovered; pages whose edges are all bounds-confirmed short-circuit. The cost is real for stroke/text-heavy pages (documented under Remaining risk). Per-strip render skipping is not added here. Left open.

Comment thread LoopLibCore/sources/preflightengine.cpp Outdated
mberrys and others added 2 commits September 30, 2026 19:31
A stroke-only rectangle on the bleed box passed the rectangle test because
piece paths are unstroked, confirming every edge with an empty margin.
GraphicPieceInfo now records whether a path is filled, and the rectangle
test rejects diagonal edges and doubled-back corners. The fill test runs
once per piece instead of per band, the per-side probe accessors collapse
into one, and the incomplete reason no longer blames only the pixel budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edges were confirmed only by images, shadings and filled rectangles, judged
on unclipped bounds. A full-bleed image clipped at trim was proven solid,
while any filled non-rectangular background (curves, pale tints) went to
raster, where near-white tints were demoted, or reported check-incomplete
when raster_confirm is off. GraphicPieceInfo now carries the clip in
effect, and an edge is confirmed by the area that images and filled paths
or glyphs actually paint inside the strip, cut by their clip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mberrys and others added 3 commits September 30, 2026 23:20
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant