Skip to content

Commit

Permalink
[Editor] Add support for printing/saving free highlight annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
calixteman committed Jan 18, 2024
1 parent 1cdbcfe commit 9709db9
Show file tree
Hide file tree
Showing 6 changed files with 608 additions and 46 deletions.
132 changes: 110 additions & 22 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -4340,19 +4359,22 @@ 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.
ink.set("IT", Name.get("InkHighlight"));
}

// Line thickness.
const bs = new Dict(xref);
ink.set("BS", bs);
Expand Down Expand Up @@ -4380,6 +4402,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 = [
Expand Down Expand Up @@ -4438,6 +4467,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 {
Expand Down
4 changes: 2 additions & 2 deletions src/core/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/display/editor/highlight.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
79 changes: 57 additions & 22 deletions src/display/editor/outliner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);

Expand All @@ -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() {
Expand All @@ -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: 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 outline;
return dest;
}

#computeMinMax(isLTR) {
Expand Down
Loading

0 comments on commit 9709db9

Please sign in to comment.