Compare commits

...

16 Commits

Author SHA1 Message Date
Tim van der Meij
e6cb600896
Merge pull request #21320 from calixteman/issue7625
Substitute a system font when an embedded CFF is truncated
2026-05-24 19:00:50 +02:00
calixteman
adcde1175e Substitute a system font when an embedded CFF is truncated
It fixes #7625.

If the Top DICT's Private DICT extends past the end of the font data,
the Local Subrs INDEX is unreachable and every CharString that calls
a subr ends up as a blank glyph. Throw from parsePrivateDict so the
existing catch in translateFont triggers fallbackToSystemFont, then
run getFontSubstitution post-construction so we pick a close local
match instead of the generic fallbackName.
2026-05-24 18:10:09 +02:00
calixteman
143a7244a3
Merge pull request #21315 from calixteman/issue18548
Keep the first /Subrs and /CharStrings block
2026-05-24 18:07:20 +02:00
Tim van der Meij
13a61b1f72
Merge pull request #21319 from Snuffleupagus/XRefWrapper-fix
Fix the `XRefWrapper` implementation, in the `src/core/editor/pdf_editor.js` file
2026-05-24 15:06:22 +02:00
Tim van der Meij
941e17296e
Merge pull request #21313 from Snuffleupagus/Annotation-OC
Add support for Optional Content in the AnnotationLayer (issue 20433)
2026-05-24 15:02:06 +02:00
calixteman
1f8eed020f Keep the first /Subrs and /CharStrings block
Some Type1 fonts (the embedded Optima variants in orw1972.pdf) ship
two /Subrs and /CharStrings blocks wrapped in save/restore frames
gated on an Adobe hires/lores runtime switch.
In such cases, we just use the first /Subrs and /CharStrings block,
which is the one that is actually used by the font renderer in Acrobat.

It fixes #18548.
2026-05-24 15:01:22 +02:00
Tim van der Meij
bbfbe5159c
Merge pull request #21324 from Snuffleupagus/scaleCharBBox
Simplify how the character BBox is scaled in `src/display/canvas_dependency_tracker.js`
2026-05-24 14:41:34 +02:00
Tim van der Meij
4daca805d6
Merge pull request #21323 from Snuffleupagus/integration-test-more-fromBase64
Use `Uint8Array.fromBase64` in the `test/integration/highlight_editor_spec.mjs` file
2026-05-24 14:37:27 +02:00
Tim van der Meij
d37e0b40a5
Merge pull request #21317 from Snuffleupagus/XfaLayerBuilder-render-shorten
Shorten the `XfaLayerBuilder.prototype.render` method
2026-05-24 14:36:33 +02:00
Tim van der Meij
46b16bd42e
Merge pull request #21322 from Snuffleupagus/fontFile-lookup-shorten
Shorten the `fontFile` lookup a tiny bit
2026-05-24 14:33:25 +02:00
Jonas Jenwald
59086fa582 Simplify how the character BBox is scaled in src/display/canvas_dependency_tracker.js
In many/most PDF documents every glyph will require that the character BBox has scaling/offset applied, which can be made a tiny bit more efficient. In particular:
 - Avoid creating one additional temporary Array for every glyph.
 - Simplify the helper function, since there's no skew-components.
2026-05-24 12:32:19 +02:00
Jonas Jenwald
057507f6ce Use Uint8Array.fromBase64 in the test/integration/highlight_editor_spec.mjs file 2026-05-24 10:56:51 +02:00
Jonas Jenwald
31c6561b91 Shorten the fontFile lookup a tiny bit
Rather than effectively duplicating code, we can use a loop instead.
2026-05-24 10:19:34 +02:00
Jonas Jenwald
05de3c8a88 Fix the XRefWrapper implementation, in the src/core/editor/pdf_editor.js file
When comparing this code with the full `XRef` class it doesn't seem to be entirely correctly implemented, since the `fetch` method is basically doing what the `fetchIfRef` method is intended to do.
2026-05-23 22:40:14 +02:00
Jonas Jenwald
bd14524536 Shorten the XfaLayerBuilder.prototype.render method
Given that the "print" intent is handled separately, there's currently a little bit of unnecessary code duplication in this method.
2026-05-23 16:33:23 +02:00
Jonas Jenwald
fb9758303b Add support for Optional Content in the AnnotationLayer (issue 20433) 2026-05-23 12:33:56 +02:00
19 changed files with 359 additions and 243 deletions

