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.
This commit is contained in:
Calixte Denizet 2025-09-04 14:53:22 +02:00
parent 7b87c220a5
commit 35c909843b
6 changed files with 108 additions and 36 deletions

View File

@ -41,12 +41,12 @@ import {
applyOpacity, applyOpacity,
changeLightness, changeLightness,
PDFDateString, PDFDateString,
renderRichText,
setLayerDimensions, setLayerDimensions,
} from "./display_utils.js"; } from "./display_utils.js";
import { AnnotationStorage } from "./annotation_storage.js"; import { AnnotationStorage } from "./annotation_storage.js";
import { ColorConverters } from "../shared/scripting_utils.js"; import { ColorConverters } from "../shared/scripting_utils.js";
import { DOMSVGFactory } from "./svg_factory.js"; import { DOMSVGFactory } from "./svg_factory.js";
import { XfaLayer } from "./xfa_layer.js";
const DEFAULT_FONT_SIZE = 9; const DEFAULT_FONT_SIZE = 9;
const GetElementsByNameSet = new WeakSet(); const GetElementsByNameSet = new WeakSet();
@ -2491,18 +2491,15 @@ class PopupElement {
header.append(modificationDate); header.append(modificationDate);
} }
const html = this.#html; renderRichText(
if (html) { {
XfaLayer.render({ html: this.#html || this.#contentsObj.str,
xfaHtml: html, dir: this.#contentsObj.dir,
intent: "richText", className: "popupContent",
div: popup, },
}); popup
popup.lastChild.classList.add("richText", "popupContent"); );
} else {
const contents = this._formatContents(this.#contentsObj);
popup.append(contents);
}
this.#container.append(popup); this.#container.append(popup);
} }
@ -2561,29 +2558,6 @@ class PopupElement {
return popupContent; return popupContent;
} }
/**
* Format the contents of the popup by adding newlines where necessary.
*
* @private
* @param {Object<string, string>} 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) { #keyDown(event) {
if (event.altKey || event.shiftKey || event.ctrlKey || event.metaKey) { if (event.altKey || event.shiftKey || event.ctrlKey || event.metaKey) {
return; return;

View File

@ -20,6 +20,7 @@ import {
Util, Util,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { XfaLayer } from "./xfa_layer.js";
const SVG_NS = "http://www.w3.org/2000/svg"; const SVG_NS = "http://www.w3.org/2000/svg";
@ -829,6 +830,31 @@ function applyOpacity(r, g, b, opacity) {
return [r, g, b]; 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 { export {
applyOpacity, applyOpacity,
changeLightness, changeLightness,
@ -851,6 +877,7 @@ export {
PDFDateString, PDFDateString,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
renderRichText,
setLayerDimensions, setLayerDimensions,
StatTimer, StatTimer,
stopEvent, stopEvent,

View File

@ -59,6 +59,7 @@ import {
PDFDateString, PDFDateString,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
renderRichText,
setLayerDimensions, setLayerDimensions,
stopEvent, stopEvent,
SupportedImageMimeTypes, SupportedImageMimeTypes,
@ -132,6 +133,7 @@ globalThis.pdfjsLib = {
PermissionFlag, PermissionFlag,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
renderRichText,
ResponseException, ResponseException,
setLayerDimensions, setLayerDimensions,
shadow, shadow,
@ -189,6 +191,7 @@ export {
PermissionFlag, PermissionFlag,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
renderRichText,
ResponseException, ResponseException,
setLayerDimensions, setLayerDimensions,
shadow, shadow,

View File

@ -21,6 +21,7 @@ import {
getRGB, getRGB,
isValidFetchUrl, isValidFetchUrl,
PDFDateString, PDFDateString,
renderRichText,
} from "../../src/display/display_utils.js"; } from "../../src/display/display_utils.js";
import { isNodeJS, toBase64Util } from "../../src/shared/util.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]); 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(
'<p dir="ltr" class="richText foo">Hello world!<br>This is a test.</p>'
)
);
});
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(
'<div style="color: red;" class="richText foo">' +
'<p style="font-size: 20px;">' +
'<span style="font-weight: bold;">Hello</span> world!</p></div>'
)
);
});
});
}); });

View File

@ -50,6 +50,7 @@ import {
PDFDateString, PDFDateString,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
renderRichText,
setLayerDimensions, setLayerDimensions,
stopEvent, stopEvent,
SupportedImageMimeTypes, SupportedImageMimeTypes,
@ -116,6 +117,7 @@ const expectedAPI = Object.freeze({
PermissionFlag, PermissionFlag,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
renderRichText,
ResponseException, ResponseException,
setLayerDimensions, setLayerDimensions,
shadow, shadow,

View File

@ -55,6 +55,7 @@ const {
PermissionFlag, PermissionFlag,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
renderRichText,
ResponseException, ResponseException,
setLayerDimensions, setLayerDimensions,
shadow, shadow,
@ -112,6 +113,7 @@ export {
PermissionFlag, PermissionFlag,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
renderRichText,
ResponseException, ResponseException,
setLayerDimensions, setLayerDimensions,
shadow, shadow,