Skip to content

Commit

Permalink
[Annotation] Add a div containing the text of a FreeText annotation (…
Browse files Browse the repository at this point in the history
…bug 1780375)

An annotation doesn't have to be in the text flow, hence it's likely a bad idea
to insert its text in the text layer. But the text must be visible from a screen
reader point of view so it must somewhere in the DOM.
So with this patch, the text from a FreeText annotation is extracted and added in
a div in its HTML counterpart, and with the patch #15237 the text should be visible
and positioned relatively to the text flow.
  • Loading branch information
calixteman committed Aug 4, 2022
1 parent 159f853 commit 3115574
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 21 deletions.
55 changes: 55 additions & 0 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,57 @@ class Annotation {
return null;
}

get hasTextContent() {
return false;
}

async extractTextContent(evaluator, task, viewBox) {
if (!this.appearance) {
return;
}

const resources = await this.loadResources(
["ExtGState", "Font", "Properties", "XObject"],
this.appearance
);

const text = [];
const buffer = [];
const sink = {
desiredSize: Math.Infinity,
ready: true,

enqueue(chunk, size) {
for (const item of chunk.items) {
buffer.push(item.str);
if (item.hasEOL) {
text.push(buffer.join(""));
buffer.length = 0;
}
}
},
};

await evaluator.getTextContent({
stream: this.appearance,
task,
resources,
includeMarkedContent: true,
combineTextItems: true,
sink,
viewBox,
});
this.reset();

if (buffer.length) {
text.push(buffer.join(""));
}

if (text.length > 0) {
this.data.textContent = text;
}
}

/**
* Get field data for usage in JS sandbox.
*
Expand Down Expand Up @@ -3250,6 +3301,10 @@ class FreeTextAnnotation extends MarkupAnnotation {
this.data.annotationType = AnnotationType.FREETEXT;
}

get hasTextContent() {
return !!this.appearance;
}

static createNewDict(annotation, xref, { apRef, ap }) {
const { color, fontSize, rect, rotation, user, value } = annotation;
const freetext = new Dict(xref);
Expand Down
66 changes: 46 additions & 20 deletions src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,30 +578,56 @@ class Page {
return tree;
}

getAnnotationsData(intent) {
return this._parsedAnnotations.then(function (annotations) {
const annotationsData = [];

if (annotations.length === 0) {
return annotationsData;
async getAnnotationsData(handler, task, intent) {
const annotations = await this._parsedAnnotations;
if (annotations.length === 0) {
return [];
}

const textContentPromises = [];
const annotationsData = [];
let partialEvaluator;

const intentAny = !!(intent & RenderingIntentFlag.ANY),
intentDisplay = !!(intent & RenderingIntentFlag.DISPLAY),
intentPrint = !!(intent & RenderingIntentFlag.PRINT);

for (const annotation of annotations) {
// Get the annotation even if it's hidden because
// JS can change its display.
const isVisible = intentAny || (intentDisplay && annotation.viewable);
if (isVisible || (intentPrint && annotation.printable)) {
annotationsData.push(annotation.data);
}
const intentAny = !!(intent & RenderingIntentFlag.ANY),
intentDisplay = !!(intent & RenderingIntentFlag.DISPLAY),
intentPrint = !!(intent & RenderingIntentFlag.PRINT);

for (const annotation of annotations) {
// Get the annotation even if it's hidden because
// JS can change its display.
if (
intentAny ||
(intentDisplay && annotation.viewable) ||
(intentPrint && annotation.printable)
) {
annotationsData.push(annotation.data);
if (annotation.hasTextContent && isVisible) {
if (!partialEvaluator) {
partialEvaluator = new PartialEvaluator({
xref: this.xref,
handler,
pageIndex: this.pageIndex,
idFactory: this._localIdFactory,
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalImageCache: this.globalImageCache,
options: this.evaluatorOptions,
});
}
textContentPromises.push(
annotation
.extractTextContent(partialEvaluator, task, this.view)
.catch(function (reason) {
warn(
`getAnnotationsData - ignoring textContent during "${task.name}" task: "${reason}".`
);
})
);
}
return annotationsData;
});
}

await Promise.all(textContentPromises);
return annotationsData;
}

get annotations() {
Expand Down
13 changes: 12 additions & 1 deletion src/core/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,18 @@ class WorkerMessageHandler {

handler.on("GetAnnotations", function ({ pageIndex, intent }) {
return pdfManager.getPage(pageIndex).then(function (page) {
return page.getAnnotationsData(intent);
const task = new WorkerTask(`GetAnnotations: page ${pageIndex}`);
startWorkerTask(task);

return page.getAnnotationsData(handler, task, intent).then(
data => {
finishWorkerTask(task);
return data;
},
reason => {
finishWorkerTask(task);
}
);
});
});

Expand Down
12 changes: 12 additions & 0 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1932,11 +1932,23 @@ class FreeTextAnnotationElement extends AnnotationElement {
parameters.data.richText?.str
);
super(parameters, { isRenderable, ignoreBorder: true });
this.textContent = parameters.data.textContent;
}

render() {
this.container.className = "freeTextAnnotation";

if (this.textContent) {
const content = document.createElement("div");
content.className = "annotationTextContent";
for (const line of this.textContent) {
const lineSpan = document.createElement("span");
lineSpan.textContent = line;
content.append(lineSpan);
}
this.container.append(content);
}

if (!this.data.hasPopup) {
this._createPopup(null, this.data);
}
Expand Down
10 changes: 10 additions & 0 deletions test/annotation_layer_builder_overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,13 @@
margin: 0;
padding: 0;
}

.annotationLayer .annotationTextContent {
position: absolute;
width: 100%;
height: 100%;
opacity: 0.4;
background-color: transparent;
color: red;
font-size: 10px;
}
29 changes: 29 additions & 0 deletions test/unit/annotation_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4101,6 +4101,35 @@ describe("annotation", function () {
OPS.endAnnotation,
]);
});

it("should extract the text from a FreeText annotation", async function () {
partialEvaluator.xref = new XRefMock();
const task = new WorkerTask("test FreeText text extraction");
const freetextAnnotation = (
await AnnotationFactory.printNewAnnotations(partialEvaluator, task, [
{
annotationType: AnnotationEditorType.FREETEXT,
rect: [12, 34, 56, 78],
rotation: 0,
fontSize: 10,
color: [0, 0, 0],
value: "Hello PDF.js\nWorld !",
},
])
)[0];

await freetextAnnotation.extractTextContent(partialEvaluator, task, [
-Infinity,
-Infinity,
Infinity,
Infinity,
]);

expect(freetextAnnotation.data.textContent).toEqual([
"Hello PDF.js",
"World !",
]);
});
});

describe("InkAnnotation", function () {
Expand Down
15 changes: 15 additions & 0 deletions web/annotation_layer_builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,18 @@
width: 100%;
height: 100%;
}

.annotationLayer .annotationTextContent {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
color: transparent;
user-select: none;
pointer-events: none;
}

.annotationLayer .annotationTextContent span {
width: 100%;
display: inline-block;
}

0 comments on commit 3115574

Please sign in to comment.