mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-05-31 07:11:00 +02:00
Merge pull request #21242 from calixteman/knockout
Render knockout transparency groups
This commit is contained in:
commit
0c66063cd4
@ -544,6 +544,47 @@ const NORMAL_CLIP = {};
|
||||
const EO_CLIP = {};
|
||||
|
||||
class CanvasGraphics {
|
||||
// Knockout group support fields.
|
||||
#knockoutGroupLevel = 0;
|
||||
|
||||
#knockoutElementDepth = 0;
|
||||
|
||||
#knockoutTempCanvasEntry = null;
|
||||
|
||||
#knockoutSavedCtx = null;
|
||||
|
||||
#knockoutSavedSMaskCtx = null;
|
||||
|
||||
// Parent ctx globalCompositeOperation (GCO) at element start. Restored on
|
||||
// tempCtx before the post-element copyCtxState so the saved ctx keeps its
|
||||
// blend mode.
|
||||
#knockoutSavedGCO = null;
|
||||
|
||||
#knockoutElementAlpha = 1;
|
||||
|
||||
/**
|
||||
* Lazy alpha-scaling filter cache, populated on the first translucent
|
||||
* knockout element. One of:
|
||||
* - `Map<alpha, url>` - when `ctx.filter` is supported; one SVG filter
|
||||
* per quantised alpha_s value (cache bounded by 8-bit alpha precision).
|
||||
* - `"none"` - no DOM available; the JS pixel-loop fallback handles
|
||||
* scaling instead.
|
||||
* Stays `undefined` until the first translucent element forces a resolve.
|
||||
* @type {Map<number, string> | "none" | undefined}
|
||||
*/
|
||||
#knockoutFilterCache;
|
||||
|
||||
// Snapshot of #groupStackMeta.at(-1) at element-begin so the right backdrop
|
||||
// is used even if nested groups push/pop during the element's lifetime.
|
||||
#knockoutElementGroupMeta = null;
|
||||
|
||||
// Per-group metadata, aligned with `groupStack`. `null` for the no-canvas
|
||||
// fast path. Otherwise: `backdropCtx` (parent ctx for non-isolated KO,
|
||||
// read directly since it's frozen), `hasInnerBackdrop` (non-isolated
|
||||
// non-KO subgroup inside a KO parent), `savedKnockoutLevel` (level to
|
||||
// restore on exit), pixel offsets, and pooled scratch entries.
|
||||
#groupStackMeta = [];
|
||||
|
||||
constructor(
|
||||
canvasCtx,
|
||||
commonObjs,
|
||||
@ -791,6 +832,22 @@ class CanvasGraphics {
|
||||
this.tempSMask = null;
|
||||
this.smaskStack.length = 0;
|
||||
|
||||
// Drop knockout state in case rendering was cancelled mid-group. Pooled
|
||||
// temp/backdrop entries are owned by the meta and freed there; the
|
||||
// active-element fields just alias into the meta, so only clear them.
|
||||
for (const meta of this.#groupStackMeta) {
|
||||
this.#destroyKnockoutPools(meta);
|
||||
}
|
||||
this.#groupStackMeta.length = 0;
|
||||
this.#knockoutTempCanvasEntry = null;
|
||||
this.#knockoutSavedCtx = null;
|
||||
this.#knockoutSavedSMaskCtx = null;
|
||||
this.#knockoutSavedGCO = null;
|
||||
this.#knockoutElementAlpha = 1;
|
||||
this.#knockoutElementGroupMeta = null;
|
||||
this.#knockoutElementDepth = 0;
|
||||
this.#knockoutGroupLevel = 0;
|
||||
|
||||
this.cachedPatterns.clear();
|
||||
|
||||
for (const cache of this._cachedBitmapsMap.values()) {
|
||||
@ -916,7 +973,7 @@ class CanvasGraphics {
|
||||
paintHeight = newHeight;
|
||||
}
|
||||
|
||||
// writeEntry is now the stale buffer — destroy it.
|
||||
// writeEntry is now the stale buffer; destroy it.
|
||||
this.canvasFactory.destroy(writeEntry);
|
||||
return {
|
||||
img: readEntry.canvas,
|
||||
@ -1460,6 +1517,310 @@ class CanvasGraphics {
|
||||
this._clearPreparedSMask();
|
||||
}
|
||||
|
||||
#createKnockoutMaskCanvas(sourceCanvas, reuseEntry = null, alpha = 1) {
|
||||
const { width, height } = sourceCanvas;
|
||||
// reuseEntry is assumed to match sourceCanvas in size (all current call
|
||||
// sites guarantee this); the mask is rebuilt in-place.
|
||||
const maskEntry = reuseEntry ?? this.canvasFactory.create(width, height);
|
||||
const maskCtx = maskEntry.context;
|
||||
// Snap alpha_s to 8-bit precision: the painted alpha we're scaling is
|
||||
// already 8-bit, so any finer-grained alpha_s is indistinguishable. Caps
|
||||
// both the local Map and the filter-factory cache at <=256 entries
|
||||
// regardless of how many distinct gstate alpha values the PDF uses.
|
||||
alpha = Math.round(alpha * 255) / 255;
|
||||
const needsAlphaScaling = alpha < 1;
|
||||
if (needsAlphaScaling && this.#knockoutFilterCache === undefined) {
|
||||
// On Safari `ctx.filter` is settable but inert: the filter URL would
|
||||
// be stored without being applied, leaving the mask unscaled.
|
||||
// Force the JS fallback there.
|
||||
this.#knockoutFilterCache = FeatureTest.isCanvasFilterSupported
|
||||
? new Map()
|
||||
: "none";
|
||||
}
|
||||
let knockoutFilter = "none";
|
||||
if (needsAlphaScaling && this.#knockoutFilterCache instanceof Map) {
|
||||
knockoutFilter = this.#knockoutFilterCache.get(alpha);
|
||||
if (!knockoutFilter) {
|
||||
knockoutFilter = this.filterFactory.addKnockoutFilter(alpha);
|
||||
this.#knockoutFilterCache.set(alpha, knockoutFilter);
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsAlphaScaling || knockoutFilter !== "none") {
|
||||
// Reused entries may carry stale pixels. Avoid the
|
||||
// globalCompositeOperation = "copy" + filter combo: that pair is
|
||||
// browser-divergent.
|
||||
if (reuseEntry) {
|
||||
maskCtx.save();
|
||||
maskCtx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
maskCtx.clearRect(0, 0, width, height);
|
||||
maskCtx.restore();
|
||||
}
|
||||
maskCtx.filter = knockoutFilter;
|
||||
maskCtx.drawImage(sourceCanvas, 0, 0);
|
||||
maskCtx.filter = "none";
|
||||
return maskEntry;
|
||||
}
|
||||
|
||||
// No-DOM fallback (Node/embedded). Scale painted alpha back to shape
|
||||
// coverage; color channels are irrelevant for destination-out/in.
|
||||
const sourceData = sourceCanvas
|
||||
.getContext("2d", { willReadFrequently: true })
|
||||
.getImageData(0, 0, width, height);
|
||||
const maskData = maskCtx.createImageData(width, height);
|
||||
const sourcePixels = sourceData.data,
|
||||
maskPixels = maskData.data;
|
||||
const alphaScale = alpha > 0 ? 1 / alpha : 1e6;
|
||||
for (let i = 3, ii = sourcePixels.length; i < ii; i += 4) {
|
||||
maskPixels[i] = Math.min(Math.round(sourcePixels[i] * alphaScale), 255);
|
||||
}
|
||||
maskCtx.putImageData(maskData, 0, 0);
|
||||
return maskEntry;
|
||||
}
|
||||
|
||||
#getOrCreatePooledEntry(meta, key, width, height) {
|
||||
let entry = meta?.[key] ?? null;
|
||||
if (
|
||||
entry &&
|
||||
(entry.canvas.width !== width || entry.canvas.height !== height)
|
||||
) {
|
||||
this.canvasFactory.destroy(entry);
|
||||
entry = null;
|
||||
}
|
||||
if (!entry) {
|
||||
entry = this.canvasFactory.create(width, height);
|
||||
if (meta) {
|
||||
meta[key] = entry;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
// Reused entry: clear any stale pixels before the caller refills it.
|
||||
const ctx = entry.context;
|
||||
ctx.save();
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.restore();
|
||||
return entry;
|
||||
}
|
||||
|
||||
#compositeKnockoutSurface(destCtx, surfaceCanvas, options = {}) {
|
||||
const {
|
||||
// Backdrop canvas for non-isolated groups, or null for isolated.
|
||||
// Passed directly (no copy) since the parent canvas is frozen while
|
||||
// the group renders.
|
||||
backdropCanvas = null,
|
||||
// Transform for destCtx before the final draw. Identity is correct
|
||||
// when destCtx and surfaceCanvas share pixel coords (per-element
|
||||
// path); pass currentMtx for the endGroup subgroup-into-parent path.
|
||||
destTransform = [1, 0, 0, 1, 0, 0],
|
||||
// Pixel origin within backdropCanvas of the region that maps onto
|
||||
// surfaceCanvas. [0,0] when the backdrop is already pre-cropped;
|
||||
// pass the (possibly compounded) group offsets in endGroup.
|
||||
backdropOffset = [0, 0],
|
||||
// Pool entry to refill in place for the knockout mask. Caller owns
|
||||
// its lifetime when provided.
|
||||
reuseMaskEntry = null,
|
||||
// Group meta to pool the backdrop scratch on. Without it the scratch
|
||||
// is allocated and destroyed locally.
|
||||
poolMeta = null,
|
||||
// Per-element surfaces already have alpha/filter baked in (defaults
|
||||
// 1/"none"). Subgroup canvases don't, so endGroup passes the parent
|
||||
// values to apply only at the final draw.
|
||||
sourceAlpha = 1,
|
||||
sourceFilter = "none",
|
||||
knockoutAlpha = 1,
|
||||
} = options;
|
||||
const { width, height } = surfaceCanvas;
|
||||
const knockoutMaskEntry = this.#createKnockoutMaskCanvas(
|
||||
surfaceCanvas,
|
||||
reuseMaskEntry,
|
||||
knockoutAlpha
|
||||
);
|
||||
const sourceCompositeOperation = destCtx.globalCompositeOperation;
|
||||
|
||||
destCtx.save();
|
||||
destCtx.setTransform(...destTransform);
|
||||
destCtx.globalAlpha = 1;
|
||||
if (FeatureTest.isCanvasFilterSupported) {
|
||||
destCtx.filter = "none";
|
||||
}
|
||||
|
||||
// Erase prior group content wherever the new surface has any coverage.
|
||||
destCtx.globalCompositeOperation = "destination-out";
|
||||
destCtx.drawImage(knockoutMaskEntry.canvas, 0, 0);
|
||||
|
||||
if (backdropCanvas) {
|
||||
// Non-isolated: refill the just-erased footprint with the backdrop,
|
||||
// pre-clipped to the same shape mask so non-element pixels stay
|
||||
// transparent (otherwise sparse groups bleed the backdrop rect).
|
||||
const [bx, by] = backdropOffset;
|
||||
const backdropEntry = this.#getOrCreatePooledEntry(
|
||||
poolMeta,
|
||||
"knockoutBackdropEntry",
|
||||
width,
|
||||
height
|
||||
);
|
||||
const backdropCtx = backdropEntry.context;
|
||||
backdropCtx.drawImage(
|
||||
backdropCanvas,
|
||||
bx,
|
||||
by,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height
|
||||
);
|
||||
backdropCtx.globalCompositeOperation = "destination-in";
|
||||
backdropCtx.drawImage(knockoutMaskEntry.canvas, 0, 0);
|
||||
// Reset the GCO so the pooled entry is in a known state for next use.
|
||||
backdropCtx.globalCompositeOperation = "source-over";
|
||||
|
||||
destCtx.globalCompositeOperation = "destination-over";
|
||||
destCtx.drawImage(backdropEntry.canvas, 0, 0);
|
||||
if (!poolMeta) {
|
||||
this.canvasFactory.destroy(backdropEntry);
|
||||
}
|
||||
}
|
||||
destCtx.globalCompositeOperation = sourceCompositeOperation;
|
||||
destCtx.globalAlpha = sourceAlpha;
|
||||
if (FeatureTest.isCanvasFilterSupported) {
|
||||
destCtx.filter = sourceFilter ?? "none";
|
||||
}
|
||||
destCtx.drawImage(surfaceCanvas, 0, 0);
|
||||
|
||||
destCtx.restore();
|
||||
if (!reuseMaskEntry) {
|
||||
this.canvasFactory.destroy(knockoutMaskEntry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a knockout element. In a KO group each element composites against
|
||||
* the initial group backdrop (transparent if isolated, parent canvas if
|
||||
* not) rather than against the running group result. We render onto a temp
|
||||
* canvas; path/clip/transform ops are mirrored back to the group canvas so
|
||||
* its state stays in sync for the next element.
|
||||
*
|
||||
* @returns {boolean} true if a knockout element was started.
|
||||
*/
|
||||
#beginKnockoutElement(alpha = 1) {
|
||||
if (
|
||||
this.#knockoutGroupLevel === 0 ||
|
||||
this.#knockoutElementDepth > 0 ||
|
||||
!this.contentVisible
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
this.#knockoutElementDepth++;
|
||||
this.#knockoutElementAlpha = alpha;
|
||||
const groupMeta = this.#groupStackMeta.at(-1);
|
||||
const { canvas } = this.ctx;
|
||||
const tempEntry = this.#getOrCreatePooledEntry(
|
||||
groupMeta,
|
||||
"knockoutTempEntry",
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
this.#knockoutTempCanvasEntry = tempEntry;
|
||||
const tempCtx = tempEntry.context;
|
||||
// Bracket-save before installing mirroring so #endKnockoutElement can
|
||||
// restore() the pooled canvas to a clean clip+transform without
|
||||
// propagating that save through the mirror.
|
||||
tempCtx.save();
|
||||
tempCtx.setTransform(this.ctx.getTransform());
|
||||
copyCtxState(this.ctx, tempCtx);
|
||||
// Force source-over for the element raster: the parent's blend mode is
|
||||
// meant for the final composite back onto the group canvas (done by
|
||||
// #compositeKnockoutSurface), not for drawing onto a transparent temp
|
||||
// (e.g. multiply on alpha=0 zeros the element's colour). Stash the
|
||||
// parent GCO and re-apply it on tempCtx before the post-element
|
||||
// copyCtxState so the saved ctx keeps the parent blend mode.
|
||||
this.#knockoutSavedGCO = tempCtx.globalCompositeOperation;
|
||||
tempCtx.globalCompositeOperation = "source-over";
|
||||
mirrorContextOperations(tempCtx, this.ctx);
|
||||
this.#knockoutElementGroupMeta = groupMeta;
|
||||
this.#knockoutSavedCtx = this.ctx;
|
||||
this.#knockoutSavedSMaskCtx = this.suspendedCtx;
|
||||
this.ctx = tempCtx;
|
||||
if (this.inSMaskMode) {
|
||||
this.suspendedCtx = tempCtx;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* End a knockout element started by `#beginKnockoutElement`. Composites
|
||||
* the rendered surface onto the group canvas with KO semantics: build a
|
||||
* shape mask from the element (painted alpha scaled back to geometric
|
||||
* coverage when alpha_s < 1), destination-out the group canvas over that
|
||||
* mask, restore the initial backdrop into the cleared footprint
|
||||
* (non-isolated only), then paint the element on top.
|
||||
*
|
||||
* @param {boolean} started - the value returned by `#beginKnockoutElement`.
|
||||
*/
|
||||
#endKnockoutElement(started) {
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
const tempEntry = this.#knockoutTempCanvasEntry;
|
||||
const savedCtx = this.#knockoutSavedCtx;
|
||||
const savedSMaskCtx = this.#knockoutSavedSMaskCtx;
|
||||
const tempCtx = tempEntry.context;
|
||||
this.#knockoutTempCanvasEntry = null;
|
||||
this.#knockoutSavedCtx = null;
|
||||
this.#knockoutSavedSMaskCtx = null;
|
||||
if (
|
||||
this.inSMaskMode &&
|
||||
this.suspendedCtx === tempCtx &&
|
||||
this.ctx !== tempCtx
|
||||
) {
|
||||
this.endSMaskMode();
|
||||
}
|
||||
if (this.inSMaskMode) {
|
||||
this.suspendedCtx = savedSMaskCtx;
|
||||
}
|
||||
this.ctx._removeMirroring();
|
||||
// Re-apply the parent GCO before copyCtxState writes it back to
|
||||
// savedCtx so #compositeKnockoutSurface sees the original blend mode.
|
||||
this.ctx.globalCompositeOperation = this.#knockoutSavedGCO;
|
||||
this.#knockoutSavedGCO = null;
|
||||
copyCtxState(this.ctx, savedCtx);
|
||||
this.ctx = savedCtx;
|
||||
const groupMeta = this.#knockoutElementGroupMeta;
|
||||
this.#knockoutElementGroupMeta = null;
|
||||
const knockoutAlpha = this.#knockoutElementAlpha;
|
||||
this.#knockoutElementAlpha = 1;
|
||||
try {
|
||||
this.#compositeKnockoutSurface(
|
||||
savedSMaskCtx ?? savedCtx,
|
||||
tempEntry.canvas,
|
||||
{
|
||||
backdropCanvas: groupMeta?.backdropCtx?.canvas ?? null,
|
||||
backdropOffset: groupMeta?.backdropCtx
|
||||
? [groupMeta.offsetX, groupMeta.offsetY]
|
||||
: [0, 0],
|
||||
reuseMaskEntry: groupMeta?.knockoutMaskEntry ?? null,
|
||||
poolMeta: groupMeta,
|
||||
knockoutAlpha,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
// Pop the begin-element bracket save so the pooled canvas re-enters
|
||||
// with a clean clip+transform stack.
|
||||
tempCtx.restore();
|
||||
// Decrement only after the canvas is fully reset, so a re-entry from
|
||||
// a compositing callback sees depth>0 and bails out.
|
||||
this.#knockoutElementDepth--;
|
||||
// Defensive: groupMeta is non-null in practice for any active KO
|
||||
// element, but if it isn't we must release the unpooled entry.
|
||||
if (!groupMeta) {
|
||||
this.canvasFactory.destroy(tempEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compose(dirtyBox) {
|
||||
if (!this.current.activeSMask) {
|
||||
return;
|
||||
@ -1477,10 +1838,21 @@ class CanvasGraphics {
|
||||
: [0, 0, this.ctx.canvas.width, this.ctx.canvas.height];
|
||||
const smask = this.current.activeSMask;
|
||||
const suspendedCtx = this.suspendedCtx;
|
||||
const applySMaskInPlace =
|
||||
this.#knockoutElementDepth > 0 && suspendedCtx === this.ctx;
|
||||
|
||||
this.composeSMask(
|
||||
applySMaskInPlace ? null : suspendedCtx,
|
||||
smask,
|
||||
this.ctx,
|
||||
dirtyBox
|
||||
);
|
||||
if (applySMaskInPlace) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.composeSMask(suspendedCtx, smask, this.ctx, dirtyBox);
|
||||
// Whatever was drawn has been moved to the suspended canvas, now clear it
|
||||
// out of the current canvas. Only the dirty box region needs clearing —
|
||||
// out of the current canvas. Only the dirty box region needs clearing;
|
||||
// everything outside it is already transparent.
|
||||
this.ctx.save();
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
@ -1574,6 +1946,10 @@ class CanvasGraphics {
|
||||
);
|
||||
}
|
||||
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = smask.blendMode || "source-over";
|
||||
@ -1807,6 +2183,8 @@ class CanvasGraphics {
|
||||
}
|
||||
|
||||
stroke(opIdx, path, consumePath = true) {
|
||||
const started =
|
||||
consumePath && this.#beginKnockoutElement(this.current.strokeAlpha);
|
||||
const ctx = this.ctx;
|
||||
const strokeColor = this.current.strokeColor;
|
||||
// For stroke we want to temporarily change the global alpha to the
|
||||
@ -1855,6 +2233,7 @@ class CanvasGraphics {
|
||||
|
||||
// Restore the global alpha to the fill alpha
|
||||
ctx.globalAlpha = this.current.fillAlpha;
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
closeStroke(opIdx, path) {
|
||||
@ -1862,6 +2241,8 @@ class CanvasGraphics {
|
||||
}
|
||||
|
||||
fill(opIdx, path, consumePath = true) {
|
||||
const started =
|
||||
consumePath && this.#beginKnockoutElement(this.current.fillAlpha);
|
||||
const ctx = this.ctx;
|
||||
const fillColor = this.current.fillColor;
|
||||
const isPatternFill = this.current.patternFill;
|
||||
@ -1881,6 +2262,7 @@ class CanvasGraphics {
|
||||
this.consumePath(opIdx, path, intersect);
|
||||
}
|
||||
this.current.tilingPatternDims = null;
|
||||
this.#endKnockoutElement(started);
|
||||
return;
|
||||
}
|
||||
const baseTransform = fillColor.isModifyingCurrentTransform()
|
||||
@ -1922,6 +2304,7 @@ class CanvasGraphics {
|
||||
if (consumePath) {
|
||||
this.consumePath(opIdx, path, intersect);
|
||||
}
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
eoFill(opIdx, path) {
|
||||
@ -1930,10 +2313,18 @@ class CanvasGraphics {
|
||||
}
|
||||
|
||||
fillStroke(opIdx, path) {
|
||||
// Fill and stroke share one KO element so they composite against the
|
||||
// initial backdrop once, not twice. Use the smaller of the two alpha_s as
|
||||
// the mask divisor: it's conservative (over-clamps the other pass's
|
||||
// mask towards 1) but keeps the mask coverage at least as large as the
|
||||
// union of fill+stroke shapes, which is what KO erasure wants.
|
||||
const started = this.#beginKnockoutElement(
|
||||
Math.min(this.current.fillAlpha, this.current.strokeAlpha)
|
||||
);
|
||||
this.fill(opIdx, path, false);
|
||||
this.stroke(opIdx, path, false);
|
||||
|
||||
this.consumePath(opIdx, path);
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
eoFillStroke(opIdx, path) {
|
||||
@ -1955,10 +2346,12 @@ class CanvasGraphics {
|
||||
}
|
||||
|
||||
rawFillPath(opIdx, path) {
|
||||
const started = this.#beginKnockoutElement(this.current.fillAlpha);
|
||||
this.ctx.fill(path);
|
||||
this.dependencyTracker
|
||||
?.recordDependencies(opIdx, Dependencies.rawFillPath)
|
||||
.recordOperation(opIdx);
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
// Clipping
|
||||
@ -2332,8 +2725,10 @@ class CanvasGraphics {
|
||||
const current = this.current;
|
||||
const font = current.font;
|
||||
if (font.isType3Font) {
|
||||
const started = this.#beginKnockoutElement(current.fillAlpha);
|
||||
this.showType3Text(opIdx, glyphs);
|
||||
this.dependencyTracker?.recordShowTextOperation(opIdx);
|
||||
this.#endKnockoutElement(started);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -2343,6 +2738,7 @@ class CanvasGraphics {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const started = this.#beginKnockoutElement(current.fillAlpha);
|
||||
const ctx = this.ctx;
|
||||
const fontSizeScale = current.fontSizeScale;
|
||||
const charSpacing = current.charSpacing;
|
||||
@ -2456,6 +2852,7 @@ class CanvasGraphics {
|
||||
current.x += width * widthAdvanceScale * textHScale;
|
||||
ctx.restore();
|
||||
this.compose();
|
||||
this.#endKnockoutElement(started);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -2571,6 +2968,7 @@ class CanvasGraphics {
|
||||
this.compose();
|
||||
|
||||
this.dependencyTracker?.recordShowTextOperation(opIdx);
|
||||
this.#endKnockoutElement(started);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -2763,6 +3161,7 @@ class CanvasGraphics {
|
||||
if (!this.contentVisible) {
|
||||
return;
|
||||
}
|
||||
const started = this.#beginKnockoutElement(this.current.fillAlpha);
|
||||
const ctx = this.ctx;
|
||||
|
||||
this.save(opIdx);
|
||||
@ -2803,6 +3202,7 @@ class CanvasGraphics {
|
||||
|
||||
this.compose(this.current.getClippedPathBoundingBox());
|
||||
this.restore(opIdx);
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
// Images
|
||||
@ -2864,31 +3264,14 @@ class CanvasGraphics {
|
||||
}
|
||||
|
||||
const currentCtx = this.ctx;
|
||||
// TODO non-isolated groups - according to Rik at adobe non-isolated
|
||||
// group results aren't usually that different and they even have tools
|
||||
// that ignore this setting. Notes from Rik on implementing:
|
||||
// - When you encounter an transparency group, create a new canvas with
|
||||
// the dimensions of the bbox
|
||||
// - copy the content from the previous canvas to the new canvas
|
||||
// - draw as usual
|
||||
// - remove the backdrop alpha:
|
||||
// alphaNew = 1 - (1 - alpha)/(1 - alphaBackdrop) with 'alpha' the alpha
|
||||
// value of your transparency group and 'alphaBackdrop' the alpha of the
|
||||
// backdrop
|
||||
// - remove background color:
|
||||
// colorNew = color - alphaNew *colorBackdrop /(1 - alphaNew)
|
||||
if (!group.isolated) {
|
||||
info("TODO: Support non-isolated groups.");
|
||||
}
|
||||
|
||||
// TODO knockout - supposedly possible with the clever use of compositing
|
||||
// modes.
|
||||
if (group.knockout) {
|
||||
warn("Knockout groups not supported.");
|
||||
if (!group.isolated && !group.knockout && this.#knockoutGroupLevel === 0) {
|
||||
info("TODO: Fully support non-isolated non-knockout groups.");
|
||||
}
|
||||
|
||||
if (
|
||||
!group.needsIsolation &&
|
||||
!group.knockout &&
|
||||
this.#knockoutGroupLevel === 0 &&
|
||||
currentCtx.globalAlpha === 1 &&
|
||||
currentCtx.globalCompositeOperation === "source-over" &&
|
||||
!inSMaskMode
|
||||
@ -2905,6 +3288,7 @@ class CanvasGraphics {
|
||||
currentCtx.clip(clip);
|
||||
}
|
||||
this.groupStack.push(null); // null = no intermediate canvas
|
||||
this.#groupStackMeta.push(null);
|
||||
this.groupLevel++;
|
||||
return;
|
||||
}
|
||||
@ -2952,6 +3336,37 @@ class CanvasGraphics {
|
||||
this.smaskGroupCanvases.push(scratchCanvas);
|
||||
}
|
||||
const groupCtx = scratchCanvas.context;
|
||||
// Non-isolated KO: keep a reference to the parent ctx (not a copy). It's
|
||||
// frozen while the group renders, so we can read from it on demand. The
|
||||
// backdrop is only restored under each element's footprint in
|
||||
// #compositeKnockoutSurface so it doesn't become part of the group
|
||||
// source itself.
|
||||
const backdropCtx = group.knockout && !group.isolated ? currentCtx : null;
|
||||
// Non-isolated non-KO subgroup inside a KO parent: at endGroup we'll
|
||||
// blend its elements against the outer KO running canvas (also frozen),
|
||||
// so just record the flag here and read ctx.canvas at composite time.
|
||||
const hasInnerBackdrop =
|
||||
!group.isolated &&
|
||||
!group.knockout &&
|
||||
!group.smask &&
|
||||
this.#knockoutGroupLevel > 0;
|
||||
|
||||
// Pool the per-element shape mask for the lifetime of this KO group.
|
||||
// Non-KO groups never call #compositeKnockoutSurface for their own
|
||||
// elements so the entry is unused there.
|
||||
const knockoutMaskEntry = group.knockout
|
||||
? this.canvasFactory.create(drawnWidth, drawnHeight)
|
||||
: null;
|
||||
|
||||
// For KO groups bump the level so inner elements get KO treatment; for
|
||||
// non-KO groups reset to 0 so an ancestor KO group doesn't apply to
|
||||
// them. Restored on endGroup.
|
||||
const savedKnockoutLevel = this.#knockoutGroupLevel;
|
||||
if (group.knockout) {
|
||||
this.#knockoutGroupLevel++;
|
||||
} else {
|
||||
this.#knockoutGroupLevel = 0;
|
||||
}
|
||||
|
||||
// Since we created a new canvas that is just the size of the bounding box
|
||||
// we have to translate the group ctx.
|
||||
@ -3032,6 +3447,17 @@ class CanvasGraphics {
|
||||
["TR", null],
|
||||
]);
|
||||
this.groupStack.push(currentCtx);
|
||||
this.#groupStackMeta.push({
|
||||
backdropCtx,
|
||||
savedKnockoutLevel,
|
||||
offsetX,
|
||||
offsetY,
|
||||
hasInnerBackdrop,
|
||||
knockoutMaskEntry,
|
||||
// Per-group scratch pools, lazily filled and freed in endGroup.
|
||||
knockoutTempEntry: null,
|
||||
knockoutBackdropEntry: null,
|
||||
});
|
||||
this.groupLevel++;
|
||||
}
|
||||
|
||||
@ -3042,6 +3468,12 @@ class CanvasGraphics {
|
||||
this.groupLevel--;
|
||||
const groupCtx = this.ctx;
|
||||
const ctx = this.groupStack.pop();
|
||||
const groupMeta = this.#groupStackMeta.pop();
|
||||
// Restore the knockout level that was in effect before this group began.
|
||||
// Simple groups (groupMeta === null) never modify the level, so skip them.
|
||||
if (groupMeta) {
|
||||
this.#knockoutGroupLevel = groupMeta.savedKnockoutLevel;
|
||||
}
|
||||
if (ctx === null) {
|
||||
// Simple group: content was drawn directly on the parent canvas.
|
||||
this.restore(opIdx);
|
||||
@ -3068,6 +3500,7 @@ class CanvasGraphics {
|
||||
this.ctx.setTransform(this.suspendedCtx.getTransform());
|
||||
}
|
||||
}
|
||||
this.#destroyKnockoutPools(groupMeta);
|
||||
} else {
|
||||
this.ctx.restore();
|
||||
const currentMtx = getCurrentTransform(this.ctx);
|
||||
@ -3080,16 +3513,122 @@ class CanvasGraphics {
|
||||
currentMtx,
|
||||
dirtyBox
|
||||
);
|
||||
this.ctx.drawImage(groupCtx.canvas, 0, 0);
|
||||
const parentGroupMeta = this.#groupStackMeta.at(-1);
|
||||
if (this.#knockoutGroupLevel > 0) {
|
||||
// The subgroup is one element of the enclosing KO group, so
|
||||
// composite it with KO semantics. Two coord systems below:
|
||||
// - `currentMtx` (`destTransform`) places the subgroup canvas in
|
||||
// the parent on the final draw, like the non-KO `drawImage`.
|
||||
// - `groupMeta.offsetX/Y` are the pixel origins beginGroup stored
|
||||
// when sizing the scratch; we use them (not `currentMtx[4]/[5]`,
|
||||
// which are PDF-transform components) to crop the backdrop.
|
||||
if (groupMeta.hasInnerBackdrop) {
|
||||
// Non-isolated subgroup inside a KO parent: blend the elements
|
||||
// against the subgroup's own initial backdrop for colour, but use
|
||||
// the elements-only scratch as the alpha mask so transparent
|
||||
// areas don't erase the parent. `ctx` is the outer KO canvas
|
||||
// (just popped); its pixels still match the subgroup's
|
||||
// beginGroup state since the subgroup draws to its own scratch.
|
||||
const { width, height } = groupCtx.canvas;
|
||||
const colorEntry = this.canvasFactory.create(width, height);
|
||||
const colorCtx = colorEntry.context;
|
||||
colorCtx.drawImage(
|
||||
ctx.canvas,
|
||||
groupMeta.offsetX,
|
||||
groupMeta.offsetY,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height
|
||||
);
|
||||
colorCtx.globalCompositeOperation = "source-over";
|
||||
colorCtx.drawImage(groupCtx.canvas, 0, 0);
|
||||
// Clip colorEntry to the subgroup's element footprint so
|
||||
// backdrop pixels outside the elements don't bleed onto the
|
||||
// parent. Built with alpha=1 (no scaling) so the mask uses the
|
||||
// subgroup's composited painted alpha directly as shape - its
|
||||
// global gstate alpha gets applied at the final draw below. The
|
||||
// mask is sized to the subgroup canvas, so we can't reuse the
|
||||
// parent KO group's pooled mask here; allocate a fresh entry
|
||||
// and reuse it for both the destination-in and the
|
||||
// destination-out below.
|
||||
const shapeMaskEntry = this.#createKnockoutMaskCanvas(
|
||||
groupCtx.canvas
|
||||
);
|
||||
colorCtx.globalCompositeOperation = "destination-in";
|
||||
colorCtx.drawImage(shapeMaskEntry.canvas, 0, 0);
|
||||
// Inline the isolated-path compositing here so we can share
|
||||
// shapeMaskEntry with the destination-in above.
|
||||
const sourceCompositeOperation = this.ctx.globalCompositeOperation;
|
||||
const sourceAlpha = this.ctx.globalAlpha;
|
||||
const sourceFilter = this.ctx.filter;
|
||||
this.ctx.save();
|
||||
this.ctx.setTransform(...currentMtx);
|
||||
this.ctx.globalAlpha = 1;
|
||||
if (FeatureTest.isCanvasFilterSupported) {
|
||||
this.ctx.filter = "none";
|
||||
}
|
||||
this.ctx.globalCompositeOperation = "destination-out";
|
||||
this.ctx.drawImage(shapeMaskEntry.canvas, 0, 0);
|
||||
this.ctx.globalCompositeOperation = sourceCompositeOperation;
|
||||
this.ctx.globalAlpha = sourceAlpha;
|
||||
if (FeatureTest.isCanvasFilterSupported) {
|
||||
this.ctx.filter = sourceFilter ?? "none";
|
||||
}
|
||||
this.ctx.drawImage(colorEntry.canvas, 0, 0);
|
||||
this.ctx.restore();
|
||||
this.canvasFactory.destroy(shapeMaskEntry);
|
||||
this.canvasFactory.destroy(colorEntry);
|
||||
} else {
|
||||
// For a non-isolated KO parent the backdrop lives one level up.
|
||||
// Compound the parent's and subgroup's offsets to crop it.
|
||||
const backdropCtx = parentGroupMeta?.backdropCtx ?? null;
|
||||
this.#compositeKnockoutSurface(this.ctx, groupCtx.canvas, {
|
||||
backdropCanvas: backdropCtx?.canvas ?? null,
|
||||
destTransform: currentMtx,
|
||||
backdropOffset: backdropCtx
|
||||
? [
|
||||
parentGroupMeta.offsetX + groupMeta.offsetX,
|
||||
parentGroupMeta.offsetY + groupMeta.offsetY,
|
||||
]
|
||||
: [0, 0],
|
||||
sourceAlpha: this.ctx.globalAlpha,
|
||||
sourceFilter: this.ctx.filter,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.ctx.drawImage(groupCtx.canvas, 0, 0);
|
||||
}
|
||||
this.ctx.restore();
|
||||
this.canvasFactory.destroy({
|
||||
canvas: groupCtx.canvas,
|
||||
context: groupCtx,
|
||||
});
|
||||
this.#destroyKnockoutPools(groupMeta);
|
||||
this.compose(dirtyBox);
|
||||
}
|
||||
}
|
||||
|
||||
#destroyKnockoutPools(groupMeta) {
|
||||
if (!groupMeta) {
|
||||
return;
|
||||
}
|
||||
if (groupMeta.knockoutMaskEntry) {
|
||||
this.canvasFactory.destroy(groupMeta.knockoutMaskEntry);
|
||||
groupMeta.knockoutMaskEntry = null;
|
||||
}
|
||||
if (groupMeta.knockoutTempEntry) {
|
||||
this.canvasFactory.destroy(groupMeta.knockoutTempEntry);
|
||||
groupMeta.knockoutTempEntry = null;
|
||||
}
|
||||
if (groupMeta.knockoutBackdropEntry) {
|
||||
this.canvasFactory.destroy(groupMeta.knockoutBackdropEntry);
|
||||
groupMeta.knockoutBackdropEntry = null;
|
||||
}
|
||||
}
|
||||
|
||||
beginAnnotation(opIdx, id, rect, transform, matrix, hasOwnCanvas) {
|
||||
// The annotations are drawn just after the page content.
|
||||
// The page content drawing can potentially have set a transform,
|
||||
@ -3184,6 +3723,7 @@ class CanvasGraphics {
|
||||
img = this.getObject(opIdx, img.data, img);
|
||||
img.count = count;
|
||||
|
||||
const started = this.#beginKnockoutElement(this.current.fillAlpha);
|
||||
const ctx = this.ctx;
|
||||
const mask = this._createMaskCanvas(opIdx, img);
|
||||
const maskCanvas = mask.canvas;
|
||||
@ -3209,6 +3749,7 @@ class CanvasGraphics {
|
||||
this.canvasFactory.destroy(mask.canvasEntry);
|
||||
}
|
||||
this.compose();
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
paintImageMaskXObjectRepeat(
|
||||
@ -3226,6 +3767,7 @@ class CanvasGraphics {
|
||||
|
||||
img = this.getObject(opIdx, img.data, img);
|
||||
|
||||
const started = this.#beginKnockoutElement(this.current.fillAlpha);
|
||||
const ctx = this.ctx;
|
||||
ctx.save();
|
||||
const currentTransform = getCurrentTransform(ctx);
|
||||
@ -3270,12 +3812,14 @@ class CanvasGraphics {
|
||||
this.compose();
|
||||
|
||||
this.dependencyTracker?.recordOperation(opIdx);
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
paintImageMaskXObjectGroup(opIdx, images) {
|
||||
if (!this.contentVisible) {
|
||||
return;
|
||||
}
|
||||
const started = this.#beginKnockoutElement(this.current.fillAlpha);
|
||||
const ctx = this.ctx;
|
||||
|
||||
const fillColor = this.current.fillColor;
|
||||
@ -3332,6 +3876,7 @@ class CanvasGraphics {
|
||||
}
|
||||
this.compose();
|
||||
this.dependencyTracker?.recordOperation(opIdx);
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
paintImageXObject(opIdx, objId) {
|
||||
@ -3401,6 +3946,7 @@ class CanvasGraphics {
|
||||
}
|
||||
const width = imgData.width;
|
||||
const height = imgData.height;
|
||||
const started = this.#beginKnockoutElement(this.current.fillAlpha);
|
||||
const ctx = this.ctx;
|
||||
|
||||
this.save(opIdx);
|
||||
@ -3479,12 +4025,14 @@ class CanvasGraphics {
|
||||
}
|
||||
this.compose();
|
||||
this.restore(opIdx);
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
paintInlineImageXObjectGroup(opIdx, imgData, map) {
|
||||
if (!this.contentVisible) {
|
||||
return;
|
||||
}
|
||||
const started = this.#beginKnockoutElement(this.current.fillAlpha);
|
||||
const ctx = this.ctx;
|
||||
let imgToPaint;
|
||||
let inlineImgCanvas = null;
|
||||
@ -3526,12 +4074,14 @@ class CanvasGraphics {
|
||||
}
|
||||
this.dependencyTracker?.recordOperation(opIdx);
|
||||
this.compose();
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
paintSolidColorImageMask(opIdx) {
|
||||
if (!this.contentVisible) {
|
||||
return;
|
||||
}
|
||||
const started = this.#beginKnockoutElement(this.current.fillAlpha);
|
||||
this.dependencyTracker
|
||||
?.resetBBox(opIdx)
|
||||
.recordBBox(opIdx, this.ctx, 0, 1, 0, 1)
|
||||
@ -3539,6 +4089,7 @@ class CanvasGraphics {
|
||||
.recordOperation(opIdx);
|
||||
this.ctx.fillRect(0, 0, 1, 1);
|
||||
this.compose();
|
||||
this.#endKnockoutElement(started);
|
||||
}
|
||||
|
||||
// Marked content
|
||||
|
||||
@ -42,6 +42,10 @@ class BaseFilterFactory {
|
||||
return "none";
|
||||
}
|
||||
|
||||
addKnockoutFilter(alpha = 0) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) {
|
||||
return "none";
|
||||
}
|
||||
@ -329,6 +333,37 @@ class DOMFilterFactory extends BaseFilterFactory {
|
||||
return url;
|
||||
}
|
||||
|
||||
addKnockoutFilter(alpha = 0) {
|
||||
// Shape alpha mask: for translucent elements, remove the opacity constant
|
||||
// from the painted alpha while preserving antialias coverage. With no
|
||||
// usable opacity, fall back to a binary mask.
|
||||
const slope = alpha > 0 ? Math.min(1 / alpha, 1e6) : 1e6;
|
||||
const key = `knockout_${slope}`;
|
||||
const value = this.#cache.get(key);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const id = `g_${this.#docId}_knockout_filter_${this.#id++}`;
|
||||
const url = this.#createUrl(id);
|
||||
this.#cache.set(key, url);
|
||||
|
||||
const filter = this.#createFilter(id);
|
||||
const feComponentTransfer = this.#document.createElementNS(
|
||||
SVG_NS,
|
||||
"feComponentTransfer"
|
||||
);
|
||||
filter.append(feComponentTransfer);
|
||||
const feFuncA = this.#document.createElementNS(SVG_NS, "feFuncA");
|
||||
// Linear feFunc clamps to [0, 1].
|
||||
feFuncA.setAttribute("type", "linear");
|
||||
feFuncA.setAttribute("slope", `${slope}`);
|
||||
feFuncA.setAttribute("intercept", "0");
|
||||
feComponentTransfer.append(feFuncA);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
addHighlightHCMFilter(filterName, fgColor, bgColor, newFgColor, newBgColor) {
|
||||
const key = `${fgColor}-${bgColor}-${newFgColor}-${newBgColor}`;
|
||||
let info = this.#hcmCache.get(filterName);
|
||||
|
||||
@ -657,6 +657,21 @@ class FeatureTest {
|
||||
});
|
||||
}
|
||||
|
||||
static get isCanvasFilterSupported() {
|
||||
let ctx;
|
||||
if (this.isOffscreenCanvasSupported) {
|
||||
ctx = new OffscreenCanvas(1, 1).getContext("2d");
|
||||
} else if (typeof document !== "undefined") {
|
||||
ctx = document.createElement("canvas").getContext("2d");
|
||||
}
|
||||
// Spec-compliant Canvas2D defaults `ctx.filter` to "none". On
|
||||
// browsers without filter support (Safari) the property is absent
|
||||
// until you assign to it, after which it behaves like an ordinary
|
||||
// JS property and stores whatever string you set without applying
|
||||
// it. Probing the default lets us detect the difference reliably.
|
||||
return shadow(this, "isCanvasFilterSupported", ctx?.filter !== undefined);
|
||||
}
|
||||
|
||||
static get isAlphaColorInputSupported() {
|
||||
return shadow(
|
||||
this,
|
||||
|
||||
7
test/pdfs/.gitignore
vendored
7
test/pdfs/.gitignore
vendored
@ -16,6 +16,12 @@
|
||||
!bug1727053.pdf
|
||||
!issue18408_reduced.pdf
|
||||
!bug1907000_reduced.pdf
|
||||
!knockout_blend_multiply.pdf
|
||||
!knockout_isolated_overlap.pdf
|
||||
!knockout_nested.pdf
|
||||
!knockout_nested_group_alpha.pdf
|
||||
!knockout_nonisolated_sparse.pdf
|
||||
!knockout_smask.pdf
|
||||
!SimFang-variant.pdf
|
||||
!bug1953099.pdf
|
||||
!issue11913.pdf
|
||||
@ -913,3 +919,4 @@
|
||||
!smask_alpha_bc.pdf
|
||||
!smask_luminosity_oob_transfer.pdf
|
||||
!operator_list_cycle.pdf
|
||||
!knockout_groups_test.pdf
|
||||
|
||||
50
test/pdfs/knockout_blend_multiply.pdf
Normal file
50
test/pdfs/knockout_blend_multiply.pdf
Normal file
@ -0,0 +1,50 @@
|
||||
%PDF-1.7
|
||||
% PDF.js test fixture
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 160] /Resources << /XObject << /G1 4 0 R >> /ExtGState << /M 5 0 R >> >> /Contents 6 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 200 160] /Resources << /ExtGState << /M 5 0 R >> >> /Group << /Type /Group /S /Transparency /CS /DeviceRGB /I false /K true >> /Length 38 >>
|
||||
stream
|
||||
q
|
||||
/M gs
|
||||
0 1 1 rg
|
||||
40 30 120 100 re
|
||||
f
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Type /ExtGState /BM /Multiply >>
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Length 37 >>
|
||||
stream
|
||||
q
|
||||
1 1 0 rg
|
||||
0 0 200 160 re
|
||||
f
|
||||
/G1 Do
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000031 00000 n
|
||||
0000000080 00000 n
|
||||
0000000137 00000 n
|
||||
0000000292 00000 n
|
||||
0000000559 00000 n
|
||||
0000000611 00000 n
|
||||
trailer
|
||||
<< /Size 7 /Root 1 0 R >>
|
||||
startxref
|
||||
697
|
||||
%%EOF
|
||||
BIN
test/pdfs/knockout_groups_test.pdf
Normal file
BIN
test/pdfs/knockout_groups_test.pdf
Normal file
Binary file not shown.
50
test/pdfs/knockout_isolated_overlap.pdf
Normal file
50
test/pdfs/knockout_isolated_overlap.pdf
Normal file
@ -0,0 +1,50 @@
|
||||
%PDF-1.7
|
||||
% PDF.js test fixture
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 160] /Resources << /XObject << /G1 4 0 R >> /ExtGState << /A 5 0 R >> >> /Contents 6 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 200 160] /Resources << /ExtGState << /A 5 0 R >> >> /Group << /Type /Group /S /Transparency /CS /DeviceRGB /I true /K true >> /Length 64 >>
|
||||
stream
|
||||
q
|
||||
/A gs
|
||||
1 0 0 rg
|
||||
20 30 100 80 re
|
||||
f
|
||||
0 0 1 rg
|
||||
70 50 100 80 re
|
||||
f
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Type /ExtGState /ca 0.5 /CA 0.5 >>
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Length 11 >>
|
||||
stream
|
||||
q
|
||||
/G1 Do
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000031 00000 n
|
||||
0000000080 00000 n
|
||||
0000000137 00000 n
|
||||
0000000292 00000 n
|
||||
0000000584 00000 n
|
||||
0000000638 00000 n
|
||||
trailer
|
||||
<< /Size 7 /Root 1 0 R >>
|
||||
startxref
|
||||
698
|
||||
%%EOF
|
||||
59
test/pdfs/knockout_nested.pdf
Normal file
59
test/pdfs/knockout_nested.pdf
Normal file
@ -0,0 +1,59 @@
|
||||
%PDF-1.7
|
||||
% PDF.js test fixture
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 160] /Resources << /XObject << /O 4 0 R >> >> /Contents 7 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 200 160] /Resources << /XObject << /C 6 0 R >> >> /Group << /Type /Group /S /Transparency /CS /DeviceRGB /I true /K true >> /Length 37 >>
|
||||
stream
|
||||
q
|
||||
1 0 0 rg
|
||||
20 30 100 80 re
|
||||
f
|
||||
/C Do
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Type /ExtGState /ca 0.5 >>
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 200 160] /Resources << /ExtGState << /Half 5 0 R >> >> /Group << /Type /Group /S /Transparency /CS /DeviceRGB /I true /K true >> /Length 40 >>
|
||||
stream
|
||||
q
|
||||
/Half gs
|
||||
0 0 1 rg
|
||||
70 30 100 80 re
|
||||
f
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Length 10 >>
|
||||
stream
|
||||
q
|
||||
/O Do
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000031 00000 n
|
||||
0000000080 00000 n
|
||||
0000000137 00000 n
|
||||
0000000265 00000 n
|
||||
0000000528 00000 n
|
||||
0000000574 00000 n
|
||||
0000000845 00000 n
|
||||
trailer
|
||||
<< /Size 8 /Root 1 0 R >>
|
||||
startxref
|
||||
905
|
||||
%%EOF
|
||||
61
test/pdfs/knockout_nested_group_alpha.pdf
Normal file
61
test/pdfs/knockout_nested_group_alpha.pdf
Normal file
@ -0,0 +1,61 @@
|
||||
%PDF-1.7
|
||||
%âãÏÓ
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 120] /Resources << /ProcSet [/PDF] /XObject << /GOuter 5 0 R >> >> /Contents 4 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Length 62 >>
|
||||
stream
|
||||
q
|
||||
1 1 1 rg
|
||||
0 0 200 120 re f
|
||||
Q
|
||||
q
|
||||
1 0 0 1 50 30 cm
|
||||
/GOuter Do
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Type /XObject /Subtype /Form /BBox [0 0 100 60] /Group << /S /Transparency /I true /K true >> /Resources << /ProcSet [/PDF] /XObject << /GInner 6 0 R >> /ExtGState << /GSalpha 7 0 R >> >> /Length 69 >>
|
||||
stream
|
||||
1 0 0 rg
|
||||
0 0 100 60 re f
|
||||
q
|
||||
/GSalpha gs
|
||||
1 0 0 1 30 10 cm
|
||||
/GInner Do
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Type /XObject /Subtype /Form /BBox [0 0 60 40] /Group << /S /Transparency /I true /K false >> /Resources << /ProcSet [/PDF] >> /Length 24 >>
|
||||
stream
|
||||
0 0 1 rg
|
||||
0 0 60 40 re f
|
||||
endstream
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Type /ExtGState /ca 0.5 /CA 0.5 >>
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000064 00000 n
|
||||
0000000121 00000 n
|
||||
0000000270 00000 n
|
||||
0000000381 00000 n
|
||||
0000000688 00000 n
|
||||
0000000889 00000 n
|
||||
trailer
|
||||
<< /Size 8 /Root 1 0 R >>
|
||||
startxref
|
||||
943
|
||||
%%EOF
|
||||
50
test/pdfs/knockout_nonisolated_sparse.pdf
Normal file
50
test/pdfs/knockout_nonisolated_sparse.pdf
Normal file
@ -0,0 +1,50 @@
|
||||
%PDF-1.7
|
||||
% PDF.js test fixture
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 220 180] /Resources << /XObject << /G 4 0 R >> /ExtGState << /M 5 0 R >> >> /Contents 6 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [20 20 180 160] /Resources << >> /Group << /Type /Group /S /Transparency /CS /DeviceRGB /I false /K true >> /Length 30 >>
|
||||
stream
|
||||
q
|
||||
0 0 1 rg
|
||||
50 50 60 60 re
|
||||
f
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Type /ExtGState /BM /Multiply >>
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Length 39 >>
|
||||
stream
|
||||
q
|
||||
0.8 g
|
||||
0 0 220 180 re
|
||||
f
|
||||
/M gs
|
||||
/G Do
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000031 00000 n
|
||||
0000000080 00000 n
|
||||
0000000137 00000 n
|
||||
0000000291 00000 n
|
||||
0000000526 00000 n
|
||||
0000000578 00000 n
|
||||
trailer
|
||||
<< /Size 7 /Root 1 0 R >>
|
||||
startxref
|
||||
667
|
||||
%%EOF
|
||||
61
test/pdfs/knockout_smask.pdf
Normal file
61
test/pdfs/knockout_smask.pdf
Normal file
@ -0,0 +1,61 @@
|
||||
%PDF-1.7
|
||||
% PDF.js test fixture
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 160] /Resources << /XObject << /G1 4 0 R >> >> /Contents 5 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 200 160] /Resources << /ExtGState << /SM 6 0 R >> >> /Group << /Type /Group /S /Transparency /CS /DeviceRGB /I true /K true >> /Length 65 >>
|
||||
stream
|
||||
q
|
||||
1 0 0 rg
|
||||
20 30 100 80 re
|
||||
f
|
||||
/SM gs
|
||||
0 0 1 rg
|
||||
70 30 100 80 re
|
||||
f
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Length 11 >>
|
||||
stream
|
||||
q
|
||||
/G1 Do
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Type /ExtGState /SMask << /Type /Mask /S /Luminosity /G 7 0 R /BC [0] >> >>
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 200 160] /Resources << >> /Group << /Type /Group /S /Transparency /CS /DeviceGray >> /Length 27 >>
|
||||
stream
|
||||
q
|
||||
0.5 g
|
||||
0 0 200 160 re
|
||||
f
|
||||
Q
|
||||
endstream
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000031 00000 n
|
||||
0000000080 00000 n
|
||||
0000000137 00000 n
|
||||
0000000266 00000 n
|
||||
0000000560 00000 n
|
||||
0000000621 00000 n
|
||||
0000000716 00000 n
|
||||
trailer
|
||||
<< /Size 8 /Root 1 0 R >>
|
||||
startxref
|
||||
930
|
||||
%%EOF
|
||||
@ -6740,6 +6740,54 @@
|
||||
"maxY": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "knockout_smask",
|
||||
"file": "pdfs/knockout_smask.pdf",
|
||||
"md5": "d89fbb623f8e5f200a324607dd30449d",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"about": "Knockout group element with an active soft mask."
|
||||
},
|
||||
{
|
||||
"id": "knockout_nested",
|
||||
"file": "pdfs/knockout_nested.pdf",
|
||||
"md5": "e3db1591b84f7654d53f553c60ec9fd1",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"about": "Nested knockout group used as an element of an outer knockout group."
|
||||
},
|
||||
{
|
||||
"id": "knockout_nested_group_alpha",
|
||||
"file": "pdfs/knockout_nested_group_alpha.pdf",
|
||||
"md5": "cec0133fe020f089a73530274c295fcc",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"about": "Nested transparency group with parent alpha used as an element of an outer knockout group."
|
||||
},
|
||||
{
|
||||
"id": "knockout_nonisolated_sparse",
|
||||
"file": "pdfs/knockout_nonisolated_sparse.pdf",
|
||||
"md5": "6c9837d997785b4d7f848fa7a96ff984",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"about": "Sparse non-isolated knockout group under a blend mode."
|
||||
},
|
||||
{
|
||||
"id": "knockout_isolated_overlap",
|
||||
"file": "pdfs/knockout_isolated_overlap.pdf",
|
||||
"md5": "b9c5dda6b8c7eb51a904766171290dc0",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"about": "Isolated knockout group with two overlapping translucent fills; the overlap area must show only the second fill (no source-over mix)."
|
||||
},
|
||||
{
|
||||
"id": "knockout_blend_multiply",
|
||||
"file": "pdfs/knockout_blend_multiply.pdf",
|
||||
"md5": "e0b68fa675e58685fbf1b916fa03e301",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"about": "Non-isolated knockout group with a Multiply blend mode element on a yellow backdrop; the element must blend against the backdrop, not against a transparent temp canvas."
|
||||
},
|
||||
{
|
||||
"id": "issue6010_1",
|
||||
"file": "pdfs/issue6010_1.pdf",
|
||||
@ -14196,5 +14244,13 @@
|
||||
"md5": "267dc76f33cc0c0b6d36ff4605f60907",
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{
|
||||
"id": "knockout_groups_test",
|
||||
"file": "pdfs/knockout_groups_test.pdf",
|
||||
"md5": "8e085d0dcea38e976f81cbfb50ece6cd",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"about": "Knockout groups composite-modes survey."
|
||||
}
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user