Merge pull request #14175 from brendandahl/smask-v2

Use a new method for handling soft masks.
This commit is contained in:
Jonas Jenwald 2021-10-23 09:27:18 +02:00 committed by GitHub
commit 52372b9378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 505 additions and 146 deletions

View File

@ -60,6 +60,136 @@ const FULL_CHUNK_HEIGHT = 16;
// Once the bug is fixed upstream, we can remove this constant and its use. // Once the bug is fixed upstream, we can remove this constant and its use.
const LINEWIDTH_SCALE_FACTOR = 1.000001; const LINEWIDTH_SCALE_FACTOR = 1.000001;
/**
* Overrides certain methods on a 2d ctx so that when they are called they
* will also call the same method on the destCtx. The methods that are
* overridden are all the transformation state modifiers, path creation, and
* save/restore. We only forward these specific methods because they are the
* only state modifiers that we cannot copy over when we switch contexts.
*
* To remove mirroring call `ctx._removeMirroring()`.
*
* @param {Object} ctx - The 2d canvas context that will duplicate its calls on
* the destCtx.
* @param {Object} destCtx - The 2d canvas context that will receive the
* forwarded calls.
*/
function mirrorContextOperations(ctx, destCtx) {
if (ctx._removeMirroring) {
throw new Error("Context is already forwarding operations.");
}
ctx.__originalSave = ctx.save;
ctx.__originalRestore = ctx.restore;
ctx.__originalRotate = ctx.rotate;
ctx.__originalScale = ctx.scale;
ctx.__originalTranslate = ctx.translate;
ctx.__originalTransform = ctx.transform;
ctx.__originalSetTransform = ctx.setTransform;
ctx.__originalResetTransform = ctx.resetTransform;
ctx.__originalClip = ctx.clip;
ctx.__originalMoveTo = ctx.moveTo;
ctx.__originalLineTo = ctx.lineTo;
ctx.__originalBezierCurveTo = ctx.bezierCurveTo;
ctx.__originalRect = ctx.rect;
ctx.__originalClosePath = ctx.closePath;
ctx.__originalBeginPath = ctx.beginPath;
ctx._removeMirroring = () => {
ctx.save = ctx.__originalSave;
ctx.restore = ctx.__originalRestore;
ctx.rotate = ctx.__originalRotate;
ctx.scale = ctx.__originalScale;
ctx.translate = ctx.__originalTranslate;
ctx.transform = ctx.__originalTransform;
ctx.setTransform = ctx.__originalSetTransform;
ctx.resetTransform = ctx.__originalResetTransform;
ctx.clip = ctx.__originalClip;
ctx.moveTo = ctx.__originalMoveTo;
ctx.lineTo = ctx.__originalLineTo;
ctx.bezierCurveTo = ctx.__originalBezierCurveTo;
ctx.rect = ctx.__originalRect;
ctx.closePath = ctx.__originalClosePath;
ctx.beginPath = ctx.__originalBeginPath;
delete ctx._removeMirroring;
};
ctx.save = function ctxSave() {
destCtx.save();
this.__originalSave();
};
ctx.restore = function ctxRestore() {
destCtx.restore();
this.__originalRestore();
};
ctx.translate = function ctxTranslate(x, y) {
destCtx.translate(x, y);
this.__originalTranslate(x, y);
};
ctx.scale = function ctxScale(x, y) {
destCtx.scale(x, y);
this.__originalScale(x, y);
};
ctx.transform = function ctxTransform(a, b, c, d, e, f) {
destCtx.transform(a, b, c, d, e, f);
this.__originalTransform(a, b, c, d, e, f);
};
ctx.setTransform = function ctxSetTransform(a, b, c, d, e, f) {
destCtx.setTransform(a, b, c, d, e, f);
this.__originalSetTransform(a, b, c, d, e, f);
};
ctx.resetTransform = function ctxResetTransform() {
destCtx.resetTransform();
this.__originalResetTransform();
};
ctx.rotate = function ctxRotate(angle) {
destCtx.rotate(angle);
this.__originalRotate(angle);
};
ctx.clip = function ctxRotate(rule) {
destCtx.clip(rule);
this.__originalClip(rule);
};
ctx.moveTo = function (x, y) {
destCtx.moveTo(x, y);
this.__originalMoveTo(x, y);
};
ctx.lineTo = function (x, y) {
destCtx.lineTo(x, y);
this.__originalLineTo(x, y);
};
ctx.bezierCurveTo = function (cp1x, cp1y, cp2x, cp2y, x, y) {
destCtx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
this.__originalBezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
};
ctx.rect = function (x, y, width, height) {
destCtx.rect(x, y, width, height);
this.__originalRect(x, y, width, height);
};
ctx.closePath = function () {
destCtx.closePath();
this.__originalClosePath();
};
ctx.beginPath = function () {
destCtx.beginPath();
this.__originalBeginPath();
};
}
function addContextCurrentTransform(ctx) { function addContextCurrentTransform(ctx) {
// If the context doesn't expose a `mozCurrentTransform`, add a JS based one. // If the context doesn't expose a `mozCurrentTransform`, add a JS based one.
if (ctx.mozCurrentTransform) { if (ctx.mozCurrentTransform) {
@ -449,7 +579,7 @@ function compileType3Glyph(imgData) {
} }
class CanvasExtraState { class CanvasExtraState {
constructor() { constructor(width, height) {
// Are soft masks and alpha values shapes or opacities? // Are soft masks and alpha values shapes or opacities?
this.alphaIsShape = false; this.alphaIsShape = false;
this.fontSize = 0; this.fontSize = 0;
@ -479,18 +609,56 @@ class CanvasExtraState {
this.strokeAlpha = 1; this.strokeAlpha = 1;
this.lineWidth = 1; this.lineWidth = 1;
this.activeSMask = null; this.activeSMask = null;
this.resumeSMaskCtx = null; // nonclonable field (see the save method below)
this.transferMaps = null; this.transferMaps = null;
this.startNewPathAndClipBox([0, 0, width, height]);
} }
clone() { clone() {
return Object.create(this); const clone = Object.create(this);
clone.clipBox = this.clipBox.slice();
return clone;
} }
setCurrentPoint(x, y) { setCurrentPoint(x, y) {
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
updatePathMinMax(transform, x, y) {
[x, y] = Util.applyTransform([x, y], transform);
this.minX = Math.min(this.minX, x);
this.minY = Math.min(this.minY, y);
this.maxX = Math.max(this.maxX, x);
this.maxY = Math.max(this.maxY, y);
}
updateCurvePathMinMax(transform, x0, y0, x1, y1, x2, y2, x3, y3) {
const box = Util.bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3);
this.updatePathMinMax(transform, box[0], box[1]);
this.updatePathMinMax(transform, box[2], box[3]);
}
getPathBoundingBox() {
return [this.minX, this.minY, this.maxX, this.maxY];
}
updateClipFromPath() {
const intersect = Util.intersect(this.clipBox, this.getPathBoundingBox());
this.startNewPathAndClipBox(intersect || [0, 0, 0, 0]);
}
startNewPathAndClipBox(box) {
this.clipBox = box;
this.minX = Infinity;
this.minY = Infinity;
this.maxX = 0;
this.maxY = 0;
}
getClippedPathBoundingBox() {
return Util.intersect(this.clipBox, this.getPathBoundingBox());
}
} }
function putBinaryImageData(ctx, imgData, transferMaps = null) { function putBinaryImageData(ctx, imgData, transferMaps = null) {
@ -816,7 +984,11 @@ function genericComposeSMask(
height, height,
subtype, subtype,
backdrop, backdrop,
transferMap transferMap,
layerOffsetX,
layerOffsetY,
maskOffsetX,
maskOffsetY
) { ) {
const hasBackdrop = !!backdrop; const hasBackdrop = !!backdrop;
const r0 = hasBackdrop ? backdrop[0] : 0; const r0 = hasBackdrop ? backdrop[0] : 0;
@ -835,41 +1007,55 @@ function genericComposeSMask(
const chunkSize = Math.min(height, Math.ceil(PIXELS_TO_PROCESS / width)); const chunkSize = Math.min(height, Math.ceil(PIXELS_TO_PROCESS / width));
for (let row = 0; row < height; row += chunkSize) { for (let row = 0; row < height; row += chunkSize) {
const chunkHeight = Math.min(chunkSize, height - row); const chunkHeight = Math.min(chunkSize, height - row);
const maskData = maskCtx.getImageData(0, row, width, chunkHeight); const maskData = maskCtx.getImageData(
const layerData = layerCtx.getImageData(0, row, width, chunkHeight); layerOffsetX - maskOffsetX,
row + (layerOffsetY - maskOffsetY),
width,
chunkHeight
);
const layerData = layerCtx.getImageData(
layerOffsetX,
row + layerOffsetY,
width,
chunkHeight
);
if (hasBackdrop) { if (hasBackdrop) {
composeSMaskBackdrop(maskData.data, r0, g0, b0); composeSMaskBackdrop(maskData.data, r0, g0, b0);
} }
composeFn(maskData.data, layerData.data, transferMap); composeFn(maskData.data, layerData.data, transferMap);
maskCtx.putImageData(layerData, 0, row); layerCtx.putImageData(layerData, layerOffsetX, row + layerOffsetY);
} }
} }
function composeSMask(ctx, smask, layerCtx) { function composeSMask(ctx, smask, layerCtx, layerBox) {
const mask = smask.canvas; const layerOffsetX = layerBox[0];
const maskCtx = smask.context; const layerOffsetY = layerBox[1];
const layerWidth = layerBox[2] - layerOffsetX;
ctx.setTransform( const layerHeight = layerBox[3] - layerOffsetY;
smask.scaleX, if (layerWidth === 0 || layerHeight === 0) {
0, return;
0, }
smask.scaleY, genericComposeSMask(
smask.context,
layerCtx,
layerWidth,
layerHeight,
smask.subtype,
smask.backdrop,
smask.transferMap,
layerOffsetX,
layerOffsetY,
smask.offsetX, smask.offsetX,
smask.offsetY smask.offsetY
); );
ctx.save();
genericComposeSMask( ctx.globalAlpha = 1;
maskCtx, ctx.globalCompositeOperation = "source-over";
layerCtx, ctx.setTransform(1, 0, 0, 1, 0, 0);
mask.width, ctx.drawImage(layerCtx.canvas, 0, 0);
mask.height, ctx.restore();
smask.subtype,
smask.backdrop,
smask.transferMap
);
ctx.drawImage(mask, 0, 0);
} }
function getImageSmoothingEnabled(transform, interpolate) { function getImageSmoothingEnabled(transform, interpolate) {
@ -907,7 +1093,10 @@ class CanvasGraphics {
optionalContentConfig optionalContentConfig
) { ) {
this.ctx = canvasCtx; this.ctx = canvasCtx;
this.current = new CanvasExtraState(); this.current = new CanvasExtraState(
this.ctx.canvas.width,
this.ctx.canvas.height
);
this.stateStack = []; this.stateStack = [];
this.pendingClip = null; this.pendingClip = null;
this.pendingEOFill = false; this.pendingEOFill = false;
@ -927,6 +1116,7 @@ class CanvasGraphics {
this.smaskStack = []; this.smaskStack = [];
this.smaskCounter = 0; this.smaskCounter = 0;
this.tempSMask = null; this.tempSMask = null;
this.suspendedCtx = null;
this.contentVisible = true; this.contentVisible = true;
this.markedContentStack = []; this.markedContentStack = [];
this.optionalContentConfig = optionalContentConfig; this.optionalContentConfig = optionalContentConfig;
@ -1319,25 +1509,9 @@ class CanvasGraphics {
this.ctx.globalCompositeOperation = value; this.ctx.globalCompositeOperation = value;
break; break;
case "SMask": case "SMask":
if (this.current.activeSMask) {
// If SMask is currrenly used, it needs to be suspended or
// finished. Suspend only makes sense when at least one save()
// was performed and state needs to be reverted on restore().
if (
this.stateStack.length > 0 &&
this.stateStack[this.stateStack.length - 1].activeSMask ===
this.current.activeSMask
) {
this.suspendSMaskGroup();
} else {
this.endSMaskGroup();
}
}
this.current.activeSMask = value ? this.tempSMask : null; this.current.activeSMask = value ? this.tempSMask : null;
if (this.current.activeSMask) {
this.beginSMaskGroup();
}
this.tempSMask = null; this.tempSMask = null;
this.checkSMaskState();
break; break;
case "TR": case "TR":
this.current.transferMaps = value; this.current.transferMaps = value;
@ -1345,10 +1519,31 @@ class CanvasGraphics {
} }
} }
beginSMaskGroup() { checkSMaskState() {
const activeSMask = this.current.activeSMask; const inSMaskMode = !!this.suspendedCtx;
const drawnWidth = activeSMask.canvas.width; if (this.current.activeSMask && !inSMaskMode) {
const drawnHeight = activeSMask.canvas.height; this.beginSMaskMode();
} else if (!this.current.activeSMask && inSMaskMode) {
this.endSMaskMode();
}
// Else, the state is okay and nothing needs to be done.
}
/**
* Soft mask mode takes the current main drawing canvas and replaces it with
* a temporary canvas. Any drawing operations that happen on the temporary
* canvas need to be composed with the main canvas that was suspended (see
* `compose()`). The temporary canvas also duplicates many of its operations
* on the suspended canvas to keep them in sync, so that when the soft mask
* mode ends any clipping paths or transformations will still be active and in
* the right order on the canvas' graphics state stack.
*/
beginSMaskMode() {
if (this.suspendedCtx) {
throw new Error("beginSMaskMode called while already in smask mode");
}
const drawnWidth = this.ctx.canvas.width;
const drawnHeight = this.ctx.canvas.height;
const cacheId = "smaskGroupAt" + this.groupLevel; const cacheId = "smaskGroupAt" + this.groupLevel;
const scratchCanvas = this.cachedCanvases.getCanvas( const scratchCanvas = this.cachedCanvases.getCanvas(
cacheId, cacheId,
@ -1356,84 +1551,57 @@ class CanvasGraphics {
drawnHeight, drawnHeight,
true true
); );
this.suspendedCtx = this.ctx;
this.ctx = scratchCanvas.context;
const ctx = this.ctx;
ctx.setTransform.apply(ctx, this.suspendedCtx.mozCurrentTransform);
copyCtxState(this.suspendedCtx, ctx);
mirrorContextOperations(ctx, this.suspendedCtx);
const currentCtx = this.ctx;
const currentTransform = currentCtx.mozCurrentTransform;
this.ctx.save();
const groupCtx = scratchCanvas.context;
groupCtx.scale(1 / activeSMask.scaleX, 1 / activeSMask.scaleY);
groupCtx.translate(-activeSMask.offsetX, -activeSMask.offsetY);
groupCtx.transform.apply(groupCtx, currentTransform);
activeSMask.startTransformInverse = groupCtx.mozCurrentTransformInverse;
copyCtxState(currentCtx, groupCtx);
this.ctx = groupCtx;
this.setGState([ this.setGState([
["BM", "source-over"], ["BM", "source-over"],
["ca", 1], ["ca", 1],
["CA", 1], ["CA", 1],
]); ]);
this.groupStack.push(currentCtx);
this.groupLevel++;
} }
suspendSMaskGroup() { endSMaskMode() {
// Similar to endSMaskGroup, the intermediate canvas has to be composed if (!this.suspendedCtx) {
// and future ctx state restored. throw new Error("endSMaskMode called while not in smask mode");
const groupCtx = this.ctx; }
this.groupLevel--; // The soft mask is done, now restore the suspended canvas as the main
this.ctx = this.groupStack.pop(); // drawing canvas.
this.ctx._removeMirroring();
copyCtxState(this.ctx, this.suspendedCtx);
this.ctx = this.suspendedCtx;
composeSMask(this.ctx, this.current.activeSMask, groupCtx); this.current.activeSMask = null;
this.suspendedCtx = null;
}
compose(dirtyBox) {
if (!this.current.activeSMask) {
return;
}
if (!dirtyBox) {
dirtyBox = [0, 0, this.ctx.canvas.width, this.ctx.canvas.height];
} else {
dirtyBox[0] = Math.floor(dirtyBox[0]);
dirtyBox[1] = Math.floor(dirtyBox[1]);
dirtyBox[2] = Math.ceil(dirtyBox[2]);
dirtyBox[3] = Math.ceil(dirtyBox[3]);
}
const smask = this.current.activeSMask;
const suspendedCtx = this.suspendedCtx;
composeSMask(suspendedCtx, smask, this.ctx, dirtyBox);
// Whatever was drawn has been moved to the suspended canvas, now clear it
// out of the current canvas.
this.ctx.save();
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.restore(); this.ctx.restore();
this.ctx.save(); // save is needed since SMask will be resumed.
copyCtxState(groupCtx, this.ctx);
// Saving state for resuming.
this.current.resumeSMaskCtx = groupCtx;
// Transform was changed in the SMask canvas, reflecting this change on
// this.ctx.
const deltaTransform = Util.transform(
this.current.activeSMask.startTransformInverse,
groupCtx.mozCurrentTransform
);
this.ctx.transform.apply(this.ctx, deltaTransform);
// SMask was composed, the results at the groupCtx can be cleared.
groupCtx.save();
groupCtx.setTransform(1, 0, 0, 1, 0, 0);
groupCtx.clearRect(0, 0, groupCtx.canvas.width, groupCtx.canvas.height);
groupCtx.restore();
}
resumeSMaskGroup() {
// Resuming state saved by suspendSMaskGroup. We don't need to restore
// any groupCtx state since restore() command (the only caller) will do
// that for us. See also beginSMaskGroup.
const groupCtx = this.current.resumeSMaskCtx;
const currentCtx = this.ctx;
this.ctx = groupCtx;
this.groupStack.push(currentCtx);
this.groupLevel++;
}
endSMaskGroup() {
const groupCtx = this.ctx;
this.groupLevel--;
this.ctx = this.groupStack.pop();
composeSMask(this.ctx, this.current.activeSMask, groupCtx);
this.ctx.restore();
copyCtxState(groupCtx, this.ctx);
// Transform was changed in the SMask canvas, reflecting this change on
// this.ctx.
const deltaTransform = Util.transform(
this.current.activeSMask.startTransformInverse,
groupCtx.mozCurrentTransform
);
this.ctx.transform.apply(this.ctx, deltaTransform);
} }
save() { save() {
@ -1441,36 +1609,22 @@ class CanvasGraphics {
const old = this.current; const old = this.current;
this.stateStack.push(old); this.stateStack.push(old);
this.current = old.clone(); this.current = old.clone();
this.current.resumeSMaskCtx = null;
} }
restore() { restore() {
// SMask was suspended, we just need to resume it. if (this.stateStack.length === 0 && this.current.activeSMask) {
if (this.current.resumeSMaskCtx) { this.endSMaskMode();
this.resumeSMaskGroup();
}
// SMask has to be finished once there is no states that are using the
// same SMask.
if (
this.current.activeSMask !== null &&
(this.stateStack.length === 0 ||
this.stateStack[this.stateStack.length - 1].activeSMask !==
this.current.activeSMask)
) {
this.endSMaskGroup();
} }
if (this.stateStack.length !== 0) { if (this.stateStack.length !== 0) {
this.current = this.stateStack.pop(); this.current = this.stateStack.pop();
this.ctx.restore(); this.ctx.restore();
this.checkSMaskState();
// Ensure that the clipping path is reset (fixes issue6413.pdf). // Ensure that the clipping path is reset (fixes issue6413.pdf).
this.pendingClip = null; this.pendingClip = null;
this._cachedGetSinglePixelWidth = null; this._cachedGetSinglePixelWidth = null;
} else {
// We've finished all the SMask groups, reflect that in our state.
this.current.activeSMask = null;
} }
} }
@ -1486,6 +1640,7 @@ class CanvasGraphics {
const current = this.current; const current = this.current;
let x = current.x, let x = current.x,
y = current.y; y = current.y;
let startX, startY;
for (let i = 0, j = 0, ii = ops.length; i < ii; i++) { for (let i = 0, j = 0, ii = ops.length; i < ii; i++) {
switch (ops[i] | 0) { switch (ops[i] | 0) {
case OPS.rectangle: case OPS.rectangle:
@ -1504,20 +1659,25 @@ class CanvasGraphics {
ctx.lineTo(xw, yh); ctx.lineTo(xw, yh);
ctx.lineTo(x, yh); ctx.lineTo(x, yh);
} }
current.updatePathMinMax(ctx.mozCurrentTransform, x, y);
current.updatePathMinMax(ctx.mozCurrentTransform, xw, yh);
ctx.closePath(); ctx.closePath();
break; break;
case OPS.moveTo: case OPS.moveTo:
x = args[j++]; x = args[j++];
y = args[j++]; y = args[j++];
ctx.moveTo(x, y); ctx.moveTo(x, y);
current.updatePathMinMax(ctx.mozCurrentTransform, x, y);
break; break;
case OPS.lineTo: case OPS.lineTo:
x = args[j++]; x = args[j++];
y = args[j++]; y = args[j++];
ctx.lineTo(x, y); ctx.lineTo(x, y);
current.updatePathMinMax(ctx.mozCurrentTransform, x, y);
break; break;
case OPS.curveTo: case OPS.curveTo:
startX = x;
startY = y;
x = args[j + 4]; x = args[j + 4];
y = args[j + 5]; y = args[j + 5];
ctx.bezierCurveTo( ctx.bezierCurveTo(
@ -1528,9 +1688,22 @@ class CanvasGraphics {
x, x,
y y
); );
current.updateCurvePathMinMax(
ctx.mozCurrentTransform,
startX,
startY,
args[j],
args[j + 1],
args[j + 2],
args[j + 3],
x,
y
);
j += 6; j += 6;
break; break;
case OPS.curveTo2: case OPS.curveTo2:
startX = x;
startY = y;
ctx.bezierCurveTo( ctx.bezierCurveTo(
x, x,
y, y,
@ -1539,14 +1712,38 @@ class CanvasGraphics {
args[j + 2], args[j + 2],
args[j + 3] args[j + 3]
); );
current.updateCurvePathMinMax(
ctx.mozCurrentTransform,
startX,
startY,
x,
y,
args[j],
args[j + 1],
args[j + 2],
args[j + 3]
);
x = args[j + 2]; x = args[j + 2];
y = args[j + 3]; y = args[j + 3];
j += 4; j += 4;
break; break;
case OPS.curveTo3: case OPS.curveTo3:
startX = x;
startY = y;
x = args[j + 2]; x = args[j + 2];
y = args[j + 3]; y = args[j + 3];
ctx.bezierCurveTo(args[j], args[j + 1], x, y, x, y); ctx.bezierCurveTo(args[j], args[j + 1], x, y, x, y);
current.updateCurvePathMinMax(
ctx.mozCurrentTransform,
startX,
startY,
args[j],
args[j + 1],
x,
y,
x,
y
);
j += 4; j += 4;
break; break;
case OPS.closePath: case OPS.closePath:
@ -1599,7 +1796,7 @@ class CanvasGraphics {
} }
} }
if (consumePath) { if (consumePath) {
this.consumePath(); this.consumePath(this.current.getClippedPathBoundingBox());
} }
// Restore the global alpha to the fill alpha // Restore the global alpha to the fill alpha
ctx.globalAlpha = this.current.fillAlpha; ctx.globalAlpha = this.current.fillAlpha;
@ -1627,7 +1824,8 @@ class CanvasGraphics {
needRestore = true; needRestore = true;
} }
if (this.contentVisible) { const intersect = this.current.getClippedPathBoundingBox();
if (this.contentVisible && intersect !== null) {
if (this.pendingEOFill) { if (this.pendingEOFill) {
ctx.fill("evenodd"); ctx.fill("evenodd");
this.pendingEOFill = false; this.pendingEOFill = false;
@ -1640,7 +1838,7 @@ class CanvasGraphics {
ctx.restore(); ctx.restore();
} }
if (consumePath) { if (consumePath) {
this.consumePath(); this.consumePath(intersect);
} }
} }
@ -2092,6 +2290,7 @@ class CanvasGraphics {
current.x += x * textHScale; current.x += x * textHScale;
} }
ctx.restore(); ctx.restore();
this.compose();
return undefined; return undefined;
} }
@ -2282,6 +2481,7 @@ class CanvasGraphics {
this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10);
} }
this.compose(this.current.getClippedPathBoundingBox());
this.restore(); this.restore();
} }
@ -2311,6 +2511,16 @@ class CanvasGraphics {
const width = bbox[2] - bbox[0]; const width = bbox[2] - bbox[0];
const height = bbox[3] - bbox[1]; const height = bbox[3] - bbox[1];
this.ctx.rect(bbox[0], bbox[1], width, height); this.ctx.rect(bbox[0], bbox[1], width, height);
this.current.updatePathMinMax(
this.ctx.mozCurrentTransform,
bbox[0],
bbox[1]
);
this.current.updatePathMinMax(
this.ctx.mozCurrentTransform,
bbox[2],
bbox[3]
);
this.clip(); this.clip();
this.endPath(); this.endPath();
} }
@ -2330,6 +2540,14 @@ class CanvasGraphics {
} }
this.save(); this.save();
// If there's an active soft mask we don't want it enabled for the group, so
// clear it out. The mask and suspended canvas will be restored in endGroup.
const suspendedCtx = this.suspendedCtx;
if (this.current.activeSMask) {
this.suspendedCtx = null;
this.current.activeSMask = null;
}
const currentCtx = this.ctx; const currentCtx = this.ctx;
// TODO non-isolated groups - according to Rik at adobe non-isolated // TODO non-isolated groups - according to Rik at adobe non-isolated
// group results aren't usually that different and they even have tools // group results aren't usually that different and they even have tools
@ -2393,6 +2611,8 @@ class CanvasGraphics {
drawnHeight = MAX_GROUP_SIZE; drawnHeight = MAX_GROUP_SIZE;
} }
this.current.startNewPathAndClipBox([0, 0, drawnWidth, drawnHeight]);
let cacheId = "groupAt" + this.groupLevel; let cacheId = "groupAt" + this.groupLevel;
if (group.smask) { if (group.smask) {
// Using two cache entries is case if masks are used one after another. // Using two cache entries is case if masks are used one after another.
@ -2432,6 +2652,7 @@ class CanvasGraphics {
currentCtx.setTransform(1, 0, 0, 1, 0, 0); currentCtx.setTransform(1, 0, 0, 1, 0, 0);
currentCtx.translate(offsetX, offsetY); currentCtx.translate(offsetX, offsetY);
currentCtx.scale(scaleX, scaleY); currentCtx.scale(scaleX, scaleY);
currentCtx.save();
} }
// The transparency group inherits all off the current graphics state // The transparency group inherits all off the current graphics state
// except the blend mode, soft mask, and alpha constants. // except the blend mode, soft mask, and alpha constants.
@ -2442,11 +2663,11 @@ class CanvasGraphics {
["ca", 1], ["ca", 1],
["CA", 1], ["CA", 1],
]); ]);
this.groupStack.push(currentCtx); this.groupStack.push({
ctx: currentCtx,
suspendedCtx,
});
this.groupLevel++; this.groupLevel++;
// Resetting mask state, masks will be applied on restore of the group.
this.current.activeSMask = null;
} }
endGroup(group) { endGroup(group) {
@ -2455,17 +2676,33 @@ class CanvasGraphics {
} }
this.groupLevel--; this.groupLevel--;
const groupCtx = this.ctx; const groupCtx = this.ctx;
this.ctx = this.groupStack.pop(); const { ctx, suspendedCtx } = this.groupStack.pop();
this.ctx = ctx;
// Turn off image smoothing to avoid sub pixel interpolation which can // Turn off image smoothing to avoid sub pixel interpolation which can
// look kind of blurry for some pdfs. // look kind of blurry for some pdfs.
this.ctx.imageSmoothingEnabled = false; this.ctx.imageSmoothingEnabled = false;
if (suspendedCtx) {
this.suspendedCtx = suspendedCtx;
}
if (group.smask) { if (group.smask) {
this.tempSMask = this.smaskStack.pop(); this.tempSMask = this.smaskStack.pop();
this.restore();
} else { } else {
this.ctx.restore();
const currentMtx = this.ctx.mozCurrentTransform;
this.restore();
this.ctx.save();
this.ctx.setTransform.apply(this.ctx, currentMtx);
const dirtyBox = Util.getAxialAlignedBoundingBox(
[0, 0, groupCtx.canvas.width, groupCtx.canvas.height],
currentMtx
);
this.ctx.drawImage(groupCtx.canvas, 0, 0); this.ctx.drawImage(groupCtx.canvas, 0, 0);
this.ctx.restore();
this.compose(dirtyBox);
} }
this.restore();
} }
beginAnnotations() { beginAnnotations() {
@ -2482,7 +2719,10 @@ class CanvasGraphics {
beginAnnotation(id, rect, transform, matrix) { beginAnnotation(id, rect, transform, matrix) {
this.save(); this.save();
resetCtxToDefault(this.ctx); resetCtxToDefault(this.ctx);
this.current = new CanvasExtraState(); this.current = new CanvasExtraState(
this.ctx.canvas.width,
this.ctx.canvas.height
);
if (Array.isArray(rect) && rect.length === 4) { if (Array.isArray(rect) && rect.length === 4) {
const width = rect[2] - rect[0]; const width = rect[2] - rect[0];
@ -2531,6 +2771,7 @@ class CanvasGraphics {
ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.drawImage(maskCanvas, mask.offsetX, mask.offsetY); ctx.drawImage(maskCanvas, mask.offsetX, mask.offsetY);
ctx.restore(); ctx.restore();
this.compose();
} }
paintImageMaskXObjectRepeat( paintImageMaskXObjectRepeat(
@ -2565,6 +2806,7 @@ class CanvasGraphics {
ctx.drawImage(mask.canvas, x, y); ctx.drawImage(mask.canvas, x, y);
} }
ctx.restore(); ctx.restore();
this.compose();
} }
paintImageMaskXObjectGroup(images) { paintImageMaskXObjectGroup(images) {
@ -2610,6 +2852,7 @@ class CanvasGraphics {
ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1); ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1);
ctx.restore(); ctx.restore();
} }
this.compose();
} }
paintImageXObject(objId) { paintImageXObject(objId) {
@ -2711,6 +2954,7 @@ class CanvasGraphics {
height: height / ctx.mozCurrentTransformInverse[3], height: height / ctx.mozCurrentTransformInverse[3],
}); });
} }
this.compose();
this.restore(); this.restore();
} }
@ -2754,6 +2998,7 @@ class CanvasGraphics {
} }
ctx.restore(); ctx.restore();
} }
this.compose();
} }
paintSolidColorImageMask() { paintSolidColorImageMask() {
@ -2761,6 +3006,7 @@ class CanvasGraphics {
return; return;
} }
this.ctx.fillRect(0, 0, 1, 1); this.ctx.fillRect(0, 0, 1, 1);
this.compose();
} }
// Marked content // Marked content
@ -2809,7 +3055,13 @@ class CanvasGraphics {
// Helper functions // Helper functions
consumePath() { consumePath(clipBox) {
if (this.pendingClip) {
this.current.updateClipFromPath();
}
if (!this.pendingClip) {
this.compose(clipBox);
}
const ctx = this.ctx; const ctx = this.ctx;
if (this.pendingClip) { if (this.pendingClip) {
if (this.pendingClip === EO_CLIP) { if (this.pendingClip === EO_CLIP) {

View File

@ -872,6 +872,78 @@ class Util {
return result; return result;
} }
// From https://github.com/adobe-webplatform/Snap.svg/blob/b365287722a72526000ac4bfcf0ce4cac2faa015/src/path.js#L852
static bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3) {
const tvalues = [],
bounds = [[], []];
let a, b, c, t, t1, t2, b2ac, sqrtb2ac;
for (let i = 0; i < 2; ++i) {
if (i === 0) {
b = 6 * x0 - 12 * x1 + 6 * x2;
a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
c = 3 * x1 - 3 * x0;
} else {
b = 6 * y0 - 12 * y1 + 6 * y2;
a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
c = 3 * y1 - 3 * y0;
}
if (Math.abs(a) < 1e-12) {
if (Math.abs(b) < 1e-12) {
continue;
}
t = -c / b;
if (0 < t && t < 1) {
tvalues.push(t);
}
continue;
}
b2ac = b * b - 4 * c * a;
sqrtb2ac = Math.sqrt(b2ac);
if (b2ac < 0) {
continue;
}
t1 = (-b + sqrtb2ac) / (2 * a);
if (0 < t1 && t1 < 1) {
tvalues.push(t1);
}
t2 = (-b - sqrtb2ac) / (2 * a);
if (0 < t2 && t2 < 1) {
tvalues.push(t2);
}
}
let j = tvalues.length,
mt;
const jlen = j;
while (j--) {
t = tvalues[j];
mt = 1 - t;
bounds[0][j] =
mt * mt * mt * x0 +
3 * mt * mt * t * x1 +
3 * mt * t * t * x2 +
t * t * t * x3;
bounds[1][j] =
mt * mt * mt * y0 +
3 * mt * mt * t * y1 +
3 * mt * t * t * y2 +
t * t * t * y3;
}
bounds[0][jlen] = x0;
bounds[1][jlen] = y0;
bounds[0][jlen + 1] = x3;
bounds[1][jlen + 1] = y3;
bounds[0].length = bounds[1].length = jlen + 2;
return [
Math.min(...bounds[0]),
Math.min(...bounds[1]),
Math.max(...bounds[0]),
Math.max(...bounds[1]),
];
}
} }
const PDFStringTranslateTable = [ const PDFStringTranslateTable = [

View File

@ -17,6 +17,7 @@
!issue2391-1.pdf !issue2391-1.pdf
!issue2391-2.pdf !issue2391-2.pdf
!issue14046.pdf !issue14046.pdf
!issue7891_bc1.pdf
!issue3214.pdf !issue3214.pdf
!issue4665.pdf !issue4665.pdf
!issue4684.pdf !issue4684.pdf
@ -106,6 +107,7 @@
!bug1057544.pdf !bug1057544.pdf
!issue11150_reduced.pdf !issue11150_reduced.pdf
!issue6127.pdf !issue6127.pdf
!issue7891_bc0.pdf
!issue11242_reduced.pdf !issue11242_reduced.pdf
!issue11279.pdf !issue11279.pdf
!issue11362.pdf !issue11362.pdf
@ -246,6 +248,7 @@
!issue2931.pdf !issue2931.pdf
!issue3323.pdf !issue3323.pdf
!issue4304.pdf !issue4304.pdf
!issue9017_reduced.pdf
!issue4379.pdf !issue4379.pdf
!issue4550.pdf !issue4550.pdf
!issue13316_reduced.pdf !issue13316_reduced.pdf
@ -381,6 +384,7 @@
!issue5044.pdf !issue5044.pdf
!issue1512r.pdf !issue1512r.pdf
!issue2128r.pdf !issue2128r.pdf
!bug1703683_page2_reduced.pdf
!issue5540.pdf !issue5540.pdf
!issue5549.pdf !issue5549.pdf
!visibility_expressions.pdf !visibility_expressions.pdf
@ -435,6 +439,7 @@
!annotation-freetext.pdf !annotation-freetext.pdf
!annotation-line.pdf !annotation-line.pdf
!evaljs.pdf !evaljs.pdf
!issue12798_page1_reduced.pdf
!annotation-line-without-appearance.pdf !annotation-line-without-appearance.pdf
!bug1669099.pdf !bug1669099.pdf
!annotation-square-circle.pdf !annotation-square-circle.pdf

Binary file not shown.

Binary file not shown.

BIN
test/pdfs/issue7891_bc0.pdf Normal file

Binary file not shown.

BIN
test/pdfs/issue7891_bc1.pdf Normal file

Binary file not shown.

Binary file not shown.

View File

@ -530,6 +530,12 @@
"rounds": 1, "rounds": 1,
"type": "eq" "type": "eq"
}, },
{ "id": "issue12798_page1_reduced",
"file": "pdfs/issue12798_page1_reduced.pdf",
"md5": "f4c3e91c181b510929ade67c1e34c5c5",
"rounds": 1,
"type": "eq"
},
{ "id": "hmm-pdf", { "id": "hmm-pdf",
"file": "pdfs/hmm.pdf", "file": "pdfs/hmm.pdf",
"md5": "e08467e60101ee5f4a59716e86db6dc9", "md5": "e08467e60101ee5f4a59716e86db6dc9",
@ -769,6 +775,12 @@
"lastPage": 1, "lastPage": 1,
"type": "eq" "type": "eq"
}, },
{ "id": "bug1703683_page2_reduced",
"file": "pdfs/bug1703683_page2_reduced.pdf",
"md5": "2b6d5d617438cf72c76c25f46ec6ad75",
"rounds": 1,
"type": "eq"
},
{ "id": "issue6707", { "id": "issue6707",
"file": "pdfs/issue6707.pdf", "file": "pdfs/issue6707.pdf",
"md5": "068ceaec23d265b1d38dfa6ab279f017", "md5": "068ceaec23d265b1d38dfa6ab279f017",
@ -883,6 +895,12 @@
"type": "eq", "type": "eq",
"annotations": true "annotations": true
}, },
{ "id": "issue9017_reduced",
"file": "pdfs/issue9017_reduced.pdf",
"md5": "8b45b3ea91778d6d98f407620d645f48",
"rounds": 1,
"type": "eq"
},
{ "id": "issue4934", { "id": "issue4934",
"file": "pdfs/issue4934.pdf", "file": "pdfs/issue4934.pdf",
"md5": "6099da44f677702ae65a648b51a2226d", "md5": "6099da44f677702ae65a648b51a2226d",
@ -3450,6 +3468,12 @@
"link": true, "link": true,
"type": "eq" "type": "eq"
}, },
{ "id": "issue7891_bc0",
"file": "pdfs/issue7891_bc0.pdf",
"md5": "744a22244a4e4708b7f1691eec155fc8",
"rounds": 1,
"type": "eq"
},
{ {
"id": "issue6165", "id": "issue6165",
"file": "pdfs/issue6165.pdf", "file": "pdfs/issue6165.pdf",
@ -4537,6 +4561,12 @@
"link": true, "link": true,
"type": "load" "type": "load"
}, },
{ "id": "issue7891_bc1",
"file": "pdfs/issue7891_bc1.pdf",
"md5": "86b1796da7dad09f93ce68a8ad495a24",
"rounds": 1,
"type": "eq"
},
{ "id": "issue3062", { "id": "issue3062",
"file": "pdfs/issue3062.pdf", "file": "pdfs/issue3062.pdf",
"md5": "206715f1258f0e117df4180d98dd4d68", "md5": "206715f1258f0e117df4180d98dd4d68",