Compare commits

..

No commits in common. "1ddf6449ac4e5c8157249dd0cd584112104fdd60" and "86a18bd5fec4261f28485f3ac854f118d04a5671" have entirely different histories.

54 changed files with 418 additions and 1552 deletions

View File

@ -24,13 +24,13 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
with:
languages: ${{ matrix.language }}
queries: security-and-quality
- name: Autobuild CodeQL
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/autobuild@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1

View File

@ -1,13 +1,9 @@
{
"chrome": {
"skipDownload": true,
"version": "stable"
},
"chrome-headless-shell": {
"skipDownload": true
"skipDownload": false
},
"firefox": {
"skipDownload": true,
"skipDownload": false,
"version": "nightly"
}
}

View File

@ -1030,7 +1030,10 @@ function createBuildNumber(done) {
const version = config.versionPrefix + buildNumber;
exec('git log --format="%h" -n 1', function (err2, stdout2, stderr2) {
const buildCommit = !err2 ? stdout2.replace("\n", "") : "";
let buildCommit = "";
if (!err2) {
buildCommit = stdout2.replace("\n", "");
}
createStringSource(
"version.json",

View File

@ -665,7 +665,7 @@ pdfjs-views-manager-pages-status-action-button-label = 관리
pdfjs-views-manager-pages-status-copy-button-label = 복사
pdfjs-views-manager-pages-status-cut-button-label = 잘라내기
pdfjs-views-manager-pages-status-delete-button-label = 삭제
pdfjs-views-manager-pages-status-export-selected-button-label = 선택 페이지 내보내기…
pdfjs-views-manager-pages-status-export-selected-button-label = 선택 페이지 내보내기…
# Variables:
# $count (Number) - the number of selected pages to be cut.
pdfjs-views-manager-status-undo-cut-label = { $count }개 페이지 잘림

View File

@ -479,21 +479,6 @@ pdfjs-editor-add-signature-cancel-button = റദ്ദാക്കുക
pdfjs-editor-add-signature-add-button = ചേൎക്കുക
pdfjs-editor-edit-signature-update-button = പുതുക്കുക
## Edit a comment dialog
pdfjs-editor-edit-comment-dialog-cancel-button = റദ്ദാക്കുക
## The view manager is a sidebar displaying different views:
## - thumbnails;
## - outline;
## - attachments;
## - layers.
## The thumbnails view is used to edit the pdf: remove/insert pages, ...
pdfjs-views-manager-status-close-button =
.title = അടയ്ക്കുക
pdfjs-views-manager-status-close-button-label = അടയ്ക്കുക
## Main menu for adding/removing signatures
pdfjs-editor-delete-signature-button1 =

73
package-lock.json generated
View File

@ -32,7 +32,7 @@
"eslint-plugin-perfectionist": "^5.9.0",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-regexp": "^3.1.0",
"eslint-plugin-unicorn": "^66.0.0",
"eslint-plugin-unicorn": "^65.0.1",
"globals": "^17.6.0",
"gulp": "^5.0.1",
"gulp-cli": "^3.1.0",
@ -5282,43 +5282,42 @@
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "66.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-66.0.0.tgz",
"integrity": "sha512-+ywdy8T3foyZ2t3nRBujGa3vfOVMobHIi5iLB0L+fogdVO3EiUJ4BAyIacogWytnweLw3hgT70LQL9KoKTY/kA==",
"version": "65.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-65.0.1.tgz",
"integrity": "sha512-daCrQrgxOoOz2uMPWB3Y3vvv/5q+ncwICI8IjoebiwtW87CaY4tAN5EEiRXTYVnf7qi1v1BGBdHOSnZLV0rx6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.29.7",
"@babel/helper-validator-identifier": "^7.28.5",
"@eslint-community/eslint-utils": "^4.9.1",
"browserslist": "^4.28.2",
"change-case": "^5.4.4",
"ci-info": "^4.4.0",
"core-js-compat": "^3.49.0",
"detect-indent": "^7.0.2",
"find-up-simple": "^1.0.1",
"globals": "^17.6.0",
"globals": "^17.4.0",
"indent-string": "^5.0.0",
"is-builtin-module": "^5.0.0",
"jsesc": "^3.1.0",
"pluralize": "^8.0.0",
"regjsparser": "^0.13.1",
"semver": "^7.8.4",
"regjsparser": "^0.13.0",
"semver": "^7.7.4",
"strip-indent": "^4.1.1"
},
"engines": {
"node": ">=22"
"node": "^20.10.0 || >=21.0.0"
},
"funding": {
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
},
"peerDependencies": {
"eslint": ">=10.4"
"eslint": ">=9.38.0"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/semver": {
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@ -7295,20 +7294,10 @@
"license": "MIT"
},
"node_modules/linkify-it": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz",
"integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
@ -7486,25 +7475,15 @@
}
},
"node_modules/markdown-it": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz",
"integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==",
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",
"linkify-it": "^5.0.1",
"linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
@ -8800,9 +8779,9 @@
"license": "MIT"
},
"node_modules/regjsparser": {
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz",
"integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==",
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
"integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@ -10666,9 +10645,9 @@
}
},
"node_modules/undici": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"version": "7.24.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.3.tgz",
"integrity": "sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA==",
"dev": true,
"license": "MIT",
"engines": {

View File

@ -27,7 +27,7 @@
"eslint-plugin-perfectionist": "^5.9.0",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-regexp": "^3.1.0",
"eslint-plugin-unicorn": "^66.0.0",
"eslint-plugin-unicorn": "^65.0.1",
"globals": "^17.6.0",
"gulp": "^5.0.1",
"gulp-cli": "^3.1.0",

View File

@ -48,7 +48,6 @@ import {
lookupMatrix,
lookupNormalRect,
lookupRect,
MissingDataException,
numberToString,
RESOURCES_KEYS_OPERATOR_LIST,
RESOURCES_KEYS_TEXT_CONTENT,
@ -1209,9 +1208,6 @@ class Annotation {
/* resources = */ null
);
} catch (ex) {
if (ex instanceof MissingDataException) {
throw ex;
}
warn(`#setOptionalContent: ${ex}`);
}
}
@ -2578,6 +2574,9 @@ class WidgetAnnotation extends Annotation {
fontSize,
totalWidth,
totalHeight,
defaultVPadding,
descent,
lineHeight,
alignment,
bidi(lines[0]).dir === "rtl",
annotationStorage
@ -2939,6 +2938,9 @@ class TextWidgetAnnotation extends WidgetAnnotation {
fontSize,
width,
height,
vPadding,
descent,
lineHeight,
alignment,
isRTL,
annotationStorage
@ -2975,18 +2977,11 @@ class TextWidgetAnnotation extends WidgetAnnotation {
previousWidth = glyphWidth;
}
const renderedComb = buf.join(" ");
// Vertically center the glyphs within the field: comb fields are mostly
// filled with uppercase letters and/or digits, hence we use the cap height
// (with a fallback on the ascent or the font size) to center them.
const vShift =
(height - (font.capHeight || font.ascent || 1) * fontSize) / 2;
return (
`/Tx BMC q ${colors}BT ` +
defaultAppearance +
` 1 0 0 1 ${numberToString(hShift)} ${numberToString(
vShift
vPadding + descent
)} Tm ${renderedComb}` +
" ET Q EMC"
);
@ -3351,7 +3346,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
value: value ? this.data.exportValue : "",
};
const name = Name.get(value ? this._onStateName : "Off");
const name = Name.get(value ? this.data.exportValue : "Off");
this.setValue(dict, name, evaluator.xref, changes);
dict.set("AS", name);
@ -3411,7 +3406,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
value: value ? this.data.buttonValue : "",
};
const name = Name.get(value ? this._onStateName : "Off");
const name = Name.get(value ? this.data.buttonValue : "Off");
if (value) {
this.setValue(dict, name, evaluator.xref, changes);
}
@ -3490,107 +3485,6 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
this._streams.push(this.checkedAppearance);
}
_getOnStateName(dict) {
const appearanceStates = dict.get("AP");
if (!(appearanceStates instanceof Dict)) {
return null;
}
const normalAppearance = appearanceStates.get("N");
if (!(normalAppearance instanceof Dict)) {
return null;
}
for (const key of normalAppearance.getKeys()) {
if (key !== "Off") {
return key;
}
}
return null;
}
_getExportValueForOptIndex(index, opt, xref) {
if (Number.isInteger(index) && index >= 0 && index < opt.length) {
const value = this._decodeFormValue(xref.fetchIfRef(opt[index]));
if (typeof value === "string") {
return value;
}
}
return null;
}
_getOptInfo(dict, onState, opt, xref) {
if (!Array.isArray(opt)) {
return null;
}
const stateToIndex = new Map();
let currentIndex = null;
const fieldParent = dict.get("Parent");
const kids = fieldParent instanceof Dict ? fieldParent.get("Kids") : null;
if (Array.isArray(kids)) {
for (let i = 0, ii = Math.min(kids.length, opt.length); i < ii; i++) {
const kid = kids[i];
if (kid instanceof Ref && isRefsEqual(kid, this.ref)) {
currentIndex = i;
}
const kidDict = xref.fetchIfRef(kid);
if (!(kidDict instanceof Dict)) {
continue;
}
if (kidDict === dict) {
currentIndex = i;
}
const kidOnState = this._getOnStateName(kidDict);
if (typeof kidOnState === "string" && !stateToIndex.has(kidOnState)) {
stateToIndex.set(kidOnState, i);
}
}
} else if (opt.length === 1 && typeof onState === "string") {
// A single widget is sometimes used as its own field dictionary.
currentIndex = 0;
stateToIndex.set(onState, 0);
}
return { currentIndex, opt, stateToIndex };
}
// The appearance state is a Name; its real export value can be overridden by
// the "Opt" array, whose entries are ordered like the field's "Kids".
_getExportValue(state, optInfo, xref) {
if (!optInfo || typeof state !== "string" || state === "Off") {
return state;
}
if (state === this._onStateName) {
const exportValue = this._getExportValueForOptIndex(
optInfo.currentIndex,
optInfo.opt,
xref
);
if (exportValue !== null) {
return exportValue;
}
}
if (optInfo.stateToIndex.has(state)) {
const exportValue = this._getExportValueForOptIndex(
optInfo.stateToIndex.get(state),
optInfo.opt,
xref
);
if (exportValue !== null) {
return exportValue;
}
}
const index = parseInt(state, 10);
if (Number.isInteger(index) && String(index) === state) {
return this._getExportValueForOptIndex(index, optInfo.opt, xref) || state;
}
return state;
}
_processCheckBox(params) {
const customAppearance = params.dict.get("AP");
let normalAppearance =
@ -3633,33 +3527,15 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
exportValues.push("Off", otherYes);
}
const onState = exportValues[1];
this._onStateName = onState;
const opt = getInheritableProperty({ dict: params.dict, key: "Opt" });
const optInfo = this._getOptInfo(params.dict, onState, opt, params.xref);
this.data.exportValue = this._getExportValue(onState, optInfo, params.xref);
// Don't use a "V" entry pointing to a non-existent appearance state,
// see e.g. bug1720411.pdf where it's an *empty* Name-instance.
if (
!exportValues.includes(this.data.fieldValue) &&
this.data.fieldValue !== this.data.exportValue
) {
if (!exportValues.includes(this.data.fieldValue)) {
this.data.fieldValue = "Off";
}
this.data.fieldValue = this._getExportValue(
this.data.fieldValue,
optInfo,
params.xref
);
this.data.defaultFieldValue = this._getExportValue(
this.data.defaultFieldValue,
optInfo,
params.xref
);
const checkedAppearance = normalAppearance?.get(onState);
this.data.exportValue = exportValues[1];
const checkedAppearance = normalAppearance?.get(this.data.exportValue);
this.checkedAppearance =
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
const uncheckedAppearance = normalAppearance?.get("Off");
@ -3703,30 +3579,14 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
if (!(normalAppearance instanceof Dict)) {
return;
}
let onState = null;
for (const key of normalAppearance.getKeys()) {
if (key !== "Off") {
onState = key;
this.data.buttonValue = key;
break;
}
}
this._onStateName = onState;
const opt = getInheritableProperty({ dict: params.dict, key: "Opt" });
const optInfo = this._getOptInfo(params.dict, onState, opt, params.xref);
this.data.buttonValue = this._getExportValue(onState, optInfo, params.xref);
this.data.fieldValue = this._getExportValue(
this.data.fieldValue,
optInfo,
params.xref
);
this.data.defaultFieldValue = this._getExportValue(
this.data.defaultFieldValue,
optInfo,
params.xref
);
const checkedAppearance = normalAppearance.get(onState);
const checkedAppearance = normalAppearance.get(this.data.buttonValue);
this.checkedAppearance =
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
const uncheckedAppearance = normalAppearance.get("Off");
@ -4405,7 +4265,10 @@ class FreeTextAnnotation extends MarkupAnnotation {
totalWidth = Math.max(totalWidth, lineWidth);
}
const hscale = totalWidth > w ? w / totalWidth : 1;
let hscale = 1;
if (totalWidth > w) {
hscale = w / totalWidth;
}
let vscale = 1;
const lineHeight = LINE_FACTOR * fontSize;
const lineAscent = (LINE_FACTOR - LINE_DESCENT_FACTOR) * fontSize;
@ -5030,7 +4893,16 @@ class HighlightAnnotation extends MarkupAnnotation {
const quadPoints = (this.data.quadPoints = getQuadPoints(dict, null));
if (quadPoints) {
if (!this.appearance) {
const resources = this.appearance?.dict.get("Resources");
if (!this.appearance || !resources?.has("ExtGState")) {
if (this.appearance) {
// Workaround for cases where there's no /ExtGState-entry directly
// available, e.g. when the appearance stream contains a /XObject of
// the /Form-type, since that causes the highlighting to completely
// obscure the PDF content below it (fixes issue13242.pdf).
warn("HighlightAnnotation - ignoring built-in appearance stream.");
}
// Default color is yellow in Acrobat Reader
const fillColor = getPdfColorArray(this.color, [1, 1, 0]);
const fillAlpha = dict.get("CA");
@ -5413,28 +5285,33 @@ class FileAttachmentAnnotation extends MarkupAnnotation {
super(params);
const { annotationGlobals, dict } = params;
const fileSpecRef = dict.getRaw("FS");
const fsDict = dict.get("FS");
const file = new FileSpec(fsDict);
/** @type {{catalog?: Catalog}} */
const { catalog } = annotationGlobals.pdfManager.pdfDocument;
// Encode the embedded content's reference in the id so it can be
// re-fetched from the xref on demand (see `Catalog.attachmentContent`)
// instead of being cached where `cleanup` would wipe it. The file-spec is
// usually indirect; when it's inline its embedded-file stream still isn't
// (streams are always indirect), so fall back to that ref.
let fileId;
if (fsDict instanceof Dict) {
let contentRef = dict.getRaw("FS");
if (!(contentRef instanceof Ref)) {
contentRef = FileSpec.pickPlatformItem(
fsDict.get("EF"),
/* raw = */ true
);
}
if (contentRef instanceof Ref) {
fileId = catalog?.getAttachmentIdForAnnotation(contentRef);
// When this annotation references an embedded file thats already in the
// catalog `NameTree` (such as `EFOpen`), reuse that `NameTree` id so the
// sidebar and annotation paths resolve the same attachment identity.
let fileId =
fileSpecRef instanceof Ref
? catalog?.attachmentIdByRef.get(fileSpecRef)
: undefined;
// Fallback ids are namespaced to keep annotation-local ids distinct from
// `NameTree` ids (which are filename-based).
if (catalog && fsDict instanceof Dict && typeof fileId !== "string") {
const baseFileId = `annotation:${this.data.id}`;
fileId = baseFileId;
let i = 1;
while (catalog.attachmentDictById.has(fileId)) {
fileId = `${baseFileId}-${i++}`;
}
// Cache only fallbacks.
catalog.attachmentDictById.set(fileId, fsDict);
}
this.data.hasOwnCanvas = this.data.noRotate;

View File

@ -119,12 +119,18 @@ function fetchRemoteDest(action) {
class Catalog {
#actualNumPages = null;
#annotationAttachmentIdByRef = new RefSetCache();
#annotationAttachmentRefById = new Map();
/** @type {RefSetCache | null} */
#attachmentIdByRef = null;
#catDict = null;
/**
* Attachment dictionaries keyed by attachment id.
*
* @type {Map<string, Dict>}
*/
attachmentDictById = new Map();
builtInCMapCache = new Map();
fontCache = new RefSetCache();
@ -158,42 +164,31 @@ class Catalog {
this.toplevelPagesDict; // eslint-disable-line no-unused-expressions
}
cloneDict() {
return this.#catDict.clone();
/**
* Attachment ids keyed by embedded-file reference.
*
* @type {RefSetCache}
*/
get attachmentIdByRef() {
if (this.#attachmentIdByRef) {
return this.#attachmentIdByRef;
}
const attachmentIdByRef = new RefSetCache();
for (const [name, ref] of this.rawEmbeddedFiles || []) {
if (!(ref instanceof Ref)) {
continue;
}
attachmentIdByRef.put(
ref,
stringToPDFString(name, /* keepEscapeSequence = */ true)
);
}
return (this.#attachmentIdByRef = attachmentIdByRef);
}
/**
* Create an id for an attachment from a FileAttachment annotation.
*
* The id is registered here rather than parsed from a public string prefix in
* `attachmentContent`, since catalog attachment names can be arbitrary PDF
* strings and may otherwise collide with annotation-local ids.
*
* @param {Ref} ref
* File-spec or embedded-file stream reference.
* @returns {string}
* Attachment id.
*/
getAttachmentIdForAnnotation(ref) {
let id = this.#annotationAttachmentIdByRef.get(ref);
if (id) {
return id;
}
const baseId = `attachmentRef:${ref.toString()}`;
id = baseId;
let i = 1;
while (
this.#annotationAttachmentRefById.has(id) ||
this.attachments?.has(id)
) {
id = `${baseId}-${i++}`;
}
this.#annotationAttachmentIdByRef.put(ref, id);
this.#annotationAttachmentRefById.set(id, ref);
return id;
cloneDict() {
return this.#catDict.clone();
}
get version() {
@ -279,18 +274,19 @@ class Catalog {
/* suppressEncryption = */ !this.xref.encrypt?.encryptMetadata
);
if (
stream instanceof BaseStream &&
isDict(stream.dict, "Metadata") &&
isName(stream.dict.get("Subtype"), "XML")
) {
// XXX: This should examine the charset the XML document defines,
// however since there are currently no real means to decode arbitrary
// charsets, let's just hope that the author of the PDF was reasonable
// enough to stick with the XML default charset, which is UTF-8.
const data = stringToUTF8String(stream.getString());
if (data) {
metadata = new MetadataParser(data).serializable;
if (stream instanceof BaseStream && stream.dict instanceof Dict) {
const type = stream.dict.get("Type");
const subtype = stream.dict.get("Subtype");
if (isName(type, "Metadata") && isName(subtype, "XML")) {
// XXX: This should examine the charset the XML document defines,
// however since there are currently no real means to decode arbitrary
// charsets, let's just hope that the author of the PDF was reasonable
// enough to stick with the XML default charset, which is UTF-8.
const data = stringToUTF8String(stream.getString());
if (data) {
metadata = new MetadataParser(data).serializable;
}
}
}
} catch (ex) {
@ -1160,25 +1156,6 @@ class Catalog {
return shadow(this, "attachments", attachments);
}
/**
* @param {string} id
* Unique attachment identifier.
* @returns {CatalogAttachmentContent | undefined}
* Content, or `undefined` when no named attachment exists for the id.
*/
#attachmentContentByName(id) {
const obj = this.#catDict.get("Names");
if (obj instanceof Dict && obj.has("EmbeddedFiles")) {
const nameTree = new NameTree(obj.getRaw("EmbeddedFiles"), this.xref);
for (const [key, value] of nameTree.getAll()) {
if (stringToPDFString(key, /* keepEscapeSequence = */ true) === id) {
return FileSpec.readContent(value);
}
}
}
return undefined;
}
/**
* Get content for an attachment.
*
@ -1188,23 +1165,19 @@ class Catalog {
* Content.
*/
attachmentContent(id) {
const namedContent = this.#attachmentContentByName(id);
if (namedContent !== undefined) {
return namedContent;
const dict = this.attachmentDictById.get(id);
if (dict) {
return FileSpec.readContent(dict);
}
// Annotation-local attachments register the reference of their embedded
// content in the catalog, so it's re-fetched from the xref on demand
// instead of being cached (which would then need to survive `cleanup`).
// The reference points either at the file-spec dictionary or, for an inline
// file-spec, straight at the embedded-file stream.
const ref = this.#annotationAttachmentRefById.get(id);
if (ref) {
const target = this.xref.fetch(ref);
if (target instanceof BaseStream) {
return FileSpec.readStreamContent(target);
const obj = this.#catDict.get("Names");
if (obj instanceof Dict && obj.has("EmbeddedFiles")) {
const nameTree = new NameTree(obj.getRaw("EmbeddedFiles"), this.xref);
for (const [key, value] of nameTree.getAll()) {
if (stringToPDFString(key, /* keepEscapeSequence = */ true) === id) {
return FileSpec.readContent(value);
}
}
return target instanceof Dict ? FileSpec.readContent(target) : null;
}
return null;
}
@ -1307,6 +1280,9 @@ class Catalog {
async cleanup(manuallyTriggered = false) {
clearGlobalCaches();
this.#attachmentIdByRef?.clear();
this.#attachmentIdByRef = null;
this.attachmentDictById.clear();
this.globalColorSpaceCache.clear();
this.globalImageCache.clear(/* onlyData = */ manuallyTriggered);
this.pageKidsCountCache.clear();

View File

@ -894,22 +894,9 @@ class CFFParser {
}
}
if (maxZoneHeight > 0) {
// The lower bound of AFDKO's valid window is `0.5 / maxZoneHeight`.
// When that bound is itself above the default BlueScale the font simply
// has small zones (e.g. Eurostile LT Std, or the SofiaPro fonts shipped
// with a near-default 0.037): even the default 0.039625 would be
// flagged as out-of-range, so this is the rendered intent and forcing
// BlueScale up only misaligns/collapses overshooting glyphs (notably
// with macOS's Core Text rasterizer). Only apply the lower clamp when
// its target does not exceed the default.
// Round the bound in order to avoid too long operand (issue 21466).
const PRECISION = 1e5;
const lowerBound = 0.5 / maxZoneHeight;
const minBlueScale =
lowerBound <= DEFAULT_BLUE_SCALE
? Math.ceil(lowerBound * PRECISION) / PRECISION
: -Infinity;
const maxBlueScale = Math.floor(PRECISION / maxZoneHeight) / PRECISION;
blueScale < DEFAULT_BLUE_SCALE ? 0.5 / maxZoneHeight : -Infinity;
const maxBlueScale = 1 / maxZoneHeight;
const clamped = MathClamp(blueScale, minBlueScale, maxBlueScale);
if (clamped !== blueScale) {
privateDict.setByName("BlueScale", clamped);

View File

@ -418,7 +418,10 @@ class FakeUnicodeFont {
[w, h] = [h, w];
}
const hscale = maxWidth > w ? w / maxWidth : 1;
let hscale = 1;
if (maxWidth > w) {
hscale = w / maxWidth;
}
let vscale = 1;
const lineHeight = LINE_FACTOR * fontSize;
const lineDescent = LINE_DESCENT_FACTOR * fontSize;

View File

@ -718,7 +718,8 @@ class Page {
"_parseStructTree",
[structTreeRoot]
);
return await this.pdfManager.ensure(structTree, "serializable");
const data = await this.pdfManager.ensure(structTree, "serializable");
return data;
} catch (ex) {
warn(`getStructTree: "${ex}".`);
return null;

View File

@ -674,13 +674,21 @@ class PDFEditor {
const classNames = node.get("C");
if (classNames instanceof Name) {
const newClassName = dedupClasses.get(classNames.name);
newNode.set("C", newClassName ? Name.get(newClassName) : classNames);
if (newClassName) {
newNode.set("C", Name.get(newClassName));
} else {
newNode.set("C", classNames);
}
} else if (Array.isArray(classNames)) {
const newClassNames = [];
for (const className of classNames) {
if (className instanceof Name) {
const newClassName = dedupClasses.get(className.name);
newClassNames.push(newClassName ? Name.get(newClassName) : className);
if (newClassName) {
newClassNames.push(Name.get(newClassName));
} else {
newClassNames.push(className);
}
}
}
newNode.set("C", newClassNames);
@ -690,7 +698,11 @@ class PDFEditor {
const roleName = node.get("S");
if (roleName instanceof Name) {
const newRoleName = dedupRoles.get(roleName.name);
newNode.set("S", newRoleName ? Name.get(newRoleName) : roleName);
if (newRoleName) {
newNode.set("S", Name.get(newRoleName));
} else {
newNode.set("S", roleName);
}
}
// Fix the ID.
@ -698,7 +710,11 @@ class PDFEditor {
if (typeof id === "string") {
const stringId = stringToPDFString(id, /* keepEscapeSequence = */ false);
const newId = dedupIDs.get(stringId);
newNode.set("ID", newId ? stringToAsciiOrUTF16BE(newId) : id);
if (newId) {
newNode.set("ID", stringToAsciiOrUTF16BE(newId));
} else {
newNode.set("ID", id);
}
}
// Table headers may contain IDs that need to be deduplicated.

View File

@ -523,7 +523,6 @@ class PartialEvaluator {
isolated: false,
knockout: false,
needsIsolation: false,
hasSoftMask: false,
isGray: false,
};
@ -574,7 +573,6 @@ class PartialEvaluator {
if (group) {
groupOptions.needsIsolation = newOpList.needsIsolation || !!smask;
groupOptions.hasSoftMask = newOpList.hasSoftMask || !!smask;
operatorList.addOp(OPS.beginGroup, [groupOptions]);
operatorList.addOp(OPS.paintFormXObjectBegin, args);
operatorList.addOpList(newOpList);
@ -3537,7 +3535,10 @@ class PartialEvaluator {
if (includeMarkedContent) {
markedContentData.level++;
const mcid = args[1] instanceof Dict ? args[1].get("MCID") : null;
let mcid = null;
if (args[1] instanceof Dict) {
mcid = args[1].get("MCID");
}
textContent.items.push({
type: "beginMarkedContentProps",
id: Number.isInteger(mcid)

View File

@ -27,6 +27,29 @@ import { stringToPDFString } from "./string_utils.js";
* @import { CatalogAttachmentContent } from "./catalog.js";
*/
/**
* Get a platform-specific item from a file-spec dictionary.
*
* Search order follows the PDF platform keys: `UF`, `F`, `Unix`, `Mac`,
* `DOS`.
*
* @param {Dict | null | undefined} dict
* Dictionary.
* @returns {unknown}
* Matching dictionary value or `null` when no key is found.
*/
function pickPlatformItem(dict) {
if (dict instanceof Dict) {
// Look for the filename in this order: UF, F, Unix, Mac, DOS
for (const key of ["UF", "F", "Unix", "Mac", "DOS"]) {
if (dict.has(key)) {
return dict.get(key);
}
}
}
return null;
}
/**
* "A PDF file can refer to the contents of another file by using a File
* Specification (PDF 1.1)", see the spec (7.11) for more details.
@ -53,7 +76,7 @@ class FileSpec {
}
get filename() {
const item = FileSpec.pickPlatformItem(this.root);
const item = pickPlatformItem(this.root);
if (item && typeof item === "string") {
// NOTE: The following replacement order is INTENTIONAL, regardless of
// what some static code analysers (e.g. CodeQL) may claim.
@ -82,31 +105,6 @@ class FileSpec {
};
}
/**
* Get a platform-specific item from a file-spec dictionary.
*
* Search order follows the PDF platform keys: `UF`, `F`, `Unix`, `Mac`,
* `DOS`.
*
* @param {Dict | null | undefined} dict
* Dictionary.
* @param {boolean} [raw]
* Return the raw (possibly indirect) value rather than the resolved one.
* @returns {unknown}
* Matching dictionary value or `null` when no key is found.
*/
static pickPlatformItem(dict, raw = false) {
if (dict instanceof Dict) {
// Look for the filename in this order: UF, F, Unix, Mac, DOS
for (const key of ["UF", "F", "Unix", "Mac", "DOS"]) {
if (dict.has(key)) {
return raw ? dict.getRaw(key) : dict.get(key);
}
}
}
return null;
}
/**
* Read attachment bytes from a file-spec dictionary.
*
@ -121,36 +119,24 @@ class FileSpec {
if (!(dict instanceof Dict)) {
return null;
}
const ef = this.pickPlatformItem(dict.get("EF"));
const ef = pickPlatformItem(dict.get("EF"));
if (!(ef instanceof BaseStream)) {
warn(
"Embedded file specification points to non-existing/invalid content"
);
return null;
}
return this.readStreamContent(ef);
}
/**
* Read the bytes of an embedded-file stream.
*
* @param {BaseStream} stream
* Embedded-file stream.
* @returns {CatalogAttachmentContent}
* Attachment bytes.
* @throws {PasswordException}
* When the bytes are encrypted and no key is available.
*/
static readStreamContent(stream) {
// Throw if we need a password but dont have one.
const encrypt = stream.dict?.xref?.encrypt;
const encrypt = dict.xref?.encrypt;
if (encrypt?.encryptionKey === null) {
throw new PasswordException(
"No password given",
PasswordResponses.NEED_PASSWORD
);
}
return stream.getBytes();
return ef.getBytes();
}
}

View File

@ -3171,7 +3171,10 @@ class Font {
// there isn't enough room to duplicate, the glyph id is left the same. In
// this case, glyph 0 may not work correctly, but that is better than
// having the whole font fail.
const glyphZeroId = dupFirstEntry ? numGlyphsOut - 1 : 0;
let glyphZeroId = numGlyphsOut - 1;
if (!dupFirstEntry) {
glyphZeroId = 0;
}
// When `cssFontInfo` is set, the font is used to render text in the HTML
// view (e.g. with Xfa) so nothing must be moved in the private use area.
@ -3245,7 +3248,10 @@ class Font {
// Type 1 fonts have a notdef inserted at the beginning, so glyph 0
// becomes glyph 1. In a CFF font glyph 0 is appended to the end of the
// char strings.
const glyphZeroId = font instanceof CFFFont ? font.numGlyphs - 1 : 1;
let glyphZeroId = 1;
if (font instanceof CFFFont) {
glyphZeroId = font.numGlyphs - 1;
}
const mapping = font.getGlyphMapping(properties);
let newMapping = null;
let newCharCodeToGlyphId = mapping;

View File

@ -830,29 +830,24 @@ class OperatorList {
* operations require being drawn in isolation (i.e. on a separate canvas).
* A group/pattern needs isolation when it uses non-default compositing
* (blend mode) or a soft mask. The result is exposed via `needsIsolation`.
*
* `hasSoftMask` separately flags the use of a soft mask: unlike a plain blend
* mode, which a non-isolated group can apply directly against its backdrop, a
* soft mask always requires a real intermediate canvas (see bug 1873345).
*/
class CheckedOperatorList extends OperatorList {
needsIsolation = false;
hasSoftMask = false;
addOp(fn, args) {
if (!this.needsIsolation || !this.hasSoftMask) {
if (!this.needsIsolation) {
if (fn === OPS.beginGroup) {
// Propagate isolation only if the nested group itself needs it.
this.needsIsolation ||= args[0].needsIsolation;
this.hasSoftMask ||= args[0].hasSoftMask;
this.needsIsolation = args[0].needsIsolation;
} else if (fn === OPS.setGState) {
for (const [key, val] of args[0]) {
if (key === "BM" && val !== "source-over") {
this.needsIsolation = true;
} else if (key === "SMask" && val !== false) {
break;
}
if (key === "SMask" && val !== false) {
this.needsIsolation = true;
this.hasSoftMask = true;
break;
}
}
}

View File

@ -285,9 +285,10 @@ class RadialAxialShading extends BaseShading {
}
colorStops.push([1, Util.makeHexColor(rPrev, gPrev, bPrev)]);
const background = dict.has("Background")
? cs.getRgbHex(dict.get("Background"), 0)
: "transparent";
let background = "transparent";
if (dict.has("Background")) {
background = cs.getRgbHex(dict.get("Background"), 0);
}
if (!extendStart) {
// Insert a color stop at the front and offset the first real color stop

View File

@ -116,19 +116,6 @@ class BasePdfManager {
return this.ensure(this.pdfDocument.catalog, prop, args);
}
async initDocument(recoveryMode) {
await this.ensureDoc("checkHeader");
await this.ensureDoc("parseStartXRef");
await this.ensureDoc("parse", [recoveryMode]);
// Check that at least the first page can be successfully loaded,
// since otherwise the XRef table is definitely not valid.
await this.ensureDoc("checkFirstPage", [recoveryMode]);
// Check that the last page can be successfully loaded, to ensure that
// `numPages` is correct, and fallback to walking the entire /Pages-tree.
await this.ensureDoc("checkLastPage", [recoveryMode]);
}
getPage(pageIndex) {
return this.pdfDocument.getPage(pageIndex);
}

View File

@ -20,10 +20,9 @@ import {
warn,
} from "../shared/util.js";
import { Dict, isDict, isName, Name, Ref, RefSetCache } from "./primitives.js";
import { lookupNormalRect, MissingDataException } from "./core_utils.js";
import { stringToAsciiOrUTF16BE, stringToPDFString } from "./string_utils.js";
import { BaseStream } from "./base_stream.js";
import { FileSpec } from "./file_spec.js";
import { lookupNormalRect } from "./core_utils.js";
import { NumberTree } from "./name_number_tree.js";
const MAX_DEPTH = 40;
@ -595,18 +594,24 @@ class StructElementNode {
}
for (let af of AFs) {
af = this.xref.fetchIfRef(af);
if (
!isDict(af, "Filespec") ||
!isName(af.get("AFRelationship"), "Supplement")
) {
if (!isDict(af, "Filespec")) {
continue;
}
const fileStream = FileSpec.pickPlatformItem(af.get("EF"));
if (
!(fileStream instanceof BaseStream) ||
!isDict(fileStream.dict, "EmbeddedFile") ||
!isName(fileStream.dict.get("Subtype"), "application/mathml+xml")
) {
if (!isName(af.get("AFRelationship"), "Supplement")) {
continue;
}
const ef = af.get("EF");
if (!(ef instanceof Dict)) {
continue;
}
const fileStream = ef.get("UF") || ef.get("F");
if (!(fileStream instanceof BaseStream)) {
continue;
}
if (!isName(fileStream.dict.get("Type"), "EmbeddedFile")) {
continue;
}
if (!isName(fileStream.dict.get("Subtype"), "application/mathml+xml")) {
continue;
}
// The default encoding for xml files is UTF-8.
@ -904,16 +909,9 @@ class StructTreePage {
obj.alt = stringToPDFString(alt);
}
if (obj.role === "Formula") {
try {
const { mathML } = node;
if (mathML) {
obj.mathML = mathML;
}
} catch (ex) {
if (ex instanceof MissingDataException) {
throw ex;
}
warn(`Ignoring mathML: "${ex}".`);
const { mathML } = node;
if (mathML) {
obj.mathML = mathML;
}
}

View File

@ -691,10 +691,13 @@ class Type1Parser {
subrs,
this.seacAnalysisEnabled
);
// It seems when FreeType encounters an error while evaluating a glyph
// that it completely ignores the glyph so we'll mimic that behaviour
// here and put an endchar to make the validator happy.
const output = !error ? charString.output : [14];
let output = charString.output;
if (error) {
// It seems when FreeType encounters an error while evaluating a glyph
// that it completely ignores the glyph so we'll mimic that behaviour
// here and put an endchar to make the validator happy.
output = [14];
}
const charStringObject = {
glyphName: glyph,
charstring: output,

View File

@ -164,7 +164,16 @@ class WorkerMessageHandler {
}
async function loadDocument(recoveryMode) {
await pdfManager.initDocument(recoveryMode);
await pdfManager.ensureDoc("checkHeader");
await pdfManager.ensureDoc("parseStartXRef");
await pdfManager.ensureDoc("parse", [recoveryMode]);
// Check that at least the first page can be successfully loaded,
// since otherwise the XRef table is definitely not valid.
await pdfManager.ensureDoc("checkFirstPage", [recoveryMode]);
// Check that the last page can be successfully loaded, to ensure that
// `numPages` is correct, and fallback to walking the entire /Pages-tree.
await pdfManager.ensureDoc("checkLastPage", [recoveryMode]);
const isPureXfa = await pdfManager.ensureDoc("isPureXfa");
if (isPureXfa) {
@ -510,17 +519,20 @@ class WorkerMessageHandler {
startWorkerTask(task);
}
pagePromises.push(
pdfManager
.getPage(i)
.then(page =>
pdfManager.getPage(i).then(async page => {
if (!page) {
return [];
}
return (
page.collectAnnotationsByType(
handler,
task,
types,
annotationPromises,
annotationGlobals
)
)
) || []
);
})
);
}
await Promise.all(pagePromises);
@ -617,7 +629,9 @@ class WorkerMessageHandler {
while (true) {
try {
await manager.requestLoadedStream();
await manager.initDocument(recoveryMode);
await manager.ensureDoc("checkHeader");
await manager.ensureDoc("parseStartXRef");
await manager.ensureDoc("parse", [recoveryMode]);
break;
} catch (e) {
if (e instanceof XRefParseException) {

View File

@ -101,6 +101,7 @@ class XFAFactory {
const missingFonts = [];
for (let typeface of this.form[$globalData].usedTypefaces) {
typeface = stripQuotes(typeface);
// eslint-disable-next-line unicorn/prefer-array-some
const font = this.form[$globalData].fontFinder.find(typeface);
if (!font) {
missingFonts.push(typeface);

View File

@ -51,7 +51,7 @@ class XMLParserBase {
return s.replaceAll(/&([^;]+);/g, (all, entity) => {
if (entity.substring(0, 2) === "#x") {
return String.fromCodePoint(parseInt(entity.substring(2), 16));
} else if (entity.at(0) === "#") {
} else if (entity.substring(0, 1) === "#") {
return String.fromCodePoint(parseInt(entity.substring(1), 10));
}
switch (entity) {

View File

@ -17,10 +17,6 @@ import {
CanvasNestedDependencyTracker,
Dependencies,
} from "./canvas_dependency_tracker.js";
import {
convertBlackAndWhiteToRGBA,
convertRGBToRGBA,
} from "../shared/image_utils.js";
import {
F32_BBOX_INIT,
FeatureTest,
@ -49,6 +45,7 @@ import {
PathType,
TilingPattern,
} from "./pattern_helper.js";
import { convertBlackAndWhiteToRGBA } from "../shared/image_utils.js";
import { MathClamp } from "../shared/math_clamp.js";
// <canvas> contexts store most of the state we need natively.
@ -320,6 +317,11 @@ class CanvasExtraState {
}
function putBinaryImageData(ctx, imgData) {
if (imgData instanceof ImageData) {
ctx.putImageData(imgData, 0, 0);
return;
}
// Put the image data to the canvas in chunks, rather than putting the
// whole image at once. This saves JS memory, because the ImageData object
// is smaller. It also possibly saves C++ memory within the implementation
@ -331,36 +333,40 @@ function putBinaryImageData(ctx, imgData) {
// will (conceptually) put pixels past the bounds of the canvas. But
// that's ok; any such pixels are ignored.
const { width, height, kind } = imgData;
const height = imgData.height,
width = imgData.width;
const partialChunkHeight = height % FULL_CHUNK_HEIGHT;
const fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
const totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
const chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT);
let srcPos = 0;
let srcPos = 0,
destPos;
const src = imgData.data;
const dest = chunkImgData.data;
let i;
let i, j, thisChunkHeight, elemsInThisChunk;
// There are multiple forms in which the pixel data can be passed, and
// imgData.kind tells us which one this is.
if (kind === ImageKind.GRAYSCALE_1BPP) {
if (imgData.kind === ImageKind.GRAYSCALE_1BPP) {
// Grayscale, 1 bit per pixel (i.e. black-and-white).
for (i = 0; i < totalChunks; i++) {
thisChunkHeight = i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight;
({ srcPos } = convertBlackAndWhiteToRGBA({
src,
srcPos,
dest,
width,
height: i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight,
height: thisChunkHeight,
}));
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
}
} else if (kind === ImageKind.RGBA_32BPP) {
} else if (imgData.kind === ImageKind.RGBA_32BPP) {
// RGBA, 32-bits per pixel.
let j = 0;
let elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4;
j = 0;
elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4;
for (i = 0; i < fullChunks; i++) {
dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
srcPos += elemsInThisChunk;
@ -374,21 +380,28 @@ function putBinaryImageData(ctx, imgData) {
ctx.putImageData(chunkImgData, 0, j);
}
} else if (kind === ImageKind.RGB_24BPP) {
} else if (imgData.kind === ImageKind.RGB_24BPP) {
// RGB, 24-bits per pixel.
thisChunkHeight = FULL_CHUNK_HEIGHT;
elemsInThisChunk = width * thisChunkHeight;
for (i = 0; i < totalChunks; i++) {
({ srcPos } = convertRGBToRGBA({
src,
srcPos,
dest: new Uint32Array(dest.buffer),
width,
height: i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight,
}));
if (i >= fullChunks) {
thisChunkHeight = partialChunkHeight;
elemsInThisChunk = width * thisChunkHeight;
}
destPos = 0;
for (j = elemsInThisChunk; j--; ) {
dest[destPos++] = src[srcPos++];
dest[destPos++] = src[srcPos++];
dest[destPos++] = src[srcPos++];
dest[destPos++] = 255;
}
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
}
} else {
throw new Error(`bad image kind: ${kind}`);
throw new Error(`bad image kind: ${imgData.kind}`);
}
}
@ -400,7 +413,8 @@ function putBinaryImageMask(ctx, imgData) {
}
// Slow path: OffscreenCanvas isn't available in the worker.
const { width, height } = imgData;
const height = imgData.height,
width = imgData.width;
const partialChunkHeight = height % FULL_CHUNK_HEIGHT;
const fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
const totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
@ -411,14 +425,18 @@ function putBinaryImageMask(ctx, imgData) {
const dest = chunkImgData.data;
for (let i = 0; i < totalChunks; i++) {
const thisChunkHeight =
i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight;
// Expand the mask so it can be used by the canvas. Any required
// inversion has already been handled.
({ srcPos } = convertBlackAndWhiteToRGBA({
src,
srcPos,
dest,
width,
height: i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight,
height: thisChunkHeight,
nonBlackColor: 0,
}));
@ -2670,13 +2688,13 @@ class CanvasGraphics {
this.showType3Text(opIdx, glyphs);
this.dependencyTracker?.recordShowTextOperation(opIdx);
this.#endKnockoutElement(started);
return;
return undefined;
}
const fontSize = current.fontSize;
if (fontSize === 0) {
this.dependencyTracker?.recordOperation(opIdx);
return;
return undefined;
}
const started = this.#beginKnockoutElement(current.fillAlpha);
@ -2794,7 +2812,7 @@ class CanvasGraphics {
ctx.restore();
this.compose();
this.#endKnockoutElement(started);
return;
return undefined;
}
let x = 0,
@ -2910,6 +2928,7 @@ class CanvasGraphics {
this.dependencyTracker?.recordShowTextOperation(opIdx);
this.#endKnockoutElement(started);
return undefined;
}
showType3Text(opIdx, glyphs) {
@ -3204,13 +3223,12 @@ class CanvasGraphics {
}
const currentCtx = this.ctx;
if (!group.isolated && !group.knockout && this.#knockoutGroupLevel === 0) {
info("TODO: Fully support non-isolated non-knockout groups.");
}
if (
// A non-isolated group blends with its backdrop, so drawing it directly
// on the parent canvas (rather than on a transparent intermediate one)
// is correct even when it contains blend modes (bug 1873345). A soft
// mask still needs its own canvas though, and an isolated group requires
// a transparent backdrop, so both keep the intermediate canvas.
(!group.needsIsolation || (!group.isolated && !group.hasSoftMask)) &&
!group.needsIsolation &&
!group.knockout &&
!group.isGray &&
this.#knockoutGroupLevel === 0 &&
@ -3229,24 +3247,12 @@ class CanvasGraphics {
}
currentCtx.clip(clip);
}
// Unlike the intermediate-canvas path below, the content is drawn
// straight onto the parent canvas with no later compositing step, so the
// inherited blend mode, alpha constants and transfer function must stay
// active here rather than being reset (issue 20722); the conditions
// above already guarantee a Normal blend and an opaque (ca === 1) state.
this.groupStack.push(null); // null = no intermediate canvas
this.#groupStackMeta.push(null);
this.groupLevel++;
return;
}
// Reached only when the direct path above didn't apply, e.g. a soft mask,
// non-default group alpha or blend mode: we still composite on a
// transparent intermediate canvas rather than the real backdrop.
if (!group.isolated && !group.knockout && this.#knockoutGroupLevel === 0) {
info("TODO: Fully support non-isolated non-knockout groups.");
}
const currentTransform = getCurrentTransform(currentCtx);
if (group.matrix) {
currentCtx.transform(...group.matrix);
@ -3990,6 +3996,12 @@ class CanvasGraphics {
const result = this.applyTransferMapsToBitmap(imgData);
imgToPaint = result.img;
inlineImgCanvas = result.canvasEntry;
} else if (
(typeof HTMLElement === "function" && imgData instanceof HTMLElement) ||
!imgData.data
) {
// typeof check is needed due to node.js support, see issue #8489
imgToPaint = imgData;
} else {
const tmpCanvas = this.canvasFactory.create(width, height);
putBinaryImageData(tmpCanvas.context, imgData);

View File

@ -343,6 +343,10 @@ class HighlightOutline extends Outline {
get box() {
return this.#box;
}
get classNamesForOutlining() {
return ["highlightOutline"];
}
}
class FreeHighlightOutliner extends FreeDrawOutliner {

View File

@ -2545,6 +2545,14 @@ class AnnotationEditorUIManager {
});
}
/**
* Check if the editor is selected.
* @param {AnnotationEditor} editor
*/
isSelected(editor) {
return this.#selectedEditors.has(editor);
}
get firstSelectedEditor() {
return this.#selectedEditors.values().next().value;
}

View File

@ -51,6 +51,13 @@ if (isNodeJS) {
warn("Cannot polyfill `DOMMatrix`, rendering may be broken.");
}
}
if (!globalThis.ImageData) {
if (canvas?.ImageData) {
globalThis.ImageData = canvas.ImageData;
} else {
warn("Cannot polyfill `ImageData`, rendering may be broken.");
}
}
if (!globalThis.Path2D) {
if (canvas?.Path2D) {
globalThis.Path2D = canvas.Path2D;

View File

@ -255,9 +255,11 @@ class EventDispatcher {
this.runCalculate(source, event);
const savedValue = (event.value = source.obj._getValue());
const formattedValue = this.runActions(source, source, event, "Format")
? event.value?.toString?.()
: null;
let formattedValue = null;
if (this.runActions(source, source, event, "Format")) {
formattedValue = event.value?.toString?.();
}
source.obj._send({
id: source.obj._id,
@ -363,9 +365,10 @@ class EventDispatcher {
}
savedValue = target.obj._getValue();
const formattedValue = this.runActions(target, target, event, "Format")
? event.value?.toString?.()
: null;
let formattedValue = null;
if (this.runActions(target, target, event, "Format")) {
formattedValue = event.value?.toString?.();
}
target.obj._send({
id: target.obj._id,

View File

@ -139,9 +139,4 @@ function grayToRGBA(src, dest) {
}
}
export {
convertBlackAndWhiteToRGBA,
convertRGBToRGBA,
convertToRGBA,
grayToRGBA,
};
export { convertBlackAndWhiteToRGBA, convertToRGBA, grayToRGBA };

View File

@ -169,43 +169,6 @@ describe("autolinker", function () {
});
});
describe("issue21458.pdf", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"issue21458.pdf",
".page[data-page-number='1'] .annotationLayer",
null,
null,
{ enableAutoLinking: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must not add links that overlap internal destinations", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForLinkAnnotations(page);
const linkIds = await page.$$eval(
".page[data-page-number='1'] .annotationLayer > .linkAnnotation > a",
annotations =>
annotations.map(a => a.getAttribute("data-element-id"))
);
expect(linkIds.length).withContext(`In ${browserName}`).toEqual(42);
linkIds.forEach(id =>
expect(id)
.withContext(`In ${browserName}`)
.not.toContain("inferred_link_")
);
})
);
});
});
describe("PR 19470", function () {
let pages;

View File

@ -537,11 +537,7 @@ describe("Comment", () => {
await page.mouse.down();
const steps = 20;
for (let i = 1; i <= steps; i++) {
const x = Math.round(startX - (extraWidth * i) / steps);
await page.mouse.move(x, startY);
await waitForBrowserTrip(page);
}
await page.mouse.move(startX - extraWidth, startY, { steps });
await page.mouse.up();
const rectAfter = await getRect(page, sidebarSelector);

View File

@ -127,11 +127,9 @@ async function waitForHavingContents(page, expected) {
});
return page.waitForFunction(
ex => {
const textLayers = document.querySelectorAll(".textLayer");
const buffer = [];
for (const [i, textLayer] of textLayers.entries()) {
const text = textLayer.textContent.trim();
buffer.push(typeof ex[i] === "string" ? text : parseInt(text, 10));
for (const textLayer of document.querySelectorAll(".textLayer")) {
buffer.push(parseInt(textLayer.textContent.trim(), 10));
}
return ex.length === buffer.length && ex.every((v, i) => v === buffer[i]);
},
@ -3398,119 +3396,6 @@ describe("Reorganize Pages View", () => {
})
);
});
it("should merge a password-protected PDF after the current page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
// Navigate to page 2 so the merged PDF is inserted after it.
await page.evaluate(() => {
window.PDFViewerApplication.page = 2;
});
await page.waitForFunction(
() => window.PDFViewerApplication.page === 2
);
await waitAndClick(page, getThumbnailSelector(2));
const handleMerged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"thumbnailsloaded",
resolve,
{ once: true }
);
});
const picker = await page.$("#viewsManagerAddFilePicker");
await picker.uploadFile(
path.join(__dirname, "../pdfs/issue6010_1.pdf")
);
// Test with an incorrect password first,
// to ensure that re-prompting works correctly.
for (const password of ["Incorrect password", "abc"]) {
await page.waitForSelector("#passwordDialog", { visible: true });
await page.type("#password", password);
await page.keyboard.press("Enter");
}
await awaitPromise(handleMerged);
// Original 3 pages + 1 merged page = 4 pages total.
await page.waitForFunction(
() => parseInt(document.getElementById("pageNumber").max, 10) === 4
);
// Focus must move to the first newly inserted page (page 3, since
// we merged after page 2).
await page.waitForFunction(
() => window.PDFViewerApplication.page === 3
);
// Pages 12 come from the original document, then the page of
// the merged PDF, then page 3 of the original shifted to the end.
await waitForHavingContents(page, [1, 2, "Issue 6010", 3]);
await waitForTextToBe(
page,
"#viewsManagerStatusActionLabel",
`${FSI}1${PDI} selected`
);
})
);
});
it("should merge a corrupt PDF (with invalid pages /Count) after the current page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
// Navigate to page 2 so the merged PDF is inserted after it.
await page.evaluate(() => {
window.PDFViewerApplication.page = 2;
});
await page.waitForFunction(
() => window.PDFViewerApplication.page === 2
);
await waitAndClick(page, getThumbnailSelector(2));
const handleMerged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"thumbnailsloaded",
resolve,
{ once: true }
);
});
const picker = await page.$("#viewsManagerAddFilePicker");
await picker.uploadFile(
path.join(__dirname, "../pdfs/poppler-91414-0-53.pdf")
);
await awaitPromise(handleMerged);
// Original 3 pages + 1 merged page = 4 pages total.
await page.waitForFunction(
() => parseInt(document.getElementById("pageNumber").max, 10) === 4
);
// Focus must move to the first newly inserted page (page 3, since
// we merged after page 2).
await page.waitForFunction(
() => window.PDFViewerApplication.page === 3
);
// Pages 12 come from the original document, then the page of
// the merged PDF, then page 3 of the original shifted to the end.
await waitForHavingContents(page, [1, 2, "foobar", 3]);
await waitForTextToBe(
page,
"#viewsManagerStatusActionLabel",
`${FSI}1${PDI} selected`
);
})
);
});
});
describe("Drag-and-drop PDF merge", () => {

View File

@ -2745,60 +2745,4 @@ describe("Interaction", () => {
);
});
});
describe("in opt_demo.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("opt_demo.pdf", getSelector("19R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must expose the Opt export value of checkboxes and radio buttons", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForScripting(page);
// Selecting a button runs a script that writes the field's value into
// the read-only "result" field (19R). The appearance states are
// indices, so without the "Opt" mapping we'd see the index here.
const cases = [
["8R", "fruit = [Cherry]"],
["6R", "fruit = [りんご]"],
["10R", "shared = [same]"],
["12R", "agree = [I Agree to terms]"],
];
for (const [id, expected] of cases) {
await page.click(getSelector(id));
await page.waitForFunction(
`${getQuerySelector("19R")}.value === ${JSON.stringify(expected)}`
);
}
})
);
});
it("must expose the Opt export value when the parent has no Kids", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForScripting(page);
// The "veg" parent carries "Opt" but no "Kids", so its export value
// is resolved from the numeric appearance-state name.
await page.click(getSelector("22R"));
await page.waitForFunction(
`${getQuerySelector("19R")}.value === "veg = [Carrot]"`
);
await page.click(getSelector("23R"));
await page.waitForFunction(
`${getQuerySelector("19R")}.value === "veg = [Potato]"`
);
})
);
});
});
});

View File

@ -151,6 +151,19 @@ function closePages(pages) {
async function closeSinglePage(page) {
const coverage = await page.evaluate(async () => {
// Collect coverage data from the worker before the document is closed.
let workerCoverage = null;
const handler =
window.PDFViewerApplication.pdfDocument?._transport?.messageHandler;
if (handler) {
try {
workerCoverage = await handler.sendWithPromise(
"GetWorkerCoverage",
null
);
} catch {}
}
// Close the viewer gracefully, and clear local storage to avoid state
// leaking from one test to another.
await window.PDFViewerApplication.testingClose();
@ -162,14 +175,16 @@ async function closeSinglePage(page) {
// logic kicks in (see https://github.com/puppeteer/puppeteer/issues/2427).
return {
page: window.__coverage__ ? JSON.stringify(window.__coverage__) : null,
workers: window.__worker_coverage__?.map(c => JSON.stringify(c)) ?? null,
worker: workerCoverage ? JSON.stringify(workerCoverage) : null,
};
});
if (coverage.page) {
mergeCoverageIntoGlobal(JSON.parse(coverage.page));
}
coverage.workers?.map(c => mergeCoverageIntoGlobal(JSON.parse(c)));
if (coverage.worker) {
mergeCoverageIntoGlobal(JSON.parse(coverage.worker));
}
await page.close({ runBeforeUnload: false });
}

View File

@ -1455,72 +1455,6 @@ describe("PDF viewer", () => {
});
});
describe("Save/download disabled when supportsDownloading is false", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
null,
null,
{ supportsDownloading: false }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must hide the download buttons and skip save/download", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector("#downloadButton", { hidden: true });
await waitAndClick(page, "#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryDownload", { hidden: true });
const triggered = await page.evaluate(async () => {
const app = window.PDFViewerApplication;
const calls = [];
const saveDocument = app.pdfDocument.saveDocument.bind(
app.pdfDocument
);
app.pdfDocument.saveDocument = (...args) => {
calls.push("saveDocument");
return saveDocument(...args);
};
// Each bail-out path dispatches a TESTING-only "downloadskipped"
// event, so we can deterministically wait for all four attempts to
// run to completion.
let skipped = 0;
const allSkipped = new Promise(resolve => {
app.eventBus.on("downloadskipped", function listener() {
if (++skipped === 4) {
app.eventBus.off("downloadskipped", listener);
resolve();
}
});
});
await app.download();
await app.save();
await app.downloadOrSave();
app.eventBus.dispatch("download", { source: null });
await allSkipped;
return { calls, skipped, downloadManager: app.downloadManager };
});
expect(triggered.downloadManager)
.withContext(`In ${browserName}`)
.toBeNull();
expect(triggered.calls).withContext(`In ${browserName}`).toEqual([]);
expect(triggered.skipped).withContext(`In ${browserName}`).toBe(4);
})
);
});
});
describe("Pinch-zoom", () => {
let pages;

View File

@ -935,6 +935,3 @@
!text_field_own_canvas_calc.pdf
!bug1802506.pdf
!checkbox_no_appearance.pdf
!opt_demo.pdf
!bug1873345.pdf
!cff_bluescale_small_zones.pdf

Binary file not shown.

View File

@ -1 +0,0 @@
https://github.com/user-attachments/files/28985267/Gumbel.Distillation.pdf

View File

@ -1,133 +0,0 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Names << /JavaScript << /Names [(doc) 20 0 R] >> >> >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 540 360] /Contents 17 0 R /Resources << /Font << /Helv 18 0 R >> >> /Annots [6 0 R 7 0 R 8 0 R 10 0 R 11 0 R 12 0 R 19 0 R 22 0 R 23 0 R] >>
endobj
4 0 obj
<< /Fields [5 0 R 9 0 R 12 0 R 19 0 R 22 0 R 23 0 R] /DR << /Font << /Helv 18 0 R >> >> /DA (/Helv 0 Tf 0 g) /NeedAppearances false >>
endobj
5 0 obj
<< /FT /Btn /Ff 49152 /T (fruit) /V /1 /DV /1 /Kids [6 0 R 7 0 R 8 0 R] /Opt [<FEFF308A30933054> (Banane) (Cherry)] >>
endobj
6 0 obj
<< /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [50 300 70 320] /AP << /N << /0 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "fruit = [" + this.getField("fruit").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
endobj
7 0 obj
<< /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [50 270 70 290] /AP << /N << /1 13 0 R /Off 14 0 R >> >> /AS /1 /A << /S /JavaScript /JS (this.getField("result").value = "fruit = [" + this.getField("fruit").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
endobj
8 0 obj
<< /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [50 240 70 260] /AP << /N << /2 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "fruit = [" + this.getField("fruit").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
endobj
9 0 obj
<< /FT /Btn /Ff 49152 /T (shared) /V /Off /Kids [10 0 R 11 0 R] /Opt [(same) (same)] >>
endobj
10 0 obj
<< /Type /Annot /Subtype /Widget /Parent 9 0 R /Rect [50 178 70 198] /AP << /N << /0 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "shared = [" + this.getField("shared").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
endobj
11 0 obj
<< /Type /Annot /Subtype /Widget /Parent 9 0 R /Rect [50 148 70 168] /AP << /N << /1 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "shared = [" + this.getField("shared").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
endobj
12 0 obj
<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /Rect [50 86 70 106] /AP << /N << /0 15 0 R /Off 16 0 R >> >> /AS /Off /Opt [(I Agree to terms)] /A << /S /JavaScript /JS (this.getField("result").value = "agree = [" + this.getField("agree").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
endobj
13 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 20 20] /Resources << >> /Length 112 >>
stream
0 0 0 rg 16 10 m 16 13.31 13.31 16 10 16 c 6.69 16 4 13.31 4 10 c 4 6.69 6.69 4 10 4 c 13.31 4 16 6.69 16 10 c f
endstream
endobj
14 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 20 20] /Resources << >> /Length 0 >>
stream
endstream
endobj
15 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 20 20] /Resources << >> /Length 35 >>
stream
0 0 0 RG 2 w 4 10 m 9 5 l 16 16 l S
endstream
endobj
16 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 20 20] /Resources << >> /Length 0 >>
stream
endstream
endobj
17 0 obj
<< /Length 929 >>
stream
BT
/Helv 9 Tf 0 g
1 0 0 1 50 332 Tm (Radio "fruit": AP states = kid indices; Opt = real export values) Tj
1 0 0 1 75 306 Tm (state 0 -> Opt[0] = JP ringo [U+308A U+3093 U+3054]) Tj
1 0 0 1 75 276 Tm (state 1 -> Opt[1] = Banane [selected]) Tj
1 0 0 1 75 246 Tm (state 2 -> Opt[2] = Cherry) Tj
1 0 0 1 50 210 Tm (Radio "shared": both buttons share Opt = "same") Tj
1 0 0 1 75 184 Tm (state 0 -> "same") Tj
1 0 0 1 75 154 Tm (state 1 -> "same") Tj
1 0 0 1 50 118 Tm (Checkbox "agree": AP state "0", Opt = "I Agree to terms") Tj
1 0 0 1 75 92 Tm (agree) Tj
1 0 0 1 330 250 Tm (Last changed field -> value:) Tj
1 0 0 1 330 182 Tm (Radio "veg" \(parent has Opt but no /Kids\)) Tj
1 0 0 1 355 154 Tm (state 0 -> Opt[0] = Carrot) Tj
1 0 0 1 355 124 Tm (state 1 -> Opt[1] = Potato [selected]) Tj
1 0 0 1 50 26 Tm (Change any option; the box shows that field's value. pdf.js shows the index, Acrobat the Opt value.) Tj
ET
endstream
endobj
18 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
endobj
19 0 obj
<< /Type /Annot /Subtype /Widget /FT /Tx /Ff 1 /T (result) /Rect [330 218 530 243] /DA (/Helv 11 Tf 0 g) /V () /MK << /BC [0] /BG [0.95 0.95 0.95] >> /F 4 >>
endobj
20 0 obj
<< /S /JavaScript /JS (0;) >>
endobj
21 0 obj
<< /FT /Btn /Ff 49152 /T (veg) /V /1 /DV /1 /Opt [(Carrot) (Potato)] >>
endobj
22 0 obj
<< /Type /Annot /Subtype /Widget /Parent 21 0 R /Rect [330 148 350 168] /AP << /N << /0 13 0 R /Off 14 0 R >> >> /AS /Off /A << /S /JavaScript /JS (this.getField("result").value = "veg = [" + this.getField("veg").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
endobj
23 0 obj
<< /Type /Annot /Subtype /Widget /Parent 21 0 R /Rect [330 118 350 138] /AP << /N << /1 13 0 R /Off 14 0 R >> >> /AS /1 /A << /S /JavaScript /JS (this.getField("result").value = "veg = [" + this.getField("veg").value + "]";) >> /MK << /BC [0] /BG [1] >> /F 4 >>
endobj
xref
0 24
0000000000 65535 f
0000000015 00000 n
0000000133 00000 n
0000000190 00000 n
0000000390 00000 n
0000000540 00000 n
0000000674 00000 n
0000000954 00000 n
0000001232 00000 n
0000001512 00000 n
0000001615 00000 n
0000001898 00000 n
0000002181 00000 n
0000002493 00000 n
0000002734 00000 n
0000002861 00000 n
0000003024 00000 n
0000003151 00000 n
0000004132 00000 n
0000004230 00000 n
0000004404 00000 n
0000004450 00000 n
0000004538 00000 n
0000004818 00000 n
trailer
<< /Size 24 /Root 1 0 R >>
startxref
5096
%%EOF

View File

@ -555,11 +555,10 @@ window.onload = function () {
window.addEventListener("keydown", function keydown(event) {
if (event.which === 84) {
// 't' switch test/ref images
const val = document.querySelector(
'input[name="which"][value="0"]:checked'
)
? 1
: 0;
let val = 0;
if (document.querySelector('input[name="which"][value="0"]:checked')) {
val = 1;
}
document
.querySelector('input[name="which"][value="' + val + '"]')
.click();

View File

@ -28,7 +28,6 @@ import {
downloadManifestFiles,
verifyManifestFiles,
} from "./downloadutils.mjs";
import { execSync } from "child_process";
import fs from "fs";
import istanbulCoverage from "istanbul-lib-coverage";
import istanbulReportGenerator from "istanbul-reports";
@ -1096,13 +1095,9 @@ async function startBrowser({
}
async function startBrowsers({ baseUrl, initializeSession, numSessions = 1 }) {
// Install the browsers.
for (const browser of ["firefox@nightly", "chrome@stable"]) {
execSync(`npx puppeteer browsers install ${browser}`, { stdio: "inherit" });
}
// Remove old browser revisions from Puppeteer's cache. The commands above can
// download new browser revisions, so this prevents the disk from filling up.
// Remove old browser revisions from Puppeteer's cache. Updating Puppeteer can
// cause new browser revisions to be downloaded, so trimming the cache will
// prevent the disk from filling up over time.
await puppeteer.trimCache();
const browserNames = ["firefox", "chrome"];

View File

@ -31,14 +31,6 @@
"link": true,
"type": "other"
},
{
"id": "issue21458",
"file": "pdfs/issue21458.pdf",
"md5": "875754beca276ab63568e06fd49e8375",
"rounds": 1,
"link": true,
"type": "other"
},
{
"id": "filled-background-range",
"file": "pdfs/filled-background.pdf",
@ -3582,15 +3574,6 @@
"rounds": 1,
"type": "eq"
},
{
"id": "cmykjpeg-disable-isOffscreenCanvasSupported",
"file": "pdfs/cmykjpeg.pdf",
"md5": "85d162b48ce98503a382d96f574f70a2",
"link": false,
"rounds": 1,
"type": "eq",
"isOffscreenCanvasSupported": false
},
{
"id": "cmykjpeg_nowasm",
"file": "pdfs/cmykjpeg.pdf",
@ -6794,13 +6777,6 @@
"type": "eq",
"about": "Every blend mode that PDF supports."
},
{
"id": "bug1873345",
"file": "pdfs/bug1873345.pdf",
"md5": "03483b94a2c02ac5f98c17ccf14de8ea",
"rounds": 1,
"type": "eq"
},
{
"id": "transparency_group",
"file": "pdfs/transparency_group.pdf",

View File

@ -26,7 +26,6 @@ import {
AnnotationFieldFlag,
AnnotationFlag,
AnnotationType,
bytesToString,
DrawOPS,
OPS,
RenderingIntentFlag,
@ -42,7 +41,6 @@ import {
} from "./test_utils.js";
import { Dict, Name, Ref, RefSetCache } from "../../src/core/primitives.js";
import { Lexer, Parser } from "../../src/core/parser.js";
import { Catalog } from "../../src/core/catalog.js";
import { FlateStream } from "../../src/core/flate_stream.js";
import { PartialEvaluator } from "../../src/core/evaluator.js";
import { StringStream } from "../../src/core/stream.js";
@ -54,10 +52,9 @@ describe("annotation", function () {
constructor(params) {
this.pdfDocument = {
catalog: {
attachmentDictById: new Map(),
attachmentIdByRef: new RefSetCache(),
baseUrl: params.docBaseUrl || null,
getAttachmentIdForAnnotation(ref) {
return `attachmentRef:${ref.toString()}`;
},
},
};
this.evaluatorOptions = {
@ -933,55 +930,6 @@ describe("annotation", function () {
}
);
it(
"should correctly parse a URI action, with a (bad) relative URI and " +
'the "docBaseUrl" parameter specified',
async function () {
// Here the /URI entry is *incorrectly* specified as a Name,
// rather than a string (see issue 4159).
const actionStream = new StringStream(
`<<
/Type /Action
/S /URI
/URI /v#2findex.php#2fFile:Logo.png
>>`
);
const parser = new Parser({
lexer: new Lexer(actionStream),
xref: null,
});
const actionDict = parser.getObj();
const annotationDict = new Dict();
annotationDict.set("Type", Name.get("Annot"));
annotationDict.set("Subtype", Name.get("Link"));
annotationDict.set("A", actionDict);
const annotationRef = Ref.get(123, 0);
const xref = new XRefMock([
{ ref: annotationRef, data: annotationDict },
]);
const pdfManager = new PDFManagerMock({
docBaseUrl: "http://www.example.com/test/pdfs/qwerty.pdf",
});
const annotationGlobals =
await AnnotationFactory.createGlobals(pdfManager);
const { data } = await AnnotationFactory.create(
xref,
annotationRef,
annotationGlobals,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.LINK);
expect(data.url).toEqual(
"http://www.example.com/v/index.php/File:Logo.png"
);
expect(data.unsafeUrl).toEqual("/v/index.php/File:Logo.png");
expect(data.dest).toBeUndefined();
}
);
it("should correctly parse a GoTo action", async function () {
const actionDict = new Dict();
actionDict.set("Type", Name.get("Action"));
@ -2103,7 +2051,7 @@ describe("annotation", function () {
annotationStorage
);
expect(appearance).toEqual(
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 3.21 Tm" +
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 3.07 Tm" +
" 2.61 0 Td (a) Tj 8 0 Td (a) Tj 8.56 0 Td (\\() Tj" +
" 7.44 0 Td (a) Tj 8 0 Td (a) Tj" +
" 8.56 0 Td (\\)) Tj 7.44 0 Td (a) Tj" +
@ -2144,7 +2092,7 @@ describe("annotation", function () {
annotationStorage
);
expect(appearance).toEqual(
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 2.5 Tm" +
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 3.07 Tm" +
" 1.5 0 Td (\x30\x53) Tj 8 0 Td (\x30\x93) Tj 8 0 Td (\x30\x6b) Tj" +
" 8 0 Td (\x30\x61) Tj 8 0 Td (\x30\x6f) Tj" +
" 8 0 Td (\x4e\x16) Tj 8 0 Td (\x75\x4c) Tj" +
@ -2546,80 +2494,6 @@ describe("annotation", function () {
expect(data.exportValue).toEqual("Checked");
});
it("should handle checkboxes with an Opt export value", async function () {
// The appearance state is the index "0"; the real export value lives in
// the "Opt" array (e.g. for values that aren't valid Name objects).
buttonWidgetDict.set("V", Name.get("0"));
buttonWidgetDict.set("DV", Name.get("0"));
buttonWidgetDict.set("Opt", ["I Agree to terms"]);
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
normalAppearanceDict.set("Off", 0);
normalAppearanceDict.set("0", 1);
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
const buttonWidgetRef = Ref.get(124, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);
const { data } = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.checkBox).toEqual(true);
expect(data.radioButton).toEqual(false);
expect(data.exportValue).toEqual("I Agree to terms");
expect(data.fieldValue).toEqual("I Agree to terms");
expect(data.defaultFieldValue).toEqual("I Agree to terms");
});
it("should handle checkboxes with an Opt export value in the parent", async function () {
const buttonWidgetRef = Ref.get(124, 0);
const parentRef = Ref.get(125, 0);
const parentDict = new Dict();
parentDict.set("V", Name.get("CheckedState"));
parentDict.set("DV", Name.get("CheckedState"));
parentDict.set("Kids", [buttonWidgetRef]);
parentDict.set("T", "CheckboxGroup");
parentDict.set("Opt", ["I Agree to terms"]);
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
normalAppearanceDict.set("Off", 0);
normalAppearanceDict.set("CheckedState", 1);
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
buttonWidgetDict.set("Parent", parentRef);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
{ ref: parentRef, data: parentDict },
]);
buttonWidgetDict.xref = parentDict.xref = xref;
const { data } = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.checkBox).toEqual(true);
expect(data.radioButton).toEqual(false);
expect(data.exportValue).toEqual("I Agree to terms");
expect(data.fieldValue).toEqual("I Agree to terms");
expect(data.defaultFieldValue).toEqual("I Agree to terms");
});
it("should handle checkboxes without export value", async function () {
buttonWidgetDict.set("V", Name.get("Checked"));
buttonWidgetDict.set("DV", Name.get("Off"));
@ -3108,126 +2982,6 @@ describe("annotation", function () {
expect(data.buttonValue).toEqual("2");
});
it("should handle radio buttons with an Opt export value", async function () {
const parentDict = new Dict();
parentDict.set("V", Name.get("1"));
parentDict.set("Opt", ["Apple", "Banane", "Cherry"]);
const normalAppearanceStateDict = new Dict();
normalAppearanceStateDict.set("2", null);
const appearanceStatesDict = new Dict();
appearanceStatesDict.set("N", normalAppearanceStateDict);
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
buttonWidgetDict.set("Parent", parentDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
const buttonWidgetRef = Ref.get(124, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);
const { data } = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.radioButton).toEqual(true);
// The field value (parent "V" = "1") and this widget's own on-state ("2")
// are both mapped through "Opt" to their real export values.
expect(data.fieldValue).toEqual("Banane");
expect(data.buttonValue).toEqual("Cherry");
});
it("should not map non-canonical numeric radio states through Opt", async function () {
const parentDict = new Dict();
parentDict.set("V", Name.get("02"));
parentDict.set("Opt", ["Apple", "Banane", "Cherry"]);
const normalAppearanceStateDict = new Dict();
normalAppearanceStateDict.set("02", null);
const appearanceStatesDict = new Dict();
appearanceStatesDict.set("N", normalAppearanceStateDict);
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
buttonWidgetDict.set("Parent", parentDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
const buttonWidgetRef = Ref.get(124, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);
const { data } = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.radioButton).toEqual(true);
expect(data.fieldValue).toEqual("02");
expect(data.buttonValue).toEqual("02");
});
it("should handle radio buttons with non-numeric Opt export values", async function () {
const appleRef = Ref.get(122, 0);
const bananaRef = Ref.get(123, 0);
const cherryRef = Ref.get(124, 0);
const parentRef = Ref.get(125, 0);
const parentDict = new Dict();
parentDict.set("V", Name.get("BananaState"));
parentDict.set("Kids", [appleRef, bananaRef, cherryRef]);
parentDict.set("T", "Fruits");
parentDict.set("Opt", ["Apple", "Banane", "Cherry"]);
const createRadioDict = state => {
const radioDict = buttonWidgetDict.clone();
const normalAppearanceStateDict = new Dict();
normalAppearanceStateDict.set(state, null);
const appearanceStatesDict = new Dict();
appearanceStatesDict.set("N", normalAppearanceStateDict);
radioDict.set("Ff", AnnotationFieldFlag.RADIO);
radioDict.set("Parent", parentRef);
radioDict.set("AP", appearanceStatesDict);
return radioDict;
};
const appleDict = createRadioDict("AppleState");
const bananaDict = createRadioDict("BananaState");
const cherryDict = createRadioDict("CherryState");
const xref = new XRefMock([
{ ref: appleRef, data: appleDict },
{ ref: bananaRef, data: bananaDict },
{ ref: cherryRef, data: cherryDict },
{ ref: parentRef, data: parentDict },
]);
appleDict.xref =
bananaDict.xref =
cherryDict.xref =
parentDict.xref =
xref;
const { data } = await AnnotationFactory.create(
xref,
cherryRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.radioButton).toEqual(true);
expect(data.fieldValue).toEqual("Banane");
expect(data.buttonValue).toEqual("Cherry");
});
it("should handle radio buttons with a field value that's not an ASCII string", async function () {
const parentDict = new Dict();
const name = "\x91I=\x91\xf0\x93\xe0\x97e3";
@ -3502,113 +3256,6 @@ describe("annotation", function () {
expect(changes.size).toEqual(0);
});
it("should save radio buttons with Opt export values", async function () {
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
normalAppearanceDict.set("CheckedState", Ref.get(314, 0));
normalAppearanceDict.set("Off", Ref.get(271, 0));
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
buttonWidgetDict.set("AP", appearanceStatesDict);
const buttonWidgetRef = Ref.get(123, 0);
const parentRef = Ref.get(456, 0);
const parentDict = new Dict();
parentDict.set("V", Name.get("Off"));
parentDict.set("Kids", [buttonWidgetRef]);
parentDict.set("T", "RadioGroup");
parentDict.set("Opt", ["I Agree to terms"]);
buttonWidgetDict.set("Parent", parentRef);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
{ ref: parentRef, data: parentDict },
]);
parentDict.xref = xref;
buttonWidgetDict.xref = xref;
partialEvaluator.xref = xref;
const task = new WorkerTask("test save");
const annotation = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(annotation.data.buttonValue).toEqual("I Agree to terms");
const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: true });
const changes = new RefSetCache();
await annotation.save(partialEvaluator, task, annotationStorage, changes);
const data = await writeChanges(changes, xref);
expect(data.length).toEqual(2);
const [radioData, parentData] = data;
radioData.data = radioData.data.replace(/\(D:\d+\)/, "(date)");
expect(radioData.ref).toEqual(Ref.get(123, 0));
expect(radioData.data).toEqual(
"123 0 obj\n" +
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 32768 " +
"/AP << /N << /CheckedState 314 0 R /Off 271 0 R>>>> " +
"/Parent 456 0 R /AS /CheckedState /M (date)>>\nendobj\n"
);
expect(parentData.ref).toEqual(Ref.get(456, 0));
expect(parentData.data).toEqual(
"456 0 obj\n<< /V /CheckedState /Kids [123 0 R] /T (RadioGroup) " +
"/Opt [(I Agree to terms)]>>\nendobj\n"
);
});
it("should save checkboxes with Opt export values", async function () {
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
normalAppearanceDict.set("CheckedState", Ref.get(314, 0));
normalAppearanceDict.set("Off", Ref.get(271, 0));
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
buttonWidgetDict.set("V", Name.get("Off"));
buttonWidgetDict.set("Opt", ["I Agree to terms"]);
const buttonWidgetRef = Ref.get(123, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);
buttonWidgetDict.xref = xref;
partialEvaluator.xref = xref;
const task = new WorkerTask("test save");
const annotation = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(annotation.data.exportValue).toEqual("I Agree to terms");
const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: true });
const changes = new RefSetCache();
await annotation.save(partialEvaluator, task, annotationStorage, changes);
const [data] = await writeChanges(changes, xref);
data.data = data.data.replace(/\(D:\d+\)/, "(date)");
expect(data.ref).toEqual(Ref.get(123, 0));
expect(data.data).toEqual(
"123 0 obj\n" +
"<< /Type /Annot /Subtype /Widget /FT /Btn " +
"/AP << /N << /CheckedState 314 0 R /Off 271 0 R>>>> " +
"/V /CheckedState /Opt [(I Agree to terms)] " +
"/AS /CheckedState /M (date)>>\nendobj\n"
);
});
it("should save radio buttons without a field value", async function () {
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
@ -4455,17 +4102,20 @@ describe("annotation", function () {
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT);
// The file-spec is an indirect object, so its reference is encoded in the
// id and re-fetched on demand.
expect(data.fileId).toEqual("attachmentRef:19R");
expect(data.fileId.startsWith("annotation:")).toEqual(true);
expect(data.file).toEqual({
rawFilename: "Test.txt",
filename: "Test.txt",
description: "abc",
});
// Content lookup and reading requires a bigger mock than used here.
expect(
pdfManagerMock.pdfDocument.catalog.attachmentDictById.has(data.fileId)
).toEqual(true);
});
it("should re-derive an inline file attachment from its embedded stream", async function () {
it("should reuse the attachment NameTree id for referenced files", async function () {
const fileStream = new StringStream(
"<<\n" +
"/Type /EmbeddedFile\n" +
@ -4481,36 +4131,41 @@ describe("annotation", function () {
allowStreams: true,
});
const fileStreamRef = Ref.get(18, 0);
const fileStreamRef = Ref.get(28, 0);
const fileStreamDict = parser.getObj();
const embeddedFileDict = new Dict();
embeddedFileDict.set("F", fileStreamRef);
// The file-spec is inline (not an indirect object), so the embedded-file
// stream's reference is encoded in the id instead.
const fileSpecRef = Ref.get(29, 0);
const fileSpecDict = new Dict();
fileSpecDict.set("Type", Name.get("Filespec"));
fileSpecDict.set("Desc", "abc");
fileSpecDict.set("EF", embeddedFileDict);
fileSpecDict.set("UF", "Test.txt");
const fileAttachmentRef = Ref.get(20, 0);
const fileAttachmentRef = Ref.get(30, 0);
const fileAttachmentDict = new Dict();
fileAttachmentDict.set("Type", Name.get("Annot"));
fileAttachmentDict.set("Subtype", Name.get("FileAttachment"));
fileAttachmentDict.set("FS", fileSpecDict);
fileAttachmentDict.set("FS", fileSpecRef);
fileAttachmentDict.set("T", "Topic");
fileAttachmentDict.set("Contents", "Test.txt");
const xref = new XRefMock([
{ ref: fileStreamRef, data: fileStreamDict },
{ ref: fileSpecRef, data: fileSpecDict },
{ ref: fileAttachmentRef, data: fileAttachmentDict },
]);
embeddedFileDict.assignXref(xref);
fileSpecDict.assignXref(xref);
fileAttachmentDict.assignXref(xref);
pdfManagerMock.pdfDocument.catalog.attachmentIdByRef.put(
fileSpecRef,
"Test.txt"
);
const { data } = await AnnotationFactory.create(
xref,
fileAttachmentRef,
@ -4518,81 +4173,17 @@ describe("annotation", function () {
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT);
expect(data.fileId).toEqual("attachmentRef:18R");
expect(data.fileId).toEqual("Test.txt");
expect(data.file).toEqual({
rawFilename: "Test.txt",
filename: "Test.txt",
description: "abc",
});
});
it("should keep named attachment ids distinct from annotation attachment ids", function () {
const annotationStreamRef = Ref.get(18, 0);
const annotationStreamDict = new Dict();
annotationStreamDict.set("Type", Name.get("EmbeddedFile"));
const annotationStream = new StringStream(
"Annotation attachment",
annotationStreamDict
);
const namedStreamRef = Ref.get(21, 0);
const namedStreamDict = new Dict();
namedStreamDict.set("Type", Name.get("EmbeddedFile"));
const namedStream = new StringStream("Named attachment", namedStreamDict);
const namedEmbeddedFileDict = new Dict();
namedEmbeddedFileDict.set("F", namedStreamRef);
const namedFileSpecRef = Ref.get(22, 0);
const namedFileSpecDict = new Dict();
namedFileSpecDict.set("Type", Name.get("Filespec"));
namedFileSpecDict.set("EF", namedEmbeddedFileDict);
namedFileSpecDict.set("F", "Named.txt");
const pagesDict = new Dict();
const embeddedFilesDict = new Dict();
embeddedFilesDict.set("Names", ["attachmentRef:18R", namedFileSpecRef]);
const namesDict = new Dict();
namesDict.set("EmbeddedFiles", embeddedFilesDict);
const catalogDict = new Dict();
catalogDict.set("Pages", pagesDict);
catalogDict.set("Names", namesDict);
const xref = new XRefMock([
{ ref: annotationStreamRef, data: annotationStream },
{ ref: namedStreamRef, data: namedStream },
{ ref: namedFileSpecRef, data: namedFileSpecDict },
]);
xref.getCatalogObj = () => catalogDict;
for (const dict of [
annotationStreamDict,
namedStreamDict,
namedEmbeddedFileDict,
namedFileSpecDict,
pagesDict,
embeddedFilesDict,
namesDict,
catalogDict,
]) {
dict.assignXref(xref);
}
const catalog = new Catalog(pdfManagerMock, xref);
const annotationId =
catalog.getAttachmentIdForAnnotation(annotationStreamRef);
expect(annotationId).toEqual("attachmentRef:18R-1");
// File should not be added as its already referenced in the `NameTree`.
expect(
bytesToString(catalog.attachmentContent("attachmentRef:18R"))
).toEqual("Named attachment");
expect(bytesToString(catalog.attachmentContent(annotationId))).toEqual(
"Annotation attachment"
);
// An unknown id resolves to no content.
expect(catalog.attachmentContent("nonexistent")).toEqual(null);
pdfManagerMock.pdfDocument.catalog.attachmentDictById.has(data.fileId)
).toEqual(false);
});
});

View File

@ -3915,34 +3915,6 @@ describe("api", function () {
await loadingTask.destroy();
});
it("gets FileAttachment annotation content that stays readable after cleanup", async function () {
// The embedded files are reachable only via the annotations (no catalog
// `/Names` tree), so their content must survive `cleanup` by being
// re-derivable from the xref.
const loadingTask = getDocument(buildGetDocumentParams("bug1230933.pdf"));
const pdfDoc = await loadingTask.promise;
const pdfPage = await pdfDoc.getPage(1);
const annotations = await pdfPage.getAnnotations();
const fileAnnotation = annotations.find(
a => a.annotationType === AnnotationType.FILEATTACHMENT
);
const { fileId } = fileAnnotation;
expect(fileId.startsWith("attachmentRef:")).toEqual(true);
const before = await pdfDoc.getAttachmentContent(fileId);
expect(before).toBeInstanceOf(Uint8Array);
expect(before.length).toEqual(234414);
await pdfDoc.cleanup();
const after = await pdfDoc.getAttachmentContent(fileId);
expect(after).toBeInstanceOf(Uint8Array);
expect(after.length).toEqual(234414);
await loadingTask.destroy();
});
it("gets annotations containing /Launch action with /FileSpec dictionary (issue 17846)", async function () {
const loadingTask = getDocument(buildGetDocumentParams("issue17846.pdf"));
const pdfDoc = await loadingTask.promise;

View File

@ -22,9 +22,6 @@ import {
CFFStrings,
CFFTopDict,
} from "../../src/core/cff_parser.js";
import { DefaultFileReaderFactory, TEST_PDFS_PATH } from "./test_utils.js";
import { PDFDocument } from "../../src/core/document.js";
import { Ref } from "../../src/core/primitives.js";
import { SEAC_ANALYSIS_ENABLED } from "../../src/core/fonts_utils.js";
import { Stream } from "../../src/core/stream.js";
@ -360,58 +357,6 @@ describe("CFFParser", function () {
);
});
it("preserves the BlueScale of an embedded CID font with small zones", async function () {
// The embedded CID-keyed CFF pairs a near-default BlueScale of 0.037 with
// 12-unit zones; clamping it up to the lower bound breaks rendering on
// macOS only, so it's guarded here rather than with a reference image.
const data = await DefaultFileReaderFactory.fetch({
path: TEST_PDFS_PATH + "cff_bluescale_small_zones.pdf",
});
const pdfManager = {
evaluatorOptions: { isOffscreenCanvasSupported: false },
password: null,
};
const pdfDocument = new PDFDocument(pdfManager, new Stream(data));
pdfDocument.parseStartXRef();
pdfDocument.xref.parse();
// Object 8 is the `/FontFile3` (`/CIDFontType0C`) stream in the fixture.
const fontProgram = pdfDocument.xref.fetch(Ref.get(8, 0)).getBytes();
const embeddedCff = new CFFParser(
new Stream(fontProgram),
{},
SEAC_ANALYSIS_ENABLED
).parse();
expect(embeddedCff.isCIDFont).toEqual(true);
expect(embeddedCff.fdArray[0].privateDict.getByName("BlueScale")).toEqual(
0.037
);
});
it("clamps BlueScale to a short decimal so the recompiled operand stays compact", function () {
// maxZoneHeight = 13 gives lower bound (0.5 / 13 = 0.038461538461538464)
// which is too long (issue 21466).
cff.topDict.privateDict = new CFFPrivateDict(cff.strings);
cff.topDict.privateDict.setByName(
"BlueValues",
[-13, 13, 530, 13, 220, 13, 30, 13]
);
cff.topDict.privateDict.setByName("BlueScale", 0.01);
cff.topDict.setByName("Private", [0, 0]);
const fontDataShortBlueScale = new CFFCompiler(cff).compile();
const reparsedCff = new CFFParser(
new Stream(fontDataShortBlueScale),
{},
SEAC_ANALYSIS_ENABLED
).parse();
const blueScale = reparsedCff.topDict.privateDict.getByName("BlueScale");
expect(blueScale).toEqual(0.03847);
expect(new CFFCompiler(cff).encodeFloat(blueScale).length).toBeLessThan(6);
});
it("refuses to add topDict key with invalid value (bug 1068432)", function () {
const topDict = cff.topDict;
const defaultValue = topDict.getByName("UnderlinePosition");

View File

@ -37,6 +37,7 @@ describe("util", function () {
expect(exception.message).toEqual("Something went wrong");
expect(exception.name).toEqual("DerivedException");
expect(exception.foo).toEqual("bar");
expect(exception.stack).toContain("BaseExceptionClosure");
});
});

View File

@ -332,7 +332,10 @@ class AnnotationLayerBuilder {
let linkAreaRects;
for (const annotation of this.#annotations) {
if (annotation.annotationType !== AnnotationType.LINK) {
if (
annotation.annotationType !== AnnotationType.LINK ||
!annotation.url
) {
continue;
}
// TODO: Add a test case to verify that we can find the intersection

View File

@ -385,7 +385,6 @@ const PDFViewerApplication = {
maxCanvasPixels: x => parseInt(x, 10),
spreadModeOnLoad: x => parseInt(x, 10),
supportsCaretBrowsingMode: x => x === "true",
supportsDownloading: x => x === "true",
viewerCssTheme: x => parseInt(x, 10),
forcePageColors: x => x === "true",
pageColorsBackground: x => x,
@ -435,16 +434,7 @@ const PDFViewerApplication = {
ignoreDestinationZoom: AppOptions.get("ignoreDestinationZoom"),
}));
const supportsDownloading = AppOptions.get("supportsDownloading");
const downloadManager = (this.downloadManager = supportsDownloading
? new DownloadManager()
: null);
if (appConfig.secondaryToolbar?.downloadButton) {
appConfig.secondaryToolbar.downloadButton.hidden = !supportsDownloading;
}
if (appConfig.toolbar?.download) {
appConfig.toolbar.download.hidden = !supportsDownloading;
}
const downloadManager = (this.downloadManager = new DownloadManager());
const findController = (this.findController = new PDFFindController({
linkService,
@ -1175,23 +1165,6 @@ const PDFViewerApplication = {
// Ignoring errors, to ensure that document closing won't break.
}
}
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("COVERAGE")) {
// Collect coverage data from the worker before the document is closed.
//
// Note that `PDFViewerApplication.open` may be invoked multiple times
// during an integration-test (see e.g. the "Merge PDF" tests).
const handler = this.pdfDocument?._transport?.messageHandler;
if (handler) {
try {
const workerCoverage = await handler.sendWithPromise(
"GetWorkerCoverage",
null
);
(window.__worker_coverage__ ??= []).push(workerCoverage);
} catch {}
}
}
const promises = [];
promises.push(this.pdfLoadingTask.destroy());
@ -1321,13 +1294,6 @@ const PDFViewerApplication = {
},
async download() {
if (!this.downloadManager) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
this.eventBus.dispatch("downloadskipped", { source: this });
}
return;
}
let data;
try {
data = await (this.pdfDocument
@ -1340,13 +1306,6 @@ const PDFViewerApplication = {
},
async save() {
if (!this.downloadManager) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
this.eventBus.dispatch("downloadskipped", { source: this });
}
return;
}
if (this._saveInProgress) {
return;
}
@ -1378,13 +1337,6 @@ const PDFViewerApplication = {
},
async downloadOrSave() {
if (!this.downloadManager) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
this.eventBus.dispatch("downloadskipped", { source: this });
}
return;
}
// In the Firefox case, this method MUST always trigger a download.
// When the user is closing a modified and unsaved document, we display a
// prompt asking for saving or not. In case they save, we must wait for
@ -2473,9 +2425,6 @@ const PDFViewerApplication = {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
return;
}
if (!this.downloadManager) {
return;
}
if (!this.pdfDocument) {
return;
}

View File

@ -102,11 +102,6 @@ const defaultOptions = {
value: true,
kind: OptionKind.BROWSER,
},
supportsDownloading: {
/** @type {boolean} */
value: true,
kind: OptionKind.BROWSER,
},
supportsIntegratedFind: {
/** @type {boolean} */
value: false,

View File

@ -57,7 +57,6 @@ class PasswordPrompt {
this.input.addEventListener("keydown", e => {
if (e.keyCode === /* Enter = */ 13) {
this.#verify();
e.preventDefault();
}
});

View File

@ -126,7 +126,7 @@ class PDFAttachmentViewer extends BaseTreeViewer {
: fallbackContent;
if (content) {
this.downloadManager?.openOrDownloadData(content, filename);
this.downloadManager.openOrDownloadData(content, filename);
}
};

View File

@ -155,10 +155,7 @@ class PDFOutlineViewer extends BaseTreeViewer {
const content = await linkService.getAttachmentContent(attachmentId);
if (content) {
this.downloadManager?.openOrDownloadData(
content,
attachment.filename
);
this.downloadManager.openOrDownloadData(content, attachment.filename);
}
};