From a52c0c6988e557b39c8ea56184a7874e58b6fc96 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Wed, 30 Jun 2021 15:09:07 -0700 Subject: [PATCH] Fix transformations when painting image masks and tiling patterns. Previously, when we filled image masks we didn't copy over the current transformation, this caused patterns to be misaligned when painted. Now we create a temporary canvas with the mask and have the transform copied over and offset it relative to where the mask would be painted. We also weren't properly offsetting tiling patterns. This isn't usually noticeable since patters repeat, but in the case of #13561 the pattern is only drawn once and has to be in the correct position to line up with the mask image. These fixes broke #11473, but highlighted that we were drawing that correctly by accident and not correctly handling negative bounding boxes on tiling patterns. Fixes #6297, #13561, #13441 Partially fixes #1344 (still blurry but boxes are in correct position now) --- src/display/canvas.js | 372 ++++++++++++++++++------------- src/display/pattern_helper.js | 53 +++-- test/pdfs/.gitignore | 1 + test/pdfs/issue13561_reduced.pdf | Bin 0 -> 16050 bytes test/test_manifest.json | 6 + 5 files changed, 253 insertions(+), 179 deletions(-) create mode 100644 test/pdfs/issue13561_reduced.pdf diff --git a/src/display/canvas.js b/src/display/canvas.js index c5a1d21d2..f1cab4b52 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -13,11 +13,6 @@ * limitations under the License. */ -import { - createMatrix, - getShadingPattern, - TilingPattern, -} from "./pattern_helper.js"; import { FONT_IDENTITY_MATRIX, IDENTITY_MATRIX, @@ -32,6 +27,7 @@ import { Util, warn, } from "../shared/util.js"; +import { getShadingPattern, TilingPattern } from "./pattern_helper.js"; // contexts store most of the state we need natively. // However, PDF needs a bit more state, which we store here. @@ -197,17 +193,6 @@ function addContextCurrentTransform(ctx) { }; } -function getAdjustmentTransformation(transform, width, height) { - // The pattern will be created at the size of the current page or form object, - // but the mask is usually scaled differently and offset, so we must account - // for these to shift and rescale the pattern to the correctly location. - let patternTransform = createMatrix(transform); - patternTransform = patternTransform.scale(1 / width, -1 / height); - patternTransform = patternTransform.translate(0, -height); - patternTransform = patternTransform.inverse(); - return patternTransform; -} - class CachedCanvases { constructor(canvasFactory) { this.canvasFactory = canvasFactory; @@ -1046,6 +1031,154 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { } } + _scaleImage(img, inverseTransform) { + // Vertical or horizontal scaling shall not be more than 2 to not lose the + // pixels during drawImage operation, painting on the temporary canvas(es) + // that are twice smaller in size. + const width = img.width; + const height = img.height; + let widthScale = Math.max( + Math.hypot(inverseTransform[0], inverseTransform[1]), + 1 + ); + let heightScale = Math.max( + Math.hypot(inverseTransform[2], inverseTransform[3]), + 1 + ); + + let paintWidth = width, + paintHeight = height; + let tmpCanvasId = "prescale1"; + let tmpCanvas, tmpCtx; + while ( + (widthScale > 2 && paintWidth > 1) || + (heightScale > 2 && paintHeight > 1) + ) { + let newWidth = paintWidth, + newHeight = paintHeight; + if (widthScale > 2 && paintWidth > 1) { + newWidth = Math.ceil(paintWidth / 2); + widthScale /= paintWidth / newWidth; + } + if (heightScale > 2 && paintHeight > 1) { + newHeight = Math.ceil(paintHeight / 2); + heightScale /= paintHeight / newHeight; + } + tmpCanvas = this.cachedCanvases.getCanvas( + tmpCanvasId, + newWidth, + newHeight + ); + tmpCtx = tmpCanvas.context; + tmpCtx.clearRect(0, 0, newWidth, newHeight); + tmpCtx.drawImage( + img, + 0, + 0, + paintWidth, + paintHeight, + 0, + 0, + newWidth, + newHeight + ); + img = tmpCanvas.canvas; + paintWidth = newWidth; + paintHeight = newHeight; + tmpCanvasId = tmpCanvasId === "prescale1" ? "prescale2" : "prescale1"; + } + return { + img, + paintWidth, + paintHeight, + }; + } + + _createMaskCanvas(img) { + const ctx = this.ctx; + const width = img.width, + height = img.height; + const fillColor = this.current.fillColor; + const isPatternFill = this.current.patternFill; + const maskCanvas = this.cachedCanvases.getCanvas( + "maskCanvas", + width, + height + ); + const maskCtx = maskCanvas.context; + putBinaryImageMask(maskCtx, img); + + // Create the mask canvas at the size it will be drawn at and also set + // its transform to match the current transform so if there are any + // patterns applied they will be applied relative to the correct + // transform. + const objToCanvas = ctx.mozCurrentTransform; + let maskToCanvas = Util.transform(objToCanvas, [ + 1 / width, + 0, + 0, + -1 / height, + 0, + 0, + ]); + maskToCanvas = Util.transform(maskToCanvas, [1, 0, 0, 1, 0, -height]); + const cord1 = Util.applyTransform([0, 0], maskToCanvas); + const cord2 = Util.applyTransform([width, height], maskToCanvas); + const rect = Util.normalizeRect([cord1[0], cord1[1], cord2[0], cord2[1]]); + const drawnWidth = Math.ceil(rect[2] - rect[0]); + const drawnHeight = Math.ceil(rect[3] - rect[1]); + const fillCanvas = this.cachedCanvases.getCanvas( + "fillCanvas", + drawnWidth, + drawnHeight, + true + ); + const fillCtx = fillCanvas.context; + // The offset will be the top-left cordinate mask. + const offsetX = Math.min(cord1[0], cord2[0]); + const offsetY = Math.min(cord1[1], cord2[1]); + fillCtx.translate(-offsetX, -offsetY); + fillCtx.transform.apply(fillCtx, maskToCanvas); + // Pre-scale if needed to improve image smoothing. + const scaled = this._scaleImage( + maskCanvas.canvas, + fillCtx.mozCurrentTransformInverse + ); + fillCtx.drawImage( + scaled.img, + 0, + 0, + scaled.img.width, + scaled.img.height, + 0, + 0, + width, + height + ); + fillCtx.globalCompositeOperation = "source-in"; + + const inverse = Util.transform(fillCtx.mozCurrentTransformInverse, [ + 1, + 0, + 0, + 1, + -offsetX, + -offsetY, + ]); + fillCtx.fillStyle = isPatternFill + ? fillColor.getPattern(ctx, this, inverse, false) + : fillColor; + + fillCtx.fillRect(0, 0, width, height); + + // Round the offsets to avoid drawing fractional pixels. + return { + canvas: fillCanvas.canvas, + offsetX: Math.round(offsetX), + offsetY: Math.round(offsetY), + }; + } + // Graphics state setLineWidth(width) { this.current.lineWidth = width; @@ -1375,7 +1508,11 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { if (typeof strokeColor === "object" && strokeColor?.getPattern) { const lineWidth = this.getSinglePixelWidth(); ctx.save(); - ctx.strokeStyle = strokeColor.getPattern(ctx, this); + ctx.strokeStyle = strokeColor.getPattern( + ctx, + this, + ctx.mozCurrentTransformInverse + ); // Prevent drawing too thin lines by enforcing a minimum line width. ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth); ctx.stroke(); @@ -1418,7 +1555,11 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { if (isPatternFill) { ctx.save(); - ctx.fillStyle = fillColor.getPattern(ctx, this); + ctx.fillStyle = fillColor.getPattern( + ctx, + this, + ctx.mozCurrentTransformInverse + ); needRestore = true; } @@ -1748,7 +1889,11 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { // TODO: Patterns are not applied correctly to text if a non-embedded // font is used. E.g. issue 8111 and ShowText-ShadingPattern.pdf. ctx.save(); - const pattern = current.fillColor.getPattern(ctx, this); + const pattern = current.fillColor.getPattern( + ctx, + this, + ctx.mozCurrentTransformInverse + ); patternTransform = ctx.mozCurrentTransform; ctx.restore(); ctx.fillStyle = pattern; @@ -2022,7 +2167,12 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { this.save(); const pattern = getShadingPattern(patternIR); - ctx.fillStyle = pattern.getPattern(ctx, this, true); + ctx.fillStyle = pattern.getPattern( + ctx, + this, + ctx.mozCurrentTransformInverse, + true + ); const inv = ctx.mozCurrentTransformInverse; if (inv) { @@ -2279,8 +2429,6 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { const ctx = this.ctx; const width = img.width, height = img.height; - const fillColor = this.current.fillColor; - const isPatternFill = this.current.patternFill; const glyph = this.processingType3; @@ -2296,35 +2444,15 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { glyph.compiled(ctx); return; } + const mask = this._createMaskCanvas(img); + const maskCanvas = mask.canvas; - const maskCanvas = this.cachedCanvases.getCanvas( - "maskCanvas", - width, - height - ); - const maskCtx = maskCanvas.context; - maskCtx.save(); - - putBinaryImageMask(maskCtx, img); - - maskCtx.globalCompositeOperation = "source-in"; - - let patternTransform = null; - if (isPatternFill) { - patternTransform = getAdjustmentTransformation( - ctx.mozCurrentTransform, - width, - height - ); - } - maskCtx.fillStyle = isPatternFill - ? fillColor.getPattern(maskCtx, this, false, patternTransform) - : fillColor; - maskCtx.fillRect(0, 0, width, height); - - maskCtx.restore(); - - this.paintInlineImageXObject(maskCanvas.canvas); + ctx.save(); + // The mask is drawn with the transform applied. Reset the current + // transform to draw to the identity. + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.drawImage(maskCanvas, mask.offsetX, mask.offsetY); + ctx.restore(); } paintImageMaskXObjectRepeat( @@ -2338,54 +2466,27 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { if (!this.contentVisible) { return; } - const width = imgData.width; - const height = imgData.height; - const fillColor = this.current.fillColor; - const isPatternFill = this.current.patternFill; - - const maskCanvas = this.cachedCanvases.getCanvas( - "maskCanvas", - width, - height - ); - const maskCtx = maskCanvas.context; - maskCtx.save(); - - putBinaryImageMask(maskCtx, imgData); - - maskCtx.globalCompositeOperation = "source-in"; - const ctx = this.ctx; - let patternTransform = null; - if (isPatternFill) { - patternTransform = getAdjustmentTransformation( - ctx.mozCurrentTransform, - width, - height - ); - } - - maskCtx.fillStyle = isPatternFill - ? fillColor.getPattern(maskCtx, this, false, patternTransform) - : fillColor; - maskCtx.fillRect(0, 0, width, height); - - maskCtx.restore(); + ctx.save(); + const currentTransform = ctx.mozCurrentTransform; + ctx.transform(scaleX, skewX, skewY, scaleY, 0, 0); + const mask = this._createMaskCanvas(imgData); + ctx.setTransform(1, 0, 0, 1, 0, 0); for (let i = 0, ii = positions.length; i < ii; i += 2) { - ctx.save(); - ctx.transform( + const trans = Util.transform(currentTransform, [ scaleX, skewX, skewY, scaleY, positions[i], - positions[i + 1] - ); - ctx.scale(1, -1); - ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1); - ctx.restore(); + positions[i + 1], + ]); + + const [x, y] = Util.applyTransform([0, 0], trans); + ctx.drawImage(mask.canvas, x, y); } + ctx.restore(); } paintImageMaskXObjectGroup(images) { @@ -2413,17 +2514,13 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { maskCtx.globalCompositeOperation = "source-in"; - let patternTransform = null; - if (isPatternFill) { - patternTransform = getAdjustmentTransformation( - ctx.mozCurrentTransform, - width, - height - ); - } - maskCtx.fillStyle = isPatternFill - ? fillColor.getPattern(maskCtx, this, false, patternTransform) + ? fillColor.getPattern( + maskCtx, + this, + ctx.mozCurrentTransformInverse, + false + ) : fillColor; maskCtx.fillRect(0, 0, width, height); @@ -2491,17 +2588,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { // scale the image to the unit square ctx.scale(1 / width, -1 / height); - const currentTransform = ctx.mozCurrentTransformInverse; - let widthScale = Math.max( - Math.hypot(currentTransform[0], currentTransform[1]), - 1 - ); - let heightScale = Math.max( - Math.hypot(currentTransform[2], currentTransform[3]), - 1 - ); - - let imgToPaint, tmpCanvas, tmpCtx; + let imgToPaint; // typeof check is needed due to node.js support, see issue #8489 if ( (typeof HTMLElement === "function" && imgData instanceof HTMLElement) || @@ -2509,61 +2596,26 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { ) { imgToPaint = imgData; } else { - tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", width, height); - tmpCtx = tmpCanvas.context; + const tmpCanvas = this.cachedCanvases.getCanvas( + "inlineImage", + width, + height + ); + const tmpCtx = tmpCanvas.context; putBinaryImageData(tmpCtx, imgData, this.current.transferMaps); imgToPaint = tmpCanvas.canvas; } - let paintWidth = width, - paintHeight = height; - let tmpCanvasId = "prescale1"; - // Vertical or horizontal scaling shall not be more than 2 to not lose the - // pixels during drawImage operation, painting on the temporary canvas(es) - // that are twice smaller in size. - while ( - (widthScale > 2 && paintWidth > 1) || - (heightScale > 2 && paintHeight > 1) - ) { - let newWidth = paintWidth, - newHeight = paintHeight; - if (widthScale > 2 && paintWidth > 1) { - newWidth = Math.ceil(paintWidth / 2); - widthScale /= paintWidth / newWidth; - } - if (heightScale > 2 && paintHeight > 1) { - newHeight = Math.ceil(paintHeight / 2); - heightScale /= paintHeight / newHeight; - } - tmpCanvas = this.cachedCanvases.getCanvas( - tmpCanvasId, - newWidth, - newHeight - ); - tmpCtx = tmpCanvas.context; - tmpCtx.clearRect(0, 0, newWidth, newHeight); - tmpCtx.drawImage( - imgToPaint, - 0, - 0, - paintWidth, - paintHeight, - 0, - 0, - newWidth, - newHeight - ); - imgToPaint = tmpCanvas.canvas; - paintWidth = newWidth; - paintHeight = newHeight; - tmpCanvasId = tmpCanvasId === "prescale1" ? "prescale2" : "prescale1"; - } - ctx.drawImage( + const scaled = this._scaleImage( imgToPaint, + ctx.mozCurrentTransformInverse + ); + ctx.drawImage( + scaled.img, 0, 0, - paintWidth, - paintHeight, + scaled.paintWidth, + scaled.paintHeight, 0, -height, width, @@ -2576,8 +2628,8 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { imgData, left: position[0], top: position[1], - width: width / currentTransform[0], - height: height / currentTransform[3], + width: width / ctx.mozCurrentTransformInverse[0], + height: height / ctx.mozCurrentTransformInverse[3], }); } this.restore(); diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index 84abe7057..6311e0e6f 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -72,7 +72,7 @@ class RadialAxialShadingPattern extends BaseShadingPattern { this._matrix = IR[8]; } - getPattern(ctx, owner, shadingFill = false, patternTransform = null) { + getPattern(ctx, owner, inverse, shadingFill = false) { const tmpCanvas = owner.cachedCanvases.getCanvas( "pattern", owner.ctx.canvas.width, @@ -121,11 +121,7 @@ class RadialAxialShadingPattern extends BaseShadingPattern { tmpCtx.fill(); const pattern = ctx.createPattern(tmpCanvas.canvas, "repeat"); - if (patternTransform) { - pattern.setTransform(patternTransform); - } else { - pattern.setTransform(createMatrix(ctx.mozCurrentTransformInverse)); - } + pattern.setTransform(createMatrix(inverse)); return pattern; } } @@ -380,7 +376,7 @@ class MeshShadingPattern extends BaseShadingPattern { }; } - getPattern(ctx, owner, shadingFill = false, patternTransform = null) { + getPattern(ctx, owner, inverse, shadingFill = false) { applyBoundingBox(ctx, this._bbox); let scale; if (shadingFill) { @@ -535,9 +531,25 @@ class TilingPattern { this.setFillAndStrokeStyleToContext(graphics, paintType, color); + let adjustedX0 = x0; + let adjustedY0 = y0; + let adjustedX1 = x1; + let adjustedY1 = y1; + // Some bounding boxes have negative x0/y0 cordinates which will cause the + // some of the drawing to be off of the canvas. To avoid this shift the + // bounding box over. + if (x0 < 0) { + adjustedX0 = 0; + adjustedX1 += Math.abs(x0); + } + if (y0 < 0) { + adjustedY0 = 0; + adjustedY1 += Math.abs(y0); + } + tmpCtx.translate(-(dimx.scale * adjustedX0), -(dimy.scale * adjustedY0)); graphics.transform(dimx.scale, 0, 0, dimy.scale, 0, 0); - this.clipBbox(graphics, bbox, x0, y0, x1, y1); + this.clipBbox(graphics, adjustedX0, adjustedY0, adjustedX1, adjustedY1); graphics.baseTransform = graphics.ctx.mozCurrentTransform.slice(); @@ -549,6 +561,8 @@ class TilingPattern { canvas: tmpCanvas.canvas, scaleX: dimx.scale, scaleY: dimy.scale, + offsetX: adjustedX0, + offsetY: adjustedY0, }; } @@ -569,14 +583,12 @@ class TilingPattern { return { scale, size }; } - clipBbox(graphics, bbox, x0, y0, x1, y1) { - if (Array.isArray(bbox) && bbox.length === 4) { - const bboxWidth = x1 - x0; - const bboxHeight = y1 - y0; - graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight); - graphics.clip(); - graphics.endPath(); - } + clipBbox(graphics, x0, y0, x1, y1) { + const bboxWidth = x1 - x0; + const bboxHeight = y1 - y0; + graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight); + graphics.clip(); + graphics.endPath(); } setFillAndStrokeStyleToContext(graphics, paintType, color) { @@ -603,10 +615,9 @@ class TilingPattern { } } - getPattern(ctx, owner, shadingFill = false, patternTransform = null) { - ctx = this.ctx; + getPattern(ctx, owner, inverse, shadingFill = false) { // PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix. - let matrix = ctx.mozCurrentTransformInverse; + let matrix = inverse; if (!shadingFill) { matrix = Util.transform(matrix, owner.baseTransform); if (this.matrix) { @@ -619,6 +630,10 @@ class TilingPattern { let domMatrix = createMatrix(matrix); // Rescale and so that the ctx.createPattern call generates a pattern with // the desired size. + domMatrix = domMatrix.translate( + temporaryPatternCanvas.offsetX, + temporaryPatternCanvas.offsetY + ); domMatrix = domMatrix.scale( 1 / temporaryPatternCanvas.scaleX, 1 / temporaryPatternCanvas.scaleY diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 88ea9f3d6..c3185e171 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -206,6 +206,7 @@ !issue11403_reduced.pdf !issue2074.pdf !scan-bad.pdf +!issue13561_reduced.pdf !bug847420.pdf !bug860632.pdf !bug894572.pdf diff --git a/test/pdfs/issue13561_reduced.pdf b/test/pdfs/issue13561_reduced.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e888b68d88af25bb4c8501a3139a0983ed475df6 GIT binary patch literal 16050 zcmeHuc{r5c`#-X8A+ltdVXR?Rj5T{>%f3Wnn8DbWv1iG?#n?jP!?Z7n%ck$VMrO67L+6*2UD>H>v&?Y zK{j%vp&yo=ya0#8GXd*kJ)Q9`09nZ4SOUIJQxIM@^bm;IMf+S}VZlZJ^Vqu_%iQd@B$2|>$7Pab;bUrZZ}xu~{d z?><2<8!gWafI3lA7n}zS&}v>GBc~?2G)z_f?*lyq=%*7IVDV@S8jl7*zk_CkcEOXE8 zKW;j$Pb@7#OfC=-!b?J>K_*Pr?*1kh+pf9 zN>n=2Oy3D%nOHlC;B-)2IDH|s7^AoN{Ql+V;?jwq4n{o3uObPm>ejs>a}QgUOLJRq zc$6u^ZegM`$T+EMlU-@wUoM^L^x3mwEO89_m^3J}cl1O5z}vGZ!?duXh7_}FqR|`Y*j_BNd$0etr99ZYNv5BFuTaBBVPAxUGz()?O<6FJGbi>oUlwmL9>DN_9 zoxTVp!$mg072|}7CL;_i<*QU&U>MK~iB9c#k1y@06@D-QD2vwWOCvi%Hvds4>kr7l z{#RsvE;0fM{~Z9b5%QDYA|fJ|mzO_$_%J^|pO%(JMn+atRK&>0=-}WWEiLWl=BA;c zAtWReA0KaJWfdMC&dJFsE-wD|?c3?;>4k-b!otGm&!0CoHokfD=JMssKp=2(a&mKX z6OYG_jg4V27)?#hix)43goNbg<{lgzaB*=xd-klRreFHr(V^dXC4GIc6e*E~CFJD|-TxMry z&zw1fMx)Ei%j@duE?l^fpP#?JzHVe>)Y8&oU|;}&K*YqvUcGwN(9l3SPi$>%Nzp1f zIk~pBc7K1LKp-HINE8b7?c2BA-QB&tJqZa38ylOrxHx%v`TF|$*49>kfB%7jfs&FE zC=@z0G$bV@RZvhsM@M(*(j{|qb9#DuR#sLR45p~4I5INA%ggKO>B-N}FDxv)w6vt8 zq!bkug~Q?Ay?dvxuP-7Za{BaXYiny|Wo34D_G8D6X=`ierKNT1)Tz9@JXu*;3kwU0fE!sI>yKIbJwWiXU+QiSr~m$-;Vdf)_6e7y?L5~=C&df z*-88}gbR_{=HwZ@#0m|Q(TIn6PSGHdOvC8}9^QX8M{`WeDr?$!SK%mqf13~z2_gXnv-w?Pop%&Y6kL?n@)I9aTqkJqO&E`1%LejidMaE;Hx_;R+iAp|sgX@+vBa_Mz% zW%mayXz7gw?~U!kt zdCr(n9cmq=kh1d)9~8{!%niz(GiHUv>ScCX_i~^(Eh8C@Ufk3r1XfRYWNV?JIzX&S zpxek)r`ShzZFt5NeUY^w1|73BX+BHL2-C`%WBTb{#>x!Ami48V&G~J z_2MqaTMGQ0#;ZjZzPb}S6ykmte5M6U`@1(Kxr=YjjE-g>K}~xHbr4AirUHejg5COb z`e@FFr2=iwop0)VKi7Xe|L#V_w@*cOh9^uYEH$5H6hE*`96#F@hz|}9UZp4v-5!EF z$1n`PsIhCiDnWD*{J5J%vluqA#~x@`qkLR;Vy zuIv#_)SOvXNcpy97jbI76Ct~tDoBis@_5dYq?XNJb6YQ;W6w02{=Q1*@q)2O1_P&X z`Le0~=?RV~=Y<6wABF+lf@EiB=QkJN;-%o$odusF_Nr{v%4x}GQQ8)}6xVK+txT_3 zUs|E6N&m#fB66&AfsUB(&w!lwbbKoN({x!i;IZ*`0_ z)@ZYf%QS8fe6hFsMX(H<^&(~{<5I#E;kj+EsR!INLmL|#JGciSJKi=MYNaQF7G18P zmsTbOeVKJk3EIGH)J34MxR;`BCoLR%K^v@-x@Xc+^lh`em_KuOjdt+mr?-}tH8b_6 zHM69{zpOMo1uBOnUD0q==Qh{zk8^90bXZ9}KQYRPLJe~fRhrG>mKKN0g}BHe+KwiB z4H5lcjfg)~Y5F-?Bt*$?x$uv~qrpL*1F{&hJ z9_Q?N4TL=1DWT^Ag>TjI>|T7_Y|*_%xq4hJXDi3mezie-*b=J!ME7`v_+v;9%ANM; zxXkI4)~{n|a|2*^MdnU4@;chyqNkXW>R`!%1S!!&5NgE8(FCgx)4X)+fMhVEB=_P&Q9JgWLPj zle6q5*+C7buiPN;Fs0XNHoY|uP%|LkkaoTK(EO~wrT=FFs}_ibmbxRIW_#_8z+*Aq zDVe0fmXBA@rUI{PPRUtUqoQ>`ta2H6A_{J+-OygdQB>Z2Or*viyT4 zeo$S12&*X@7qR4-$+n>YT0Zai1~x4BTjv%~s^FNOUD#HarO@Y@7j}w{F+k<3JGcRQ z7a23*77I3Vj7R~^7;pEJ&c17xq0n4uXttuJJ-Dfw z!FraWeA+5hKIF@oEebOy8aEOoQ?E6m|FI`xXUCCo(aQT~gIC1Is>G{j;ZDc&K(fXk zqGl1iZcBlrU>NoR1|rvRMVzlp8@qm%bzDLjhky{%MA0DHww>70Y_T#2YNE2qbaxZL zApi24w%|{DTh3nrSQ*EsTE)|nqJ#vV8+-3(++~xStdm8{%P(oadzThVw>6iW_{L!U zt#ZCOVlNkX99~1WxXWqxAXo^m>YDR%yp_Jtvb2UKZxYX_!!nYbTt72d<)5|y7`pxp z42BYi<&|jYpYXou-$(8 zEYBpEnE18RoRmXzgTsr_9g(TkS5Ng2&P=|v^zk=8IE`?-d%kr<++S^Iop4gm zv3RtP-!>j14>GyJ1D2*E_Lp`)98tLNA+3mlS`(lg==Q>{<=celkUr~mPB7T6b=f9^ zwg|@5GbpTVOzrM~y3-uE$sTR}q+)$0+sK#0{D!ckN^CL%{i6K0W9NpwBEAq+hI=iP zW)N~V{^~>Q&pThahh?_}=d-gN$u1^znR2KYcs5igOMv5JW*Lxe>{=9y6z8bA#6~}K zpN3Bm?(fb}m4P;+vl8cg#7j&HMAh(czh)VD07=qEvwMI#Grqp44`yI zQ`$A0m(n+f5k%X~V&ct?ye=5Fg2zNSGucM}kaH9h+wQpsP0wmF*H6^12q6U(z~9 zYmT+1U*~5x;V|OgfZp49D1*{oISovdMF|d8-uM=s9Iq|yJGjcaw81YAG--iJcOM!_Zpmx2=^75jqN}pYy204F; z;Plxws3<(MdC^R^7oo_IF+orW+EPu@Bt2M2qB?HkLwq-O_Kj8ZRZ1R~zH?-(d|5x& zaFnQAKXFTSOb8W5>A2RAckj&8X9C!#SM>w?tgIt4dU*u|IFnnLFU)_IY_zdHdv=`k zFt$Y{Jgw9Tm{dB&8u+?;rmH*iQ-Pc~QU4$~v`w9!S}5K2O_s1y6oBCAfa-pi8A|?Q zykaXJPTCe3l{5vP2N7TkgxAux^{ADsq~6kG28zlA zAXDf~AynJ`9Jr-7g-Sv|MlT0KWQ@g!T*&IuP+VZj2Ia0N?7!dH%u;Q7KSA%W(FAP` z|5WfMDDznQAw#pTZD2ua9KR}*<(W`8*yMd>a=i!R_! ze5r3gKa}BrQpO{7I$3eQ>&5ujJNg%fR_~!dH%VD}+Ju{+($Aa{nhduVUtPOr?>B*$ zOej*gXtCbX&t+moDZ<9Q1j$_%30Hs$9np{U%v7r@T2p%EC_NqV3O&v%#5wWxTwRfE zU7kbvM4bG{iu9>wHdddd&lv+AowosT?BTVw50q4j?%tI_Q)xsQs*vk6F|lMikTnR? zWi$mRUsfv!Hg#}{x@<4kd-o-!lkSkgr2g~FGI1T9$p^6mF#{>&@aV1w!e{3_?B_ik zbOSQax!vbO4A{&P=VULJ&6RsTa`!Yn<&_t1?b(wcnnNes;H8r>CyQ?k8=em^3j7j* z{v>pX1vXCSsiO8xttac*6;w zz-?-(rS79DAcD5(cuOyEfM>{83~^;}bzoZy?N;r(%6GBrTY3I7Cnyn}<^4ve$C%L~ zXq=FMiKIEo&&XdDwh!g?W{);sryQDWJNTkTc#K+6Hk&b@ya_B0TKu~G#_rrQ&ElX? zV{@nc4jJ3=hYohS5!->WN*#B(}KeJ`9;gXV!MO!7|=db{H!Ia?H-ryT0}Qi z&4^x;$t3yS;xk!W&q2T`4mGjfkHK9T8FT(Z93`D&!aTJ}WmbR*)yy7fGpi|}ZS#d%JHmclJ!C&|aX;_rHW>!H)BaEZ2d1Rp{HoG+O zt~G~jZ`{$CcjHvIw$NKG2jW(UW7wXb>Br~oleI_&g-Pz_#Hz9pF3u%k9aY|p2q5s0 z7%#t3)=W}Q`a^t0%c;B8{(4HU*C%uff+_}C;;tiaE(#}V$475#-{~!rwa5rDOj>z$Yy6FC@@kGDy3 zy=^PrdrRKB@*amvd{bJU2U|RXldxR)u&~#u9>$_&o-pWT|DjK&0zhegr(A1sx}tt= zem7((zjBJVbO^Ek$S&s%?aJA38_!|N;*>aW8U^M_=;G1OXHO2>7{&7TV&1p2)vo!# z0CsD$Sv8}ZsT!}N*unb9Gr%)k5t6q@UARS6qnG#<3kQ&2i;gL3^q25>&oofcec|hj z)NeCr(206nTtRJot5T!nf+XsZO4xm_< zT5D_R>1V?bfVu8+JVoeJ7$D4kzr)u1NJhA(tNzrj4{@3&dU>)w>Q%Zddj~{?x!SBG zK6u4hxaXl-60ULIY4=w10GpZ9H6t)+m>3qvmgN(2wy1;=Th3kUc=36UjuW;AxXWT< zSs}R6Pc=OR!Uzk9bfu?EpEj(B(%684N&^Egd3LgM@N9P=V*F(#is94F(ERG=9%$i%UNG2Ef4JbA#(SGR{aEc_p6tsq1Fi+8*(7oSB$NjpI0M%2rv$_E5}E3>3l?5;$34U zHs4(;OmE202@I)GS3kLPEMkuQ1~gLgTc-wI>1}Gd5b6}B?RMEShnddChMGuii(Y%7 zG&{nwD$23^L#}%D8JidP^tiiC%!=W+LWU9#tn;n_7hKe>C=gd7xt<;s{_wPZKt8kD zsQZlLNZ~++x}WXvO~Wvg82z0bNP!85!hEEn*PQ`1wt^>0B`NA6i8LyH%fg@odF+$x z?=P)xX_=Z`uYVCje^bX}-*)(zE}K#PMkf<@VbN71(rY?Llv9NR=EcR>g!i*0A93+S z8tXmPA+I}XNjy4_8b+@nScaxSI{Ggr&(i3bb`4dQKjfD+U8buzyE`3qFU7K6&J60h zfqdpV(@yy8tvGTt!{;3ts!@|k*G^o?Sg?jwYB?sxRC5?de5ms3O?o73Anhh~HM{5p zrETEh`T;@8W@v_@h7oioBqSkW$D)&Z7Zhw81!E|De=#T3g+2-UWGy{<=7SQ)FUU0;V-Ds+EAEcwd#*!(_FxmnLOhb@q$YOH!aD%aGTa5->^3 za)uGbaYW{aMR%@B;DX*q)?dlN$3AR#ehF;aSBHB(86L9`5XD^^A%xr9IYsowZDjS) zD83kw%n?-DNz4Bz_i*3h60!v(&n=8b4rh>sxZlE2U$36}@r;{_Psb z6@fi~)H~hr)yfi@)f?;WClcV6>R*A}Ra{lCpn&vhBl>}O;!41^g|`oFIe0U`z^y6o ziq$7iZT-oBRrlP6KB~GuNW46-ho;*-r>NV!_O@KqY%s4bPHIA0(A-)JtWT2 zv690x-YF~pwwZzp<7V*!q#nDR2i z3J2ABzMF>1XC2zx`)kV$U+A+}=5(!39s#B~-6)LAS7cmH@xy)JwgSW{qi$6XiIor^ ziEm{|TleY`A6PUAKWq=s+4x*(b4$It)t0A?d{L3%DR2M^$|xvqc6~TG9V&5c>Q=my ztT5qdjGQceN13Q{=^e?6ORtNU-P^xL8g*AVdw6nQV`c7r5nMED)W|$h(3)5Jh?&Fp zen)HVqOZ+eAqHeKvstl>Px{J<8*d5wRFx4G4$CojV+7BJ*ox71B&h3GbFANuRX29& zZGDY){AA9&%(+CirLNYVC$yFl?07Cmjw(YKWN3Cpv<@HUMI9%>tD6Ij@tp@>L zbX!OYrK)r;FA6pJ;1VIgv46kKuNsh7O#fnnXz=(LN_-uon<|BWIV|y5vAe}B^AWwd zH>y51ro$AMNo-~lBp5X*4_M%i-mx}-0)nfMLC2~CJRtqR0U^+hua!0EL<$=Nnrybj z5eEyS>Zs#YIE-BLw_^uSog&ZE%_X^&2CM;NJ56`ihT~)M=_i9V#!4}D>?W}u?1B?z zTSYQ18GIV_#T6LaQ2f+MROVsGO#?0#CwW_4(L*IjBC3aE#gAPlY}z)l$V7|*oR(Ys z>Q^#aqGl~B94rW~qt>@528;=x5wF`SWER+m9t85Xx)I9O-)y&>FJ7+1tS=i+FCH4* z7Be_$_Aymmw>`CoP{ws5C{S-`@6jWLZ)`15h;F0qW(K|)J9NH{=#Fo#gpk8%&)Y+Ph^k|%wi*-X80$44%*?Lj;u z+o)S|RFQnv#9J9F8A*RvS?>u#gR~{xvdTDbtebFiQH#T4uU>x3L0pWJD6`P*6MpWH zK-sMqf>Js{ws$8mhrgKI|AN6C3Q};#9-Dn7VU{0aA4cLNIQL99GvLCx1{;I8H$Y<5 zE+MEZj|gH!G363F4QVfms@3&Ht}J7w4a7L}ixeWml+@EgCsO3}y_E5xIH?j08YO>! zlGBQ_J<@DYqi$XX%f1p5lg5KsIFj+DnEv^5{-;OsPv(h_7L$$UQ->nNrbd9~K3Cm{ zx1fOAZX1n|?8vLa@x4;{apx`)gQ$AH8d25C<=+{(dH>$s)oQi;_K?x@E1^~y=R7m< zDEg6ngN=*p&B@~FOxJmbh_HY?ACyYmY(Ry2Jm#K`Ow{R5I;~9%y;UxGU!u=46Q?PY-sUvfqg z?Zs%}IhmQvOUHU+go@`=6f2M9`KM#cF4wK8 z9QIN;@@geAW0hu!1%&`Zj>jvnIXW*%rCpWvIXpTUGx%@NQu5ZgKSH<~l}`IWa_WE> z^**jqzE)Iw?#Sk+Jf2-06&EtOqCIx3|(&(0Vz1D=ir{nfQag6mI5!l9g4mB7H?tdZ2_c;JAKlc$ZmupeHZ~3o0#M$ z7;x9^APZ$va7I!Egg$Pg{KiG`W2nswaZ|=vN;;`^;BUL>dnfaMkf@_JrNcR0+qOpD4cr@r-e9VUy_Gj~M>VjKGF0ytTcnqZE4a<#NL$XCraG|R7TiKuEn9Lr# zXOO$ti=hn@iZ9Zd>{9$UIsX(`aUmCXP!e(&MIu_|*eX6M=oXadxI-(+{6xzjPRq zzMM;;IyyQ|3#{*N<*Dq~9?Pg*Z>Q-~c*ZGExf3m5ud5z^oD7CC)oHUzm789>GtaprH@q)1JHTDdt|t}(J{+FH5Gc)lx+{^&*7`6 z6z^2yL8hw_HF$8QyW)z^o2cYoa;}0`*-JNQS_%}*TyiWb#>6P)EwaJG@PHU~Eb7&|7k~Czu zKPM5pDL@v^Le(g)Lq?~gONNqLDbsvn^F%*tAgh*pWPt1b7{ZaP!T*yiX(VLONk)fH z_XhNJpFGfd0&DR+lJ%zDw_+*3dH>z3v2VBwf7Ca>R%=eq}3y@{Tjwk zkyfqWqq1{lLOGlb{!JtDMB@u4RRG<)1XcRjj%i37Wmi2)GwE%SoT7d&tkWK@M*ofm z3fHcdF}Olzj44AI=8IgOXOq<`;AYkvA@wZ`(chDF)eg}&9#KY@4Ln0N6y@lAB@Oqo z=ZCBKyrzkOQ)lz>esqkVj_=mKZtc#fXb#g048;nk!WDUW5VAb469I&dhwSLZXf-D; z1x>bEjIEoTmNBvHCC=KJ2^E)ESgW6*G~C6>1|RZBD)EW^D#=Aq*k4Aza!l2YoqQ(T zd>^AH`0>g7y^8XV)z5u>Nz6LlViY>p_yT51RlniN#jTvU4qvrdWAYMf?&{A7!L3h{7o#5Z9<_+8JjqDOAl;HdQ z3C0v>dV2`RDaa|r8y`G)Qr}r_{ZNo_{5XkypkaMhB%wCQX~zODytXlPIeF^76BUm9 zIHx=)!c_3?VG1g9P-?L(zetOiFR-qX#@Z+FDoqzfFb2bWf>&W_D@ukpS&e`IM@J<_r(Q~ zc#?;VKB>bSiM?k_;?v9xDAC8_s1)Visoh*z7G#b`)+5xm#-lfytOWm$`$W^i4Lo-@&ob zxH%ck#4(h7pZUTjX0y+*~~3patK>(0Ly5##uiq3ZQBv zIa#U7vE)CYbx7;`3$1@rbfDn>f%i({lj@Oh`NeqE(9kh6(?$pVB)$Gd+WAhxM2X|m?b_iO{(y7Gfi#8hX;N8&Uq4)bMz`Qi z6NjbAR8^8eD_tjlTV+Ve9NkW1NXw@LSe!mm9>b?3`k0TpP_YC5@_ATENz((F70BB3 z-4$wqgtS?hKDXR@R5e?8R8@!DrE@alb=>4vf;kzZ?#9uNaaq*1Terueu|`L!^v+rx z=`j}U5=%9m~xrT#XDC?_$(+SIs#DLSub&5jWulUkumeV+s zlg@)#Uj;oljh#^xs#CH}@0Nc2kuMh$l-i&4qmER4D9#V&WN%6TUGf#b^kb5RL(w5N z0-V~MN||5vxg2*p3xdCL7s*3me-K4;5W^R^VtMqdQN$Pe=!2JL+h1=)ERE6})K;#2 zF*_K&ar6Lf`sQ=xL6w=nw~q3*m5}hUic?=>4I^IKl&`q{(1>mNQK@#YED`$o?%^%s zi`@aE#FtKn^XsFrH8SMIA09`-DehZP+xMNW>w)LUJLdKCK6Mtnf2A{L-jCVlvTu*1 zpS)v^{FU2}tF7jF8w69W=6}?spS%5J-uGnQ6M0W($lU#SN#DQpljc&<{k~$~>t1($ zx5*D{3aT;k`srBvHP_RZ!_q7s$W*?v!o)@JlL09Hw0C}Oou8ot4wwDq*H#AlKZt36 zk&A!v(UeXFk(j)0SUkW1>x}bM7F=s?69nKe%7T_M1`q>p4Xg_e72=CE4LM`x7~)_K_Ln-hyq0BFCx643O%W)K9V5l?% z{3j)hphraM(ZLUA; z>UfgUEj%5uf7vCajL11bkg`$^2qej*970}7-ceRo3IUhFV2~K7JQ4=~%kFQwzu6_B zt6=2oh4FX9`l_fo`g%E#)=m@WhsU|QV|@WgY0?G+h*{!DsWX9o;!20B@k{=voge7@ zw&Q<dm1{}-N)@gK|oh3h{;^t-_S1{eLWry)`Xsj^@o=~?IBbBX@zYpcNtfoJtIR0ZOP&xSjHWtjK7W!xiySCk&=cD3~O!fm7wGnli z_TuNv(ypFBKv@@RapHx^dy}2Fk^&2wk%R>_7Uw-Kx%D<-G*7i1I>VX^OP`6n9shp! zP44X{RgV?IUJ&gswBN_^yk9yt$)7XEK}~Zxj$->G7bc)9<+Rbg=4&!bZ3Oz=llZX; z55fMv-h3aq(GdTTtG()M>tw~wmMvnR&V{}?3LYQmy4M|6v0oBIde=6+t}yrSK>YTm z?awcY;QxOUr2m-|4VU}3OlY~ExnjQ(p&|cBgeGO}IgAwabv?}O?|S%rpzGn|-mV}d e&0{A@S_qkF-VyeH$KhATGZacnQU9Lv{C@z2eJYXw literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index f238600fc..be54ed1d2 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -878,6 +878,12 @@ "lastPage": 1, "type": "eq" }, + { "id": "issue13561_reduced", + "file": "pdfs/issue13561_reduced.pdf", + "md5": "e68c315d6349530180dd90f93027147e", + "rounds": 1, + "type": "eq" + }, { "id": "issue5202", "file": "pdfs/issue5202.pdf", "md5": "bb9cc69211112e66aab40828086a4e5a",