Skip to content

Commit 7def6d1

Browse files
committed
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
1 parent f8b2a99 commit 7def6d1

File tree

6 files changed

+57
-21
lines changed

6 files changed

+57
-21
lines changed

src/display/canvas.js

+39-21
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ function mirrorContextOperations(ctx, destCtx) {
191191
}
192192

193193
function addContextCurrentTransform(ctx) {
194+
if (ctx._transformStack) {
195+
// Reset the transform stack.
196+
ctx._transformStack = [];
197+
}
194198
// If the context doesn't expose a `mozCurrentTransform`, add a JS based one.
195199
if (ctx.mozCurrentTransform) {
196200
return;
@@ -265,6 +269,9 @@ function addContextCurrentTransform(ctx) {
265269
};
266270

267271
ctx.restore = function ctxRestore() {
272+
if (this._transformStack.length === 0) {
273+
warn("Tried to restore a ctx when the stack was already empty.");
274+
}
268275
const prev = this._transformStack.pop();
269276
if (prev) {
270277
this._transformMatrix = prev;
@@ -1242,7 +1249,7 @@ class CanvasGraphics {
12421249

12431250
endDrawing() {
12441251
// Finishing all opened operations such as SMask group painting.
1245-
while (this.stateStack.length || this.current.activeSMask !== null) {
1252+
while (this.stateStack.length || this.inSMaskMode) {
12461253
this.restore();
12471254
}
12481255

@@ -1503,8 +1510,12 @@ class CanvasGraphics {
15031510
}
15041511
}
15051512

1513+
get inSMaskMode() {
1514+
return !!this.suspendedCtx;
1515+
}
1516+
15061517
checkSMaskState() {
1507-
const inSMaskMode = !!this.suspendedCtx;
1518+
const inSMaskMode = this.inSMaskMode;
15081519
if (this.current.activeSMask && !inSMaskMode) {
15091520
this.beginSMaskMode();
15101521
} else if (!this.current.activeSMask && inSMaskMode) {
@@ -1523,7 +1534,7 @@ class CanvasGraphics {
15231534
* the right order on the canvas' graphics state stack.
15241535
*/
15251536
beginSMaskMode() {
1526-
if (this.suspendedCtx) {
1537+
if (this.inSMaskMode) {
15271538
throw new Error("beginSMaskMode called while already in smask mode");
15281539
}
15291540
const drawnWidth = this.ctx.canvas.width;
@@ -1550,7 +1561,7 @@ class CanvasGraphics {
15501561
}
15511562

15521563
endSMaskMode() {
1553-
if (!this.suspendedCtx) {
1564+
if (!this.inSMaskMode) {
15541565
throw new Error("endSMaskMode called while not in smask mode");
15551566
}
15561567
// The soft mask is done, now restore the suspended canvas as the main
@@ -1559,7 +1570,6 @@ class CanvasGraphics {
15591570
copyCtxState(this.ctx, this.suspendedCtx);
15601571
this.ctx = this.suspendedCtx;
15611572

1562-
this.current.activeSMask = null;
15631573
this.suspendedCtx = null;
15641574
}
15651575

@@ -1589,20 +1599,36 @@ class CanvasGraphics {
15891599
}
15901600

15911601
save() {
1592-
this.ctx.save();
1602+
if (this.inSMaskMode) {
1603+
// SMask mode may be turned on/off causing us to lose graphics state.
1604+
// Copy the temporary canvas state to the main(suspended) canvas to keep
1605+
// it in sync.
1606+
copyCtxState(this.ctx, this.suspendedCtx);
1607+
// Don't bother calling save on the temporary canvas since state is not
1608+
// saved there.
1609+
this.suspendedCtx.save();
1610+
} else {
1611+
this.ctx.save();
1612+
}
15931613
const old = this.current;
15941614
this.stateStack.push(old);
15951615
this.current = old.clone();
15961616
}
15971617

15981618
restore() {
1599-
if (this.stateStack.length === 0 && this.current.activeSMask) {
1619+
if (this.stateStack.length === 0 && this.inSMaskMode) {
16001620
this.endSMaskMode();
16011621
}
1602-
16031622
if (this.stateStack.length !== 0) {
16041623
this.current = this.stateStack.pop();
1605-
this.ctx.restore();
1624+
if (this.inSMaskMode) {
1625+
// Graphics state is stored on the main(suspended) canvas. Restore its
1626+
// state then copy it over to the temporary canvas.
1627+
this.suspendedCtx.restore();
1628+
copyCtxState(this.suspendedCtx, this.ctx);
1629+
} else {
1630+
this.ctx.restore();
1631+
}
16061632
this.checkSMaskState();
16071633

16081634
// Ensure that the clipping path is reset (fixes issue6413.pdf).
@@ -2525,9 +2551,8 @@ class CanvasGraphics {
25252551
this.save();
25262552
// If there's an active soft mask we don't want it enabled for the group, so
25272553
// clear it out. The mask and suspended canvas will be restored in endGroup.
2528-
const suspendedCtx = this.suspendedCtx;
2529-
if (this.current.activeSMask) {
2530-
this.suspendedCtx = null;
2554+
if (this.inSMaskMode) {
2555+
this.endSMaskMode();
25312556
this.current.activeSMask = null;
25322557
}
25332558

@@ -2646,10 +2671,7 @@ class CanvasGraphics {
26462671
["ca", 1],
26472672
["CA", 1],
26482673
]);
2649-
this.groupStack.push({
2650-
ctx: currentCtx,
2651-
suspendedCtx,
2652-
});
2674+
this.groupStack.push(currentCtx);
26532675
this.groupLevel++;
26542676
}
26552677

@@ -2659,16 +2681,12 @@ class CanvasGraphics {
26592681
}
26602682
this.groupLevel--;
26612683
const groupCtx = this.ctx;
2662-
const { ctx, suspendedCtx } = this.groupStack.pop();
2684+
const ctx = this.groupStack.pop();
26632685
this.ctx = ctx;
26642686
// Turn off image smoothing to avoid sub pixel interpolation which can
26652687
// look kind of blurry for some pdfs.
26662688
this.ctx.imageSmoothingEnabled = false;
26672689

2668-
if (suspendedCtx) {
2669-
this.suspendedCtx = suspendedCtx;
2670-
}
2671-
26722690
if (group.smask) {
26732691
this.tempSMask = this.smaskStack.pop();
26742692
this.restore();

src/display/pattern_helper.js

+4
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,10 @@ class TilingPattern {
577577
tmpCtx.translate(-(dimx.scale * adjustedX0), -(dimy.scale * adjustedY0));
578578
graphics.transform(dimx.scale, 0, 0, dimy.scale, 0, 0);
579579

580+
// To match CanvasGraphics beginDrawing we must save the context here or
581+
// else we end up with unbalanced save/restores.
582+
tmpCtx.save();
583+
580584
this.clipBbox(graphics, adjustedX0, adjustedY0, adjustedX1, adjustedY1);
581585

582586
graphics.baseTransform = graphics.ctx.mozCurrentTransform.slice();

test/pdfs/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
!issue8565.pdf
176176
!clippath.pdf
177177
!issue8795_reduced.pdf
178+
!bug1755507.pdf
178179
!close-path-bug.pdf
179180
!issue6019.pdf
180181
!issue6621.pdf
@@ -491,6 +492,7 @@
491492
!pr12564.pdf
492493
!pr12828.pdf
493494
!secHandler.pdf
495+
!issue14297.pdf
494496
!rc_annotation.pdf
495497
!issue14267.pdf
496498
!PDFBOX-4352-0.pdf

test/pdfs/bug1755507.pdf

487 KB
Binary file not shown.

test/pdfs/issue14297.pdf

237 KB
Binary file not shown.

test/test_manifest.json

+12
Original file line numberDiff line numberDiff line change
@@ -2157,6 +2157,12 @@
21572157
"type": "eq",
21582158
"about": "Glyph that gets mapped to unicode non-breaking-space."
21592159
},
2160+
{ "id": "issue14297",
2161+
"file": "pdfs/issue14297.pdf",
2162+
"md5": "46eb3d4d4bc47c8009fc4c699d213a18",
2163+
"rounds": 1,
2164+
"type": "eq"
2165+
},
21602166
{ "id": "simpletype3font",
21612167
"file": "pdfs/simpletype3font.pdf",
21622168
"md5": "b374c7543920840c61999e9e86939f99",
@@ -2506,6 +2512,12 @@
25062512
"lastPage": 2,
25072513
"type": "eq"
25082514
},
2515+
{ "id": "bug1755507",
2516+
"file": "pdfs/bug1755507.pdf",
2517+
"md5": "319d73b8fd680cdc583d69b7f7ab29e9",
2518+
"rounds": 1,
2519+
"type": "eq"
2520+
},
25092521
{ "id": "issue9367",
25102522
"file": "pdfs/issue9367.pdf",
25112523
"md5": "81a2c6f1fe5d1bb00ff0479aa6547155",

0 commit comments

Comments
 (0)