Fix canvas state getting out of sync from smasks. (bug 1755507)

Soft masks can be enabled/disabled at anytime and at different
points in the save/restore stack. This can lead to
the amount of save/restores becoming unbalanced across the
two canvases. Instead of save/restoring on the temporary canvas
change it so we only track state on the main (suspended canvas).

I was also getting an out balance stack from patterns, so I've also
fixed that and added a warning that will at least show up on chrome.
It would be nice to add this so Firefox at some point too.

Fixes #11328, #14297 and bug 1755507
This commit is contained in:
Brendan Dahl 2022-02-15 16:44:50 -08:00
parent f8b2a99ddc
commit 7def6d12c8
6 changed files with 57 additions and 21 deletions

View File

@ -191,6 +191,10 @@ function mirrorContextOperations(ctx, destCtx) {
}
function addContextCurrentTransform(ctx) {
if (ctx._transformStack) {
// Reset the transform stack.
ctx._transformStack = [];
}
// If the context doesn't expose a `mozCurrentTransform`, add a JS based one.
if (ctx.mozCurrentTransform) {
return;
@ -265,6 +269,9 @@ function addContextCurrentTransform(ctx) {
};
ctx.restore = function ctxRestore() {
if (this._transformStack.length === 0) {
warn("Tried to restore a ctx when the stack was already empty.");
}
const prev = this._transformStack.pop();
if (prev) {
this._transformMatrix = prev;
@ -1242,7 +1249,7 @@ class CanvasGraphics {
endDrawing() {
// Finishing all opened operations such as SMask group painting.
while (this.stateStack.length || this.current.activeSMask !== null) {
while (this.stateStack.length || this.inSMaskMode) {
this.restore();
}
@ -1503,8 +1510,12 @@ class CanvasGraphics {
}
}
get inSMaskMode() {
return !!this.suspendedCtx;
}
checkSMaskState() {
const inSMaskMode = !!this.suspendedCtx;
const inSMaskMode = this.inSMaskMode;
if (this.current.activeSMask && !inSMaskMode) {
this.beginSMaskMode();
} else if (!this.current.activeSMask && inSMaskMode) {
@ -1523,7 +1534,7 @@ class CanvasGraphics {
* the right order on the canvas' graphics state stack.
*/
beginSMaskMode() {
if (this.suspendedCtx) {
if (this.inSMaskMode) {
throw new Error("beginSMaskMode called while already in smask mode");
}
const drawnWidth = this.ctx.canvas.width;
@ -1550,7 +1561,7 @@ class CanvasGraphics {
}
endSMaskMode() {
if (!this.suspendedCtx) {
if (!this.inSMaskMode) {
throw new Error("endSMaskMode called while not in smask mode");
}
// The soft mask is done, now restore the suspended canvas as the main
@ -1559,7 +1570,6 @@ class CanvasGraphics {
copyCtxState(this.ctx, this.suspendedCtx);
this.ctx = this.suspendedCtx;
this.current.activeSMask = null;
this.suspendedCtx = null;
}
@ -1589,20 +1599,36 @@ class CanvasGraphics {
}
save() {
this.ctx.save();
if (this.inSMaskMode) {
// SMask mode may be turned on/off causing us to lose graphics state.
// Copy the temporary canvas state to the main(suspended) canvas to keep
// it in sync.
copyCtxState(this.ctx, this.suspendedCtx);
// Don't bother calling save on the temporary canvas since state is not
// saved there.
this.suspendedCtx.save();
} else {
this.ctx.save();
}
const old = this.current;
this.stateStack.push(old);
this.current = old.clone();
}
restore() {
if (this.stateStack.length === 0 && this.current.activeSMask) {
if (this.stateStack.length === 0 && this.inSMaskMode) {
this.endSMaskMode();
}
if (this.stateStack.length !== 0) {
this.current = this.stateStack.pop();
this.ctx.restore();
if (this.inSMaskMode) {
// Graphics state is stored on the main(suspended) canvas. Restore its
// state then copy it over to the temporary canvas.
this.suspendedCtx.restore();
copyCtxState(this.suspendedCtx, this.ctx);
} else {
this.ctx.restore();
}
this.checkSMaskState();
// Ensure that the clipping path is reset (fixes issue6413.pdf).
@ -2525,9 +2551,8 @@ 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;
if (this.inSMaskMode) {
this.endSMaskMode();
this.current.activeSMask = null;
}
@ -2646,10 +2671,7 @@ class CanvasGraphics {
["ca", 1],
["CA", 1],
]);
this.groupStack.push({
ctx: currentCtx,
suspendedCtx,
});
this.groupStack.push(currentCtx);
this.groupLevel++;
}
@ -2659,16 +2681,12 @@ class CanvasGraphics {
}
this.groupLevel--;
const groupCtx = this.ctx;
const { ctx, suspendedCtx } = this.groupStack.pop();
const ctx = 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();

View File

@ -577,6 +577,10 @@ class TilingPattern {
tmpCtx.translate(-(dimx.scale * adjustedX0), -(dimy.scale * adjustedY0));
graphics.transform(dimx.scale, 0, 0, dimy.scale, 0, 0);
// To match CanvasGraphics beginDrawing we must save the context here or
// else we end up with unbalanced save/restores.
tmpCtx.save();
this.clipBbox(graphics, adjustedX0, adjustedY0, adjustedX1, adjustedY1);
graphics.baseTransform = graphics.ctx.mozCurrentTransform.slice();

View File

@ -175,6 +175,7 @@
!issue8565.pdf
!clippath.pdf
!issue8795_reduced.pdf
!bug1755507.pdf
!close-path-bug.pdf
!issue6019.pdf
!issue6621.pdf
@ -491,6 +492,7 @@
!pr12564.pdf
!pr12828.pdf
!secHandler.pdf
!issue14297.pdf
!rc_annotation.pdf
!issue14267.pdf
!PDFBOX-4352-0.pdf

BIN
test/pdfs/bug1755507.pdf Normal file

Binary file not shown.

BIN
test/pdfs/issue14297.pdf Normal file

Binary file not shown.

View File

@ -2157,6 +2157,12 @@
"type": "eq",
"about": "Glyph that gets mapped to unicode non-breaking-space."
},
{ "id": "issue14297",
"file": "pdfs/issue14297.pdf",
"md5": "46eb3d4d4bc47c8009fc4c699d213a18",
"rounds": 1,
"type": "eq"
},
{ "id": "simpletype3font",
"file": "pdfs/simpletype3font.pdf",
"md5": "b374c7543920840c61999e9e86939f99",
@ -2506,6 +2512,12 @@
"lastPage": 2,
"type": "eq"
},
{ "id": "bug1755507",
"file": "pdfs/bug1755507.pdf",
"md5": "319d73b8fd680cdc583d69b7f7ab29e9",
"rounds": 1,
"type": "eq"
},
{ "id": "issue9367",
"file": "pdfs/issue9367.pdf",
"md5": "81a2c6f1fe5d1bb00ff0479aa6547155",