From 35c909843beeba5a60114a68e8c8ad6cb87888c1 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Thu, 4 Sep 2025 14:53:22 +0200 Subject: [PATCH] Add a new function renderRichText to be used in the annotation layer and which will be used in order to make the contents of the new popup used for comments. --- src/display/annotation_layer.js | 46 ++++++------------------ src/display/display_utils.js | 27 ++++++++++++++ src/pdf.js | 3 ++ test/unit/display_utils_spec.js | 64 +++++++++++++++++++++++++++++++++ test/unit/pdf_spec.js | 2 ++ web/pdfjs.js | 2 ++ 6 files changed, 108 insertions(+), 36 deletions(-) diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index c72625c7c..912349361 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -41,12 +41,12 @@ import { applyOpacity, changeLightness, PDFDateString, + renderRichText, setLayerDimensions, } from "./display_utils.js"; import { AnnotationStorage } from "./annotation_storage.js"; import { ColorConverters } from "../shared/scripting_utils.js"; import { DOMSVGFactory } from "./svg_factory.js"; -import { XfaLayer } from "./xfa_layer.js"; const DEFAULT_FONT_SIZE = 9; const GetElementsByNameSet = new WeakSet(); @@ -2491,18 +2491,15 @@ class PopupElement { header.append(modificationDate); } - const html = this.#html; - if (html) { - XfaLayer.render({ - xfaHtml: html, - intent: "richText", - div: popup, - }); - popup.lastChild.classList.add("richText", "popupContent"); - } else { - const contents = this._formatContents(this.#contentsObj); - popup.append(contents); - } + renderRichText( + { + html: this.#html || this.#contentsObj.str, + dir: this.#contentsObj.dir, + className: "popupContent", + }, + popup + ); + this.#container.append(popup); } @@ -2561,29 +2558,6 @@ class PopupElement { return popupContent; } - /** - * Format the contents of the popup by adding newlines where necessary. - * - * @private - * @param {Object} contentsObj - * @memberof PopupElement - * @returns {HTMLParagraphElement} - */ - _formatContents({ str, dir }) { - const p = document.createElement("p"); - p.classList.add("popupContent"); - p.dir = dir; - const lines = str.split(/(?:\r\n?|\n)/); - for (let i = 0, ii = lines.length; i < ii; ++i) { - const line = lines[i]; - p.append(document.createTextNode(line)); - if (i < ii - 1) { - p.append(document.createElement("br")); - } - } - return p; - } - #keyDown(event) { if (event.altKey || event.shiftKey || event.ctrlKey || event.metaKey) { return; diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 0cb65303f..097c83cfa 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -20,6 +20,7 @@ import { Util, warn, } from "../shared/util.js"; +import { XfaLayer } from "./xfa_layer.js"; const SVG_NS = "http://www.w3.org/2000/svg"; @@ -829,6 +830,31 @@ function applyOpacity(r, g, b, opacity) { return [r, g, b]; } +function renderRichText({ html, dir, className }, container) { + const fragment = document.createDocumentFragment(); + if (typeof html === "string") { + const p = document.createElement("p"); + p.dir = dir || "auto"; + const lines = html.split(/(?:\r\n?|\n)/); + for (let i = 0, ii = lines.length; i < ii; ++i) { + const line = lines[i]; + p.append(document.createTextNode(line)); + if (i < ii - 1) { + p.append(document.createElement("br")); + } + } + fragment.append(p); + } else { + XfaLayer.render({ + xfaHtml: html, + div: fragment, + intent: "richText", + }); + } + fragment.firstChild.classList.add("richText", className); + container.append(fragment); +} + export { applyOpacity, changeLightness, @@ -851,6 +877,7 @@ export { PDFDateString, PixelsPerInch, RenderingCancelledException, + renderRichText, setLayerDimensions, StatTimer, stopEvent, diff --git a/src/pdf.js b/src/pdf.js index 54bf98079..4c43c23f1 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -59,6 +59,7 @@ import { PDFDateString, PixelsPerInch, RenderingCancelledException, + renderRichText, setLayerDimensions, stopEvent, SupportedImageMimeTypes, @@ -132,6 +133,7 @@ globalThis.pdfjsLib = { PermissionFlag, PixelsPerInch, RenderingCancelledException, + renderRichText, ResponseException, setLayerDimensions, shadow, @@ -189,6 +191,7 @@ export { PermissionFlag, PixelsPerInch, RenderingCancelledException, + renderRichText, ResponseException, setLayerDimensions, shadow, diff --git a/test/unit/display_utils_spec.js b/test/unit/display_utils_spec.js index cb26189d0..8f8c3d7e0 100644 --- a/test/unit/display_utils_spec.js +++ b/test/unit/display_utils_spec.js @@ -21,6 +21,7 @@ import { getRGB, isValidFetchUrl, PDFDateString, + renderRichText, } from "../../src/display/display_utils.js"; import { isNodeJS, toBase64Util } from "../../src/shared/util.js"; @@ -342,4 +343,67 @@ describe("display_utils", function () { expect(applyOpacity(123, 45, 67, ctx.globalAlpha)).toEqual([r, g, b]); }); }); + + describe("renderRichText", function () { + // Unlike other tests we cannot simply compare the HTML-strings since + // Chrome and Firefox produce different results. Instead we compare sets + // containing the individual parts of the HTML-strings. + const splitParts = s => new Set(s.split(/[<>/ ]+/).filter(x => x)); + + it("should render plain text", function () { + if (isNodeJS) { + pending("DOM is not supported in Node.js."); + } + const container = document.createElement("div"); + renderRichText( + { + html: "Hello world!\nThis is a test.", + dir: "ltr", + className: "foo", + }, + container + ); + expect(splitParts(container.innerHTML)).toEqual( + splitParts( + '

Hello world!
This is a test.

' + ) + ); + }); + + it("should render XFA rich text", function () { + if (isNodeJS) { + pending("DOM is not supported in Node.js."); + } + const container = document.createElement("div"); + const xfaHtml = { + name: "div", + attributes: { style: { color: "red" } }, + children: [ + { + name: "p", + attributes: { style: { fontSize: "20px" } }, + children: [ + { + name: "span", + attributes: { style: { fontWeight: "bold" } }, + value: "Hello", + }, + { name: "#text", value: " world!" }, + ], + }, + ], + }; + renderRichText( + { html: xfaHtml, dir: "ltr", className: "foo" }, + container + ); + expect(splitParts(container.innerHTML)).toEqual( + splitParts( + '
' + + '

' + + 'Hello world!

' + ) + ); + }); + }); }); diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 8451ec7b3..0a897bd70 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -50,6 +50,7 @@ import { PDFDateString, PixelsPerInch, RenderingCancelledException, + renderRichText, setLayerDimensions, stopEvent, SupportedImageMimeTypes, @@ -116,6 +117,7 @@ const expectedAPI = Object.freeze({ PermissionFlag, PixelsPerInch, RenderingCancelledException, + renderRichText, ResponseException, setLayerDimensions, shadow, diff --git a/web/pdfjs.js b/web/pdfjs.js index c1d0aa59d..820c2960a 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -55,6 +55,7 @@ const { PermissionFlag, PixelsPerInch, RenderingCancelledException, + renderRichText, ResponseException, setLayerDimensions, shadow, @@ -112,6 +113,7 @@ export { PermissionFlag, PixelsPerInch, RenderingCancelledException, + renderRichText, ResponseException, setLayerDimensions, shadow,