Fix non-standard quadpoints orders for annotations

This change requires us to use valid quadpoints arrays in the existing
unit tests too due to the normalization.
This commit is contained in:
Tim van der Meij 2020-12-05 21:27:38 +01:00
parent e3b6a9fb23
commit 012e15f7a3
No known key found for this signature in database
GPG Key ID: 8C3FD2925A5F2762
5 changed files with 112 additions and 49 deletions

View File

@ -227,7 +227,36 @@ function getQuadPoints(dict, rect) {
quadPointsLists[i].push({ x, y }); quadPointsLists[i].push({ x, y });
} }
} }
return quadPointsLists;
// The PDF specification states in section 12.5.6.10 (figure 64) that the
// order of the quadpoints should be bottom left, bottom right, top right
// and top left. However, in practice PDF files use a different order,
// namely bottom left, bottom right, top left and top right (this is also
// mentioned on https://github.com/highkite/pdfAnnotate#QuadPoints), so
// this is the actual order we should work with. However, the situation is
// even worse since Adobe's own applications and other applications violate
// the specification and create annotations with other orders, namely top
// left, top right, bottom left and bottom right or even top left, top right,
// bottom right and bottom left. To avoid inconsistency and broken rendering,
// we normalize all lists to put the quadpoints in the same standard order
// (see https://stackoverflow.com/a/10729881).
return quadPointsLists.map(quadPointsList => {
const [minX, maxX, minY, maxY] = quadPointsList.reduce(
([mX, MX, mY, MY], quadPoint) => [
Math.min(mX, quadPoint.x),
Math.max(MX, quadPoint.x),
Math.min(mY, quadPoint.y),
Math.max(MY, quadPoint.y),
],
[Number.MAX_VALUE, Number.MIN_VALUE, Number.MAX_VALUE, Number.MIN_VALUE]
);
return [
{ x: minX, y: maxY },
{ x: maxX, y: maxY },
{ x: minX, y: minY },
{ x: maxX, y: minY },
];
});
} }
function getTransformMatrix(rect, bbox, matrix) { function getTransformMatrix(rect, bbox, matrix) {

View File

@ -392,6 +392,7 @@
!issue11442_reduced.pdf !issue11442_reduced.pdf
!issue11549_reduced.pdf !issue11549_reduced.pdf
!issue8097_reduced.pdf !issue8097_reduced.pdf
!quadpoints.pdf
!transparent.pdf !transparent.pdf
!xobject-image.pdf !xobject-image.pdf
!ccitt_EndOfBlock_false.pdf !ccitt_EndOfBlock_false.pdf

BIN
test/pdfs/quadpoints.pdf Normal file

Binary file not shown.

View File

@ -4805,6 +4805,13 @@
"lastPage": 1, "lastPage": 1,
"type": "text" "type": "text"
}, },
{ "id": "quadpoints",
"file": "pdfs/quadpoints.pdf",
"md5": "aadbc9bf826b4604c49a994fc8cd72c1",
"rounds": 1,
"type": "eq",
"annotations": true
},
{ "id": "operator-in-TJ-array", { "id": "operator-in-TJ-array",
"file": "pdfs/operator-in-TJ-array.pdf", "file": "pdfs/operator-in-TJ-array.pdf",
"md5": "dfe0f15a45be18eca142adaf760984ee", "md5": "dfe0f15a45be18eca142adaf760984ee",

View File

@ -216,41 +216,67 @@ describe("annotation", function () {
} }
}); });
it("should process valid quadpoints arrays", function () { it("should process quadpoints in the standard order", function () {
rect = [10, 10, 20, 20]; rect = [10, 10, 20, 20];
dict.set("QuadPoints", [ dict.set("QuadPoints", [
10,
20,
20,
20,
10,
10,
20,
10,
11,
19,
19,
19,
11, 11,
11, 11,
12, 19,
12, 11,
13,
13,
14,
14,
15,
15,
16,
16,
17,
17,
18,
18,
]); ]);
expect(getQuadPoints(dict, rect)).toEqual([ expect(getQuadPoints(dict, rect)).toEqual([
[ [
{ x: 11, y: 11 }, { x: 10, y: 20 },
{ x: 12, y: 12 }, { x: 20, y: 20 },
{ x: 13, y: 13 }, { x: 10, y: 10 },
{ x: 14, y: 14 }, { x: 20, y: 10 },
], ],
[ [
{ x: 15, y: 15 }, { x: 11, y: 19 },
{ x: 16, y: 16 }, { x: 19, y: 19 },
{ x: 17, y: 17 }, { x: 11, y: 11 },
{ x: 18, y: 18 }, { x: 19, y: 11 },
], ],
]); ]);
}); });
it("should normalize and process quadpoints in non-standard orders", function () {
rect = [10, 10, 20, 20];
const nonStandardOrders = [
// Bottom left, bottom right, top right and top left.
[10, 20, 20, 20, 20, 10, 10, 10],
// Top left, top right, bottom left and bottom right.
[10, 10, 20, 10, 10, 20, 20, 20],
// Top left, top right, bottom right and bottom left.
[10, 10, 20, 10, 20, 20, 10, 20],
];
for (const nonStandardOrder of nonStandardOrders) {
dict.set("QuadPoints", nonStandardOrder);
expect(getQuadPoints(dict, rect)).toEqual([
[
{ x: 10, y: 20 },
{ x: 20, y: 20 },
{ x: 10, y: 10 },
{ x: 20, y: 10 },
],
]);
}
});
}); });
describe("Annotation", function () { describe("Annotation", function () {
@ -1265,7 +1291,7 @@ describe("annotation", function () {
annotationDict.set("Type", Name.get("Annot")); annotationDict.set("Type", Name.get("Annot"));
annotationDict.set("Subtype", Name.get("Link")); annotationDict.set("Subtype", Name.get("Link"));
annotationDict.set("Rect", [10, 10, 20, 20]); annotationDict.set("Rect", [10, 10, 20, 20]);
annotationDict.set("QuadPoints", [11, 11, 12, 12, 13, 13, 14, 14]); annotationDict.set("QuadPoints", [10, 20, 20, 20, 10, 10, 20, 10]);
const annotationRef = Ref.get(121, 0); const annotationRef = Ref.get(121, 0);
const xref = new XRefMock([{ ref: annotationRef, data: annotationDict }]); const xref = new XRefMock([{ ref: annotationRef, data: annotationDict }]);
@ -1279,10 +1305,10 @@ describe("annotation", function () {
expect(data.annotationType).toEqual(AnnotationType.LINK); expect(data.annotationType).toEqual(AnnotationType.LINK);
expect(data.quadPoints).toEqual([ expect(data.quadPoints).toEqual([
[ [
{ x: 11, y: 11 }, { x: 10, y: 20 },
{ x: 12, y: 12 }, { x: 20, y: 20 },
{ x: 13, y: 13 }, { x: 10, y: 10 },
{ x: 14, y: 14 }, { x: 20, y: 10 },
], ],
]); ]);
done(); done();
@ -3636,7 +3662,7 @@ describe("annotation", function () {
highlightDict.set("Type", Name.get("Annot")); highlightDict.set("Type", Name.get("Annot"));
highlightDict.set("Subtype", Name.get("Highlight")); highlightDict.set("Subtype", Name.get("Highlight"));
highlightDict.set("Rect", [10, 10, 20, 20]); highlightDict.set("Rect", [10, 10, 20, 20]);
highlightDict.set("QuadPoints", [11, 11, 12, 12, 13, 13, 14, 14]); highlightDict.set("QuadPoints", [10, 20, 20, 20, 10, 10, 20, 10]);
const highlightRef = Ref.get(121, 0); const highlightRef = Ref.get(121, 0);
const xref = new XRefMock([{ ref: highlightRef, data: highlightDict }]); const xref = new XRefMock([{ ref: highlightRef, data: highlightDict }]);
@ -3650,10 +3676,10 @@ describe("annotation", function () {
expect(data.annotationType).toEqual(AnnotationType.HIGHLIGHT); expect(data.annotationType).toEqual(AnnotationType.HIGHLIGHT);
expect(data.quadPoints).toEqual([ expect(data.quadPoints).toEqual([
[ [
{ x: 11, y: 11 }, { x: 10, y: 20 },
{ x: 12, y: 12 }, { x: 20, y: 20 },
{ x: 13, y: 13 }, { x: 10, y: 10 },
{ x: 14, y: 14 }, { x: 20, y: 10 },
], ],
]); ]);
done(); done();
@ -3709,7 +3735,7 @@ describe("annotation", function () {
underlineDict.set("Type", Name.get("Annot")); underlineDict.set("Type", Name.get("Annot"));
underlineDict.set("Subtype", Name.get("Underline")); underlineDict.set("Subtype", Name.get("Underline"));
underlineDict.set("Rect", [10, 10, 20, 20]); underlineDict.set("Rect", [10, 10, 20, 20]);
underlineDict.set("QuadPoints", [11, 11, 12, 12, 13, 13, 14, 14]); underlineDict.set("QuadPoints", [10, 20, 20, 20, 10, 10, 20, 10]);
const underlineRef = Ref.get(121, 0); const underlineRef = Ref.get(121, 0);
const xref = new XRefMock([{ ref: underlineRef, data: underlineDict }]); const xref = new XRefMock([{ ref: underlineRef, data: underlineDict }]);
@ -3723,10 +3749,10 @@ describe("annotation", function () {
expect(data.annotationType).toEqual(AnnotationType.UNDERLINE); expect(data.annotationType).toEqual(AnnotationType.UNDERLINE);
expect(data.quadPoints).toEqual([ expect(data.quadPoints).toEqual([
[ [
{ x: 11, y: 11 }, { x: 10, y: 20 },
{ x: 12, y: 12 }, { x: 20, y: 20 },
{ x: 13, y: 13 }, { x: 10, y: 10 },
{ x: 14, y: 14 }, { x: 20, y: 10 },
], ],
]); ]);
done(); done();
@ -3760,7 +3786,7 @@ describe("annotation", function () {
squigglyDict.set("Type", Name.get("Annot")); squigglyDict.set("Type", Name.get("Annot"));
squigglyDict.set("Subtype", Name.get("Squiggly")); squigglyDict.set("Subtype", Name.get("Squiggly"));
squigglyDict.set("Rect", [10, 10, 20, 20]); squigglyDict.set("Rect", [10, 10, 20, 20]);
squigglyDict.set("QuadPoints", [11, 11, 12, 12, 13, 13, 14, 14]); squigglyDict.set("QuadPoints", [10, 20, 20, 20, 10, 10, 20, 10]);
const squigglyRef = Ref.get(121, 0); const squigglyRef = Ref.get(121, 0);
const xref = new XRefMock([{ ref: squigglyRef, data: squigglyDict }]); const xref = new XRefMock([{ ref: squigglyRef, data: squigglyDict }]);
@ -3774,10 +3800,10 @@ describe("annotation", function () {
expect(data.annotationType).toEqual(AnnotationType.SQUIGGLY); expect(data.annotationType).toEqual(AnnotationType.SQUIGGLY);
expect(data.quadPoints).toEqual([ expect(data.quadPoints).toEqual([
[ [
{ x: 11, y: 11 }, { x: 10, y: 20 },
{ x: 12, y: 12 }, { x: 20, y: 20 },
{ x: 13, y: 13 }, { x: 10, y: 10 },
{ x: 14, y: 14 }, { x: 20, y: 10 },
], ],
]); ]);
done(); done();
@ -3811,7 +3837,7 @@ describe("annotation", function () {
strikeOutDict.set("Type", Name.get("Annot")); strikeOutDict.set("Type", Name.get("Annot"));
strikeOutDict.set("Subtype", Name.get("StrikeOut")); strikeOutDict.set("Subtype", Name.get("StrikeOut"));
strikeOutDict.set("Rect", [10, 10, 20, 20]); strikeOutDict.set("Rect", [10, 10, 20, 20]);
strikeOutDict.set("QuadPoints", [11, 11, 12, 12, 13, 13, 14, 14]); strikeOutDict.set("QuadPoints", [10, 20, 20, 20, 10, 10, 20, 10]);
const strikeOutRef = Ref.get(121, 0); const strikeOutRef = Ref.get(121, 0);
const xref = new XRefMock([{ ref: strikeOutRef, data: strikeOutDict }]); const xref = new XRefMock([{ ref: strikeOutRef, data: strikeOutDict }]);
@ -3825,10 +3851,10 @@ describe("annotation", function () {
expect(data.annotationType).toEqual(AnnotationType.STRIKEOUT); expect(data.annotationType).toEqual(AnnotationType.STRIKEOUT);
expect(data.quadPoints).toEqual([ expect(data.quadPoints).toEqual([
[ [
{ x: 11, y: 11 }, { x: 10, y: 20 },
{ x: 12, y: 12 }, { x: 20, y: 20 },
{ x: 13, y: 13 }, { x: 10, y: 10 },
{ x: 14, y: 14 }, { x: 20, y: 10 },
], ],
]); ]);
done(); done();