Merge pull request #14175 from brendandahl/smask-v2
Use a new method for handling soft masks.
This commit is contained in:
		
						commit
						52372b9378
					
				| @ -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) { | ||||
| @ -449,7 +579,7 @@ function compileType3Glyph(imgData) { | ||||
| } | ||||
| 
 | ||||
| class CanvasExtraState { | ||||
|   constructor() { | ||||
|   constructor(width, height) { | ||||
|     // Are soft masks and alpha values shapes or opacities?
 | ||||
|     this.alphaIsShape = false; | ||||
|     this.fontSize = 0; | ||||
| @ -479,18 +609,56 @@ class CanvasExtraState { | ||||
|     this.strokeAlpha = 1; | ||||
|     this.lineWidth = 1; | ||||
|     this.activeSMask = null; | ||||
|     this.resumeSMaskCtx = null; // nonclonable field (see the save method below)
 | ||||
|     this.transferMaps = null; | ||||
| 
 | ||||
|     this.startNewPathAndClipBox([0, 0, width, height]); | ||||
|   } | ||||
| 
 | ||||
|   clone() { | ||||
|     return Object.create(this); | ||||
|     const clone = Object.create(this); | ||||
|     clone.clipBox = this.clipBox.slice(); | ||||
|     return clone; | ||||
|   } | ||||
| 
 | ||||
|   setCurrentPoint(x, y) { | ||||
|     this.x = x; | ||||
|     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) { | ||||
| @ -816,7 +984,11 @@ function genericComposeSMask( | ||||
|   height, | ||||
|   subtype, | ||||
|   backdrop, | ||||
|   transferMap | ||||
|   transferMap, | ||||
|   layerOffsetX, | ||||
|   layerOffsetY, | ||||
|   maskOffsetX, | ||||
|   maskOffsetY | ||||
| ) { | ||||
|   const hasBackdrop = !!backdrop; | ||||
|   const r0 = hasBackdrop ? backdrop[0] : 0; | ||||
| @ -835,41 +1007,55 @@ 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; | ||||
|   if (layerWidth === 0 || layerHeight === 0) { | ||||
|     return; | ||||
|   } | ||||
|   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) { | ||||
| @ -907,7 +1093,10 @@ class CanvasGraphics { | ||||
|     optionalContentConfig | ||||
|   ) { | ||||
|     this.ctx = canvasCtx; | ||||
|     this.current = new CanvasExtraState(); | ||||
|     this.current = new CanvasExtraState( | ||||
|       this.ctx.canvas.width, | ||||
|       this.ctx.canvas.height | ||||
|     ); | ||||
|     this.stateStack = []; | ||||
|     this.pendingClip = null; | ||||
|     this.pendingEOFill = false; | ||||
| @ -927,6 +1116,7 @@ class CanvasGraphics { | ||||
|     this.smaskStack = []; | ||||
|     this.smaskCounter = 0; | ||||
|     this.tempSMask = null; | ||||
|     this.suspendedCtx = null; | ||||
|     this.contentVisible = true; | ||||
|     this.markedContentStack = []; | ||||
|     this.optionalContentConfig = optionalContentConfig; | ||||
| @ -1319,25 +1509,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 +1519,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 +1551,57 @@ 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]; | ||||
|     } 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.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 +1609,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; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -1486,6 +1640,7 @@ class CanvasGraphics { | ||||
|     const current = this.current; | ||||
|     let x = current.x, | ||||
|       y = current.y; | ||||
|     let startX, startY; | ||||
|     for (let i = 0, j = 0, ii = ops.length; i < ii; i++) { | ||||
|       switch (ops[i] | 0) { | ||||
|         case OPS.rectangle: | ||||
| @ -1504,20 +1659,25 @@ class CanvasGraphics { | ||||
|             ctx.lineTo(xw, yh); | ||||
|             ctx.lineTo(x, yh); | ||||
|           } | ||||
| 
 | ||||
|           current.updatePathMinMax(ctx.mozCurrentTransform, x, y); | ||||
|           current.updatePathMinMax(ctx.mozCurrentTransform, xw, yh); | ||||
|           ctx.closePath(); | ||||
|           break; | ||||
|         case OPS.moveTo: | ||||
|           x = args[j++]; | ||||
|           y = args[j++]; | ||||
|           ctx.moveTo(x, y); | ||||
|           current.updatePathMinMax(ctx.mozCurrentTransform, x, y); | ||||
|           break; | ||||
|         case OPS.lineTo: | ||||
|           x = args[j++]; | ||||
|           y = args[j++]; | ||||
|           ctx.lineTo(x, y); | ||||
|           current.updatePathMinMax(ctx.mozCurrentTransform, x, y); | ||||
|           break; | ||||
|         case OPS.curveTo: | ||||
|           startX = x; | ||||
|           startY = y; | ||||
|           x = args[j + 4]; | ||||
|           y = args[j + 5]; | ||||
|           ctx.bezierCurveTo( | ||||
| @ -1528,9 +1688,22 @@ class CanvasGraphics { | ||||
|             x, | ||||
|             y | ||||
|           ); | ||||
|           current.updateCurvePathMinMax( | ||||
|             ctx.mozCurrentTransform, | ||||
|             startX, | ||||
|             startY, | ||||
|             args[j], | ||||
|             args[j + 1], | ||||
|             args[j + 2], | ||||
|             args[j + 3], | ||||
|             x, | ||||
|             y | ||||
|           ); | ||||
|           j += 6; | ||||
|           break; | ||||
|         case OPS.curveTo2: | ||||
|           startX = x; | ||||
|           startY = y; | ||||
|           ctx.bezierCurveTo( | ||||
|             x, | ||||
|             y, | ||||
| @ -1539,14 +1712,38 @@ class CanvasGraphics { | ||||
|             args[j + 2], | ||||
|             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]; | ||||
|           y = args[j + 3]; | ||||
|           j += 4; | ||||
|           break; | ||||
|         case OPS.curveTo3: | ||||
|           startX = x; | ||||
|           startY = y; | ||||
|           x = args[j + 2]; | ||||
|           y = args[j + 3]; | ||||
|           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; | ||||
|           break; | ||||
|         case OPS.closePath: | ||||
| @ -1599,7 +1796,7 @@ class CanvasGraphics { | ||||
|       } | ||||
|     } | ||||
|     if (consumePath) { | ||||
|       this.consumePath(); | ||||
|       this.consumePath(this.current.getClippedPathBoundingBox()); | ||||
|     } | ||||
|     // Restore the global alpha to the fill alpha
 | ||||
|     ctx.globalAlpha = this.current.fillAlpha; | ||||
| @ -1627,7 +1824,8 @@ class CanvasGraphics { | ||||
|       needRestore = true; | ||||
|     } | ||||
| 
 | ||||
|     if (this.contentVisible) { | ||||
|     const intersect = this.current.getClippedPathBoundingBox(); | ||||
|     if (this.contentVisible && intersect !== null) { | ||||
|       if (this.pendingEOFill) { | ||||
|         ctx.fill("evenodd"); | ||||
|         this.pendingEOFill = false; | ||||
| @ -1640,7 +1838,7 @@ class CanvasGraphics { | ||||
|       ctx.restore(); | ||||
|     } | ||||
|     if (consumePath) { | ||||
|       this.consumePath(); | ||||
|       this.consumePath(intersect); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -2092,6 +2290,7 @@ class CanvasGraphics { | ||||
|       current.x += x * textHScale; | ||||
|     } | ||||
|     ctx.restore(); | ||||
|     this.compose(); | ||||
|     return undefined; | ||||
|   } | ||||
| 
 | ||||
| @ -2282,6 +2481,7 @@ class CanvasGraphics { | ||||
|       this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); | ||||
|     } | ||||
| 
 | ||||
|     this.compose(this.current.getClippedPathBoundingBox()); | ||||
|     this.restore(); | ||||
|   } | ||||
| 
 | ||||
| @ -2311,6 +2511,16 @@ class CanvasGraphics { | ||||
|       const width = bbox[2] - bbox[0]; | ||||
|       const height = bbox[3] - bbox[1]; | ||||
|       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.endPath(); | ||||
|     } | ||||
| @ -2330,6 +2540,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
 | ||||
| @ -2393,6 +2611,8 @@ class CanvasGraphics { | ||||
|       drawnHeight = MAX_GROUP_SIZE; | ||||
|     } | ||||
| 
 | ||||
|     this.current.startNewPathAndClipBox([0, 0, drawnWidth, drawnHeight]); | ||||
| 
 | ||||
|     let cacheId = "groupAt" + this.groupLevel; | ||||
|     if (group.smask) { | ||||
|       // 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.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 +2663,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 +2676,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() { | ||||
| @ -2482,7 +2719,10 @@ class CanvasGraphics { | ||||
|   beginAnnotation(id, rect, transform, matrix) { | ||||
|     this.save(); | ||||
|     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) { | ||||
|       const width = rect[2] - rect[0]; | ||||
| @ -2531,6 +2771,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 +2806,7 @@ class CanvasGraphics { | ||||
|       ctx.drawImage(mask.canvas, x, y); | ||||
|     } | ||||
|     ctx.restore(); | ||||
|     this.compose(); | ||||
|   } | ||||
| 
 | ||||
|   paintImageMaskXObjectGroup(images) { | ||||
| @ -2610,6 +2852,7 @@ class CanvasGraphics { | ||||
|       ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1); | ||||
|       ctx.restore(); | ||||
|     } | ||||
|     this.compose(); | ||||
|   } | ||||
| 
 | ||||
|   paintImageXObject(objId) { | ||||
| @ -2711,6 +2954,7 @@ class CanvasGraphics { | ||||
|         height: height / ctx.mozCurrentTransformInverse[3], | ||||
|       }); | ||||
|     } | ||||
|     this.compose(); | ||||
|     this.restore(); | ||||
|   } | ||||
| 
 | ||||
| @ -2754,6 +2998,7 @@ class CanvasGraphics { | ||||
|       } | ||||
|       ctx.restore(); | ||||
|     } | ||||
|     this.compose(); | ||||
|   } | ||||
| 
 | ||||
|   paintSolidColorImageMask() { | ||||
| @ -2761,6 +3006,7 @@ class CanvasGraphics { | ||||
|       return; | ||||
|     } | ||||
|     this.ctx.fillRect(0, 0, 1, 1); | ||||
|     this.compose(); | ||||
|   } | ||||
| 
 | ||||
|   // Marked content
 | ||||
| @ -2809,7 +3055,13 @@ class CanvasGraphics { | ||||
| 
 | ||||
|   // Helper functions
 | ||||
| 
 | ||||
|   consumePath() { | ||||
|   consumePath(clipBox) { | ||||
|     if (this.pendingClip) { | ||||
|       this.current.updateClipFromPath(); | ||||
|     } | ||||
|     if (!this.pendingClip) { | ||||
|       this.compose(clipBox); | ||||
|     } | ||||
|     const ctx = this.ctx; | ||||
|     if (this.pendingClip) { | ||||
|       if (this.pendingClip === EO_CLIP) { | ||||
|  | ||||
| @ -872,6 +872,78 @@ class Util { | ||||
| 
 | ||||
|     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 = [ | ||||
|  | ||||
							
								
								
									
										5
									
								
								test/pdfs/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								test/pdfs/.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -17,6 +17,7 @@ | ||||
| !issue2391-1.pdf | ||||
| !issue2391-2.pdf | ||||
| !issue14046.pdf | ||||
| !issue7891_bc1.pdf | ||||
| !issue3214.pdf | ||||
| !issue4665.pdf | ||||
| !issue4684.pdf | ||||
| @ -106,6 +107,7 @@ | ||||
| !bug1057544.pdf | ||||
| !issue11150_reduced.pdf | ||||
| !issue6127.pdf | ||||
| !issue7891_bc0.pdf | ||||
| !issue11242_reduced.pdf | ||||
| !issue11279.pdf | ||||
| !issue11362.pdf | ||||
| @ -246,6 +248,7 @@ | ||||
| !issue2931.pdf | ||||
| !issue3323.pdf | ||||
| !issue4304.pdf | ||||
| !issue9017_reduced.pdf | ||||
| !issue4379.pdf | ||||
| !issue4550.pdf | ||||
| !issue13316_reduced.pdf | ||||
| @ -381,6 +384,7 @@ | ||||
| !issue5044.pdf | ||||
| !issue1512r.pdf | ||||
| !issue2128r.pdf | ||||
| !bug1703683_page2_reduced.pdf | ||||
| !issue5540.pdf | ||||
| !issue5549.pdf | ||||
| !visibility_expressions.pdf | ||||
| @ -435,6 +439,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", | ||||
| @ -3450,6 +3468,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", | ||||
| @ -4537,6 +4561,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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user