Merge pull request #15267 from calixteman/freetext_a11y

[Annotation] Add a div containing the text of a FreeText annotation (bug 1780375)
This commit is contained in:
calixteman 2022-08-04 11:49:29 +02:00 committed by GitHub
commit b985eaa98c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 178 additions and 20 deletions

View File

@ -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.
*
@ -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);

View File

@ -578,30 +578,56 @@ class Page {
return tree;
}
getAnnotationsData(intent) {
return this._parsedAnnotations.then(function (annotations) {
const annotationsData = [];
async getAnnotationsData(handler, task, intent) {
const annotations = await this._parsedAnnotations;
if (annotations.length === 0) {
return [];
}
if (annotations.length === 0) {
return annotationsData;
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() {

View File

@ -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);
}
);
});
});

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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 () {

View File

@ -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;
}