[api-minor] Add basic support for RTL text-content in PopupAnnotations (issue 14046)

In order to implement this, we utilize the existing `bidi` function to infer the text-direction of /T and /Contents entries. While this may not be perfect in cases where one PopupAnnotation mixes LTR and RTL languages, it should work well enough in most cases.
To avoid having to add *two new* properties in lots of annotations, supplementing the existing `title`/`contents`-properties, this patch instead re-factors the existing code such that the properties are replaced by Objects (containing `str` and `dir`).

*Please note:* In order avoid breaking existing third-party implementations, `GENERIC`-builds of the PDF.js library will still provide the old `title`/`contents`-properties on annotations returned by `PDFPageProxy.getAnnotations`.
This commit is contained in:
Jonas Jenwald 2021-09-24 17:30:56 +02:00
parent 104e049338
commit 1dcd2f0cd3
8 changed files with 124 additions and 57 deletions

View File

@ -47,6 +47,7 @@ import {
Name, Name,
RefSet, RefSet,
} from "./primitives.js"; } from "./primitives.js";
import { bidi } from "./bidi.js";
import { Catalog } from "./catalog.js"; import { Catalog } from "./catalog.js";
import { ColorSpace } from "./colorspace.js"; import { ColorSpace } from "./colorspace.js";
import { FileSpec } from "./file_spec.js"; import { FileSpec } from "./file_spec.js";
@ -356,6 +357,7 @@ class Annotation {
constructor(params) { constructor(params) {
const dict = params.dict; const dict = params.dict;
this.setTitle(dict.get("T"));
this.setContents(dict.get("Contents")); this.setContents(dict.get("Contents"));
this.setModificationDate(dict.get("M")); this.setModificationDate(dict.get("M"));
this.setFlags(dict.get("F")); this.setFlags(dict.get("F"));
@ -374,7 +376,7 @@ class Annotation {
annotationFlags: this.flags, annotationFlags: this.flags,
borderStyle: this.borderStyle, borderStyle: this.borderStyle,
color: this.color, color: this.color,
contents: this.contents, contentsObj: this._contents,
hasAppearance: !!this.appearance, hasAppearance: !!this.appearance,
id: params.id, id: params.id,
modificationDate: this.modificationDate, modificationDate: this.modificationDate,
@ -500,17 +502,35 @@ class Annotation {
return this._isPrintable(this.flags); return this._isPrintable(this.flags);
} }
/**
* @private
*/
_parseStringHelper(data) {
const str = typeof data === "string" ? stringToPDFString(data) : "";
const dir = str && bidi(str).dir === "rtl" ? "rtl" : "ltr";
return { str, dir };
}
/**
* Set the title.
*
* @param {string} title - The title of the annotation, used e.g. with
* PopupAnnotations.
*/
setTitle(title) {
this._title = this._parseStringHelper(title);
}
/** /**
* Set the contents. * Set the contents.
* *
* @public
* @memberof Annotation
* @param {string} contents - Text to display for the annotation or, if the * @param {string} contents - Text to display for the annotation or, if the
* type of annotation does not display text, a * type of annotation does not display text, a
* description of the annotation's contents * description of the annotation's contents
*/ */
setContents(contents) { setContents(contents) {
this.contents = stringToPDFString(contents || ""); this._contents = this._parseStringHelper(contents);
} }
/** /**
@ -1014,10 +1034,11 @@ class MarkupAnnotation extends Annotation {
// the group attributes from the primary annotation. // the group attributes from the primary annotation.
const parent = dict.get("IRT"); const parent = dict.get("IRT");
this.data.title = stringToPDFString(parent.get("T") || ""); this.setTitle(parent.get("T"));
this.data.titleObj = this._title;
this.setContents(parent.get("Contents")); this.setContents(parent.get("Contents"));
this.data.contents = this.contents; this.data.contentsObj = this._contents;
if (!parent.has("CreationDate")) { if (!parent.has("CreationDate")) {
this.data.creationDate = null; this.data.creationDate = null;
@ -1043,7 +1064,7 @@ class MarkupAnnotation extends Annotation {
this.data.color = this.color; this.data.color = this.color;
} }
} else { } else {
this.data.title = stringToPDFString(dict.get("T") || ""); this.data.titleObj = this._title;
this.setCreationDate(dict.get("CreationDate")); this.setCreationDate(dict.get("CreationDate"));
this.data.creationDate = this.creationDate; this.data.creationDate = this.creationDate;
@ -2405,8 +2426,11 @@ class PopupAnnotation extends Annotation {
} }
} }
this.data.title = stringToPDFString(parentItem.get("T") || ""); this.setTitle(parentItem.get("T"));
this.data.contents = stringToPDFString(parentItem.get("Contents") || ""); this.data.titleObj = this._title;
this.setContents(parentItem.get("Contents"));
this.data.contentsObj = this._contents;
} }
} }

View File

@ -120,7 +120,7 @@ function createBidiText(str, isLTR, vertical = false) {
const chars = []; const chars = [];
const types = []; const types = [];
function bidi(str, startLevel, vertical) { function bidi(str, startLevel = -1, vertical = false) {
let isLTR = true; let isLTR = true;
const strLength = str.length; const strLength = str.length;
if (strLength === 0 || vertical) { if (strLength === 0 || vertical) {

View File

@ -320,9 +320,9 @@ class AnnotationElement {
container, container,
trigger, trigger,
color: data.color, color: data.color,
title: data.title, titleObj: data.titleObj,
modificationDate: data.modificationDate, modificationDate: data.modificationDate,
contents: data.contents, contentsObj: data.contentsObj,
hideWrapper: true, hideWrapper: true,
}); });
const popup = popupElement.render(); const popup = popupElement.render();
@ -562,8 +562,8 @@ class TextAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { isRenderable }); super(parameters, { isRenderable });
} }
@ -1392,7 +1392,9 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
class PopupAnnotationElement extends AnnotationElement { class PopupAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!(parameters.data.title || parameters.data.contents); const isRenderable = !!(
parameters.data.titleObj?.str || parameters.data.contentsObj?.str
);
super(parameters, { isRenderable }); super(parameters, { isRenderable });
} }
@ -1424,9 +1426,9 @@ class PopupAnnotationElement extends AnnotationElement {
container: this.container, container: this.container,
trigger: Array.from(parentElements), trigger: Array.from(parentElements),
color: this.data.color, color: this.data.color,
title: this.data.title, titleObj: this.data.titleObj,
modificationDate: this.data.modificationDate, modificationDate: this.data.modificationDate,
contents: this.data.contents, contentsObj: this.data.contentsObj,
}); });
// Position the popup next to the parent annotation's container. // Position the popup next to the parent annotation's container.
@ -1456,9 +1458,9 @@ class PopupElement {
this.container = parameters.container; this.container = parameters.container;
this.trigger = parameters.trigger; this.trigger = parameters.trigger;
this.color = parameters.color; this.color = parameters.color;
this.title = parameters.title; this.titleObj = parameters.titleObj;
this.modificationDate = parameters.modificationDate; this.modificationDate = parameters.modificationDate;
this.contents = parameters.contents; this.contentsObj = parameters.contentsObj;
this.hideWrapper = parameters.hideWrapper || false; this.hideWrapper = parameters.hideWrapper || false;
this.pinned = false; this.pinned = false;
@ -1490,7 +1492,8 @@ class PopupElement {
} }
const title = document.createElement("h1"); const title = document.createElement("h1");
title.textContent = this.title; title.dir = this.titleObj.dir;
title.textContent = this.titleObj.str;
popup.appendChild(title); popup.appendChild(title);
// The modification date is shown in the popup instead of the creation // The modification date is shown in the popup instead of the creation
@ -1508,7 +1511,7 @@ class PopupElement {
popup.appendChild(modificationDate); popup.appendChild(modificationDate);
} }
const contents = this._formatContents(this.contents); const contents = this._formatContents(this.contentsObj);
popup.appendChild(contents); popup.appendChild(contents);
if (!Array.isArray(this.trigger)) { if (!Array.isArray(this.trigger)) {
@ -1531,13 +1534,14 @@ class PopupElement {
* Format the contents of the popup by adding newlines where necessary. * Format the contents of the popup by adding newlines where necessary.
* *
* @private * @private
* @param {string} contents * @param {Object<string, string>} contentsObj
* @memberof PopupElement * @memberof PopupElement
* @returns {HTMLParagraphElement} * @returns {HTMLParagraphElement}
*/ */
_formatContents(contents) { _formatContents({ str, dir }) {
const p = document.createElement("p"); const p = document.createElement("p");
const lines = contents.split(/(?:\r\n?|\n)/); p.dir = dir;
const lines = str.split(/(?:\r\n?|\n)/);
for (let i = 0, ii = lines.length; i < ii; ++i) { for (let i = 0, ii = lines.length; i < ii; ++i) {
const line = lines[i]; const line = lines[i];
p.appendChild(document.createTextNode(line)); p.appendChild(document.createTextNode(line));
@ -1601,8 +1605,8 @@ class FreeTextAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -1621,8 +1625,8 @@ class LineAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -1665,8 +1669,8 @@ class SquareAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -1712,8 +1716,8 @@ class CircleAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -1759,8 +1763,8 @@ class PolylineAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
@ -1824,8 +1828,8 @@ class CaretAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -1844,8 +1848,8 @@ class InkAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
@ -1903,8 +1907,8 @@ class HighlightAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { super(parameters, {
isRenderable, isRenderable,
@ -1931,8 +1935,8 @@ class UnderlineAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { super(parameters, {
isRenderable, isRenderable,
@ -1959,8 +1963,8 @@ class SquigglyAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { super(parameters, {
isRenderable, isRenderable,
@ -1987,8 +1991,8 @@ class StrikeOutAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { super(parameters, {
isRenderable, isRenderable,
@ -2015,8 +2019,8 @@ class StampAnnotationElement extends AnnotationElement {
constructor(parameters) { constructor(parameters) {
const isRenderable = !!( const isRenderable = !!(
parameters.data.hasPopup || parameters.data.hasPopup ||
parameters.data.title || parameters.data.titleObj?.str ||
parameters.data.contents parameters.data.contentsObj?.str
); );
super(parameters, { isRenderable, ignoreBorder: true }); super(parameters, { isRenderable, ignoreBorder: true });
} }
@ -2055,7 +2059,10 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
trigger.style.width = this.container.style.width; trigger.style.width = this.container.style.width;
trigger.addEventListener("dblclick", this._download.bind(this)); trigger.addEventListener("dblclick", this._download.bind(this));
if (!this.data.hasPopup && (this.data.title || this.data.contents)) { if (
!this.data.hasPopup &&
(this.data.titleObj?.str || this.data.contentsObj?.str)
) {
this._createPopup(trigger, this.data); this._createPopup(trigger, this.data);
} }

View File

@ -1305,6 +1305,34 @@ class PDFPageProxy {
intentArgs.renderingIntent intentArgs.renderingIntent
); );
this._annotationPromises.set(intentArgs.cacheKey, promise); this._annotationPromises.set(intentArgs.cacheKey, promise);
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
promise = promise.then(annotations => {
for (const annotation of annotations) {
if (annotation.titleObj !== undefined) {
Object.defineProperty(annotation, "title", {
get() {
deprecated(
"`title`-property on annotation, please use `titleObj` instead."
);
return annotation.titleObj.str;
},
});
}
if (annotation.contentsObj !== undefined) {
Object.defineProperty(annotation, "contents", {
get() {
deprecated(
"`contents`-property on annotation, please use `contentsObj` instead."
);
return annotation.contentsObj.str;
},
});
}
}
return annotations;
});
}
} }
return promise; return promise;
} }

View File

@ -15,6 +15,7 @@
!bug1727053.pdf !bug1727053.pdf
!issue2391-1.pdf !issue2391-1.pdf
!issue2391-2.pdf !issue2391-2.pdf
!issue14046.pdf
!issue3214.pdf !issue3214.pdf
!issue4665.pdf !issue4665.pdf
!issue4684.pdf !issue4684.pdf

BIN
test/pdfs/issue14046.pdf Normal file

Binary file not shown.

View File

@ -5929,5 +5929,12 @@
"link": true, "link": true,
"forms": true, "forms": true,
"type": "eq" "type": "eq"
},
{ "id": "issue14046",
"file": "pdfs/issue14046.pdf",
"md5": "dbb5d4e284ca9cc3219e04af6ce64e13",
"rounds": 1,
"type": "eq",
"annotations": true
} }
] ]

View File

@ -312,14 +312,14 @@ describe("annotation", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({ dict, ref });
annotation.setContents("Foo bar baz"); annotation.setContents("Foo bar baz");
expect(annotation.contents).toEqual("Foo bar baz"); expect(annotation._contents).toEqual({ str: "Foo bar baz", dir: "ltr" });
}); });
it("should not set and get invalid contents", function () { it("should not set and get invalid contents", function () {
const annotation = new Annotation({ dict, ref }); const annotation = new Annotation({ dict, ref });
annotation.setContents(undefined); annotation.setContents(undefined);
expect(annotation.contents).toEqual(""); expect(annotation._contents).toEqual({ str: "", dir: "ltr" });
}); });
it("should set and get a valid modification date", function () { it("should set and get a valid modification date", function () {
@ -610,8 +610,8 @@ describe("annotation", function () {
); );
expect(data.inReplyTo).toEqual(annotationRef.toString()); expect(data.inReplyTo).toEqual(annotationRef.toString());
expect(data.replyType).toEqual("Group"); expect(data.replyType).toEqual("Group");
expect(data.title).toEqual("ParentTitle"); expect(data.titleObj).toEqual({ str: "ParentTitle", dir: "ltr" });
expect(data.contents).toEqual("ParentText"); expect(data.contentsObj).toEqual({ str: "ParentText", dir: "ltr" });
expect(data.creationDate).toEqual("D:20180423"); expect(data.creationDate).toEqual("D:20180423");
expect(data.modificationDate).toEqual("D:20190423"); expect(data.modificationDate).toEqual("D:20190423");
expect(data.color).toEqual(new Uint8ClampedArray([0, 0, 255])); expect(data.color).toEqual(new Uint8ClampedArray([0, 0, 255]));
@ -665,8 +665,8 @@ describe("annotation", function () {
); );
expect(data.inReplyTo).toEqual(annotationRef.toString()); expect(data.inReplyTo).toEqual(annotationRef.toString());
expect(data.replyType).toEqual("R"); expect(data.replyType).toEqual("R");
expect(data.title).toEqual("ReplyTitle"); expect(data.titleObj).toEqual({ str: "ReplyTitle", dir: "ltr" });
expect(data.contents).toEqual("ReplyText"); expect(data.contentsObj).toEqual({ str: "ReplyText", dir: "ltr" });
expect(data.creationDate).toEqual("D:20180523"); expect(data.creationDate).toEqual("D:20180523");
expect(data.modificationDate).toEqual("D:20190523"); expect(data.modificationDate).toEqual("D:20190523");
expect(data.color).toEqual(new Uint8ClampedArray([102, 102, 102])); expect(data.color).toEqual(new Uint8ClampedArray([102, 102, 102]));
@ -3621,8 +3621,8 @@ describe("annotation", function () {
pdfManagerMock, pdfManagerMock,
idFactoryMock idFactoryMock
); );
expect(data.title).toEqual("Correct Title"); expect(data.titleObj).toEqual({ str: "Correct Title", dir: "ltr" });
expect(data.contents).toEqual("Correct Text"); expect(data.contentsObj).toEqual({ str: "Correct Text", dir: "ltr" });
expect(data.modificationDate).toEqual("D:20190423"); expect(data.modificationDate).toEqual("D:20190423");
expect(data.color).toEqual(new Uint8ClampedArray([0, 0, 255])); expect(data.color).toEqual(new Uint8ClampedArray([0, 0, 255]));
} }