Merge pull request #17531 from calixteman/editor_free_highlight_print_save

[Editor] Add support for printing/saving free highlight annotations
This commit is contained in:
calixteman 2024-01-19 15:23:29 +01:00 committed by GitHub
commit f6c4b29aa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 611 additions and 46 deletions

View File

@ -355,13 +355,19 @@ class AnnotationFactory {
);
break;
case AnnotationEditorType.HIGHLIGHT:
promises.push(
HighlightAnnotation.createNewAnnotation(
xref,
annotation,
dependencies
)
);
if (annotation.quadPoints) {
promises.push(
HighlightAnnotation.createNewAnnotation(
xref,
annotation,
dependencies
)
);
} else {
promises.push(
InkAnnotation.createNewAnnotation(xref, annotation, dependencies)
);
}
break;
case AnnotationEditorType.INK:
promises.push(
@ -439,16 +445,29 @@ class AnnotationFactory {
);
break;
case AnnotationEditorType.HIGHLIGHT:
promises.push(
HighlightAnnotation.createNewPrintAnnotation(
annotationGlobals,
xref,
annotation,
{
evaluatorOptions: options,
}
)
);
if (annotation.quadPoints) {
promises.push(
HighlightAnnotation.createNewPrintAnnotation(
annotationGlobals,
xref,
annotation,
{
evaluatorOptions: options,
}
)
);
} else {
promises.push(
InkAnnotation.createNewPrintAnnotation(
annotationGlobals,
xref,
annotation,
{
evaluatorOptions: options,
}
)
);
}
break;
case AnnotationEditorType.INK:
promises.push(
@ -4340,19 +4359,25 @@ class InkAnnotation extends MarkupAnnotation {
}
static createNewDict(annotation, xref, { apRef, ap }) {
const { color, opacity, paths, rect, rotation, thickness } = annotation;
const { color, opacity, paths, outlines, rect, rotation, thickness } =
annotation;
const ink = new Dict(xref);
ink.set("Type", Name.get("Annot"));
ink.set("Subtype", Name.get("Ink"));
ink.set("CreationDate", `D:${getModificationDate()}`);
ink.set("Rect", rect);
ink.set(
"InkList",
paths.map(p => p.points)
);
ink.set("InkList", outlines?.points || paths.map(p => p.points));
ink.set("F", 4);
ink.set("Rotate", rotation);
if (outlines) {
// Free highlight.
// There's nothing about this in the spec, but it's used when highlighting
// in Edge's viewer. Acrobat takes into account this parameter to indicate
// that the Ink is used for highlighting.
ink.set("IT", Name.get("InkHighlight"));
}
// Line thickness.
const bs = new Dict(xref);
ink.set("BS", bs);
@ -4380,6 +4405,13 @@ class InkAnnotation extends MarkupAnnotation {
}
static async createNewAppearanceStream(annotation, xref, params) {
if (annotation.outlines) {
return this.createNewAppearanceStreamForHighlight(
annotation,
xref,
params
);
}
const { color, rect, paths, thickness, opacity } = annotation;
const appearanceBuffer = [
@ -4438,6 +4470,65 @@ class InkAnnotation extends MarkupAnnotation {
return ap;
}
static async createNewAppearanceStreamForHighlight(annotation, xref, params) {
const {
color,
rect,
outlines: { outline },
opacity,
} = annotation;
const appearanceBuffer = [
`${getPdfColor(color, /* isFill */ true)}`,
"/R0 gs",
];
appearanceBuffer.push(
`${numberToString(outline[4])} ${numberToString(outline[5])} m`
);
for (let i = 6, ii = outline.length; i < ii; i += 6) {
if (isNaN(outline[i]) || outline[i] === null) {
appearanceBuffer.push(
`${numberToString(outline[i + 4])} ${numberToString(
outline[i + 5]
)} l`
);
} else {
const curve = outline
.slice(i, i + 6)
.map(numberToString)
.join(" ");
appearanceBuffer.push(`${curve} c`);
}
}
appearanceBuffer.push("h f");
const appearance = appearanceBuffer.join("\n");
const appearanceStreamDict = new Dict(xref);
appearanceStreamDict.set("FormType", 1);
appearanceStreamDict.set("Subtype", Name.get("Form"));
appearanceStreamDict.set("Type", Name.get("XObject"));
appearanceStreamDict.set("BBox", rect);
appearanceStreamDict.set("Length", appearance.length);
const resources = new Dict(xref);
const extGState = new Dict(xref);
resources.set("ExtGState", extGState);
appearanceStreamDict.set("Resources", resources);
const r0 = new Dict(xref);
extGState.set("R0", r0);
r0.set("BM", Name.get("Multiply"));
if (opacity !== 1) {
r0.set("ca", opacity);
r0.set("Type", Name.get("ExtGState"));
}
const ap = new StringStream(appearance);
ap.dict = appearanceStreamDict;
return ap;
}
}
class HighlightAnnotation extends MarkupAnnotation {

View File

@ -32,7 +32,7 @@ async function writeObject(ref, obj, buffer, { encrypt = null }) {
await writeDict(obj, buffer, transform);
} else if (obj instanceof BaseStream) {
await writeStream(obj, buffer, transform);
} else if (Array.isArray(obj)) {
} else if (Array.isArray(obj) || ArrayBuffer.isView(obj)) {
await writeArray(obj, buffer, transform);
}
buffer.push("\nendobj\n");
@ -132,7 +132,7 @@ async function writeValue(value, buffer, transform) {
buffer.push(`/${escapePDFName(value.name)}`);
} else if (value instanceof Ref) {
buffer.push(`${value.num} ${value.gen} R`);
} else if (Array.isArray(value)) {
} else if (Array.isArray(value) || ArrayBuffer.isView(value)) {
await writeArray(value, buffer, transform);
} else if (typeof value === "string") {
if (transform) {

View File

@ -597,6 +597,7 @@ class HighlightEditor extends AnnotationEditor {
annotationType: AnnotationEditorType.HIGHLIGHT,
color,
opacity: this.#opacity,
thickness: 2 * HighlightEditor._defaultThickness,
quadPoints: this.#serializeBoxes(rect),
outlines: this.#serializeOutlines(rect),
pageIndex: this.pageIndex,

View File

@ -596,6 +596,12 @@ class FreeOutliner {
const lastBottom = last.subarray(16, 18);
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
const points = new Float64Array(this.#points?.length ?? 0);
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = (this.#points[i] - layerX) / layerWidth;
points[i + 1] = (this.#points[i + 1] - layerY) / layerHeight;
}
if (isNaN(last[6]) && !this.isEmpty()) {
// We've only two points.
const outline = new Float64Array(24);
@ -628,7 +634,12 @@ class FreeOutliner {
],
0
);
return new FreeHighlightOutline(outline, this.#innerMargin, isLTR);
return new FreeHighlightOutline(
outline,
points,
this.#innerMargin,
isLTR
);
}
const outline = new Float64Array(
@ -675,7 +686,7 @@ class FreeOutliner {
}
}
outline.set([NaN, NaN, NaN, NaN, bottom[4], bottom[5]], N);
return new FreeHighlightOutline(outline, this.#innerMargin, isLTR);
return new FreeHighlightOutline(outline, points, this.#innerMargin, isLTR);
}
}
@ -684,11 +695,14 @@ class FreeHighlightOutline extends Outline {
#innerMargin;
#points;
#outline;
constructor(outline, innerMargin, isLTR) {
constructor(outline, points, innerMargin, isLTR) {
super();
this.#outline = outline;
this.#points = points;
this.#innerMargin = innerMargin;
this.#computeMinMax(isLTR);
@ -697,6 +711,10 @@ class FreeHighlightOutline extends Outline {
outline[i] = (outline[i] - x) / width;
outline[i + 1] = (outline[i + 1] - y) / height;
}
for (let i = 0, ii = points.length; i < ii; i += 2) {
points[i] = (points[i] - x) / width;
points[i + 1] = (points[i + 1] - y) / height;
}
}
toSVGPath() {
@ -717,36 +735,53 @@ class FreeHighlightOutline extends Outline {
}
serialize([blX, blY, trX, trY], rotation) {
const src = this.#outline;
const outline = new Float64Array(src.length);
const width = trX - blX;
const height = trY - blY;
let outline;
let points;
switch (rotation) {
case 0:
for (let i = 0, ii = src.length; i < ii; i += 2) {
outline[i] = blX + src[i] * width;
outline[i + 1] = trY - src[i + 1] * height;
}
outline = this.#rescale(this.#outline, blX, trY, width, -height);
points = this.#rescale(this.#points, blX, trY, width, -height);
break;
case 90:
for (let i = 0, ii = src.length; i < ii; i += 2) {
outline[i] = blX + src[i + 1] * width;
outline[i + 1] = blY + src[i] * height;
}
outline = this.#rescaleAndSwap(this.#outline, blX, blY, width, height);
points = this.#rescaleAndSwap(this.#points, blX, blY, width, height);
break;
case 180:
for (let i = 0, ii = src.length; i < ii; i += 2) {
outline[i] = trX - src[i] * width;
outline[i + 1] = blY + src[i + 1] * height;
}
outline = this.#rescale(this.#outline, trX, blY, -width, height);
points = this.#rescale(this.#points, trX, blY, -width, height);
break;
case 270:
for (let i = 0, ii = src.length; i < ii; i += 2) {
outline[i] = trX - src[i + 1] * width;
outline[i + 1] = trY - src[i] * height;
}
outline = this.#rescaleAndSwap(
this.#outline,
trX,
trY,
-width,
-height
);
points = this.#rescaleAndSwap(this.#points, trX, trY, -width, -height);
break;
}
return outline;
return { outline: Array.from(outline), points: [Array.from(points)] };
}
#rescale(src, tx, ty, sx, sy) {
const dest = new Float64Array(src.length);
for (let i = 0, ii = src.length; i < ii; i += 2) {
dest[i] = tx + src[i] * sx;
dest[i + 1] = ty + src[i + 1] * sy;
}
return dest;
}
#rescaleAndSwap(src, tx, ty, sx, sy) {
const dest = new Float64Array(src.length);
for (let i = 0, ii = src.length; i < ii; i += 2) {
dest[i] = tx + src[i + 1] * sx;
dest[i + 1] = ty + src[i] * sy;
}
return dest;
}
#computeMinMax(isLTR) {

View File

@ -9324,5 +9324,324 @@
"structTreeParentId": null
}
}
},
{
"id": "tracemonkey-free-highlights",
"file": "pdfs/tracemonkey.pdf",
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
"rounds": 1,
"lastPage": 1,
"type": "eq",
"print": true,
"annotationStorage": {
"pdfjs_internal_editor_0": {
"annotationType": 9,
"color": [83, 255, 188],
"opacity": 1,
"thickness": 20,
"quadPoints": null,
"outlines": {
"outline": [
null,
null,
null,
null,
74.55,
697.13,
78.51721472766205,
696.5192209591247,
79.29044208205713,
696.4224516456007,
80.05486887178773,
696.3379903011609,
80.81929566151834,
696.253528956721,
81.57617646110613,
696.1784636392651,
82.32551127055109,
696.1127943487935,
83.07484607999606,
696.047125058322,
83.81784340794434,
695.9886636959667,
84.55450325439585,
695.9374102617279,
null,
null,
null,
null,
85.65949302407317,
695.8605301103697,
null,
null,
null,
null,
87.03393143844572,
715.8149755833261,
85.48426194954816,
715.9204201846449,
84.87281761152917,
715.9671985492615,
84.26979673105028,
716.0185774379673,
83.66677585057136,
716.0699563266733,
83.07457849467843,
716.1266344280523,
82.4932046633715,
716.1886117421047,
81.9118308320646,
716.2505890561571,
81.34439691081282,
716.3184503202297,
80.7909028996162,
716.3921955343225,
null,
null,
null,
null,
77.95775603227756,
716.8481332058944
],
"points": [
[
76.25849755587352, 706.9926836109859, 86.14596273693202,
705.2879914026346, 88.1916451881855, 705.2879914026346,
90.23732763943899, 705.2879914026346, 92.28301009069247,
705.2879914026346, 94.32869254194594, 705.2879914026346
]
]
},
"pageIndex": 0,
"rect": [
73.94723907946947, 695.0685301103697, 87.64593143844571,
717.6401332058945
],
"rotation": 0,
"structTreeParentId": null
},
"pdfjs_internal_editor_1": {
"annotationType": 9,
"color": [128, 235, 255],
"opacity": 1,
"thickness": 20,
"quadPoints": null,
"outlines": {
"outline": [
null,
null,
null,
null,
260.9547742578311,
674.9666484352592,
265.0119754286617,
674.7200369564051,
265.85057115055446,
674.6816601253416,
266.8029560253001,
674.6436779453497,
267.7553409000457,
674.6056957653577,
268.641418670916,
674.586241754074,
269.46118933791087,
674.5853159114982,
null,
null,
null,
null,
270.6908453384032,
674.5839271476348,
null,
null,
null,
null,
270.6417128415015,
694.586095213411,
269.2921378713866,
694.5754310173227,
268.590908898233,
694.5902107638636,
267.72828093997174,
694.624035935381,
266.8656529817105,
694.6578611068985,
266.117024794879,
694.6914601264726,
265.48239637947734,
694.7248329941037,
null,
null,
null,
null,
262.2852690081439,
694.9245750690663
],
"points": [
[
261.6200216329875, 684.9456117521628, 271.8481748830951,
684.263717601745, 273.8938055331166, 684.263717601745,
276.6213130664787, 684.263717601745, 278.6669437165002,
684.6046646769539
]
]
},
"pageIndex": 0,
"rect": [
260.342789755033, 673.7919070919245, 271.30282984120134,
695.7165951247766
],
"rotation": 90,
"structTreeParentId": null
},
"pdfjs_internal_editor_2": {
"annotationType": 9,
"color": [255, 203, 230],
"opacity": 1,
"thickness": 20,
"quadPoints": null,
"outlines": {
"outline": [
null,
null,
null,
null,
350.1249530717911,
692.6633944602168,
345.38916289103776,
693.0766494920856,
344.5712435088917,
693.1558187178291,
343.92282969955727,
693.2395419718531,
343.2744158902228,
693.3232652258769,
342.6222941207318,
693.408487931081,
341.96646439108423,
693.4952100874651,
null,
null,
null,
null,
340.9827197966128,
693.6252933220413,
null,
null,
null,
null,
338.36438011302045,
673.7956810233082,
340.14908906785814,
673.5603604426211,
340.9899862433436,
673.4556612067797,
341.95789701237936,
673.3403909673718,
342.92580778141513,
673.2251207279638,
343.81937416539495,
673.1375137282872,
344.63859616431887,
673.0775699683421,
null,
null,
null,
null,
348.1346569560641,
672.7609372902139
],
"points": [
[
349.12980501392764, 682.7121658752152, 338.9013927576602,
683.734981200226, 336.1738161559889, 683.734981200226,
333.7871866295265, 684.4168580835666, 331.74150417827303,
684.7577965252368
]
]
},
"pageIndex": 0,
"rect": [
337.7523801130204, 671.9689372902138, 350.7369530717911,
694.4172933220414
],
"rotation": 180,
"structTreeParentId": null
},
"pdfjs_internal_editor_3": {
"annotationType": 9,
"color": [255, 79, 95],
"opacity": 1,
"thickness": 20,
"quadPoints": null,
"outlines": {
"outline": [
null,
null,
null,
null,
534.1504350586464,
715.0615382740357,
530.2182783647162,
715.0615382740357,
529.5364014813756,
715.0615382740357,
528.8545245980351,
715.0615382740357,
528.1726477146946,
715.0615382740357,
527.4339477577424,
715.0615382740357,
526.6384247271785,
715.0615382740357,
null,
null,
null,
null,
525.4451401813326,
715.0615382740357,
null,
null,
null,
null,
525.4451401813326,
695.0593098617794,
527.4339477577424,
695.0593098617794,
528.1726477146946,
695.0593098617794,
528.8545245980351,
695.0593098617794,
529.5364014813756,
695.0593098617794,
530.2182783647162,
695.0593098617794,
530.9001552480566,
695.0593098617794,
null,
null,
null,
null,
534.1504350586464,
695.0593098617794
],
"points": [
[
534.1504350586464, 705.0604240679075, 523.9222818085387,
705.0604240679075, 521.8766511585172, 705.0604240679075,
519.8310205084957, 705.0604240679075, 517.444451416804,
705.0604240679075
]
]
},
"pageIndex": 0,
"rect": [
524.8331556785345, 694.2672898060691, 534.7624195614445,
715.853558329746
],
"rotation": 270,
"structTreeParentId": null
}
}
}
]

View File

@ -4708,6 +4708,125 @@ describe("annotation", function () {
OPS.endAnnotation,
]);
});
it("should create a new free Highlight annotation", async function () {
partialEvaluator.xref = new XRefMock();
const task = new WorkerTask("test free Highlight creation");
const data = await AnnotationFactory.saveNewAnnotations(
partialEvaluator,
task,
[
{
annotationType: AnnotationEditorType.HIGHLIGHT,
rect: [12, 34, 56, 78],
rotation: 0,
opacity: 1,
color: [0, 0, 0],
thickness: 3.14,
quadPoints: null,
outlines: {
outline: Float64Array.from([
NaN,
NaN,
8,
9,
10,
11,
NaN,
NaN,
12,
13,
14,
15,
]),
points: [Float64Array.from([16, 17, 18, 19])],
},
},
]
);
const base = data.annotations[0].data.replace(/\(D:\d+\)/, "(date)");
expect(base).toEqual(
"1 0 obj\n" +
"<< /Type /Annot /Subtype /Ink /CreationDate (date) /Rect [12 34 56 78] " +
"/InkList [[16 17 18 19]] /F 4 /Rotate 0 /IT /InkHighlight /BS << /W 3.14>> " +
"/C [0 0 0] /CA 1 /AP << /N 2 0 R>>>>\n" +
"endobj\n"
);
const appearance = data.dependencies[0].data;
expect(appearance).toEqual(
"2 0 obj\n" +
"<< /FormType 1 /Subtype /Form /Type /XObject /BBox [12 34 56 78] " +
"/Length 30 /Resources << /ExtGState << /R0 << /BM /Multiply>>>>>>>> " +
"stream\n" +
"0 g\n" +
"/R0 gs\n" +
"10 11 m\n" +
"14 15 l\n" +
"h f\n" +
"endstream\n" +
"endobj\n"
);
});
it("should render a new free Highlight annotation for printing", async function () {
partialEvaluator.xref = new XRefMock();
const task = new WorkerTask("test free Highlight printing");
const highlightAnnotation = (
await AnnotationFactory.printNewAnnotations(
annotationGlobalsMock,
partialEvaluator,
task,
[
{
annotationType: AnnotationEditorType.HIGHLIGHT,
rect: [12, 34, 56, 78],
rotation: 0,
opacity: 0.5,
color: [0, 255, 0],
thickness: 3.14,
quadPoints: null,
outlines: {
outline: Float64Array.from([
NaN,
NaN,
8,
9,
10,
11,
NaN,
NaN,
12,
13,
14,
15,
]),
points: [Float64Array.from([16, 17, 18, 19])],
},
},
]
)
)[0];
const { opList } = await highlightAnnotation.getOperatorList(
partialEvaluator,
task,
RenderingIntentFlag.PRINT,
false,
null
);
expect(opList.argsArray.length).toEqual(6);
expect(opList.fnArray).toEqual([
OPS.beginAnnotation,
OPS.setFillRGBColor,
OPS.setGState,
OPS.constructPath,
OPS.fill,
OPS.endAnnotation,
]);
});
});
describe("UnderlineAnnotation", function () {