Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e9bec03
test(preflight): add clipped off-page and obscured-content regression…
mberrys Oct 1, 2026
9bfd801
feat(preflight): judge off-page and obscured content on clipped paint…
mberrys Oct 1, 2026
c69414f
test(preflight): span the page in the off-page clip regression case (…
mberrys Oct 1, 2026
5ae8baa
docs(preflight): land off-page-content-clipping and obscured-content-…
mberrys Oct 1, 2026
994e89d
test(preflight): add compositor-authority white-overprint and transpa…
mberrys Oct 1, 2026
249444a
feat(preflight): judge white overprint and transparency interaction o…
mberrys Oct 1, 2026
c63055e
fix(preflight): count only real transparency as overprint interaction…
mberrys Oct 1, 2026
705a04e
docs(preflight): land white-overprint-renderer and transparency-rip-i…
mberrys Oct 1, 2026
4fa8558
style(preflight): clang-format the files touched for the compositor p…
mberrys Oct 1, 2026
6ea3891
test(preflight): add sparse-mark bleed regression fixtures (#120)
mberrys Oct 1, 2026
fcecd63
wip(preflight): bleed strip coverage floor and unconfirmed edges (#120)
mberrys Oct 1, 2026
7eebe64
wip(preflight): confirm bleed edges by solid coverage floor (#120)
mberrys Oct 1, 2026
2573b17
wip(preflight): manifest expectations (#120)
mberrys Oct 1, 2026
830d8eb
wip(preflight): bleed coverage tests (#120)
mberrys Oct 1, 2026
e88ec5a
docs(preflight): land bleed-raster-strip-depth, document coverage flo…
mberrys Oct 1, 2026
0559ee6
style(preflight): clang-format bleed probe and engine (#120)
mberrys Oct 1, 2026
32f03dd
perf(preflight): track occlusion only for obscured-content
mberrys Oct 1, 2026
998bbbf
fix(preflight): key compositor probes by dpi and scan nested overprint
mberrys Oct 1, 2026
79d44ad
fix(preflight): do not count keylines or bowties as solid bleed
mberrys Oct 1, 2026
1b1bf0b
fix(preflight): confirm bleed from painted area under the clip
mberrys Oct 1, 2026
bd58484
fix(architecture): add UnitTestsBleedMarginProbe proof lane
mberrys Oct 1, 2026
04b2b68
fix(preflight): bound bleed content by clip-aware paintable bounds
mberrys Oct 1, 2026
f15c944
docs(changes): drop resolved stroked-rectangle residual limit
mberrys Oct 1, 2026
6ce3659
Merge #159 (issue-118 clip-aware geometry) into issue-119 branch
claude Oct 1, 2026
716e8f1
Merge issue-119 branch (includes #159) into issue-120 branch
claude Oct 1, 2026
73bb522
Merge dev into issue-118 branch
claude Oct 1, 2026
2c3f618
Merge issue-118 branch (with dev) into issue-119 branch
claude Oct 1, 2026
d026746
Merge issue-119 branch (with dev) into issue-120 branch
claude Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
328 changes: 252 additions & 76 deletions LoopLibCore/sources/pdfbleedmarginprobe.cpp

Large diffs are not rendered by default.

35 changes: 27 additions & 8 deletions LoopLibCore/sources/pdfbleedmarginprobe.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ struct LOOPLIBCORESHARED_EXPORT PDFBleedMarginProbeSettings
QMarginsF bleedMM = QMarginsF(3.0, 3.0, 3.0, 3.0);
bool fastOnly = false;
qint64 maxRasterPixels = 250LL * 1000 * 1000;
/// Share of a bleed strip that artwork must cover (by bounds) or ink (by raster)
/// before the edge counts as populated. A margin holding only a few stray marks is
/// below it and is reported as empty.
qreal minEdgeCoverage = 0.10;
};

struct LOOPLIBCORESHARED_EXPORT PDFBleedMarginProbeEdgeResult
Expand All @@ -49,6 +53,12 @@ struct LOOPLIBCORESHARED_EXPORT PDFBleedMarginProbeEdgeResult
int inkPixels = 0;
int totalPixels = 0;
QRectF stripRect;
/// Upper bound of the share of the strip covered by artwork bounds (0..1), set by the fast pass.
qreal boundsCoverage = 0.0;
/// False when hasContent rests on bounds that touch enough of the strip, but the filled
/// geometry (images, filled paths and glyphs, clipped) paints less than minEdgeCoverage of
/// it, and no raster measurement confirmed it.
bool confirmed = true;
};

struct LOOPLIBCORESHARED_EXPORT PDFBleedMarginProbeResult
Expand All @@ -62,22 +72,31 @@ struct LOOPLIBCORESHARED_EXPORT PDFBleedMarginProbeResult
{
return left.hasContent && right.hasContent && top.hasContent && bottom.hasContent;
}

bool allEdgesConfirmed() const
{
return left.confirmed && right.confirmed && top.confirmed && bottom.confirmed;
}
};

/// Probes whether rendered artwork on a page extends into the bleed margin.
///
/// Fast path: unions bounding rects from `PDFPrecompiledPage::calculateGraphicPieceInfos`
/// and compares against the reference box expanded by the bleed amount. No rasterization.
/// Fast path: measures how much of each bleed strip the bounding rects from
/// `PDFPrecompiledPage::calculateGraphicPieceInfos` cover. A strip is populated when that
/// coverage reaches `minEdgeCoverage`, so a few stray marks do not count. No rasterization.
/// The edge is confirmed only when images and filled paths, cut by their clip, actually paint
/// `minEdgeCoverage` of the strip; strokes alone leave it unconfirmed.
///
/// Raster path (raster_confirm): renders only the four edge strips at probe_dpi
/// and counts non-background pixels against a threshold.
/// Raster path (raster_confirm): renders the four edge strips at probe_dpi and counts
/// non-background pixels against `minEdgeCoverage`. It confirms or demotes every edge the bounds
/// pass could not prove solid, and upgrades empty edges that the raster finds inked.
class LOOPLIBCORESHARED_EXPORT PDFBleedMarginProbe
{
public:
explicit PDFBleedMarginProbe(PDFDocumentSession* session);

/// Full probe: fast bounds pass first, then raster confirmation if the settings
/// request it and the fast pass flagged missing content on any side.
/// Full probe: fast bounds pass first, then raster confirmation if the settings request it
/// and the fast pass left any side empty or unconfirmed.
PDFBleedMarginProbeResult probe(const PDFPage* page,
size_t pageIndex,
const PDFBleedMarginProbeSettings& settings);
Expand All @@ -98,6 +117,6 @@ class LOOPLIBCORESHARED_EXPORT PDFBleedMarginProbe
PDFDocumentSession* m_session;
};

} // namespace pdf
} // namespace pdf

#endif // PDFBLEEDMARGINPROBE_H
#endif // PDFBLEEDMARGINPROBE_H
137 changes: 106 additions & 31 deletions LoopLibCore/sources/pdfinkcoverageprobe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,52 @@ QRectF resolveAnalysisBox(const PDFPage* page, PDFInkCoverageAnalysisBox request
return media;
}

/// Device raster of an analysis box at a resolution. An empty size means the box
/// cannot be rasterized.
struct CompositorRaster
{
QSize size;
QTransform pagePointToDevice;
bool budgetExceeded = false;
};

CompositorRaster planCompositorRaster(const PDFPage* page, const QRectF& analysisBox, int dpi, qint64 maxRasterPixels)
{
CompositorRaster raster;
const PageRotation pageRotation = page->getPageRotation();
const QRectF rotatedAnalysisBox = PDFPage::getRotatedBox(analysisBox, pageRotation).normalized();
const qreal pointToPixel = static_cast<qreal>(dpi) / 72.0;
const double widthReal = std::ceil(rotatedAnalysisBox.width() * pointToPixel);
const double heightReal = std::ceil(rotatedAnalysisBox.height() * pointToPixel);

if (!std::isfinite(widthReal) || !std::isfinite(heightReal) || widthReal <= 0.0 || heightReal <= 0.0 || widthReal > static_cast<double>(std::numeric_limits<int>::max()) || heightReal > static_cast<double>(std::numeric_limits<int>::max()))
{
return raster;
}

const int width = qMax(1, static_cast<int>(widthReal));
const int height = qMax(1, static_cast<int>(heightReal));
if (maxRasterPixels > 0 && static_cast<qint64>(width) * static_cast<qint64>(height) > maxRasterPixels)
{
raster.budgetExceeded = true;
return raster;
}

raster.size = QSize(width, height);
raster.pagePointToDevice = PDFRenderer::createMediaBoxToDevicePointMatrix(rotatedAnalysisBox, QRect(QPoint(0, 0), raster.size), pageRotation);
return raster;
}

PDFTransparencyRendererSettings compositorRendererSettings()
{
PDFTransparencyRendererSettings rendererSettings;
rendererSettings.flags.setFlag(PDFTransparencyRendererSettings::ActiveColorMask, false);
rendererSettings.flags.setFlag(PDFTransparencyRendererSettings::SeparationSimulation, true);
rendererSettings.activeColorMask = PDFPixelFormat::getAllColorsMask();
rendererSettings.renderPolicy = PDFRenderPolicy::forPreflightAnalysis();
return rendererSettings;
}

} // namespace

PDFInkCoverageProbe::PDFInkCoverageProbe(PDFDocumentSession* session) :
Expand Down Expand Up @@ -114,39 +160,15 @@ PDFInkCoverageProbeResult PDFInkCoverageProbe::probe(const PDFPage* page,
return result;
}

const PageRotation pageRotation = page->getPageRotation();
const QRectF rotatedAnalysisBox = PDFPage::getRotatedBox(analysisBox, pageRotation).normalized();
const QSizeF mediaSize = rotatedAnalysisBox.size();
const qreal pointToPixel = static_cast<qreal>(settings.dpi) / 72.0;
const double widthReal = std::ceil(mediaSize.width() * pointToPixel);
const double heightReal = std::ceil(mediaSize.height() * pointToPixel);

if (!std::isfinite(widthReal) || !std::isfinite(heightReal) || widthReal <= 0.0 || heightReal <= 0.0 || widthReal > static_cast<double>(std::numeric_limits<int>::max()) || heightReal > static_cast<double>(std::numeric_limits<int>::max()))
{
return result;
}

const int width = qMax(1, static_cast<int>(widthReal));
const int height = qMax(1, static_cast<int>(heightReal));
const qint64 rasterPixels = static_cast<qint64>(width) * static_cast<qint64>(height);
if (settings.maxRasterPixels > 0 && rasterPixels > settings.maxRasterPixels)
const CompositorRaster raster = planCompositorRaster(page, analysisBox, settings.dpi, settings.maxRasterPixels);
result.budgetExceeded = raster.budgetExceeded;
if (raster.size.isEmpty())
{
result.budgetExceeded = true;
return result;
}

PDFTransparencyRendererSettings rendererSettings;
PDFTransparencyRendererSettings rendererSettings = compositorRendererSettings();
rendererSettings.flags.setFlag(PDFTransparencyRendererSettings::SaveOriginalProcessImage, true);
rendererSettings.flags.setFlag(PDFTransparencyRendererSettings::ActiveColorMask, false);
rendererSettings.flags.setFlag(PDFTransparencyRendererSettings::SeparationSimulation, true);
rendererSettings.activeColorMask = PDFPixelFormat::getAllColorsMask();
rendererSettings.renderPolicy = PDFRenderPolicy::forPreflightAnalysis();

const QSize imageSize(width, height);
const QTransform pagePointToDevice = PDFRenderer::createMediaBoxToDevicePointMatrix(
rotatedAnalysisBox,
QRect(QPoint(0, 0), imageSize),
pageRotation);
PDFInkMapper inkMapper(nullptr, document);
inkMapper.createSpotColors(true);

Expand All @@ -157,8 +179,8 @@ PDFInkCoverageProbeResult PDFInkCoverageProbe::probe(const PDFPage* page,
m_session->getOptionalContentActivity(),
&inkMapper,
rendererSettings,
pagePointToDevice);
renderer.beginPaint(imageSize);
raster.pagePointToDevice);
renderer.beginPaint(raster.size);
renderer.processContents();
renderer.endPaint();
result.diagnostics = renderer.getRenderDiagnostics();
Expand Down Expand Up @@ -269,7 +291,7 @@ PDFInkCoverageProbeResult PDFInkCoverageProbe::probe(const PDFPage* page,
}

bool invertible = false;
const QTransform deviceToPage = pagePointToDevice.inverted(&invertible);
const QTransform deviceToPage = raster.pagePointToDevice.inverted(&invertible);
if (!invertible)
{
result.regions.clear();
Expand Down Expand Up @@ -310,4 +332,57 @@ PDFInkCoverageProbeResult PDFInkCoverageProbe::probe(const PDFPage* page,
return result;
}

PDFOverprintProbe::PDFOverprintProbe(PDFDocumentSession* session) :
m_session(session)
{
}

PDFOverprintProbeResult PDFOverprintProbe::probe(const PDFPage* page, int dpi, qint64 maxRasterPixels)
{
PDFOverprintProbeResult result;
if (!page || !m_session || dpi <= 0)
{
return result;
}

PDFDocument* document = m_session->getDocument();
if (!document)
{
return result;
}

const QRectF analysisBox = resolveAnalysisBox(page, PDFInkCoverageAnalysisBox::Bleed);
if (!isUsableBox(analysisBox))
{
return result;
}

const CompositorRaster raster = planCompositorRaster(page, analysisBox, dpi, maxRasterPixels);
result.budgetExceeded = raster.budgetExceeded;
if (raster.size.isEmpty())
{
return result;
}

PDFInkMapper inkMapper(nullptr, document);
inkMapper.createSpotColors(true);

PDFTransparencyRenderer renderer(page,
document,
m_session->getFontCache(),
m_session->getCMS(),
m_session->getOptionalContentActivity(),
&inkMapper,
compositorRendererSettings(),
raster.pagePointToDevice);
renderer.beginPaint(raster.size);
renderer.processContents();
renderer.endPaint();

result.rendered = true;
result.diagnostics = renderer.getRenderDiagnostics();
result.observation = renderer.getOverprintObservation();
return result;
}

} // namespace pdf
21 changes: 21 additions & 0 deletions LoopLibCore/sources/pdfinkcoverageprobe.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,27 @@ class LOOPLIBCORESHARED_EXPORT PDFInkCoverageProbe
PDFDocumentSession* m_session;
};

