mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-26 10:05:47 +02:00
Compare commits
No commits in common. "1ddf6449ac4e5c8157249dd0cd584112104fdd60" and "86a18bd5fec4261f28485f3ac854f118d04a5671" have entirely different histories.
1ddf6449ac
...
86a18bd5fe
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -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
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
{
|
||||
"chrome": {
|
||||
"skipDownload": true,
|
||||
"version": "stable"
|
||||
},
|
||||
"chrome-headless-shell": {
|
||||
"skipDownload": true
|
||||
"skipDownload": false
|
||||
},
|
||||
"firefox": {
|
||||
"skipDownload": true,
|
||||
"skipDownload": false,
|
||||
"version": "nightly"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 }개 페이지 잘림
|
||||
|
||||
@ -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
73
package-lock.json
generated
@ -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": {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 that’s 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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 don’t 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -343,6 +343,10 @@ class HighlightOutline extends Outline {
|
||||
get box() {
|
||||
return this.#box;
|
||||
}
|
||||
|
||||
get classNamesForOutlining() {
|
||||
return ["highlightOutline"];
|
||||
}
|
||||
}
|
||||
|
||||
class FreeHighlightOutliner extends FreeDrawOutliner {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -139,9 +139,4 @@ function grayToRGBA(src, dest) {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
convertBlackAndWhiteToRGBA,
|
||||
convertRGBToRGBA,
|
||||
convertToRGBA,
|
||||
grayToRGBA,
|
||||
};
|
||||
export { convertBlackAndWhiteToRGBA, convertToRGBA, grayToRGBA };
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 1–2 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 1–2 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", () => {
|
||||
|
||||
@ -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]"`
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
3
test/pdfs/.gitignore
vendored
3
test/pdfs/.gitignore
vendored
@ -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.
Binary file not shown.
@ -1 +0,0 @@
|
||||
https://github.com/user-attachments/files/28985267/Gumbel.Distillation.pdf
|
||||
@ -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
|
||||
@ -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();
|
||||
|
||||
@ -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"];
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 it’s 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
53
web/app.js
53
web/app.js
@ -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;
|
||||
}
|
||||
|
||||
@ -102,11 +102,6 @@ const defaultOptions = {
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsDownloading: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER,
|
||||
},
|
||||
supportsIntegratedFind: {
|
||||
/** @type {boolean} */
|
||||
value: false,
|
||||
|
||||
@ -57,7 +57,6 @@ class PasswordPrompt {
|
||||
this.input.addEventListener("keydown", e => {
|
||||
if (e.keyCode === /* Enter = */ 13) {
|
||||
this.#verify();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ class PDFAttachmentViewer extends BaseTreeViewer {
|
||||
: fallbackContent;
|
||||
|
||||
if (content) {
|
||||
this.downloadManager?.openOrDownloadData(content, filename);
|
||||
this.downloadManager.openOrDownloadData(content, filename);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user