Skip to content

Commit d64f334

Browse files
committed
[Editor] Add support for printing/saving free highlight annotations
1 parent 1cdbcfe commit d64f334

File tree

6 files changed

+611
-46
lines changed

6 files changed

+611
-46
lines changed

src/core/annotation.js

+113-22
Original file line numberDiff line numberDiff line change
@@ -355,13 +355,19 @@ class AnnotationFactory {
355355
);
356356
break;
357357
case AnnotationEditorType.HIGHLIGHT:
358-
promises.push(
359-
HighlightAnnotation.createNewAnnotation(
360-
xref,
361-
annotation,
362-
dependencies
363-
)
364-
);
358+
if (annotation.quadPoints) {
359+
promises.push(
360+
HighlightAnnotation.createNewAnnotation(
361+
xref,
362+
annotation,
363+
dependencies
364+
)
365+
);
366+
} else {
367+
promises.push(
368+
InkAnnotation.createNewAnnotation(xref, annotation, dependencies)
369+
);
370+
}
365371
break;
366372
case AnnotationEditorType.INK:
367373
promises.push(
@@ -439,16 +445,29 @@ class AnnotationFactory {
439445
);
440446
break;
441447
case AnnotationEditorType.HIGHLIGHT:
442-
promises.push(
443-
HighlightAnnotation.createNewPrintAnnotation(
444-
annotationGlobals,
445-
xref,
446-
annotation,
447-
{
448-
evaluatorOptions: options,
449-
}
450-
)
451-
);
448+
if (annotation.quadPoints) {
449+
promises.push(
450+
HighlightAnnotation.createNewPrintAnnotation(
451+
annotationGlobals,
452+
xref,
453+
annotation,
454+
{
455+
evaluatorOptions: options,
456+
}
457+
)
458+
);
459+
} else {
460+
promises.push(
461+
InkAnnotation.createNewPrintAnnotation(
462+
annotationGlobals,
463+
xref,
464+
annotation,
465+
{
466+
evaluatorOptions: options,
467+
}
468+
)
469+
);
470+
}
452471
break;
453472
case AnnotationEditorType.INK:
454473
promises.push(
@@ -4340,19 +4359,25 @@ class InkAnnotation extends MarkupAnnotation {
43404359
}
43414360

43424361
static createNewDict(annotation, xref, { apRef, ap }) {
4343-
const { color, opacity, paths, rect, rotation, thickness } = annotation;
4362+
const { color, opacity, paths, outlines, rect, rotation, thickness } =
4363+
annotation;
43444364
const ink = new Dict(xref);
43454365
ink.set("Type", Name.get("Annot"));
43464366
ink.set("Subtype", Name.get("Ink"));
43474367
ink.set("CreationDate", `D:${getModificationDate()}`);
43484368
ink.set("Rect", rect);
4349-
ink.set(
4350-
"InkList",
4351-
paths.map(p => p.points)
4352-
);
4369+
ink.set("InkList", outlines?.points || paths.map(p => p.points));
43534370
ink.set("F", 4);
43544371
ink.set("Rotate", rotation);
43554372

4373+
if (outlines) {
4374+
// Free highlight.
4375+
// There's nothing about this in the spec, but it's used when highlighting
4376+
// in Edge's viewer. Acrobat takes into account this parameter to indicate
4377+
// that the Ink is used for highlighting.
4378+
ink.set("IT", Name.get("InkHighlight"));
4379+
}
4380+
43564381
// Line thickness.
43574382
const bs = new Dict(xref);
43584383
ink.set("BS", bs);
@@ -4380,6 +4405,13 @@ class InkAnnotation extends MarkupAnnotation {
43804405
}
43814406

43824407
static async createNewAppearanceStream(annotation, xref, params) {
4408+
if (annotation.outlines) {
4409+
return this.createNewAppearanceStreamForHighlight(
4410+
annotation,
4411+
xref,
4412+
params
4413+
);
4414+
}
43834415
const { color, rect, paths, thickness, opacity } = annotation;
43844416

43854417
const appearanceBuffer = [
@@ -4438,6 +4470,65 @@ class InkAnnotation extends MarkupAnnotation {
44384470

44394471
return ap;
44404472
}
4473+
4474+
static async createNewAppearanceStreamForHighlight(annotation, xref, params) {
4475+
const {
4476+
color,
4477+
rect,
4478+
outlines: { outline },
4479+
opacity,
4480+
} = annotation;
4481+
const appearanceBuffer = [
4482+
`${getPdfColor(color, /* isFill */ true)}`,
4483+
"/R0 gs",
4484+
];
4485+
4486+
appearanceBuffer.push(
4487+
`${numberToString(outline[4])} ${numberToString(outline[5])} m`
4488+
);
4489+
for (let i = 6, ii = outline.length; i < ii; i += 6) {
4490+
if (isNaN(outline[i]) || outline[i] === null) {
4491+
appearanceBuffer.push(
4492+
`${numberToString(outline[i + 4])} ${numberToString(
4493+
outline[i + 5]
4494+
)} l`
4495+
);
4496+
} else {
4497+
const curve = outline
4498+
.slice(i, i + 6)
4499+
.map(numberToString)
4500+
.join(" ");
4501+
appearanceBuffer.push(`${curve} c`);
4502+
}
4503+
}
4504+
appearanceBuffer.push("h f");
4505+
const appearance = appearanceBuffer.join("\n");
4506+
4507+
const appearanceStreamDict = new Dict(xref);
4508+
appearanceStreamDict.set("FormType", 1);
4509+
appearanceStreamDict.set("Subtype", Name.get("Form"));
4510+
appearanceStreamDict.set("Type", Name.get("XObject"));
4511+
appearanceStreamDict.set("BBox", rect);
4512+
appearanceStreamDict.set("Length", appearance.length);
4513+
4514+
const resources = new Dict(xref);
4515+
const extGState = new Dict(xref);
4516+
resources.set("ExtGState", extGState);
4517+
appearanceStreamDict.set("Resources", resources);
4518+
const r0 = new Dict(xref);
4519+
extGState.set("R0", r0);
4520+
r0.set("BM", Name.get("Multiply"));
4521+
4522+
if (opacity !== 1) {
4523+
r0.set("ca", opacity);
4524+
r0.set("Type", Name.get("ExtGState"));
4525+
}
4526+
4527+
const ap = new StringStream(appearance);
4528+
ap.dict = appearanceStreamDict;
4529+
4530+
return ap;
4531+
}
44414532
}
44424533

44434534
class HighlightAnnotation extends MarkupAnnotation {

src/core/writer.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async function writeObject(ref, obj, buffer, { encrypt = null }) {
3232
await writeDict(obj, buffer, transform);
3333
} else if (obj instanceof BaseStream) {
3434
await writeStream(obj, buffer, transform);
35-
} else if (Array.isArray(obj)) {
35+
} else if (Array.isArray(obj) || ArrayBuffer.isView(obj)) {
3636
await writeArray(obj, buffer, transform);
3737
}
3838
buffer.push("\nendobj\n");
@@ -132,7 +132,7 @@ async function writeValue(value, buffer, transform) {
132132
buffer.push(`/${escapePDFName(value.name)}`);
133133
} else if (value instanceof Ref) {
134134
buffer.push(`${value.num} ${value.gen} R`);
135-
} else if (Array.isArray(value)) {
135+
} else if (Array.isArray(value) || ArrayBuffer.isView(value)) {
136136
await writeArray(value, buffer, transform);
137137
} else if (typeof value === "string") {
138138
if (transform) {

src/display/editor/highlight.js

+1
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@ class HighlightEditor extends AnnotationEditor {
597597
annotationType: AnnotationEditorType.HIGHLIGHT,
598598
color,
599599
opacity: this.#opacity,
600+
thickness: 2 * HighlightEditor._defaultThickness,
600601
quadPoints: this.#serializeBoxes(rect),
601602
outlines: this.#serializeOutlines(rect),
602603
pageIndex: this.pageIndex,

src/display/editor/outliner.js

+57-22
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,12 @@ class FreeOutliner {
596596
const lastBottom = last.subarray(16, 18);
597597
const [layerX, layerY, layerWidth, layerHeight] = this.#box;
598598

599+
const points = new Float64Array(this.#points?.length ?? 0);
600+
for (let i = 0, ii = points.length; i < ii; i += 2) {
601+
points[i] = (this.#points[i] - layerX) / layerWidth;
602+
points[i + 1] = (this.#points[i + 1] - layerY) / layerHeight;
603+
}
604+
599605
if (isNaN(last[6]) && !this.isEmpty()) {
600606
// We've only two points.
601607
const outline = new Float64Array(24);
@@ -628,7 +634,12 @@ class FreeOutliner {
628634
],
629635
0
630636
);
631-
return new FreeHighlightOutline(outline, this.#innerMargin, isLTR);
637+
return new FreeHighlightOutline(
638+
outline,
639+
points,
640+
this.#innerMargin,
641+
isLTR
642+
);
632643
}
633644

634645
const outline = new Float64Array(
@@ -675,7 +686,7 @@ class FreeOutliner {
675686
}
676687
}
677688
outline.set([NaN, NaN, NaN, NaN, bottom[4], bottom[5]], N);
678-
return new FreeHighlightOutline(outline, this.#innerMargin, isLTR);
689+
return new FreeHighlightOutline(outline, points, this.#innerMargin, isLTR);
679690
}
680691
}
681692

@@ -684,11 +695,14 @@ class FreeHighlightOutline extends Outline {
684695

685696
#innerMargin;
686697

698+
#points;
699+
687700
#outline;
688701

689-
constructor(outline, innerMargin, isLTR) {
702+
constructor(outline, points, innerMargin, isLTR) {
690703
super();
691704
this.#outline = outline;
705+
this.#points = points;
692706
this.#innerMargin = innerMargin;
693707
this.#computeMinMax(isLTR);
694708

@@ -697,6 +711,10 @@ class FreeHighlightOutline extends Outline {
697711
outline[i] = (outline[i] - x) / width;
698712
outline[i + 1] = (outline[i + 1] - y) / height;
699713
}
714+
for (let i = 0, ii = points.length; i < ii; i += 2) {
715+
points[i] = (points[i] - x) / width;
716+
points[i + 1] = (points[i + 1] - y) / height;
717+
}
700718
}
701719

702720
toSVGPath() {
@@ -717,36 +735,53 @@ class FreeHighlightOutline extends Outline {
717735
}
718736

719737
serialize([blX, blY, trX, trY], rotation) {
720-
const src = this.#outline;
721-
const outline = new Float64Array(src.length);
722738
const width = trX - blX;
723739
const height = trY - blY;
740+
let outline;
741+
let points;
724742
switch (rotation) {
725743
case 0:
726-
for (let i = 0, ii = src.length; i < ii; i += 2) {
727-
outline[i] = blX + src[i] * width;
728-
outline[i + 1] = trY - src[i + 1] * height;
729-
}
744+
outline = this.#rescale(this.#outline, blX, trY, width, -height);
745+
points = this.#rescale(this.#points, blX, trY, width, -height);
730746
break;
731747
case 90:
732-
for (let i = 0, ii = src.length; i < ii; i += 2) {
733-
outline[i] = blX + src[i + 1] * width;
734-
outline[i + 1] = blY + src[i] * height;
735-
}
748+
outline = this.#rescaleAndSwap(this.#outline, blX, blY, width, height);
749+
points = this.#rescaleAndSwap(this.#points, blX, blY, width, height);
736750
break;
737751
case 180:
738-
for (let i = 0, ii = src.length; i < ii; i += 2) {
739-
outline[i] = trX - src[i] * width;
740-
outline[i + 1] = blY + src[i + 1] * height;
741-
}
752+
outline = this.#rescale(this.#outline, trX, blY, -width, height);
753+
points = this.#rescale(this.#points, trX, blY, -width, height);
742754
break;
743755
case 270:
744-
for (let i = 0, ii = src.length; i < ii; i += 2) {
745-
outline[i] = trX - src[i + 1] * width;
746-
outline[i + 1] = trY - src[i] * height;
747-
}
756+
outline = this.#rescaleAndSwap(
757+
this.#outline,
758+
trX,
759+
trY,
760+
-width,
761+
-height
762+
);
763+
points = this.#rescaleAndSwap(this.#points, trX, trY, -width, -height);
764+
break;
765+
}
766+
return { outline: Array.from(outline), points: [Array.from(points)] };
767+
}
768+
769+
#rescale(src, tx, ty, sx, sy) {
770+
const dest = new Float64Array(src.length);
771+
for (let i = 0, ii = src.length; i < ii; i += 2) {
772+
dest[i] = tx + src[i] * sx;
773+
dest[i + 1] = ty + src[i + 1] * sy;
774+
}
775+
return dest;
776+
}
777+
778+
#rescaleAndSwap(src, tx, ty, sx, sy) {
779+
const dest = new Float64Array(src.length);
780+
for (let i = 0, ii = src.length; i < ii; i += 2) {
781+
dest[i] = tx + src[i + 1] * sx;
782+
dest[i + 1] = ty + src[i] * sy;
748783
}
749-
return outline;
784+
return dest;
750785
}
751786

752787
#computeMinMax(isLTR) {

0 commit comments

Comments
 (0)