View File

@ -76,6 +76,7 @@ import { FileSpec } from "./file_spec.js";
import { JpegStream } from "./jpeg_stream.js";
import { ObjectLoader } from "./object_loader.js";
import { OperatorList } from "./operator_list.js";
import { parseMarkedContentProps } from "./evaluator_utils.js";
import { XFAFactory } from "./xfa/factory.js";
class AnnotationFactory {
@ -663,6 +664,8 @@ function getTransformMatrix(rect, bbox, matrix) {
}
class Annotation {
_oc = undefined;
constructor(params) {
const { annotationGlobals, dict, orphanFields, ref, subtype, xref } =
params;
@ -679,7 +682,7 @@ class Annotation {
this.setColor(dict.getArray("C"));
this.setBorderStyle(dict);
this.setAppearance(dict);
this.setOptionalContent(dict);
this.#setOptionalContent(xref, dict);
const MK = dict.get("MK");
this.setBorderAndBackgroundColors(MK);
@ -710,6 +713,7 @@ class Annotation {
hasAppearance: !!this.appearance,
id: params.id,
modificationDate: this.modificationDate,
oc: this._oc,
rect: this.rectangle,
subtype,
hasOwnCanvas: false,
@ -1169,14 +1173,17 @@ class Annotation {
}
}
setOptionalContent(dict) {
this.oc = null;
const oc = dict.get("OC");
if (oc instanceof Name) {
warn("setOptionalContent: Support for /Name-entry is not implemented.");
} else if (oc instanceof Dict) {
this.oc = oc;
#setOptionalContent(xref, dict) {
if (dict.has("OC")) {
try {
this._oc = parseMarkedContentProps(
xref,
dict.get("OC"),
/* resources = */ null
);
} catch (ex) {
warn(`#setOptionalContent: ${ex}`);
}
}
}
@ -1229,13 +1236,7 @@ class Annotation {
const opList = new OperatorList();
let optionalContent;
if (this.oc) {
optionalContent = await evaluator.parseMarkedContentProps(
this.oc,
/* resources = */ null
);
}
const optionalContent = this._oc;
if (optionalContent !== undefined) {
opList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
}
@ -2110,13 +2111,7 @@ class WidgetAnnotation extends Annotation {
const bbox = [0, 0, this.width, this.height];
const transform = getTransformMatrix(this.data.rect, bbox, matrix);
let optionalContent;
if (this.oc) {
optionalContent = await evaluator.parseMarkedContentProps(
this.oc,
/* resources = */ null
);
}
const optionalContent = this._oc;
if (optionalContent !== undefined) {
opList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
}

View File

@ -787,6 +787,12 @@ class CFFParser {
this.emptyPrivateDictionary(parentDict);
return;
}
// The Private DICT extends past the end of the font data, which means
// the embedded font is truncated; abort so the caller can substitute a
// system font instead of rendering blank glyphs (issue 7625).
if (offset + size > this.bytes.length) {
throw new FormatError("CFF Private DICT extends past end of font");
}
const privateDictEnd = offset + size;
const dictData = this.bytes.subarray(offset, privateDictEnd);

View File

@ -87,25 +87,28 @@ class XRefWrapper {
this._getNewRef = getNewRef;
}
fetch(ref) {
return ref instanceof Ref ? this.entries[ref.num] : ref;
}
fetchIfRefAsync(ref) {
return Promise.resolve(this.fetch(ref));
}
fetchIfRef(ref) {
return this.fetch(ref);
}
fetchAsync(ref) {
return Promise.resolve(this.fetch(ref));
}
getNewTemporaryRef() {
return this._getNewRef();
}
fetchIfRef(obj) {
return obj instanceof Ref ? this.fetch(obj) : obj;
}
fetch(ref) {
if (!(ref instanceof Ref)) {
throw new Error("ref object is not a reference");
}
return this.entries[ref.num];
}
async fetchIfRefAsync(obj) {
return obj instanceof Ref ? this.fetchAsync(obj) : obj;
}
async fetchAsync(ref) {
return this.fetch(ref);
}
}
class PDFEditor {
@ -193,8 +196,7 @@ class PDFEditor {
* @returns {Ref}
*/
get newRef() {
const ref = Ref.get(this.newRefCount++, 0);
return ref;
return Ref.get(this.newRefCount++, 0);
}
/**
@ -518,9 +520,9 @@ class PDFEditor {
attributes = [attributes];
}
for (let attr of attributes) {
attr = this.xrefWrapper.fetch(attr);
attr = this.xrefWrapper.fetchIfRef(attr);
if (isName(attr.get("O"), "Table") && attr.has("Headers")) {
const headers = this.xrefWrapper.fetch(attr.getRaw("Headers"));
const headers = this.xrefWrapper.fetchIfRef(attr.getRaw("Headers"));
if (Array.isArray(headers)) {
for (let i = 0, ii = headers.length; i < ii; i++) {
const newId = dedupIDs.get(

View File

@ -87,6 +87,7 @@ import { getGlyphsUnicode } from "./glyphlist.js";
import { getMetrics } from "./metrics.js";
import { getUnicodeForGlyph } from "./unicode.js";
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
import { parseMarkedContentProps } from "./evaluator_utils.js";
import { PDFImage } from "./image.js";
import { Stream } from "./stream.js";
import { stringToPDFString } from "./string_utils.js";
@ -1651,105 +1652,8 @@ class PartialEvaluator {
throw new FormatError(`Unknown PatternName: ${patternName}`);
}
_parseVisibilityExpression(array, nestingCounter, currentResult) {
const MAX_NESTING = 10;
if (++nestingCounter > MAX_NESTING) {
warn("Visibility expression is too deeply nested");
return;
}
const length = array.length;
const operator = this.xref.fetchIfRef(array[0]);
if (length < 2 || !(operator instanceof Name)) {
warn("Invalid visibility expression");
return;
}
switch (operator.name) {
case "And":
case "Or":
case "Not":
currentResult.push(operator.name);
break;
default:
warn(`Invalid operator ${operator.name} in visibility expression`);
return;
}
for (let i = 1; i < length; i++) {
const raw = array[i];
const object = this.xref.fetchIfRef(raw);
if (Array.isArray(object)) {
const nestedResult = [];
currentResult.push(nestedResult);
// Recursively parse a subarray.
this._parseVisibilityExpression(object, nestingCounter, nestedResult);
} else if (raw instanceof Ref) {
// Reference to an OCG dictionary.
currentResult.push(raw.toString());
}
}
}
async parseMarkedContentProps(contentProperties, resources) {
let optionalContent;
if (contentProperties instanceof Name) {
const properties = resources.get("Properties");
optionalContent = properties.get(contentProperties.name);
} else if (contentProperties instanceof Dict) {
optionalContent = contentProperties;
} else {
throw new FormatError("Optional content properties malformed.");
}
const optionalContentType = optionalContent.get("Type")?.name;
if (optionalContentType === "OCG") {
return {
type: optionalContentType,
id: optionalContent.objId,
};
} else if (optionalContentType === "OCMD") {
const expression = optionalContent.get("VE");
if (Array.isArray(expression)) {
const result = [];
this._parseVisibilityExpression(expression, 0, result);
if (result.length > 0) {
return {
type: "OCMD",
expression: result,
};
}
}
const optionalContentGroups = optionalContent.get("OCGs");
if (
Array.isArray(optionalContentGroups) ||
optionalContentGroups instanceof Dict
) {
const groupIds = [];
if (Array.isArray(optionalContentGroups)) {
for (const ocg of optionalContentGroups) {
groupIds.push(ocg.toString());
}
} else {
// Dictionary, just use the obj id.
groupIds.push(optionalContentGroups.objId);
}
return {
type: optionalContentType,
ids: groupIds,
policy:
optionalContent.get("P") instanceof Name
? optionalContent.get("P").name
: null,
expression: null,
};
} else if (optionalContentGroups instanceof Ref) {
return {
type: optionalContentType,
id: optionalContentGroups.toString(),
};
}
}
return null;
return parseMarkedContentProps(this.xref, contentProperties, resources);
}
async getOperatorList({
@ -4666,18 +4570,11 @@ class PartialEvaluator {
let fontFile, fontFileN, subtype, length1, length2, length3;
try {
fontFile = descriptor.get("FontFile");
if (fontFile) {
fontFileN = 1;
} else {
fontFile = descriptor.get("FontFile2");
for (const n of ["FontFile", "FontFile2", "FontFile3"]) {
fontFile = descriptor.get(n);
if (fontFile) {
fontFileN = 2;
} else {
fontFile = descriptor.get("FontFile3");
if (fontFile) {
fontFileN = 3;
}
fontFileN = n;
break;
}
}
@ -4831,7 +4728,35 @@ class PartialEvaluator {
const newProperties = await this.extractDataStructures(dict, properties);
this.extractWidths(dict, descriptor, newProperties);
return new Font(fontName.name, fontFile, newProperties, this.options);
const font = new Font(fontName.name, fontFile, newProperties, this.options);
// The embedded font may have been too corrupt to parse, in which case
// we ended up in the fallback path without a substitution selected.
// Try the substitution map now so text renders in a font close to what
// the document asked for (issue 7625).
if (
font.missingFile &&
!font.systemFontInfo &&
!isType3Font &&
this.options.useSystemFonts
) {
const standardFontName = getStandardFontName(fontName.name);
const substitution = getFontSubstitution(
this.systemFontCache,
this.idFactory,
this.options.standardFontDataUrl,
fontName.name,
standardFontName,
type
);
if (substitution) {
if (substitution.guessFallback) {
substitution.guessFallback = false;
substitution.css += `,${font.fallbackName}`;
}
font.systemFontInfo = substitution;
}
}
return font;
}
static buildFontPaths(font, glyphs, handler, evaluatorOptions) {

123
src/core/evaluator_utils.js Normal file
View File

@ -0,0 +1,123 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Dict, Name, Ref } from "./primitives.js";
import { FormatError, warn } from "../shared/util.js";
function _parseVisibilityExpression(
xref,
array,
nestingCounter,
currentResult
) {
const MAX_NESTING = 10;
if (++nestingCounter > MAX_NESTING) {
warn("Visibility expression is too deeply nested");
return;
}
const length = array.length;
const operator = xref.fetchIfRef(array[0]);
if (length < 2 || !(operator instanceof Name)) {
warn("Invalid visibility expression");
return;
}
switch (operator.name) {
case "And":
case "Or":
case "Not":
currentResult.push(operator.name);
break;
default:
warn(`Invalid operator ${operator.name} in visibility expression`);
return;
}
for (let i = 1; i < length; i++) {
const raw = array[i];
const object = xref.fetchIfRef(raw);
if (Array.isArray(object)) {
const nestedResult = [];
currentResult.push(nestedResult);
// Recursively parse a subarray.
_parseVisibilityExpression(xref, object, nestingCounter, nestedResult);
} else if (raw instanceof Ref) {
// Reference to an OCG dictionary.
currentResult.push(raw.toString());
}
}
}
function parseMarkedContentProps(xref, contentProperties, resources) {
let optionalContent;
if (contentProperties instanceof Name) {
const properties = resources.get("Properties");
optionalContent = properties.get(contentProperties.name);
} else if (contentProperties instanceof Dict) {
optionalContent = contentProperties;
} else {
throw new FormatError("Optional content properties malformed.");
}
const optionalContentType = optionalContent.get("Type")?.name;
if (optionalContentType === "OCG") {
return {
type: optionalContentType,
id: optionalContent.objId,
};
} else if (optionalContentType === "OCMD") {
const expression = optionalContent.get("VE");
if (Array.isArray(expression)) {
const result = [];
_parseVisibilityExpression(xref, expression, 0, result);
if (result.length > 0) {
return {
type: "OCMD",
expression: result,
};
}
}
const optionalContentGroups = optionalContent.get("OCGs");
if (
Array.isArray(optionalContentGroups) ||
optionalContentGroups instanceof Dict
) {
const groupIds = [];
if (Array.isArray(optionalContentGroups)) {
for (const ocg of optionalContentGroups) {
groupIds.push(ocg.toString());
}
} else {
// Dictionary, just use the obj id.
groupIds.push(optionalContentGroups.objId);
}
const p = optionalContent.get("P");
return {
type: optionalContentType,
ids: groupIds,
policy: p instanceof Name ? p.name : null,
expression: null,
};
} else if (optionalContentGroups instanceof Ref) {
return {
type: optionalContentType,
id: optionalContentGroups.toString(),
};
}
}
return null;
}
export { parseMarkedContentProps };

View File

@ -2698,7 +2698,7 @@ class Font {
if (
(header.version === "OTTO" &&
(!properties.composite ||
(properties.fontFileN === 3 && parsedCff?.isCIDFont))) ||
(properties.fontFileN === "FontFile3" && parsedCff?.isCIDFont))) ||
!tables.head ||
!tables.hhea ||
!tables.maxp ||

View File

@ -566,6 +566,13 @@ class Type1Parser {
},
};
let token, length, data, lenIV;
// Some fonts (e.g. those embedded in issue18548.pdf) define a second
// `/Subrs` and `/CharStrings` block that the PostScript runtime selects
// conditionally (e.g. high-resolution variants). Testing with other
// viewers shows that none of them actually use these conditional blocks,
// so we can "safely" ignore them.
let subrsParsed = false;
let charStringsParsed = false;
while ((token = this.getToken()) !== null) {
if (token !== "/") {
continue;
@ -573,6 +580,10 @@ class Type1Parser {
token = this.getToken();
switch (token) {
case "CharStrings":
if (charStringsParsed) {
break;
}
charStringsParsed = true;
// The number immediately following CharStrings must be greater or
// equal to the number of CharStrings.
this.getToken();
@ -610,6 +621,10 @@ class Type1Parser {
}
break;
case "Subrs":
if (subrsParsed) {
break;
}
subrsParsed = true;
this.readInt(); // num
this.getToken(); // read in 'array'
while (this.getToken() === "dup") {

View File

@ -16,6 +16,8 @@
/** @typedef {import("./api").PDFPageProxy} PDFPageProxy */
/** @typedef {import("./page_viewport").PageViewport} PageViewport */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
// eslint-disable-next-line max-len
/** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
@ -886,6 +888,18 @@ class AnnotationElement {
});
}
updateOC(optionalContentConfig) {
if (!this.data.oc || !optionalContentConfig) {
return;
}
const isVisible = optionalContentConfig.isVisible(this.data.oc);
if (isVisible) {
this.show();
} else {
this.hide();
}
}
get width() {
return this.data.rect[2] - this.data.rect[0];
}
@ -3755,7 +3769,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
* @property {TextAccessibilityManager} [accessibilityManager]
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]
* @property {StructTreeLayerBuilder} [structTreeLayer]
* @property {CommentManager} [commentManager] - The comment manager instance.
* @property {OptionalContentConfig} [optionalContentConfig]
*/
/**
@ -3827,7 +3841,7 @@ class AnnotationLayer {
* @memberof AnnotationLayer
*/
async render(params) {
const { annotations } = params;
const { annotations, optionalContentConfig } = params;
const layer = this.div;
setLayerDimensions(layer, this.viewport);
@ -3892,6 +3906,7 @@ class AnnotationLayer {
if (data.hidden) {
rendered.style.visibility = "hidden";
}
element.updateOC(optionalContentConfig);
if (element._isEditable) {
this.#editableAnnotations.set(element.data.id, element);
@ -4052,11 +4067,14 @@ class AnnotationLayer {
* @param {AnnotationLayerParameters} viewport
* @memberof AnnotationLayer
*/
update({ viewport }) {
update({ viewport, optionalContentConfig }) {
const layer = this.div;
this.viewport = viewport;
setLayerDimensions(layer, { rotation: viewport.rotation });
for (const element of this.#elements) {
element.updateOC(optionalContentConfig);
}
this.#setAnnotationCanvasMap();
layer.hidden = false;
}

View File

@ -28,54 +28,32 @@ function expandBBox(array, index, minX, minY, maxX, maxY) {
array[index * 4 + 3] = Math.max(array[index * 4 + 3], maxY);
}
// Apply a scaling matrix to some min/max values.
// If a scaling factor is negative then min and max must be swapped.
function scaleMinMax(transform, minMax) {
function scaleCharBBox(scaleX, scaleY, x, y, bbox) {
let temp;
if (transform[0]) {
if (transform[0] < 0) {
temp = minMax[0];
minMax[0] = minMax[2];
minMax[2] = temp;
if (scaleX) {
if (scaleX < 0) {
temp = bbox[0];
bbox[0] = bbox[2];
bbox[2] = temp;
}
minMax[0] *= transform[0];
minMax[2] *= transform[0];
bbox[0] *= scaleX;
bbox[2] *= scaleX;
if (transform[3] < 0) {
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
if (scaleY < 0) {
temp = bbox[1];
bbox[1] = bbox[3];
bbox[3] = temp;
}
minMax[1] *= transform[3];
minMax[3] *= transform[3];
bbox[1] *= scaleY;
bbox[3] *= scaleY;
} else {
temp = minMax[0];
minMax[0] = minMax[1];
minMax[1] = temp;
temp = minMax[2];
minMax[2] = minMax[3];
minMax[3] = temp;
if (transform[1] < 0) {
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
}
minMax[1] *= transform[1];
minMax[3] *= transform[1];
if (transform[2] < 0) {
temp = minMax[0];
minMax[0] = minMax[2];
minMax[2] = temp;
}
minMax[0] *= transform[2];
minMax[2] *= transform[2];
bbox.fill(0);
}
minMax[0] += transform[4];
minMax[1] += transform[5];
minMax[2] += transform[4];
minMax[3] += transform[5];
bbox[0] += x;
bbox[1] += y;
bbox[2] += x;
bbox[3] += y;
}
// This is computed rathter than hard-coded to keep into
@ -663,7 +641,7 @@ class CanvasDependencyTracker {
computedBBox = [0, 0, 0, 0];
Util.axialAlignedBoundingBox(fontBBox, font.fontMatrix, computedBBox);
if (scale !== 1 || x !== 0 || y !== 0) {
scaleMinMax([scale, 0, 0, -scale, x, y], computedBBox);
scaleCharBBox(scale, -scale, x, y, computedBBox);
}
if (isBBoxTrustworthy) {

View File

@ -242,7 +242,8 @@ class Rasterize {
fieldObjects,
page,
imageResourcesPath,
renderForms = false
renderForms = false,
optionalContentConfigPromise = null
) {
try {
const { svg, foreignObject, style, div } = this.createContainer(viewport);
@ -263,6 +264,7 @@ class Rasterize {
imageResourcesPath,
renderForms,
fieldObjects,
optionalContentConfig: await optionalContentConfigPromise,
};
// Ensure that the annotationLayer gets translated.
@ -1355,7 +1357,8 @@ class Driver {
task.fieldObjects,
page,
IMAGE_RESOURCES_PATH,
renderForms
renderForms,
task.optionalContentConfigPromise
).then(() => {
completeRender(false);
});

View File

@ -2271,9 +2271,7 @@ describe("Highlight Editor", () => {
const pdfData = fs.readFileSync(pdfPath).toString("base64");
const dataTransfer = await page.evaluateHandle(data => {
const transfer = new DataTransfer();
const view = Uint8Array.from(atob(data), code =>
code.charCodeAt(0)
);
const view = Uint8Array.fromBase64(data);
const file = new File([view], "basicapi.pdf", {
type: "application/pdf",
});

View File

@ -922,3 +922,4 @@
!knockout_groups_test.pdf
!issue18032.pdf
!Embedded_font.pdf
!issue18548_reduced.pdf

Binary file not shown.

View File

@ -0,0 +1 @@
https://web.archive.org/web/20251107005559/https://octopdf.com/octopdf-sample.pdf

View File

@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/467169/Er.aestetik.en.loftestang.for.laering.pdf

View File

@ -1956,6 +1956,42 @@
"rounds": 1,
"type": "eq"
},
{
"id": "issue20433-initial",
"file": "pdfs/issue20433.pdf",
"md5": "3a550da7807540982ed457397667db79",
"link": true,
"rounds": 1,
"firstPage": 2,
"type": "eq",
"forms": true
},
{
"id": "issue20433-no-form",
"file": "pdfs/issue20433.pdf",
"md5": "3a550da7807540982ed457397667db79",
"link": true,
"rounds": 1,
"firstPage": 2,
"type": "eq",
"forms": true,
"optionalContent": {
"73R": false
}
},
{
"id": "issue20433-no-mathml",
"file": "pdfs/issue20433.pdf",
"md5": "3a550da7807540982ed457397667db79",
"link": true,
"rounds": 1,
"firstPage": 2,
"type": "eq",
"forms": true,
"optionalContent": {
"115R": false
}
},
{
"id": "issue13845",
"file": "pdfs/issue13845.pdf",
@ -14267,5 +14303,22 @@
"md5": "b68dd5a3e6833d1af94e295fe1d60285",
"rounds": 1,
"type": "eq"
},
{
"id": "issue18548_reduced",
"file": "pdfs/issue18548_reduced.pdf",
"md5": "39d15f7f810bd89a4e5858df9c75ca4e",
"rounds": 1,
"type": "eq"
},
{
"id": "issue7625",
"file": "pdfs/issue7625.pdf",
"md5": "77ca0a41da767dca31ca45219e2ae202",
"link": true,
"rounds": 1,
"firstPage": 1,
"lastPage": 1,
"type": "eq"
}
]

View File

@ -63,6 +63,7 @@ import { PresentationModeState } from "./ui_utils.js";
* @property {PageViewport} viewport
* @property {string} [intent] - The default value is "display".
* @property {StructTreeLayerBuilder} [structTreeLayer]
* @property {Promise} [optionalContentConfigPromise]
*/
class AnnotationLayerBuilder {
@ -125,8 +126,15 @@ class AnnotationLayerBuilder {
* @returns {Promise<void>} A promise that is resolved when rendering of the
* annotations is complete.
*/
async render({ viewport, intent = "display", structTreeLayer = null }) {
async render({
viewport,
intent = "display",
structTreeLayer = null,
optionalContentConfigPromise = null,
}) {
if (this.div) {
const optionalContentConfig = await optionalContentConfigPromise;
if (this._cancelled || !this.annotationLayer) {
return;
}
@ -134,15 +142,18 @@ class AnnotationLayerBuilder {
// transformation matrices.
this.annotationLayer.update({
viewport: viewport.clone({ dontFlip: true }),
optionalContentConfig,
});
return;
}
const [annotations, hasJSActions, fieldObjects] = await Promise.all([
this.pdfPage.getAnnotations({ intent }),
this._hasJSActionsPromise,
this._fieldObjectsPromise,
]);
const [annotations, hasJSActions, fieldObjects, optionalContentConfig] =
await Promise.all([
this.pdfPage.getAnnotations({ intent }),
this._hasJSActionsPromise,
this._fieldObjectsPromise,
optionalContentConfigPromise,
]);
if (this._cancelled) {
return;
}
@ -169,6 +180,7 @@ class AnnotationLayerBuilder {
enableScripting: this.enableScripting,
hasJSActions,
fieldObjects,
optionalContentConfig,
});
this.#annotations = annotations;

View File

@ -464,6 +464,7 @@ class PDFPageView extends BasePDFPageView {
viewport: this.viewport,
intent: "display",
structTreeLayer: this.structTreeLayer,
optionalContentConfigPromise: this._optionalContentConfigPromise,
});
} catch (ex) {
console.error("#renderAnnotationLayer:", ex);

View File

@ -37,6 +37,10 @@ import { XfaLayer } from "pdfjs-lib";
*/
class XfaLayerBuilder {
#cancelled = false;
div = null;
/**
* @param {XfaLayerBuilderOptions} options
*/
@ -50,9 +54,6 @@ class XfaLayerBuilder {
this.annotationStorage = annotationStorage;
this.linkService = linkService;
this.xfaHtml = xfaHtml;
this.div = null;
this._cancelled = false;
}
/**
@ -62,50 +63,33 @@ class XfaLayerBuilder {
* with a `textDivs` property that can be used with the TextHighlighter.
*/
async render({ viewport, intent = "display" }) {
let xfaHtml;
if (intent === "print") {
const parameters = {
viewport: viewport.clone({ dontFlip: true }),
div: this.div,
xfaHtml: this.xfaHtml,
annotationStorage: this.annotationStorage,
linkService: this.linkService,
intent,
};
xfaHtml = this.xfaHtml;
} else {
xfaHtml = await this.pdfPage.getXfa();
// Create an xfa layer div and render the form
this.div = document.createElement("div");
parameters.div = this.div;
return XfaLayer.render(parameters);
if (this.#cancelled || !xfaHtml) {
return { textDivs: [] };
}
}
// intent === "display"
const xfaHtml = await this.pdfPage.getXfa();
if (this._cancelled || !xfaHtml) {
return { textDivs: [] };
}
const parameters = {
// Create an xfa layer div and render the form
const hasDiv = !!this.div;
const params = {
viewport: viewport.clone({ dontFlip: true }),
div: this.div,
div: (this.div ??= document.createElement("div")),
xfaHtml,
annotationStorage: this.annotationStorage,
linkService: this.linkService,
intent,
};
if (this.div) {
return XfaLayer.update(parameters);
}
// Create an xfa layer div and render the form
this.div = document.createElement("div");
parameters.div = this.div;
return XfaLayer.render(parameters);
return hasDiv ? XfaLayer.update(params) : XfaLayer.render(params);
}
cancel() {
this._cancelled = true;
this.#cancelled = true;
}
hide() {