struct LOOPLIBCORESHARED_EXPORT PDFOverprintProbeResult
{
bool rendered = false; // false when rasterization was unavailable or over budget
bool budgetExceeded = false;
PDFRenderDiagnostics diagnostics;
PDFOverprintObservation observation;
};

/// Renders a page on the overprint-accurate compositor (the Output Preview path) and
/// reports the overprint that compositor applied, instead of reading page-view state.
class LOOPLIBCORESHARED_EXPORT PDFOverprintProbe
{
public:
explicit PDFOverprintProbe(PDFDocumentSession* session);

PDFOverprintProbeResult probe(const PDFPage* page, int dpi, qint64 maxRasterPixels);

private:
PDFDocumentSession* m_session;
};

} // namespace pdf

#endif // PDFINKCOVERAGEPROBE_H
12 changes: 11 additions & 1 deletion LoopLibCore/sources/pdfpainter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,8 @@ PDFPrecompiledPage::GraphicPieceInfos PDFPrecompiledPage::calculateGraphicPieceI
struct State
{
QTransform matrix;
QPainterPath clipPath;
bool clipped = false;
};
std::stack<State> stateStack;
stateStack.emplace();
Expand Down Expand Up @@ -926,6 +928,9 @@ PDFPrecompiledPage::GraphicPieceInfos PDFPrecompiledPage::calculateGraphicPieceI
info.type = data.isText ? GraphicPieceInfo::Type::Text : GraphicPieceInfo::Type::VectorGraphics;
info.boundingRect = pagePath.controlPointRect();
info.pagePath = pagePath;
info.isFilled = data.brush.style() != Qt::NoBrush;
info.isClipped = stateStack.top().clipped;
info.clipPath = stateStack.top().clipPath;

