diff --git a/src/core/annotation.js b/src/core/annotation.js index 92dbef74f..211052c32 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -23,7 +23,6 @@ import { assert, escapeString, getModificationDate, - isArrayEqual, isAscii, isString, OPS, @@ -1070,6 +1069,8 @@ class MarkupAnnotation extends Annotation { strokeColor, fillColor, blendMode, + strokeAlpha, + fillAlpha, pointsCallback, }) { let minX = Number.MAX_VALUE; @@ -1124,6 +1125,12 @@ class MarkupAnnotation extends Annotation { if (blendMode) { gsDict.set("BM", Name.get(blendMode)); } + if (typeof strokeAlpha === "number") { + gsDict.set("CA", strokeAlpha); + } + if (typeof fillAlpha === "number") { + gsDict.set("ca", fillAlpha); + } const stateDict = new Dict(xref); stateDict.set("GS0", gsDict); @@ -2400,24 +2407,42 @@ class LineAnnotation extends MarkupAnnotation { const strokeColor = this.color ? Array.from(this.color).map(c => c / 255) : [0, 0, 0]; + const strokeAlpha = parameters.dict.get("CA"); - const borderWidth = this.borderStyle.width; + // The default fill color is transparent. Setting the fill colour is + // necessary if/when we want to add support for non-default line endings. + let fillColor = null, + interiorColor = parameters.dict.getArray("IC"); + if (interiorColor) { + interiorColor = getRgbColor(interiorColor); + fillColor = interiorColor + ? Array.from(interiorColor).map(c => c / 255) + : null; + } + const fillAlpha = fillColor ? strokeAlpha : null; - // If the /Rect-entry is empty, create a fallback rectangle such that we - // get similar rendering/highlighting behaviour as in Adobe Reader. - if (isArrayEqual(this.rectangle, [0, 0, 0, 0])) { - this.rectangle = [ - this.data.lineCoordinates[0] - 2 * borderWidth, - this.data.lineCoordinates[1] - 2 * borderWidth, - this.data.lineCoordinates[2] + 2 * borderWidth, - this.data.lineCoordinates[3] + 2 * borderWidth, - ]; + const borderWidth = this.borderStyle.width || 1, + borderAdjust = 2 * borderWidth; + + // If the /Rect-entry is empty/wrong, create a fallback rectangle so that + // we get similar rendering/highlighting behaviour as in Adobe Reader. + const bbox = [ + this.data.lineCoordinates[0] - borderAdjust, + this.data.lineCoordinates[1] - borderAdjust, + this.data.lineCoordinates[2] + borderAdjust, + this.data.lineCoordinates[3] + borderAdjust, + ]; + if (!Util.intersect(this.rectangle, bbox)) { + this.rectangle = bbox; } this._setDefaultAppearance({ xref: parameters.xref, extra: `${borderWidth} w`, strokeColor, + fillColor, + strokeAlpha, + fillAlpha, pointsCallback: (buffer, points) => { buffer.push( `${lineCoordinates[0]} ${lineCoordinates[1]} m`, @@ -2447,22 +2472,26 @@ class SquareAnnotation extends MarkupAnnotation { const strokeColor = this.color ? Array.from(this.color).map(c => c / 255) : [0, 0, 0]; + const strokeAlpha = parameters.dict.get("CA"); // The default fill color is transparent. - let fillColor = null; - let interiorColor = parameters.dict.getArray("IC"); + let fillColor = null, + interiorColor = parameters.dict.getArray("IC"); if (interiorColor) { interiorColor = getRgbColor(interiorColor); fillColor = interiorColor ? Array.from(interiorColor).map(c => c / 255) : null; } + const fillAlpha = fillColor ? strokeAlpha : null; this._setDefaultAppearance({ xref: parameters.xref, extra: `${this.borderStyle.width} w`, strokeColor, fillColor, + strokeAlpha, + fillAlpha, pointsCallback: (buffer, points) => { const x = points[2].x + this.borderStyle.width / 2; const y = points[2].y + this.borderStyle.width / 2; @@ -2492,6 +2521,7 @@ class CircleAnnotation extends MarkupAnnotation { const strokeColor = this.color ? Array.from(this.color).map(c => c / 255) : [0, 0, 0]; + const strokeAlpha = parameters.dict.get("CA"); // The default fill color is transparent. let fillColor = null; @@ -2502,6 +2532,7 @@ class CircleAnnotation extends MarkupAnnotation { ? Array.from(interiorColor).map(c => c / 255) : null; } + const fillAlpha = fillColor ? strokeAlpha : null; // Circles are approximated by Bézier curves with four segments since // there is no circle primitive in the PDF specification. For the control @@ -2513,6 +2544,8 @@ class CircleAnnotation extends MarkupAnnotation { extra: `${this.borderStyle.width} w`, strokeColor, fillColor, + strokeAlpha, + fillAlpha, pointsCallback: (buffer, points) => { const x0 = points[0].x + this.borderStyle.width / 2; const y0 = points[0].y - this.borderStyle.width / 2; @@ -2569,13 +2602,29 @@ class PolylineAnnotation extends MarkupAnnotation { const strokeColor = this.color ? Array.from(this.color).map(c => c / 255) : [0, 0, 0]; + const strokeAlpha = parameters.dict.get("CA"); - const borderWidth = this.borderStyle.width || 1; + const borderWidth = this.borderStyle.width || 1, + borderAdjust = 2 * borderWidth; + + // If the /Rect-entry is empty/wrong, create a fallback rectangle so that + // we get similar rendering/highlighting behaviour as in Adobe Reader. + const bbox = [Infinity, Infinity, -Infinity, -Infinity]; + for (const vertex of this.data.vertices) { + bbox[0] = Math.min(bbox[0], vertex.x - borderAdjust); + bbox[1] = Math.min(bbox[1], vertex.y - borderAdjust); + bbox[2] = Math.max(bbox[2], vertex.x + borderAdjust); + bbox[3] = Math.max(bbox[3], vertex.y + borderAdjust); + } + if (!Util.intersect(this.rectangle, bbox)) { + this.rectangle = bbox; + } this._setDefaultAppearance({ xref: parameters.xref, extra: `${borderWidth} w`, strokeColor, + strokeAlpha, pointsCallback: (buffer, points) => { const vertices = this.data.vertices; for (let i = 0, ii = vertices.length; i < ii; i++) { @@ -2639,13 +2688,31 @@ class InkAnnotation extends MarkupAnnotation { const strokeColor = this.color ? Array.from(this.color).map(c => c / 255) : [0, 0, 0]; + const strokeAlpha = parameters.dict.get("CA"); - const borderWidth = this.borderStyle.width || 1; + const borderWidth = this.borderStyle.width || 1, + borderAdjust = 2 * borderWidth; + + // If the /Rect-entry is empty/wrong, create a fallback rectangle so that + // we get similar rendering/highlighting behaviour as in Adobe Reader. + const bbox = [Infinity, Infinity, -Infinity, -Infinity]; + for (const inkLists of this.data.inkLists) { + for (const vertex of inkLists) { + bbox[0] = Math.min(bbox[0], vertex.x - borderAdjust); + bbox[1] = Math.min(bbox[1], vertex.y - borderAdjust); + bbox[2] = Math.max(bbox[2], vertex.x + borderAdjust); + bbox[3] = Math.max(bbox[3], vertex.y + borderAdjust); + } + } + if (!Util.intersect(this.rectangle, bbox)) { + this.rectangle = bbox; + } this._setDefaultAppearance({ xref: parameters.xref, extra: `${borderWidth} w`, strokeColor, + strokeAlpha, pointsCallback: (buffer, points) => { // According to the specification, see "12.5.6.13 Ink Annotations": // When drawn, the points shall be connected by straight lines or @@ -2691,10 +2758,13 @@ class HighlightAnnotation extends MarkupAnnotation { const fillColor = this.color ? Array.from(this.color).map(c => c / 255) : [1, 1, 0]; + const fillAlpha = parameters.dict.get("CA"); + this._setDefaultAppearance({ xref: parameters.xref, fillColor, blendMode: "Multiply", + fillAlpha, pointsCallback: (buffer, points) => { buffer.push( `${points[0].x} ${points[0].y} m`, @@ -2728,10 +2798,13 @@ class UnderlineAnnotation extends MarkupAnnotation { const strokeColor = this.color ? Array.from(this.color).map(c => c / 255) : [0, 0, 0]; + const strokeAlpha = parameters.dict.get("CA"); + this._setDefaultAppearance({ xref: parameters.xref, extra: "[] 0 d 1 w", strokeColor, + strokeAlpha, pointsCallback: (buffer, points) => { buffer.push( `${points[2].x} ${points[2].y} m`, @@ -2764,10 +2837,13 @@ class SquigglyAnnotation extends MarkupAnnotation { const strokeColor = this.color ? Array.from(this.color).map(c => c / 255) : [0, 0, 0]; + const strokeAlpha = parameters.dict.get("CA"); + this._setDefaultAppearance({ xref: parameters.xref, extra: "[] 0 d 1 w", strokeColor, + strokeAlpha, pointsCallback: (buffer, points) => { const dy = (points[0].y - points[2].y) / 6; let shift = dy; @@ -2807,10 +2883,13 @@ class StrikeOutAnnotation extends MarkupAnnotation { const strokeColor = this.color ? Array.from(this.color).map(c => c / 255) : [0, 0, 0]; + const strokeAlpha = parameters.dict.get("CA"); + this._setDefaultAppearance({ xref: parameters.xref, extra: "[] 0 d 1 w", strokeColor, + strokeAlpha, pointsCallback: (buffer, points) => { buffer.push( `${(points[0].x + points[2].x) / 2} ` + diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index ea1044a8d..dc9fcb60d 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -124,6 +124,7 @@ !bug1068432.pdf !issue12295.pdf !bug1146106.pdf +!issue13447.pdf !bug1245391_reduced.pdf !bug1252420.pdf !bug1513120_reduced.pdf diff --git a/test/pdfs/issue13447.pdf b/test/pdfs/issue13447.pdf new file mode 100644 index 000000000..1b2559842 Binary files /dev/null and b/test/pdfs/issue13447.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 494dc9664..798c51481 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -115,6 +115,19 @@ "type": "eq", "about": "Causes cmap to be created with invalid glyph ids." }, + { "id": "issue13447-eq", + "file": "pdfs/issue13447.pdf", + "md5": "a729709950d69ec1cfc516bf559e995d", + "rounds": 1, + "type": "eq" + }, + { "id": "issue13447-annotations", + "file": "pdfs/issue13447.pdf", + "md5": "a729709950d69ec1cfc516bf559e995d", + "rounds": 1, + "type": "eq", + "annotations": true + }, { "id": "bug946506", "file": "pdfs/bug946506.pdf", "md5": "c28911b5c31bdc337c2ce404c5971cfc",