diff --git a/src/core/annotation.js b/src/core/annotation.js
index e7e8c2939..b518486fd 100644
--- a/src/core/annotation.js
+++ b/src/core/annotation.js
@@ -55,6 +55,7 @@ import { ObjectLoader } from "./object_loader.js";
import { OperatorList } from "./operator_list.js";
import { StringStream } from "./stream.js";
import { writeDict } from "./writer.js";
+import { XFAFactory } from "./xfa/factory.js";
class AnnotationFactory {
/**
@@ -1098,6 +1099,10 @@ class MarkupAnnotation extends Annotation {
this.data.color = null;
}
}
+
+ if (dict.has("RC")) {
+ this.data.richText = XFAFactory.getRichTextAsHtml(dict.get("RC"));
+ }
}
/**
@@ -2545,6 +2550,10 @@ class PopupAnnotation extends Annotation {
this.setContents(parentItem.get("Contents"));
this.data.contentsObj = this._contents;
+
+ if (parentItem.has("RC")) {
+ this.data.richText = XFAFactory.getRichTextAsHtml(parentItem.get("RC"));
+ }
}
}
diff --git a/src/core/xfa/builder.js b/src/core/xfa/builder.js
index 19bd1f21b..fd88684aa 100644
--- a/src/core/xfa/builder.js
+++ b/src/core/xfa/builder.js
@@ -66,7 +66,7 @@ class Empty extends XFAObject {
}
class Builder {
- constructor() {
+ constructor(rootNameSpace = null) {
this._namespaceStack = [];
this._nsAgnosticLevel = 0;
@@ -76,7 +76,8 @@ class Builder {
this._nextNsId = Math.max(
...Object.values(NamespaceIds).map(({ id }) => id)
);
- this._currentNamespace = new UnknownNamespace(++this._nextNsId);
+ this._currentNamespace =
+ rootNameSpace || new UnknownNamespace(++this._nextNsId);
}
buildRoot(ids) {
diff --git a/src/core/xfa/factory.js b/src/core/xfa/factory.js
index e21d07863..6547ab1d8 100644
--- a/src/core/xfa/factory.js
+++ b/src/core/xfa/factory.js
@@ -13,13 +13,20 @@
* limitations under the License.
*/
-import { $globalData, $toHTML } from "./xfa_object.js";
+import {
+ $appendChild,
+ $globalData,
+ $nodeName,
+ $text,
+ $toHTML,
+} from "./xfa_object.js";
import { Binder } from "./bind.js";
import { DataHandler } from "./data.js";
import { FontFinder } from "./fonts.js";
import { stripQuotes } from "./utils.js";
import { warn } from "../../shared/util.js";
import { XFAParser } from "./parser.js";
+import { XhtmlNamespace } from "./xhtml.js";
class XFAFactory {
constructor(data) {
@@ -106,6 +113,43 @@ class XFAFactory {
}
return Object.values(data).join("");
}
+
+ static getRichTextAsHtml(rc) {
+ if (!rc || typeof rc !== "string") {
+ return null;
+ }
+
+ try {
+ let root = new XFAParser(XhtmlNamespace, /* richText */ true).parse(rc);
+ if (!["body", "xhtml"].includes(root[$nodeName])) {
+ // No body, so create one.
+ const newRoot = XhtmlNamespace.body({});
+ newRoot[$appendChild](root);
+ root = newRoot;
+ }
+
+ const result = root[$toHTML]();
+ if (!result.success) {
+ return null;
+ }
+
+ const { html } = result;
+ const { attributes } = html;
+ if (attributes) {
+ if (attributes.class) {
+ attributes.class = attributes.class.filter(
+ attr => !attr.startsWith("xfa")
+ );
+ }
+ attributes.dir = "auto";
+ }
+
+ return { html, str: root[$text]() };
+ } catch (e) {
+ warn(`XFA - an error occurred during parsing of rich text: ${e}`);
+ }
+ return null;
+ }
}
export { XFAFactory };
diff --git a/src/core/xfa/html_utils.js b/src/core/xfa/html_utils.js
index 463611f9c..2f850ab37 100644
--- a/src/core/xfa/html_utils.js
+++ b/src/core/xfa/html_utils.js
@@ -606,10 +606,16 @@ function setPara(node, nodeStyle, value) {
}
function setFontFamily(xfaFont, node, fontFinder, style) {
- const name = stripQuotes(xfaFont.typeface);
- const typeface = fontFinder.find(name);
+ if (!fontFinder) {
+ // The font cannot be found in the pdf so use the default one.
+ delete style.fontFamily;
+ return;
+ }
+ const name = stripQuotes(xfaFont.typeface);
style.fontFamily = `"${name}"`;
+
+ const typeface = fontFinder.find(name);
if (typeface) {
const { fontFamily } = typeface.regular.cssFontInfo;
if (fontFamily !== name) {
diff --git a/src/core/xfa/parser.js b/src/core/xfa/parser.js
index 19a197772..f8c86e293 100644
--- a/src/core/xfa/parser.js
+++ b/src/core/xfa/parser.js
@@ -30,9 +30,9 @@ import { Builder } from "./builder.js";
import { warn } from "../../shared/util.js";
class XFAParser extends XMLParserBase {
- constructor() {
+ constructor(rootNameSpace = null, richText = false) {
super();
- this._builder = new Builder();
+ this._builder = new Builder(rootNameSpace);
this._stack = [];
this._globalData = {
usedTypefaces: new Set(),
@@ -42,6 +42,7 @@ class XFAParser extends XMLParserBase {
this._errorCode = XMLParserErrorCode.NoError;
this._whiteRegex = /^\s+$/;
this._nbsps = /\xa0+/g;
+ this._richText = richText;
}
parse(data) {
@@ -60,8 +61,8 @@ class XFAParser extends XMLParserBase {
// Normally by definition a   is unbreakable
// but in real life Acrobat can break strings on  .
text = text.replace(this._nbsps, match => match.slice(1) + " ");
- if (this._current[$acceptWhitespace]()) {
- this._current[$onText](text);
+ if (this._richText || this._current[$acceptWhitespace]()) {
+ this._current[$onText](text, this._richText);
return;
}
diff --git a/src/core/xfa/xhtml.js b/src/core/xfa/xhtml.js
index cdc6fef31..510747db4 100644
--- a/src/core/xfa/xhtml.js
+++ b/src/core/xfa/xhtml.js
@@ -20,6 +20,7 @@ import {
$content,
$extra,
$getChildren,
+ $getParent,
$globalData,
$nodeName,
$onText,
@@ -38,6 +39,7 @@ import {
import { getMeasurement, HTMLResult, stripQuotes } from "./utils.js";
const XHTML_NS_ID = NamespaceIds.xhtml.id;
+const $richText = Symbol();
const VALID_STYLES = new Set([
"color",
@@ -109,6 +111,7 @@ const StyleMapping = new Map([
const spacesRegExp = /\s+/g;
const crlfRegExp = /[\r\n]+/g;
+const crlfForRichTextRegExp = /\r\n?/g;
function mapStyle(styleStr, node) {
const style = Object.create(null);
@@ -185,6 +188,7 @@ const NoWhites = new Set(["body", "html"]);
class XhtmlObject extends XmlObject {
constructor(attributes, name) {
super(XHTML_NS_ID, name);
+ this[$richText] = false;
this.style = attributes.style || "";
}
@@ -197,11 +201,16 @@ class XhtmlObject extends XmlObject {
return !NoWhites.has(this[$nodeName]);
}
- [$onText](str) {
- str = str.replace(crlfRegExp, "");
- if (!this.style.includes("xfa-spacerun:yes")) {
- str = str.replace(spacesRegExp, " ");
+ [$onText](str, richText = false) {
+ if (!richText) {
+ str = str.replace(crlfRegExp, "");
+ if (!this.style.includes("xfa-spacerun:yes")) {
+ str = str.replace(spacesRegExp, " ");
+ }
+ } else {
+ this[$richText] = true;
}
+
if (str) {
this[$content] += str;
}
@@ -311,6 +320,15 @@ class XhtmlObject extends XmlObject {
return HTMLResult.EMPTY;
}
+ let value;
+ if (this[$richText]) {
+ value = this[$content]
+ ? this[$content].replace(crlfForRichTextRegExp, "\n")
+ : undefined;
+ } else {
+ value = this[$content] || undefined;
+ }
+
return HTMLResult.success({
name: this[$nodeName],
attributes: {
@@ -318,7 +336,7 @@ class XhtmlObject extends XmlObject {
style: mapStyle(this.style, this),
},
children,
- value: this[$content] || "",
+ value,
});
}
}
@@ -457,6 +475,10 @@ class P extends XhtmlObject {
}
[$text]() {
+ const siblings = this[$getParent]()[$getChildren]();
+ if (siblings[siblings.length - 1] === this) {
+ return super[$text]();
+ }
return super[$text]() + "\n";
}
}
diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js
index 05189bb2a..ecff7d96c 100644
--- a/src/display/annotation_layer.js
+++ b/src/display/annotation_layer.js
@@ -30,6 +30,7 @@ import {
} from "./display_utils.js";
import { AnnotationStorage } from "./annotation_storage.js";
import { ColorConverters } from "../shared/scripting_utils.js";
+import { XfaLayer } from "./xfa_layer.js";
const DEFAULT_TAB_INDEX = 1000;
const GetElementsByNameSet = new WeakSet();
@@ -322,6 +323,7 @@ class AnnotationElement {
titleObj: data.titleObj,
modificationDate: data.modificationDate,
contentsObj: data.contentsObj,
+ richText: data.richText,
hideWrapper: true,
});
const popup = popupElement.render();
@@ -676,7 +678,8 @@ class TextAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable });
}
@@ -1546,7 +1549,9 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
class PopupAnnotationElement extends AnnotationElement {
constructor(parameters) {
const isRenderable = !!(
- parameters.data.titleObj?.str || parameters.data.contentsObj?.str
+ parameters.data.titleObj?.str ||
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable });
}
@@ -1582,6 +1587,7 @@ class PopupAnnotationElement extends AnnotationElement {
titleObj: this.data.titleObj,
modificationDate: this.data.modificationDate,
contentsObj: this.data.contentsObj,
+ richText: this.data.richText,
});
// Position the popup next to the parent annotation's container.
@@ -1614,6 +1620,7 @@ class PopupElement {
this.titleObj = parameters.titleObj;
this.modificationDate = parameters.modificationDate;
this.contentsObj = parameters.contentsObj;
+ this.richText = parameters.richText;
this.hideWrapper = parameters.hideWrapper || false;
this.pinned = false;
@@ -1655,6 +1662,7 @@ class PopupElement {
const dateObject = PDFDateString.toDateObject(this.modificationDate);
if (dateObject) {
const modificationDate = document.createElement("span");
+ modificationDate.className = "popupDate";
modificationDate.textContent = "{{date}}, {{time}}";
modificationDate.dataset.l10nId = "annotation_date_string";
modificationDate.dataset.l10nArgs = JSON.stringify({
@@ -1664,8 +1672,20 @@ class PopupElement {
popup.appendChild(modificationDate);
}
- const contents = this._formatContents(this.contentsObj);
- popup.appendChild(contents);
+ if (
+ this.richText?.str &&
+ (!this.contentsObj?.str || this.contentsObj.str === this.richText.str)
+ ) {
+ XfaLayer.render({
+ xfa: this.richText.html,
+ intent: "richText",
+ div: popup,
+ });
+ popup.lastChild.className = "richText popupContent";
+ } else {
+ const contents = this._formatContents(this.contentsObj);
+ popup.appendChild(contents);
+ }
if (!Array.isArray(this.trigger)) {
this.trigger = [this.trigger];
@@ -1693,6 +1713,7 @@ class PopupElement {
*/
_formatContents({ str, dir }) {
const p = document.createElement("p");
+ p.className = "popupContent";
p.dir = dir;
const lines = str.split(/(?:\r\n?|\n)/);
for (let i = 0, ii = lines.length; i < ii; ++i) {
@@ -1759,7 +1780,8 @@ class FreeTextAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable, ignoreBorder: true });
}
@@ -1779,7 +1801,8 @@ class LineAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable, ignoreBorder: true });
}
@@ -1824,7 +1847,8 @@ class SquareAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable, ignoreBorder: true });
}
@@ -1871,7 +1895,8 @@ class CircleAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable, ignoreBorder: true });
}
@@ -1918,7 +1943,8 @@ class PolylineAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable, ignoreBorder: true });
@@ -1983,7 +2009,8 @@ class CaretAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable, ignoreBorder: true });
}
@@ -2003,7 +2030,8 @@ class InkAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable, ignoreBorder: true });
@@ -2062,7 +2090,8 @@ class HighlightAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, {
isRenderable,
@@ -2090,7 +2119,8 @@ class UnderlineAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, {
isRenderable,
@@ -2118,7 +2148,8 @@ class SquigglyAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, {
isRenderable,
@@ -2146,7 +2177,8 @@ class StrikeOutAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, {
isRenderable,
@@ -2174,7 +2206,8 @@ class StampAnnotationElement extends AnnotationElement {
const isRenderable = !!(
parameters.data.hasPopup ||
parameters.data.titleObj?.str ||
- parameters.data.contentsObj?.str
+ parameters.data.contentsObj?.str ||
+ parameters.data.richText?.str
);
super(parameters, { isRenderable, ignoreBorder: true });
}
@@ -2215,7 +2248,9 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
if (
!this.data.hasPopup &&
- (this.data.titleObj?.str || this.data.contentsObj?.str)
+ (this.data.titleObj?.str ||
+ this.data.contentsObj?.str ||
+ this.data.richText)
) {
this._createPopup(trigger, this.data);
}
diff --git a/src/display/xfa_layer.js b/src/display/xfa_layer.js
index 2844b5bff..a1417ebf8 100644
--- a/src/display/xfa_layer.js
+++ b/src/display/xfa_layer.js
@@ -106,7 +106,9 @@ class XfaLayer {
if (key === "textContent") {
html.textContent = value;
} else if (key === "class") {
- html.setAttribute(key, value.join(" "));
+ if (value.length) {
+ html.setAttribute(key, value.join(" "));
+ }
} else {
if (isHTMLAnchorElement && (key === "href" || key === "newWindow")) {
continue; // Handled below.
@@ -159,11 +161,16 @@ class XfaLayer {
const rootDiv = parameters.div;
rootDiv.appendChild(rootHtml);
- const transform = `matrix(${parameters.viewport.transform.join(",")})`;
- rootDiv.style.transform = transform;
+
+ if (parameters.viewport) {
+ const transform = `matrix(${parameters.viewport.transform.join(",")})`;
+ rootDiv.style.transform = transform;
+ }
// Set defaults.
- rootDiv.setAttribute("class", "xfaLayer xfaFont");
+ if (intent !== "richText") {
+ rootDiv.setAttribute("class", "xfaLayer xfaFont");
+ }
// Text nodes used for the text highlighter.
const textDivs = [];
diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore
index f819af613..75a725cb6 100644
--- a/test/pdfs/.gitignore
+++ b/test/pdfs/.gitignore
@@ -479,3 +479,4 @@
!pr12564.pdf
!pr12828.pdf
!secHandler.pdf
+!rc_annotation.pdf
diff --git a/test/pdfs/issue13915.pdf.link b/test/pdfs/issue13915.pdf.link
new file mode 100644
index 000000000..d518c5a1f
--- /dev/null
+++ b/test/pdfs/issue13915.pdf.link
@@ -0,0 +1 @@
+https://github.com/mozilla/pdf.js/files/7024938/2102.12353.pdf
diff --git a/test/pdfs/rc_annotation.pdf b/test/pdfs/rc_annotation.pdf
new file mode 100644
index 000000000..08fffb413
Binary files /dev/null and b/test/pdfs/rc_annotation.pdf differ
diff --git a/test/test_manifest.json b/test/test_manifest.json
index 2bdc9c9b2..acdcf6f84 100644
--- a/test/test_manifest.json
+++ b/test/test_manifest.json
@@ -137,6 +137,24 @@
"type": "eq",
"annotations": true
},
+ { "id": "issue2966",
+ "file": "pdfs/rc_annotation.pdf",
+ "md5": "7b978a8c2871b8902656adb67f7bd117",
+ "rounds": 1,
+ "lastPage": 1,
+ "type": "eq",
+ "annotations": true
+ },
+ { "id": "issue13915",
+ "file": "pdfs/issue13915.pdf",
+ "md5": "fef3108733bbf80ea8551feedb427b1c",
+ "rounds": 1,
+ "firstPage": 51,
+ "lastPage": 51,
+ "link": true,
+ "type": "eq",
+ "annotations": true
+ },
{ "id": "bug946506",
"file": "pdfs/bug946506.pdf",
"md5": "c28911b5c31bdc337c2ce404c5971cfc",
diff --git a/test/unit/xfa_parser_spec.js b/test/unit/xfa_parser_spec.js
index 3648ea2f4..5e5aa43e4 100644
--- a/test/unit/xfa_parser_spec.js
+++ b/test/unit/xfa_parser_spec.js
@@ -46,7 +46,7 @@ describe("XFAParser", function () {
forbidden
- enabled
+ enabled
http://d.e.f
@@ -414,7 +414,7 @@ describe("XFAParser", function () {
[
" The first line of this paragraph is indented a half-inch.\n",
" Successive lines are not indented.\n",
- " This is the last line of the paragraph.\n \n",
+ " This is the last line of the paragraph.\n ",
].join("")
);
});
diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css
index 2608291ff..6368d3381 100644
--- a/web/annotation_layer_builder.css
+++ b/web/annotation_layer_builder.css
@@ -192,17 +192,21 @@
display: inline-block;
}
-.annotationLayer .popup span {
+.annotationLayer .popupDate {
display: inline-block;
margin-left: 5px;
}
-.annotationLayer .popup p {
+.annotationLayer .popupContent {
border-top: 1px solid rgba(51, 51, 51, 1);
margin-top: 2px;
padding-top: 2px;
}
+.annotationLayer .richText > * {
+ white-space: pre-wrap;
+}
+
.annotationLayer .highlightAnnotation,
.annotationLayer .underlineAnnotation,
.annotationLayer .squigglyAnnotation,