Use a new method for handling soft masks.
The old method of handling soft masks had a number of issues where the temporary drawing canvas and the suspended main canvas could get out of sync (e.g. mismatched save/restores or clip state) or we could end up compositing at the wrong time. A good example of things getting out sync is the reduced test case in #9017. To fix this I've changed two big things: 1) Duplicate all the needed graphics state from the temporary canvas to the suspended main canvas. This ensure the canvases stay in sync so that when we switch back to the main canvas the graphics state stack is the same (e.g. transforms, clip paths). 2) Immediately composite after each drawing operation. This ensures that if there's an active clip region that we'll still be able to composite the correct portions of the canvas. Note: This solution could be avoided by using getImageData and putImageData since those ignore clipping region, but this is very very slow. Note2: I also think the old way of only compositing at the end of the soft mask is incorrect and can lead to wrong colors if drawing over the same region, but in practice this doesn't seem to matter much. Fixes: #5781 Fixes: #5853 Fixes: #7267 Fixes: #7891 Fixes: #8403 Fixes: #8624 Fixes: #12798 Fixes: #13891 Fixes: #9017 (reduced test case) Fixes: https://bugzilla.mozilla.org/show_bug.cgi?id=1703683
This commit is contained in:
parent
6863f36880
commit
2d1f9ff7a3
@ -60,6 +60,136 @@ const FULL_CHUNK_HEIGHT = 16;
|
||||
// Once the bug is fixed upstream, we can remove this constant and its use.
|
||||
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) {
|
||||
// If the context doesn't expose a `mozCurrentTransform`, add a JS based one.
|
||||
if (ctx.mozCurrentTransform) {
|
||||
@ -479,7 +609,6 @@ class CanvasExtraState {
|
||||
this.strokeAlpha = 1;
|
||||
this.lineWidth = 1;
|
||||
this.activeSMask = null;
|
||||
this.resumeSMaskCtx = null; // nonclonable field (see the save method below)
|
||||
this.transferMaps = null;
|
||||
}
|
||||
|
||||
@ -816,7 +945,11 @@ function genericComposeSMask(
|
||||
height,
|
||||
subtype,
|
||||
backdrop,
|
||||
transferMap
|
||||
transferMap,
|
||||
layerOffsetX,
|
||||
layerOffsetY,
|
||||
maskOffsetX,
|
||||
maskOffsetY
|
||||
) {
|
||||
const hasBackdrop = !!backdrop;
|
||||
const r0 = hasBackdrop ? backdrop[0] : 0;
|
||||
@ -835,41 +968,52 @@ function genericComposeSMask(
|
||||
const chunkSize = Math.min(height, Math.ceil(PIXELS_TO_PROCESS / width));
|
||||
for (let row = 0; row < height; row += chunkSize) {
|
||||
const chunkHeight = Math.min(chunkSize, height - row);
|
||||
const maskData = maskCtx.getImageData(0, row, width, chunkHeight);
|
||||
const layerData = layerCtx.getImageData(0, row, width, chunkHeight);
|
||||
const maskData = maskCtx.getImageData(
|
||||
layerOffsetX - maskOffsetX,
|
||||
row + (layerOffsetY - maskOffsetY),
|
||||
width,
|
||||
chunkHeight
|
||||
);
|
||||
const layerData = layerCtx.getImageData(
|
||||
layerOffsetX,
|
||||
row + layerOffsetY,
|
||||
width,
|
||||
chunkHeight
|
||||
);
|
||||
|
||||
if (hasBackdrop) {
|
||||
composeSMaskBackdrop(maskData.data, r0, g0, b0);
|
||||
}
|
||||
composeFn(maskData.data, layerData.data, transferMap);
|
||||
|
||||
maskCtx.putImageData(layerData, 0, row);
|
||||
layerCtx.putImageData(layerData, layerOffsetX, row + layerOffsetY);
|
||||
}
|
||||
}
|
||||
|
||||
function composeSMask(ctx, smask, layerCtx) {
|
||||
const mask = smask.canvas;
|
||||
const maskCtx = smask.context;
|
||||
|
||||
ctx.setTransform(
|
||||
smask.scaleX,
|
||||
0,
|
||||
0,
|
||||
smask.scaleY,
|
||||
function composeSMask(ctx, smask, layerCtx, layerBox) {
|
||||
const layerOffsetX = layerBox[0];
|
||||
const layerOffsetY = layerBox[1];
|
||||
const layerWidth = layerBox[2] - layerOffsetX;
|
||||
const layerHeight = layerBox[3] - layerOffsetY;
|
||||
genericComposeSMask(
|
||||
smask.context,
|
||||
layerCtx,
|
||||
layerWidth,
|
||||
layerHeight,
|
||||
smask.subtype,
|
||||
smask.backdrop,
|
||||
smask.transferMap,
|
||||
layerOffsetX,
|
||||
layerOffsetY,
|
||||
smask.offsetX,
|
||||
smask.offsetY
|
||||
);
|
||||
|
||||
genericComposeSMask(
|
||||
maskCtx,
|
||||
layerCtx,
|
||||
mask.width,
|
||||
mask.height,
|
||||
smask.subtype,
|
||||
smask.backdrop,
|
||||
smask.transferMap
|
||||
);
|
||||
ctx.drawImage(mask, 0, 0);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.drawImage(layerCtx.canvas, 0, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function getImageSmoothingEnabled(transform, interpolate) {
|
||||
@ -927,6 +1071,7 @@ class CanvasGraphics {
|
||||
this.smaskStack = [];
|
||||
this.smaskCounter = 0;
|
||||
this.tempSMask = null;
|
||||
this.suspendedCtx = null;
|
||||
this.contentVisible = true;
|
||||
this.markedContentStack = [];
|
||||
this.optionalContentConfig = optionalContentConfig;
|
||||
@ -1319,25 +1464,9 @@ class CanvasGraphics {
|
||||
this.ctx.globalCompositeOperation = value;
|
||||
break;
|
||||
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;
|
||||
if (this.current.activeSMask) {
|
||||
this.beginSMaskGroup();
|
||||
}
|
||||
this.tempSMask = null;
|
||||
this.checkSMaskState();
|
||||
break;
|
||||
case "TR":
|
||||
this.current.transferMaps = value;
|
||||
@ -1345,10 +1474,31 @@ class CanvasGraphics {
|
||||
}
|
||||
}
|
||||
|
||||
beginSMaskGroup() {
|
||||
const activeSMask = this.current.activeSMask;
|
||||
const drawnWidth = activeSMask.canvas.width;
|
||||
const drawnHeight = activeSMask.canvas.height;
|
||||
checkSMaskState() {
|
||||
const inSMaskMode = !!this.suspendedCtx;
|
||||
if (this.current.activeSMask && !inSMaskMode) {
|
||||
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 scratchCanvas = this.cachedCanvases.getCanvas(
|
||||
cacheId,
|
||||
@ -1356,84 +1506,51 @@ class CanvasGraphics {
|
||||
drawnHeight,
|
||||
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([
|
||||
["BM", "source-over"],
|
||||
["ca", 1],
|
||||
["CA", 1],
|
||||
]);
|
||||
this.groupStack.push(currentCtx);
|
||||
this.groupLevel++;
|
||||
}
|
||||
|
||||
suspendSMaskGroup() {
|
||||
// Similar to endSMaskGroup, the intermediate canvas has to be composed
|
||||
// and future ctx state restored.
|
||||
const groupCtx = this.ctx;
|
||||
this.groupLevel--;
|
||||
this.ctx = this.groupStack.pop();
|
||||
endSMaskMode() {
|
||||
if (!this.suspendedCtx) {
|
||||
throw new Error("endSMaskMode called while not in smask mode");
|
||||
}
|
||||
// The soft mask is done, now restore the suspended canvas as the main
|
||||
// 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];
|
||||
}
|
||||
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.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() {
|
||||
@ -1441,36 +1558,22 @@ class CanvasGraphics {
|
||||
const old = this.current;
|
||||
this.stateStack.push(old);
|
||||
this.current = old.clone();
|
||||
this.current.resumeSMaskCtx = null;
|
||||
}
|
||||
|
||||
restore() {
|
||||
// SMask was suspended, we just need to resume it.
|
||||
if (this.current.resumeSMaskCtx) {
|
||||
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 && this.current.activeSMask) {
|
||||
this.endSMaskMode();
|
||||
}
|
||||
|
||||
if (this.stateStack.length !== 0) {
|
||||
this.current = this.stateStack.pop();
|
||||
this.ctx.restore();
|
||||
this.checkSMaskState();
|
||||
|
||||
// Ensure that the clipping path is reset (fixes issue6413.pdf).
|
||||
this.pendingClip = null;
|
||||
|
||||
this._cachedGetSinglePixelWidth = null;
|
||||
} else {
|
||||
// We've finished all the SMask groups, reflect that in our state.
|
||||
this.current.activeSMask = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2092,6 +2195,7 @@ class CanvasGraphics {
|
||||
current.x += x * textHScale;
|
||||
}
|
||||
ctx.restore();
|
||||
this.compose();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -2256,6 +2360,7 @@ class CanvasGraphics {
|
||||
);
|
||||
|
||||
const inv = ctx.mozCurrentTransformInverse;
|
||||
let dirtyBox = [0, 0, ctx.canvas.width, ctx.canvas.height];
|
||||
if (inv) {
|
||||
const canvas = ctx.canvas;
|
||||
const width = canvas.width;
|
||||
@ -2270,6 +2375,10 @@ class CanvasGraphics {
|
||||
const y0 = Math.min(bl[1], br[1], ul[1], ur[1]);
|
||||
const x1 = Math.max(bl[0], br[0], ul[0], ur[0]);
|
||||
const y1 = Math.max(bl[1], br[1], ul[1], ur[1]);
|
||||
dirtyBox = Util.getAxialAlignedBoundingBox(
|
||||
[x0, y0, x1, y1],
|
||||
ctx.mozCurrentTransform
|
||||
);
|
||||
|
||||
this.ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
|
||||
} else {
|
||||
@ -2282,6 +2391,7 @@ class CanvasGraphics {
|
||||
this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10);
|
||||
}
|
||||
|
||||
this.compose(dirtyBox);
|
||||
this.restore();
|
||||
}
|
||||
|
||||
@ -2330,6 +2440,14 @@ class CanvasGraphics {
|
||||
}
|
||||
|
||||
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;
|
||||
// TODO non-isolated groups - according to Rik at adobe non-isolated
|
||||
// group results aren't usually that different and they even have tools
|
||||
@ -2432,6 +2550,7 @@ class CanvasGraphics {
|
||||
currentCtx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
currentCtx.translate(offsetX, offsetY);
|
||||
currentCtx.scale(scaleX, scaleY);
|
||||
currentCtx.save();
|
||||
}
|
||||
// The transparency group inherits all off the current graphics state
|
||||
// except the blend mode, soft mask, and alpha constants.
|
||||
@ -2442,11 +2561,11 @@ class CanvasGraphics {
|
||||
["ca", 1],
|
||||
["CA", 1],
|
||||
]);
|
||||
this.groupStack.push(currentCtx);
|
||||
this.groupStack.push({
|
||||
ctx: currentCtx,
|
||||
suspendedCtx,
|
||||
});
|
||||
this.groupLevel++;
|
||||
|
||||
// Resetting mask state, masks will be applied on restore of the group.
|
||||
this.current.activeSMask = null;
|
||||
}
|
||||
|
||||
endGroup(group) {
|
||||
@ -2455,17 +2574,33 @@ class CanvasGraphics {
|
||||
}
|
||||
this.groupLevel--;
|
||||
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
|
||||
// look kind of blurry for some pdfs.
|
||||
this.ctx.imageSmoothingEnabled = false;
|
||||
|
||||
if (suspendedCtx) {
|
||||
this.suspendedCtx = suspendedCtx;
|
||||
}
|
||||
|
||||
if (group.smask) {
|
||||
this.tempSMask = this.smaskStack.pop();
|
||||
this.restore();
|
||||
} 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.restore();
|
||||
this.compose(dirtyBox);
|
||||
}
|
||||
this.restore();
|
||||
}
|
||||
|
||||
beginAnnotations() {
|
||||
@ -2531,6 +2666,7 @@ class CanvasGraphics {
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.drawImage(maskCanvas, mask.offsetX, mask.offsetY);
|
||||
ctx.restore();
|
||||
this.compose();
|
||||
}
|
||||
|
||||
paintImageMaskXObjectRepeat(
|
||||
@ -2565,6 +2701,7 @@ class CanvasGraphics {
|
||||
ctx.drawImage(mask.canvas, x, y);
|
||||
}
|
||||
ctx.restore();
|
||||
this.compose();
|
||||
}
|
||||
|
||||
paintImageMaskXObjectGroup(images) {
|
||||
@ -2610,6 +2747,7 @@ class CanvasGraphics {
|
||||
ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1);
|
||||
ctx.restore();
|
||||
}
|
||||
this.compose();
|
||||
}
|
||||
|
||||
paintImageXObject(objId) {
|
||||
@ -2711,6 +2849,7 @@ class CanvasGraphics {
|
||||
height: height / ctx.mozCurrentTransformInverse[3],
|
||||
});
|
||||
}
|
||||
this.compose();
|
||||
this.restore();
|
||||
}
|
||||
|
||||
@ -2754,6 +2893,7 @@ class CanvasGraphics {
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
this.compose();
|
||||
}
|
||||
|
||||
paintSolidColorImageMask() {
|
||||
@ -2761,6 +2901,7 @@ class CanvasGraphics {
|
||||
return;
|
||||
}
|
||||
this.ctx.fillRect(0, 0, 1, 1);
|
||||
this.compose();
|
||||
}
|
||||
|
||||
// Marked content
|
||||
@ -2810,6 +2951,9 @@ class CanvasGraphics {
|
||||
// Helper functions
|
||||
|
||||
consumePath() {
|
||||
if (!this.pendingClip) {
|
||||
this.compose();
|
||||
}
|
||||
const ctx = this.ctx;
|
||||
if (this.pendingClip) {
|
||||
if (this.pendingClip === EO_CLIP) {
|
||||
|
5
test/pdfs/.gitignore
vendored
5
test/pdfs/.gitignore
vendored
@ -16,6 +16,7 @@
|
||||
!issue2391-1.pdf
|
||||
!issue2391-2.pdf
|
||||
!issue14046.pdf
|
||||
!issue7891_bc1.pdf
|
||||
!issue3214.pdf
|
||||
!issue4665.pdf
|
||||
!issue4684.pdf
|
||||
@ -105,6 +106,7 @@
|
||||
!bug1057544.pdf
|
||||
!issue11150_reduced.pdf
|
||||
!issue6127.pdf
|
||||
!issue7891_bc0.pdf
|
||||
!issue11242_reduced.pdf
|
||||
!issue11279.pdf
|
||||
!issue11362.pdf
|
||||
@ -244,6 +246,7 @@
|
||||
!issue2931.pdf
|
||||
!issue3323.pdf
|
||||
!issue4304.pdf
|
||||
!issue9017_reduced.pdf
|
||||
!issue4379.pdf
|
||||
!issue4550.pdf
|
||||
!issue13316_reduced.pdf
|
||||
@ -378,6 +381,7 @@
|
||||
!issue5044.pdf
|
||||
!issue1512r.pdf
|
||||
!issue2128r.pdf
|
||||
!bug1703683_page2_reduced.pdf
|
||||
!issue5540.pdf
|
||||
!issue5549.pdf
|
||||
!visibility_expressions.pdf
|
||||
@ -432,6 +436,7 @@
|
||||
!annotation-freetext.pdf
|
||||
!annotation-line.pdf
|
||||
!evaljs.pdf
|
||||
!issue12798_page1_reduced.pdf
|
||||
!annotation-line-without-appearance.pdf
|
||||
!bug1669099.pdf
|
||||
!annotation-square-circle.pdf
|
||||
|
BIN
test/pdfs/bug1703683_page2_reduced.pdf
Normal file
BIN
test/pdfs/bug1703683_page2_reduced.pdf
Normal file
Binary file not shown.
BIN
test/pdfs/issue12798_page1_reduced.pdf
Normal file
BIN
test/pdfs/issue12798_page1_reduced.pdf
Normal file
Binary file not shown.
BIN
test/pdfs/issue7891_bc0.pdf
Normal file
BIN
test/pdfs/issue7891_bc0.pdf
Normal file
Binary file not shown.
BIN
test/pdfs/issue7891_bc1.pdf
Normal file
BIN
test/pdfs/issue7891_bc1.pdf
Normal file
Binary file not shown.
BIN
test/pdfs/issue9017_reduced.pdf
Normal file
BIN
test/pdfs/issue9017_reduced.pdf
Normal file
Binary file not shown.
@ -530,6 +530,12 @@
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "issue12798_page1_reduced",
|
||||
"file": "pdfs/issue12798_page1_reduced.pdf",
|
||||
"md5": "f4c3e91c181b510929ade67c1e34c5c5",
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "hmm-pdf",
|
||||
"file": "pdfs/hmm.pdf",
|
||||
"md5": "e08467e60101ee5f4a59716e86db6dc9",
|
||||
@ -769,6 +775,12 @@
|
||||
"lastPage": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "bug1703683_page2_reduced",
|
||||
"file": "pdfs/bug1703683_page2_reduced.pdf",
|
||||
"md5": "2b6d5d617438cf72c76c25f46ec6ad75",
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "issue6707",
|
||||
"file": "pdfs/issue6707.pdf",
|
||||
"md5": "068ceaec23d265b1d38dfa6ab279f017",
|
||||
@ -883,6 +895,12 @@
|
||||
"type": "eq",
|
||||
"annotations": true
|
||||
},
|
||||
{ "id": "issue9017_reduced",
|
||||
"file": "pdfs/issue9017_reduced.pdf",
|
||||
"md5": "8b45b3ea91778d6d98f407620d645f48",
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "issue4934",
|
||||
"file": "pdfs/issue4934.pdf",
|
||||
"md5": "6099da44f677702ae65a648b51a2226d",
|
||||
@ -3442,6 +3460,12 @@
|
||||
"link": true,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "issue7891_bc0",
|
||||
"file": "pdfs/issue7891_bc0.pdf",
|
||||
"md5": "744a22244a4e4708b7f1691eec155fc8",
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{
|
||||
"id": "issue6165",
|
||||
"file": "pdfs/issue6165.pdf",
|
||||
@ -4529,6 +4553,12 @@
|
||||
"link": true,
|
||||
"type": "load"
|
||||
},
|
||||
{ "id": "issue7891_bc1",
|
||||
"file": "pdfs/issue7891_bc1.pdf",
|
||||
"md5": "86b1796da7dad09f93ce68a8ad495a24",
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "issue3062",
|
||||
"file": "pdfs/issue3062.pdf",
|
||||
"md5": "206715f1258f0e117df4180d98dd4d68",
|
||||
|
Loading…
Reference in New Issue
Block a user