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,