Support rich content in markup annotation

- use the xfa parser but in the xhtml namespace.
This commit is contained in:
Calixte Denizet 2021-10-24 17:29:30 +02:00
parent 0e7614df7f
commit cf8dc750d6
14 changed files with 188 additions and 39 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &nbsp is unbreakable
// but in real life Acrobat can break strings on &nbsp.
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;
}

View File

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

View File

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

View File

@ -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 = [];

View File

@ -479,3 +479,4 @@
!pr12564.pdf
!pr12828.pdf
!secHandler.pdf
!rc_annotation.pdf

View File

@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/7024938/2102.12353.pdf

BIN
test/pdfs/rc_annotation.pdf Normal file

Binary file not shown.

View File

@ -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",

View File

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

View File

@ -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,