Skip to content

Commit 1dfd84f

Browse files
committed
Implement rendering square/circle annotations without appearance stream
1 parent 8e74278 commit 1dfd84f

File tree

4 files changed

+150
-31
lines changed

4 files changed

+150
-31
lines changed

src/core/annotation.js

+142-31
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,33 @@ class AnnotationFactory {
192192
}
193193
}
194194

195+
function getRgbColor(color) {
196+
const rgbColor = new Uint8ClampedArray(3);
197+
if (!Array.isArray(color)) {
198+
return rgbColor;
199+
}
200+
201+
switch (color.length) {
202+
case 0: // Transparent, which we indicate with a null value
203+
return null;
204+
205+
case 1: // Convert grayscale to RGB
206+
ColorSpace.singletons.gray.getRgbItem(color, 0, rgbColor, 0);
207+
return rgbColor;
208+
209+
case 3: // Convert RGB percentages to RGB
210+
ColorSpace.singletons.rgb.getRgbItem(color, 0, rgbColor, 0);
211+
return rgbColor;
212+
213+
case 4: // Convert CMYK to RGB
214+
ColorSpace.singletons.cmyk.getRgbItem(color, 0, rgbColor, 0);
215+
return rgbColor;
216+
217+
default:
218+
return rgbColor;
219+
}
220+
}
221+
195222
function getQuadPoints(dict, rect) {
196223
if (!dict.has("QuadPoints")) {
197224
return null;
@@ -462,36 +489,7 @@ class Annotation {
462489
* 4 (CMYK) elements
463490
*/
464491
setColor(color) {
465-
const rgbColor = new Uint8ClampedArray(3);
466-
if (!Array.isArray(color)) {
467-
this.color = rgbColor;
468-
return;
469-
}
470-
471-
switch (color.length) {
472-
case 0: // Transparent, which we indicate with a null value
473-
this.color = null;
474-
break;
475-
476-
case 1: // Convert grayscale to RGB
477-
ColorSpace.singletons.gray.getRgbItem(color, 0, rgbColor, 0);
478-
this.color = rgbColor;
479-
break;
480-
481-
case 3: // Convert RGB percentages to RGB
482-
ColorSpace.singletons.rgb.getRgbItem(color, 0, rgbColor, 0);
483-
this.color = rgbColor;
484-
break;
485-
486-
case 4: // Convert CMYK to RGB
487-
ColorSpace.singletons.cmyk.getRgbItem(color, 0, rgbColor, 0);
488-
this.color = rgbColor;
489-
break;
490-
491-
default:
492-
this.color = rgbColor;
493-
break;
494-
}
492+
this.color = getRgbColor(color);
495493
}
496494

497495
/**
@@ -929,7 +927,22 @@ class MarkupAnnotation extends Annotation {
929927
buffer.push(`${fillColor[0]} ${fillColor[1]} ${fillColor[2]} rg`);
930928
}
931929

932-
for (const points of this.data.quadPoints) {
930+
let pointsArray = this.data.quadPoints;
931+
if (!pointsArray) {
932+
// If there are no quadpoints, the rectangle should be used instead.
933+
// Convert the rectangle definition to a points array similar to how the
934+
// quadpoints are defined.
935+
pointsArray = [
936+
[
937+
{ x: this.rectangle[0], y: this.rectangle[3] },
938+
{ x: this.rectangle[2], y: this.rectangle[3] },
939+
{ x: this.rectangle[0], y: this.rectangle[1] },
940+
{ x: this.rectangle[2], y: this.rectangle[1] },
941+
],
942+
];
943+
}
944+
945+
for (const points of pointsArray) {
933946
const [mX, MX, mY, MY] = pointsCallback(buffer, points);
934947
minX = Math.min(minX, mX);
935948
maxX = Math.max(maxX, MX);
@@ -2278,6 +2291,43 @@ class SquareAnnotation extends MarkupAnnotation {
22782291
super(parameters);
22792292

22802293
this.data.annotationType = AnnotationType.SQUARE;
2294+
2295+
if (!this.appearance) {
2296+
// The default stroke color is black.
2297+
const strokeColor = this.color
2298+
? Array.from(this.color).map(c => c / 255)
2299+
: [0, 0, 0];
2300+
2301+
// The default fill color is transparent.
2302+
let fillColor = null;
2303+
let interiorColor = parameters.dict.getArray("IC");
2304+
if (interiorColor) {
2305+
interiorColor = getRgbColor(interiorColor);
2306+
fillColor = interiorColor
2307+
? Array.from(interiorColor).map(c => c / 255)
2308+
: null;
2309+
}
2310+
2311+
this._setDefaultAppearance({
2312+
xref: parameters.xref,
2313+
extra: `${this.borderStyle.width} w`,
2314+
strokeColor,
2315+
fillColor,
2316+
pointsCallback: (buffer, points) => {
2317+
const x = points[2].x + this.borderStyle.width / 2;
2318+
const y = points[2].y + this.borderStyle.width / 2;
2319+
const width = points[3].x - points[2].x - this.borderStyle.width;
2320+
const height = points[1].y - points[3].y - this.borderStyle.width;
2321+
buffer.push(`${x} ${y} ${width} ${height} re`);
2322+
if (fillColor) {
2323+
buffer.push("B");
2324+
} else {
2325+
buffer.push("S");
2326+
}
2327+
return [points[0].x, points[1].x, points[3].y, points[1].y];
2328+
},
2329+
});
2330+
}
22812331
}
22822332
}
22832333

@@ -2286,6 +2336,67 @@ class CircleAnnotation extends MarkupAnnotation {
22862336
super(parameters);
22872337

22882338
this.data.annotationType = AnnotationType.CIRCLE;
2339+
2340+
if (!this.appearance) {
2341+
// The default stroke color is black.
2342+
const strokeColor = this.color
2343+
? Array.from(this.color).map(c => c / 255)
2344+
: [0, 0, 0];
2345+
2346+
// The default fill color is transparent.
2347+
let fillColor = null;
2348+
let interiorColor = parameters.dict.getArray("IC");
2349+
if (interiorColor) {
2350+
interiorColor = getRgbColor(interiorColor);
2351+
fillColor = interiorColor
2352+
? Array.from(interiorColor).map(c => c / 255)
2353+
: null;
2354+
}
2355+
2356+
this._setDefaultAppearance({
2357+
xref: parameters.xref,
2358+
extra: `${this.borderStyle.width} w`,
2359+
strokeColor,
2360+
fillColor,
2361+
pointsCallback: (buffer, points) => {
2362+
// Circles are approximated by Bézier curves with four segments since
2363+
// there is no circle primitive in the PDF specification. For the
2364+
// control points distance, see https://stackoverflow.com/a/27863181.
2365+
const controlPointsDistance = (4 / 3) * Math.tan(Math.PI / (2 * 4));
2366+
2367+
const x0 = points[0].x + this.borderStyle.width;
2368+
const y0 = points[0].y - this.borderStyle.width;
2369+
const x1 = points[3].x - this.borderStyle.width;
2370+
const y1 = points[3].y + this.borderStyle.width;
2371+
const xMid = x0 + (x1 - x0) / 2;
2372+
const yMid = y0 + (y1 - y0) / 2;
2373+
const xOffset = ((x1 - x0) / 2) * controlPointsDistance;
2374+
const yOffset = ((y1 - y0) / 2) * controlPointsDistance;
2375+
2376+
buffer.push(`${xMid} ${y1} m`);
2377+
buffer.push(
2378+
`${xMid + xOffset} ${y1} ${x1} ${yMid + yOffset} ${x1} ${yMid} c`
2379+
);
2380+
buffer.push(
2381+
`${x1} ${yMid - yOffset} ${xMid + xOffset} ${y0} ${xMid} ${y0} c`
2382+
);
2383+
buffer.push(
2384+
`${xMid - xOffset} ${y0} ${x0} ${yMid - yOffset} ${x0} ${yMid} c`
2385+
);
2386+
buffer.push(
2387+
`${x0} ${yMid + yOffset} ${xMid - xOffset} ${y1} ${xMid} ${y1} c`
2388+
);
2389+
2390+
buffer.push("h");
2391+
if (fillColor) {
2392+
buffer.push("B");
2393+
} else {
2394+
buffer.push("S");
2395+
}
2396+
return [points[0].x, points[1].x, points[3].y, points[1].y];
2397+
},
2398+
});
2399+
}
22892400
}
22902401
}
22912402

test/pdfs/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@
395395
!annotation-line.pdf
396396
!bug1669099.pdf
397397
!annotation-square-circle.pdf
398+
!annotation-square-circle-without-appearance.pdf
398399
!annotation-stamp.pdf
399400
!annotation-fileattachment.pdf
400401
!annotation-text-widget.pdf
Binary file not shown.

test/test_manifest.json

+7
Original file line numberDiff line numberDiff line change
@@ -4724,6 +4724,13 @@
47244724
"type": "eq",
47254725
"annotations": true
47264726
},
4727+
{ "id": "annotation-square-circle-without-appearance",
4728+
"file": "pdfs/annotation-square-circle-without-appearance.pdf",
4729+
"md5": "5b2a5e1e918137993d26a0cd8b0947f6",
4730+
"rounds": 1,
4731+
"annotations": true,
4732+
"type": "eq"
4733+
},
47274734
{ "id": "annotation-stamp",
47284735
"file": "pdfs/annotation-stamp.pdf",
47294736
"md5": "0a04d7ce1ad103cb3c033d26855d6ec7",

0 commit comments

Comments
 (0)