Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Editor] Add support for printing/saving free highlight annotations #17531

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 113 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,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);
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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 {
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)) {
Snuffleupagus marked this conversation as resolved.
Show resolved Hide resolved
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