const int elementCount = pagePath.elementCount();
for (int i = 0; i < elementCount; ++i)
Expand Down Expand Up @@ -976,6 +981,8 @@ PDFPrecompiledPage::GraphicPieceInfos PDFPrecompiledPage::calculateGraphicPieceI
info.type = GraphicPieceInfo::Type::Image;
info.boundingRect = pagePath.controlPointRect();
info.pagePath = pagePath;
info.isClipped = stateStack.top().clipped;
info.clipPath = stateStack.top().clipPath;

const int elementCount = pagePath.elementCount();
for (int i = 0; i < elementCount; ++i)
Expand Down Expand Up @@ -1059,7 +1066,10 @@ PDFPrecompiledPage::GraphicPieceInfos PDFPrecompiledPage::calculateGraphicPieceI

case InstructionType::Clip:
{
// Do nothing, we are just collecting information
State& state = stateStack.top();
const QPainterPath pageClip = state.matrix.map(m_clips[instruction.dataIndex].clipPath);
state.clipPath = state.clipped ? state.clipPath.intersected(pageClip) : pageClip;
state.clipped = true;
break;
}

Expand Down
3 changes: 3 additions & 0 deletions LoopLibCore/sources/pdfpainter.h
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,9 @@ class LOOPLIBCORESHARED_EXPORT PDFPrecompiledPage
std::array<uint8_t, 64> hash = {}; ///< Hash of all data
std::array<uint8_t, 64> imageHash = {}; ///< Hash of the image only
QPainterPath pagePath;
bool isFilled = false; ///< Path piece painted with a fill brush (pagePath is the unstroked path)
bool isClipped = false; ///< A clip was in effect when the piece was painted
QPainterPath clipPath; ///< Clip in effect, in page coordinates (valid when isClipped)
};

using GraphicPieceInfos = std::vector<GraphicPieceInfo>;
Expand Down
Loading
Loading