diff --git a/src/core/annotation.js b/src/core/annotation.js index f1b82f7e0..02dc2c273 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -192,6 +192,33 @@ class AnnotationFactory { } } +function getRgbColor(color) { + const rgbColor = new Uint8ClampedArray(3); + if (!Array.isArray(color)) { + return rgbColor; + } + + switch (color.length) { + case 0: // Transparent, which we indicate with a null value + return null; + + case 1: // Convert grayscale to RGB + ColorSpace.singletons.gray.getRgbItem(color, 0, rgbColor, 0); + return rgbColor; + + case 3: // Convert RGB percentages to RGB + ColorSpace.singletons.rgb.getRgbItem(color, 0, rgbColor, 0); + return rgbColor; + + case 4: // Convert CMYK to RGB + ColorSpace.singletons.cmyk.getRgbItem(color, 0, rgbColor, 0); + return rgbColor; + + default: + return rgbColor; + } +} + function getQuadPoints(dict, rect) { if (!dict.has("QuadPoints")) { return null; @@ -462,36 +489,7 @@ class Annotation { * 4 (CMYK) elements */ setColor(color) { - const rgbColor = new Uint8ClampedArray(3); - if (!Array.isArray(color)) { - this.color = rgbColor; - return; - } - - switch (color.length) { - case 0: // Transparent, which we indicate with a null value - this.color = null; - break; - - case 1: // Convert grayscale to RGB - ColorSpace.singletons.gray.getRgbItem(color, 0, rgbColor, 0); - this.color = rgbColor; - break; - - case 3: // Convert RGB percentages to RGB - ColorSpace.singletons.rgb.getRgbItem(color, 0, rgbColor, 0); - this.color = rgbColor; - break; - - case 4: // Convert CMYK to RGB - ColorSpace.singletons.cmyk.getRgbItem(color, 0, rgbColor, 0); - this.color = rgbColor; - break; - - default: - this.color = rgbColor; - break; - } + this.color = getRgbColor(color); } /** @@ -929,7 +927,22 @@ class MarkupAnnotation extends Annotation { buffer.push(`${fillColor[0]} ${fillColor[1]} ${fillColor[2]} rg`); } - for (const points of this.data.quadPoints) { + let pointsArray = this.data.quadPoints; + if (!pointsArray) { + // If there are no quadpoints, the rectangle should be used instead. + // Convert the rectangle definition to a points array similar to how the + // quadpoints are defined. + pointsArray = [ + [ + { x: this.rectangle[0], y: this.rectangle[3] }, + { x: this.rectangle[2], y: this.rectangle[3] }, + { x: this.rectangle[0], y: this.rectangle[1] }, + { x: this.rectangle[2], y: this.rectangle[1] }, + ], + ]; + } + + for (const points of pointsArray) { const [mX, MX, mY, MY] = pointsCallback(buffer, points); minX = Math.min(minX, mX); maxX = Math.max(maxX, MX); @@ -2278,6 +2291,43 @@ class SquareAnnotation extends MarkupAnnotation { super(parameters); this.data.annotationType = AnnotationType.SQUARE; + + if (!this.appearance) { + // The default stroke color is black. + const strokeColor = this.color + ? Array.from(this.color).map(c => c / 255) + : [0, 0, 0]; + + // The default fill color is transparent. + let fillColor = null; + let interiorColor = parameters.dict.getArray("IC"); + if (interiorColor) { + interiorColor = getRgbColor(interiorColor); + fillColor = interiorColor + ? Array.from(interiorColor).map(c => c / 255) + : null; + } + + this._setDefaultAppearance({ + xref: parameters.xref, + extra: `${this.borderStyle.width} w`, + strokeColor, + fillColor, + pointsCallback: (buffer, points) => { + const x = points[2].x + this.borderStyle.width / 2; + const y = points[2].y + this.borderStyle.width / 2; + const width = points[3].x - points[2].x - this.borderStyle.width; + const height = points[1].y - points[3].y - this.borderStyle.width; + buffer.push(`${x} ${y} ${width} ${height} re`); + if (fillColor) { + buffer.push("B"); + } else { + buffer.push("S"); + } + return [points[0].x, points[1].x, points[3].y, points[1].y]; + }, + }); + } } } @@ -2286,6 +2336,67 @@ class CircleAnnotation extends MarkupAnnotation { super(parameters); this.data.annotationType = AnnotationType.CIRCLE; + + if (!this.appearance) { + // The default stroke color is black. + const strokeColor = this.color + ? Array.from(this.color).map(c => c / 255) + : [0, 0, 0]; + + // The default fill color is transparent. + let fillColor = null; + let interiorColor = parameters.dict.getArray("IC"); + if (interiorColor) { + interiorColor = getRgbColor(interiorColor); + fillColor = interiorColor + ? Array.from(interiorColor).map(c => c / 255) + : null; + } + + // Circles are approximated by Bézier curves with four segments since + // there is no circle primitive in the PDF specification. For the control + // points distance, see https://stackoverflow.com/a/27863181. + const controlPointsDistance = (4 / 3) * Math.tan(Math.PI / (2 * 4)); + + this._setDefaultAppearance({ + xref: parameters.xref, + extra: `${this.borderStyle.width} w`, + strokeColor, + fillColor, + pointsCallback: (buffer, points) => { + const x0 = points[0].x + this.borderStyle.width / 2; + const y0 = points[0].y - this.borderStyle.width / 2; + const x1 = points[3].x - this.borderStyle.width / 2; + const y1 = points[3].y + this.borderStyle.width / 2; + const xMid = x0 + (x1 - x0) / 2; + const yMid = y0 + (y1 - y0) / 2; + const xOffset = ((x1 - x0) / 2) * controlPointsDistance; + const yOffset = ((y1 - y0) / 2) * controlPointsDistance; + + buffer.push(`${xMid} ${y1} m`); + buffer.push( + `${xMid + xOffset} ${y1} ${x1} ${yMid + yOffset} ${x1} ${yMid} c` + ); + buffer.push( + `${x1} ${yMid - yOffset} ${xMid + xOffset} ${y0} ${xMid} ${y0} c` + ); + buffer.push( + `${xMid - xOffset} ${y0} ${x0} ${yMid - yOffset} ${x0} ${yMid} c` + ); + buffer.push( + `${x0} ${yMid + yOffset} ${xMid - xOffset} ${y1} ${xMid} ${y1} c` + ); + + buffer.push("h"); + if (fillColor) { + buffer.push("B"); + } else { + buffer.push("S"); + } + return [points[0].x, points[1].x, points[3].y, points[1].y]; + }, + }); + } } } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index f5e03e23c..324651782 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -395,6 +395,7 @@ !annotation-line.pdf !bug1669099.pdf !annotation-square-circle.pdf +!annotation-square-circle-without-appearance.pdf !annotation-stamp.pdf !annotation-fileattachment.pdf !annotation-text-widget.pdf diff --git a/test/pdfs/annotation-square-circle-without-appearance.pdf b/test/pdfs/annotation-square-circle-without-appearance.pdf new file mode 100644 index 000000000..263f4f262 Binary files /dev/null and b/test/pdfs/annotation-square-circle-without-appearance.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 1658a0d8a..c8b7f3497 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -4724,6 +4724,13 @@ "type": "eq", "annotations": true }, + { "id": "annotation-square-circle-without-appearance", + "file": "pdfs/annotation-square-circle-without-appearance.pdf", + "md5": "5b2a5e1e918137993d26a0cd8b0947f6", + "rounds": 1, + "annotations": true, + "type": "eq" + }, { "id": "annotation-stamp", "file": "pdfs/annotation-stamp.pdf", "md5": "0a04d7ce1ad103cb3c033d26855d6ec7",