mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-08-02 20:37:21 +02:00
Compare commits
64 Commits
86a18bd5fe
...
1ddf6449ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ddf6449ac | ||
|
|
28a7606c14 | ||
|
|
9c9b465fd2 | ||
|
|
8ebc2382e3 | ||
|
|
38daede697 | ||
|
|
86b901fcde | ||
|
|
1d8e952062 | ||
|
|
018ba66228 | ||
|
|
a911ce22e5 | ||
|
|
a46ee2b647 | ||
|
|
bd6541864b | ||
|
|
124228e318 | ||
|
|
bff30726fa | ||
|
|
34516bcec3 | ||
|
|
bade1f3190 | ||
|
|
00e1aabe93 | ||
|
|
dfa673290b | ||
|
|
59df671552 | ||
|
|
87361aa094 | ||
|
|
786019eb0d | ||
|
|
e20c810dd4 | ||
|
|
07d4c1018a | ||
|
|
e74be44919 | ||
|
|
b4b0a3fa04 | ||
|
|
a443a635a1 | ||
|
|
3956ac1b39 | ||
|
|
9d9fb06d7f | ||
|
|
187c22126a | ||
|
|
ece1e2ed0c | ||
|
|
2ed018ec2d | ||
|
|
eae42379f2 | ||
|
|
d28030f838 | ||
|
|
cbefb334fd | ||
|
|
5432642250 | ||
|
|
f0dc2166ab | ||
|
|
9ed97a859f | ||
|
|
8bc13d502b | ||
|
|
36eae2c978 | ||
|
|
bc99fc0678 | ||
|
|
fdeed2af5e | ||
|
|
082ad21387 | ||
|
|
6cdd3c19fd | ||
|
|
6c0ad865c6 | ||
|
|
7f7b38b424 | ||
|
|
7d3ec9da1e | ||
|
|
67e6ff0090 | ||
|
|
2550b91be9 | ||
|
|
d9e4cc5f65 | ||
|
|
f781ac33da | ||
|
|
3a09329113 | ||
|
|
0ea67ed96f | ||
|
|
92bd2dbb38 | ||
|
|
26b4206d87 | ||
|
|
6bfefa53da | ||
|
|
66c22b1fc5 | ||
|
|
8560125056 | ||
|
|
ed1b2f91be | ||
|
|
bfcafbc004 | ||
|
|
2dc73ad2a7 | ||
|
|
feec28583d | ||
|
|
1373aa4a48 | ||
|
|
e1c930adfe | ||
|
|
d305b542df | ||
|
|
55c8516944 |
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -24,13 +24,13 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
|
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: security-and-quality
|
queries: security-and-quality
|
||||||
|
|
||||||
- name: Autobuild CodeQL
|
- name: Autobuild CodeQL
|
||||||
uses: github/codeql-action/autobuild@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
|
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
|
|
||||||
- name: Perform CodeQL analysis
|
- name: Perform CodeQL analysis
|
||||||
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
|
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
{
|
{
|
||||||
"chrome": {
|
"chrome": {
|
||||||
"skipDownload": false
|
"skipDownload": true,
|
||||||
|
"version": "stable"
|
||||||
|
},
|
||||||
|
"chrome-headless-shell": {
|
||||||
|
"skipDownload": true
|
||||||
},
|
},
|
||||||
"firefox": {
|
"firefox": {
|
||||||
"skipDownload": false,
|
"skipDownload": true,
|
||||||
"version": "nightly"
|
"version": "nightly"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1030,10 +1030,7 @@ function createBuildNumber(done) {
|
|||||||
const version = config.versionPrefix + buildNumber;
|
const version = config.versionPrefix + buildNumber;
|
||||||
|
|
||||||
exec('git log --format="%h" -n 1', function (err2, stdout2, stderr2) {
|
exec('git log --format="%h" -n 1', function (err2, stdout2, stderr2) {
|
||||||
let buildCommit = "";
|
const buildCommit = !err2 ? stdout2.replace("\n", "") : "";
|
||||||
if (!err2) {
|
|
||||||
buildCommit = stdout2.replace("\n", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
createStringSource(
|
createStringSource(
|
||||||
"version.json",
|
"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-copy-button-label = 복사
|
||||||
pdfjs-views-manager-pages-status-cut-button-label = 잘라내기
|
pdfjs-views-manager-pages-status-cut-button-label = 잘라내기
|
||||||
pdfjs-views-manager-pages-status-delete-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:
|
# Variables:
|
||||||
# $count (Number) - the number of selected pages to be cut.
|
# $count (Number) - the number of selected pages to be cut.
|
||||||
pdfjs-views-manager-status-undo-cut-label = { $count }개 페이지 잘림
|
pdfjs-views-manager-status-undo-cut-label = { $count }개 페이지 잘림
|
||||||
|
|||||||
@ -479,6 +479,21 @@ pdfjs-editor-add-signature-cancel-button = റദ്ദാക്കുക
|
|||||||
pdfjs-editor-add-signature-add-button = ചേൎക്കുക
|
pdfjs-editor-add-signature-add-button = ചേൎക്കുക
|
||||||
pdfjs-editor-edit-signature-update-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
|
## Main menu for adding/removing signatures
|
||||||
|
|
||||||
pdfjs-editor-delete-signature-button1 =
|
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-perfectionist": "^5.9.0",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"eslint-plugin-regexp": "^3.1.0",
|
"eslint-plugin-regexp": "^3.1.0",
|
||||||
"eslint-plugin-unicorn": "^65.0.1",
|
"eslint-plugin-unicorn": "^66.0.0",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"gulp": "^5.0.1",
|
"gulp": "^5.0.1",
|
||||||
"gulp-cli": "^3.1.0",
|
"gulp-cli": "^3.1.0",
|
||||||
@ -5282,42 +5282,43 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn": {
|
"node_modules/eslint-plugin-unicorn": {
|
||||||
"version": "65.0.1",
|
"version": "66.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-65.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-66.0.0.tgz",
|
||||||
"integrity": "sha512-daCrQrgxOoOz2uMPWB3Y3vvv/5q+ncwICI8IjoebiwtW87CaY4tAN5EEiRXTYVnf7qi1v1BGBdHOSnZLV0rx6A==",
|
"integrity": "sha512-+ywdy8T3foyZ2t3nRBujGa3vfOVMobHIi5iLB0L+fogdVO3EiUJ4BAyIacogWytnweLw3hgT70LQL9KoKTY/kA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.28.5",
|
"@babel/helper-validator-identifier": "^7.29.7",
|
||||||
"@eslint-community/eslint-utils": "^4.9.1",
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
|
"browserslist": "^4.28.2",
|
||||||
"change-case": "^5.4.4",
|
"change-case": "^5.4.4",
|
||||||
"ci-info": "^4.4.0",
|
"ci-info": "^4.4.0",
|
||||||
"core-js-compat": "^3.49.0",
|
"core-js-compat": "^3.49.0",
|
||||||
"detect-indent": "^7.0.2",
|
"detect-indent": "^7.0.2",
|
||||||
"find-up-simple": "^1.0.1",
|
"find-up-simple": "^1.0.1",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.6.0",
|
||||||
"indent-string": "^5.0.0",
|
"indent-string": "^5.0.0",
|
||||||
"is-builtin-module": "^5.0.0",
|
"is-builtin-module": "^5.0.0",
|
||||||
"jsesc": "^3.1.0",
|
"jsesc": "^3.1.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"regjsparser": "^0.13.0",
|
"regjsparser": "^0.13.1",
|
||||||
"semver": "^7.7.4",
|
"semver": "^7.8.4",
|
||||||
"strip-indent": "^4.1.1"
|
"strip-indent": "^4.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.10.0 || >=21.0.0"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">=9.38.0"
|
"eslint": ">=10.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/semver": {
|
"node_modules/eslint-plugin-unicorn/node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
@ -7294,10 +7295,20 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/linkify-it": {
|
"node_modules/linkify-it": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz",
|
||||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
"integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"uc.micro": "^2.0.0"
|
"uc.micro": "^2.0.0"
|
||||||
@ -7475,15 +7486,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/markdown-it": {
|
"node_modules/markdown-it": {
|
||||||
"version": "14.1.1",
|
"version": "14.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz",
|
||||||
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
"integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/markdown-it"
|
||||||
|
}
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1",
|
"argparse": "^2.0.1",
|
||||||
"entities": "^4.4.0",
|
"entities": "^4.4.0",
|
||||||
"linkify-it": "^5.0.0",
|
"linkify-it": "^5.0.1",
|
||||||
"mdurl": "^2.0.0",
|
"mdurl": "^2.0.0",
|
||||||
"punycode.js": "^2.3.1",
|
"punycode.js": "^2.3.1",
|
||||||
"uc.micro": "^2.1.0"
|
"uc.micro": "^2.1.0"
|
||||||
@ -8779,9 +8800,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/regjsparser": {
|
"node_modules/regjsparser": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz",
|
||||||
"integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
|
"integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -10645,9 +10666,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "7.24.3",
|
"version": "7.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.3.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
|
||||||
"integrity": "sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA==",
|
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
"eslint-plugin-perfectionist": "^5.9.0",
|
"eslint-plugin-perfectionist": "^5.9.0",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"eslint-plugin-regexp": "^3.1.0",
|
"eslint-plugin-regexp": "^3.1.0",
|
||||||
"eslint-plugin-unicorn": "^65.0.1",
|
"eslint-plugin-unicorn": "^66.0.0",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"gulp": "^5.0.1",
|
"gulp": "^5.0.1",
|
||||||
"gulp-cli": "^3.1.0",
|
"gulp-cli": "^3.1.0",
|
||||||
|
|||||||
@ -48,6 +48,7 @@ import {
|
|||||||
lookupMatrix,
|
lookupMatrix,
|
||||||
lookupNormalRect,
|
lookupNormalRect,
|
||||||
lookupRect,
|
lookupRect,
|
||||||
|
MissingDataException,
|
||||||
numberToString,
|
numberToString,
|
||||||
RESOURCES_KEYS_OPERATOR_LIST,
|
RESOURCES_KEYS_OPERATOR_LIST,
|
||||||
RESOURCES_KEYS_TEXT_CONTENT,
|
RESOURCES_KEYS_TEXT_CONTENT,
|
||||||
@ -1208,6 +1209,9 @@ class Annotation {
|
|||||||
/* resources = */ null
|
/* resources = */ null
|
||||||
);
|
);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
|
if (ex instanceof MissingDataException) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
warn(`#setOptionalContent: ${ex}`);
|
warn(`#setOptionalContent: ${ex}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2574,9 +2578,6 @@ class WidgetAnnotation extends Annotation {
|
|||||||
fontSize,
|
fontSize,
|
||||||
totalWidth,
|
totalWidth,
|
||||||
totalHeight,
|
totalHeight,
|
||||||
defaultVPadding,
|
|
||||||
descent,
|
|
||||||
lineHeight,
|
|
||||||
alignment,
|
alignment,
|
||||||
bidi(lines[0]).dir === "rtl",
|
bidi(lines[0]).dir === "rtl",
|
||||||
annotationStorage
|
annotationStorage
|
||||||
@ -2938,9 +2939,6 @@ class TextWidgetAnnotation extends WidgetAnnotation {
|
|||||||
fontSize,
|
fontSize,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
vPadding,
|
|
||||||
descent,
|
|
||||||
lineHeight,
|
|
||||||
alignment,
|
alignment,
|
||||||
isRTL,
|
isRTL,
|
||||||
annotationStorage
|
annotationStorage
|
||||||
@ -2977,11 +2975,18 @@ class TextWidgetAnnotation extends WidgetAnnotation {
|
|||||||
previousWidth = glyphWidth;
|
previousWidth = glyphWidth;
|
||||||
}
|
}
|
||||||
const renderedComb = buf.join(" ");
|
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 (
|
return (
|
||||||
`/Tx BMC q ${colors}BT ` +
|
`/Tx BMC q ${colors}BT ` +
|
||||||
defaultAppearance +
|
defaultAppearance +
|
||||||
` 1 0 0 1 ${numberToString(hShift)} ${numberToString(
|
` 1 0 0 1 ${numberToString(hShift)} ${numberToString(
|
||||||
vPadding + descent
|
vShift
|
||||||
)} Tm ${renderedComb}` +
|
)} Tm ${renderedComb}` +
|
||||||
" ET Q EMC"
|
" ET Q EMC"
|
||||||
);
|
);
|
||||||
@ -3346,7 +3351,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||||||
value: value ? this.data.exportValue : "",
|
value: value ? this.data.exportValue : "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const name = Name.get(value ? this.data.exportValue : "Off");
|
const name = Name.get(value ? this._onStateName : "Off");
|
||||||
this.setValue(dict, name, evaluator.xref, changes);
|
this.setValue(dict, name, evaluator.xref, changes);
|
||||||
|
|
||||||
dict.set("AS", name);
|
dict.set("AS", name);
|
||||||
@ -3406,7 +3411,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||||||
value: value ? this.data.buttonValue : "",
|
value: value ? this.data.buttonValue : "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const name = Name.get(value ? this.data.buttonValue : "Off");
|
const name = Name.get(value ? this._onStateName : "Off");
|
||||||
if (value) {
|
if (value) {
|
||||||
this.setValue(dict, name, evaluator.xref, changes);
|
this.setValue(dict, name, evaluator.xref, changes);
|
||||||
}
|
}
|
||||||
@ -3485,6 +3490,107 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||||||
this._streams.push(this.checkedAppearance);
|
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) {
|
_processCheckBox(params) {
|
||||||
const customAppearance = params.dict.get("AP");
|
const customAppearance = params.dict.get("AP");
|
||||||
let normalAppearance =
|
let normalAppearance =
|
||||||
@ -3527,15 +3633,33 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||||||
exportValues.push("Off", otherYes);
|
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,
|
// 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.
|
// see e.g. bug1720411.pdf where it's an *empty* Name-instance.
|
||||||
if (!exportValues.includes(this.data.fieldValue)) {
|
if (
|
||||||
|
!exportValues.includes(this.data.fieldValue) &&
|
||||||
|
this.data.fieldValue !== this.data.exportValue
|
||||||
|
) {
|
||||||
this.data.fieldValue = "Off";
|
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
|
||||||
|
);
|
||||||
|
|
||||||
this.data.exportValue = exportValues[1];
|
const checkedAppearance = normalAppearance?.get(onState);
|
||||||
|
|
||||||
const checkedAppearance = normalAppearance?.get(this.data.exportValue);
|
|
||||||
this.checkedAppearance =
|
this.checkedAppearance =
|
||||||
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
|
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
|
||||||
const uncheckedAppearance = normalAppearance?.get("Off");
|
const uncheckedAppearance = normalAppearance?.get("Off");
|
||||||
@ -3579,14 +3703,30 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||||||
if (!(normalAppearance instanceof Dict)) {
|
if (!(normalAppearance instanceof Dict)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let onState = null;
|
||||||
for (const key of normalAppearance.getKeys()) {
|
for (const key of normalAppearance.getKeys()) {
|
||||||
if (key !== "Off") {
|
if (key !== "Off") {
|
||||||
this.data.buttonValue = key;
|
onState = key;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this._onStateName = onState;
|
||||||
|
|
||||||
const checkedAppearance = normalAppearance.get(this.data.buttonValue);
|
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);
|
||||||
this.checkedAppearance =
|
this.checkedAppearance =
|
||||||
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
|
checkedAppearance instanceof BaseStream ? checkedAppearance : null;
|
||||||
const uncheckedAppearance = normalAppearance.get("Off");
|
const uncheckedAppearance = normalAppearance.get("Off");
|
||||||
@ -4265,10 +4405,7 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
|||||||
totalWidth = Math.max(totalWidth, lineWidth);
|
totalWidth = Math.max(totalWidth, lineWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hscale = 1;
|
const hscale = totalWidth > w ? w / totalWidth : 1;
|
||||||
if (totalWidth > w) {
|
|
||||||
hscale = w / totalWidth;
|
|
||||||
}
|
|
||||||
let vscale = 1;
|
let vscale = 1;
|
||||||
const lineHeight = LINE_FACTOR * fontSize;
|
const lineHeight = LINE_FACTOR * fontSize;
|
||||||
const lineAscent = (LINE_FACTOR - LINE_DESCENT_FACTOR) * fontSize;
|
const lineAscent = (LINE_FACTOR - LINE_DESCENT_FACTOR) * fontSize;
|
||||||
@ -4893,16 +5030,7 @@ class HighlightAnnotation extends MarkupAnnotation {
|
|||||||
|
|
||||||
const quadPoints = (this.data.quadPoints = getQuadPoints(dict, null));
|
const quadPoints = (this.data.quadPoints = getQuadPoints(dict, null));
|
||||||
if (quadPoints) {
|
if (quadPoints) {
|
||||||
const resources = this.appearance?.dict.get("Resources");
|
if (!this.appearance) {
|
||||||
|
|
||||||
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
|
// Default color is yellow in Acrobat Reader
|
||||||
const fillColor = getPdfColorArray(this.color, [1, 1, 0]);
|
const fillColor = getPdfColorArray(this.color, [1, 1, 0]);
|
||||||
const fillAlpha = dict.get("CA");
|
const fillAlpha = dict.get("CA");
|
||||||
@ -5285,33 +5413,28 @@ class FileAttachmentAnnotation extends MarkupAnnotation {
|
|||||||
super(params);
|
super(params);
|
||||||
|
|
||||||
const { annotationGlobals, dict } = params;
|
const { annotationGlobals, dict } = params;
|
||||||
const fileSpecRef = dict.getRaw("FS");
|
|
||||||
const fsDict = dict.get("FS");
|
const fsDict = dict.get("FS");
|
||||||
const file = new FileSpec(fsDict);
|
const file = new FileSpec(fsDict);
|
||||||
/** @type {{catalog?: Catalog}} */
|
/** @type {{catalog?: Catalog}} */
|
||||||
const { catalog } = annotationGlobals.pdfManager.pdfDocument;
|
const { catalog } = annotationGlobals.pdfManager.pdfDocument;
|
||||||
|
|
||||||
// When this annotation references an embedded file that’s already in the
|
// Encode the embedded content's reference in the id so it can be
|
||||||
// catalog `NameTree` (such as `EFOpen`), reuse that `NameTree` id so the
|
// re-fetched from the xref on demand (see `Catalog.attachmentContent`)
|
||||||
// sidebar and annotation paths resolve the same attachment identity.
|
// instead of being cached where `cleanup` would wipe it. The file-spec is
|
||||||
let fileId =
|
// usually indirect; when it's inline its embedded-file stream still isn't
|
||||||
fileSpecRef instanceof Ref
|
// (streams are always indirect), so fall back to that ref.
|
||||||
? catalog?.attachmentIdByRef.get(fileSpecRef)
|
let fileId;
|
||||||
: undefined;
|
if (fsDict instanceof Dict) {
|
||||||
|
let contentRef = dict.getRaw("FS");
|
||||||
// Fallback ids are namespaced to keep annotation-local ids distinct from
|
if (!(contentRef instanceof Ref)) {
|
||||||
// `NameTree` ids (which are filename-based).
|
contentRef = FileSpec.pickPlatformItem(
|
||||||
if (catalog && fsDict instanceof Dict && typeof fileId !== "string") {
|
fsDict.get("EF"),
|
||||||
const baseFileId = `annotation:${this.data.id}`;
|
/* raw = */ true
|
||||||
fileId = baseFileId;
|
);
|
||||||
|
}
|
||||||
let i = 1;
|
if (contentRef instanceof Ref) {
|
||||||
while (catalog.attachmentDictById.has(fileId)) {
|
fileId = catalog?.getAttachmentIdForAnnotation(contentRef);
|
||||||
fileId = `${baseFileId}-${i++}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache only fallbacks.
|
|
||||||
catalog.attachmentDictById.set(fileId, fsDict);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.data.hasOwnCanvas = this.data.noRotate;
|
this.data.hasOwnCanvas = this.data.noRotate;
|
||||||
|
|||||||
@ -119,18 +119,12 @@ function fetchRemoteDest(action) {
|
|||||||
class Catalog {
|
class Catalog {
|
||||||
#actualNumPages = null;
|
#actualNumPages = null;
|
||||||
|
|
||||||
/** @type {RefSetCache | null} */
|
#annotationAttachmentIdByRef = new RefSetCache();
|
||||||
#attachmentIdByRef = null;
|
|
||||||
|
#annotationAttachmentRefById = new Map();
|
||||||
|
|
||||||
#catDict = null;
|
#catDict = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Attachment dictionaries keyed by attachment id.
|
|
||||||
*
|
|
||||||
* @type {Map<string, Dict>}
|
|
||||||
*/
|
|
||||||
attachmentDictById = new Map();
|
|
||||||
|
|
||||||
builtInCMapCache = new Map();
|
builtInCMapCache = new Map();
|
||||||
|
|
||||||
fontCache = new RefSetCache();
|
fontCache = new RefSetCache();
|
||||||
@ -164,33 +158,44 @@ class Catalog {
|
|||||||
this.toplevelPagesDict; // eslint-disable-line no-unused-expressions
|
this.toplevelPagesDict; // eslint-disable-line no-unused-expressions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
cloneDict() {
|
cloneDict() {
|
||||||
return this.#catDict.clone();
|
return this.#catDict.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
get version() {
|
get version() {
|
||||||
const version = this.#catDict.get("Version");
|
const version = this.#catDict.get("Version");
|
||||||
if (version instanceof Name) {
|
if (version instanceof Name) {
|
||||||
@ -274,19 +279,18 @@ class Catalog {
|
|||||||
/* suppressEncryption = */ !this.xref.encrypt?.encryptMetadata
|
/* suppressEncryption = */ !this.xref.encrypt?.encryptMetadata
|
||||||
);
|
);
|
||||||
|
|
||||||
if (stream instanceof BaseStream && stream.dict instanceof Dict) {
|
if (
|
||||||
const type = stream.dict.get("Type");
|
stream instanceof BaseStream &&
|
||||||
const subtype = stream.dict.get("Subtype");
|
isDict(stream.dict, "Metadata") &&
|
||||||
|
isName(stream.dict.get("Subtype"), "XML")
|
||||||
if (isName(type, "Metadata") && isName(subtype, "XML")) {
|
) {
|
||||||
// XXX: This should examine the charset the XML document defines,
|
// XXX: This should examine the charset the XML document defines,
|
||||||
// however since there are currently no real means to decode arbitrary
|
// however since there are currently no real means to decode arbitrary
|
||||||
// charsets, let's just hope that the author of the PDF was reasonable
|
// 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.
|
// enough to stick with the XML default charset, which is UTF-8.
|
||||||
const data = stringToUTF8String(stream.getString());
|
const data = stringToUTF8String(stream.getString());
|
||||||
if (data) {
|
if (data) {
|
||||||
metadata = new MetadataParser(data).serializable;
|
metadata = new MetadataParser(data).serializable;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@ -1157,19 +1161,12 @@ class Catalog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get content for an attachment.
|
|
||||||
*
|
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
* Unique attachment identifier (required).
|
* Unique attachment identifier.
|
||||||
* @returns {CatalogAttachmentContent}
|
* @returns {CatalogAttachmentContent | undefined}
|
||||||
* Content.
|
* Content, or `undefined` when no named attachment exists for the id.
|
||||||
*/
|
*/
|
||||||
attachmentContent(id) {
|
#attachmentContentByName(id) {
|
||||||
const dict = this.attachmentDictById.get(id);
|
|
||||||
if (dict) {
|
|
||||||
return FileSpec.readContent(dict);
|
|
||||||
}
|
|
||||||
|
|
||||||
const obj = this.#catDict.get("Names");
|
const obj = this.#catDict.get("Names");
|
||||||
if (obj instanceof Dict && obj.has("EmbeddedFiles")) {
|
if (obj instanceof Dict && obj.has("EmbeddedFiles")) {
|
||||||
const nameTree = new NameTree(obj.getRaw("EmbeddedFiles"), this.xref);
|
const nameTree = new NameTree(obj.getRaw("EmbeddedFiles"), this.xref);
|
||||||
@ -1179,6 +1176,36 @@ class Catalog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content for an attachment.
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
* Unique attachment identifier (required).
|
||||||
|
* @returns {CatalogAttachmentContent}
|
||||||
|
* Content.
|
||||||
|
*/
|
||||||
|
attachmentContent(id) {
|
||||||
|
const namedContent = this.#attachmentContentByName(id);
|
||||||
|
if (namedContent !== undefined) {
|
||||||
|
return namedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
return target instanceof Dict ? FileSpec.readContent(target) : null;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1280,9 +1307,6 @@ class Catalog {
|
|||||||
|
|
||||||
async cleanup(manuallyTriggered = false) {
|
async cleanup(manuallyTriggered = false) {
|
||||||
clearGlobalCaches();
|
clearGlobalCaches();
|
||||||
this.#attachmentIdByRef?.clear();
|
|
||||||
this.#attachmentIdByRef = null;
|
|
||||||
this.attachmentDictById.clear();
|
|
||||||
this.globalColorSpaceCache.clear();
|
this.globalColorSpaceCache.clear();
|
||||||
this.globalImageCache.clear(/* onlyData = */ manuallyTriggered);
|
this.globalImageCache.clear(/* onlyData = */ manuallyTriggered);
|
||||||
this.pageKidsCountCache.clear();
|
this.pageKidsCountCache.clear();
|
||||||
|
|||||||
@ -894,9 +894,22 @@ class CFFParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (maxZoneHeight > 0) {
|
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 =
|
const minBlueScale =
|
||||||
blueScale < DEFAULT_BLUE_SCALE ? 0.5 / maxZoneHeight : -Infinity;
|
lowerBound <= DEFAULT_BLUE_SCALE
|
||||||
const maxBlueScale = 1 / maxZoneHeight;
|
? Math.ceil(lowerBound * PRECISION) / PRECISION
|
||||||
|
: -Infinity;
|
||||||
|
const maxBlueScale = Math.floor(PRECISION / maxZoneHeight) / PRECISION;
|
||||||
const clamped = MathClamp(blueScale, minBlueScale, maxBlueScale);
|
const clamped = MathClamp(blueScale, minBlueScale, maxBlueScale);
|
||||||
if (clamped !== blueScale) {
|
if (clamped !== blueScale) {
|
||||||
privateDict.setByName("BlueScale", clamped);
|
privateDict.setByName("BlueScale", clamped);
|
||||||
|
|||||||
@ -418,10 +418,7 @@ class FakeUnicodeFont {
|
|||||||
[w, h] = [h, w];
|
[w, h] = [h, w];
|
||||||
}
|
}
|
||||||
|
|
||||||
let hscale = 1;
|
const hscale = maxWidth > w ? w / maxWidth : 1;
|
||||||
if (maxWidth > w) {
|
|
||||||
hscale = w / maxWidth;
|
|
||||||
}
|
|
||||||
let vscale = 1;
|
let vscale = 1;
|
||||||
const lineHeight = LINE_FACTOR * fontSize;
|
const lineHeight = LINE_FACTOR * fontSize;
|
||||||
const lineDescent = LINE_DESCENT_FACTOR * fontSize;
|
const lineDescent = LINE_DESCENT_FACTOR * fontSize;
|
||||||
|
|||||||
@ -718,8 +718,7 @@ class Page {
|
|||||||
"_parseStructTree",
|
"_parseStructTree",
|
||||||
[structTreeRoot]
|
[structTreeRoot]
|
||||||
);
|
);
|
||||||
const data = await this.pdfManager.ensure(structTree, "serializable");
|
return await this.pdfManager.ensure(structTree, "serializable");
|
||||||
return data;
|
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
warn(`getStructTree: "${ex}".`);
|
warn(`getStructTree: "${ex}".`);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -674,21 +674,13 @@ class PDFEditor {
|
|||||||
const classNames = node.get("C");
|
const classNames = node.get("C");
|
||||||
if (classNames instanceof Name) {
|
if (classNames instanceof Name) {
|
||||||
const newClassName = dedupClasses.get(classNames.name);
|
const newClassName = dedupClasses.get(classNames.name);
|
||||||
if (newClassName) {
|
newNode.set("C", newClassName ? Name.get(newClassName) : classNames);
|
||||||
newNode.set("C", Name.get(newClassName));
|
|
||||||
} else {
|
|
||||||
newNode.set("C", classNames);
|
|
||||||
}
|
|
||||||
} else if (Array.isArray(classNames)) {
|
} else if (Array.isArray(classNames)) {
|
||||||
const newClassNames = [];
|
const newClassNames = [];
|
||||||
for (const className of classNames) {
|
for (const className of classNames) {
|
||||||
if (className instanceof Name) {
|
if (className instanceof Name) {
|
||||||
const newClassName = dedupClasses.get(className.name);
|
const newClassName = dedupClasses.get(className.name);
|
||||||
if (newClassName) {
|
newClassNames.push(newClassName ? Name.get(newClassName) : className);
|
||||||
newClassNames.push(Name.get(newClassName));
|
|
||||||
} else {
|
|
||||||
newClassNames.push(className);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newNode.set("C", newClassNames);
|
newNode.set("C", newClassNames);
|
||||||
@ -698,11 +690,7 @@ class PDFEditor {
|
|||||||
const roleName = node.get("S");
|
const roleName = node.get("S");
|
||||||
if (roleName instanceof Name) {
|
if (roleName instanceof Name) {
|
||||||
const newRoleName = dedupRoles.get(roleName.name);
|
const newRoleName = dedupRoles.get(roleName.name);
|
||||||
if (newRoleName) {
|
newNode.set("S", newRoleName ? Name.get(newRoleName) : roleName);
|
||||||
newNode.set("S", Name.get(newRoleName));
|
|
||||||
} else {
|
|
||||||
newNode.set("S", roleName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix the ID.
|
// Fix the ID.
|
||||||
@ -710,11 +698,7 @@ class PDFEditor {
|
|||||||
if (typeof id === "string") {
|
if (typeof id === "string") {
|
||||||
const stringId = stringToPDFString(id, /* keepEscapeSequence = */ false);
|
const stringId = stringToPDFString(id, /* keepEscapeSequence = */ false);
|
||||||
const newId = dedupIDs.get(stringId);
|
const newId = dedupIDs.get(stringId);
|
||||||
if (newId) {
|
newNode.set("ID", newId ? stringToAsciiOrUTF16BE(newId) : id);
|
||||||
newNode.set("ID", stringToAsciiOrUTF16BE(newId));
|
|
||||||
} else {
|
|
||||||
newNode.set("ID", id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table headers may contain IDs that need to be deduplicated.
|
// Table headers may contain IDs that need to be deduplicated.
|
||||||
|
|||||||
@ -523,6 +523,7 @@ class PartialEvaluator {
|
|||||||
isolated: false,
|
isolated: false,
|
||||||
knockout: false,
|
knockout: false,
|
||||||
needsIsolation: false,
|
needsIsolation: false,
|
||||||
|
hasSoftMask: false,
|
||||||
isGray: false,
|
isGray: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -573,6 +574,7 @@ class PartialEvaluator {
|
|||||||
|
|
||||||
if (group) {
|
if (group) {
|
||||||
groupOptions.needsIsolation = newOpList.needsIsolation || !!smask;
|
groupOptions.needsIsolation = newOpList.needsIsolation || !!smask;
|
||||||
|
groupOptions.hasSoftMask = newOpList.hasSoftMask || !!smask;
|
||||||
operatorList.addOp(OPS.beginGroup, [groupOptions]);
|
operatorList.addOp(OPS.beginGroup, [groupOptions]);
|
||||||
operatorList.addOp(OPS.paintFormXObjectBegin, args);
|
operatorList.addOp(OPS.paintFormXObjectBegin, args);
|
||||||
operatorList.addOpList(newOpList);
|
operatorList.addOpList(newOpList);
|
||||||
@ -3535,10 +3537,7 @@ class PartialEvaluator {
|
|||||||
if (includeMarkedContent) {
|
if (includeMarkedContent) {
|
||||||
markedContentData.level++;
|
markedContentData.level++;
|
||||||
|
|
||||||
let mcid = null;
|
const mcid = args[1] instanceof Dict ? args[1].get("MCID") : null;
|
||||||
if (args[1] instanceof Dict) {
|
|
||||||
mcid = args[1].get("MCID");
|
|
||||||
}
|
|
||||||
textContent.items.push({
|
textContent.items.push({
|
||||||
type: "beginMarkedContentProps",
|
type: "beginMarkedContentProps",
|
||||||
id: Number.isInteger(mcid)
|
id: Number.isInteger(mcid)
|
||||||
|
|||||||
@ -27,29 +27,6 @@ import { stringToPDFString } from "./string_utils.js";
|
|||||||
* @import { CatalogAttachmentContent } from "./catalog.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
|
* "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.
|
* Specification (PDF 1.1)", see the spec (7.11) for more details.
|
||||||
@ -76,7 +53,7 @@ class FileSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get filename() {
|
get filename() {
|
||||||
const item = pickPlatformItem(this.root);
|
const item = FileSpec.pickPlatformItem(this.root);
|
||||||
if (item && typeof item === "string") {
|
if (item && typeof item === "string") {
|
||||||
// NOTE: The following replacement order is INTENTIONAL, regardless of
|
// NOTE: The following replacement order is INTENTIONAL, regardless of
|
||||||
// what some static code analysers (e.g. CodeQL) may claim.
|
// what some static code analysers (e.g. CodeQL) may claim.
|
||||||
@ -105,6 +82,31 @@ 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.
|
* Read attachment bytes from a file-spec dictionary.
|
||||||
*
|
*
|
||||||
@ -119,24 +121,36 @@ class FileSpec {
|
|||||||
if (!(dict instanceof Dict)) {
|
if (!(dict instanceof Dict)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const ef = pickPlatformItem(dict.get("EF"));
|
const ef = this.pickPlatformItem(dict.get("EF"));
|
||||||
if (!(ef instanceof BaseStream)) {
|
if (!(ef instanceof BaseStream)) {
|
||||||
warn(
|
warn(
|
||||||
"Embedded file specification points to non-existing/invalid content"
|
"Embedded file specification points to non-existing/invalid content"
|
||||||
);
|
);
|
||||||
return null;
|
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.
|
// Throw if we need a password but don’t have one.
|
||||||
const encrypt = dict.xref?.encrypt;
|
const encrypt = stream.dict?.xref?.encrypt;
|
||||||
if (encrypt?.encryptionKey === null) {
|
if (encrypt?.encryptionKey === null) {
|
||||||
throw new PasswordException(
|
throw new PasswordException(
|
||||||
"No password given",
|
"No password given",
|
||||||
PasswordResponses.NEED_PASSWORD
|
PasswordResponses.NEED_PASSWORD
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return stream.getBytes();
|
||||||
return ef.getBytes();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3171,10 +3171,7 @@ class Font {
|
|||||||
// there isn't enough room to duplicate, the glyph id is left the same. In
|
// 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
|
// this case, glyph 0 may not work correctly, but that is better than
|
||||||
// having the whole font fail.
|
// having the whole font fail.
|
||||||
let glyphZeroId = numGlyphsOut - 1;
|
const glyphZeroId = dupFirstEntry ? numGlyphsOut - 1 : 0;
|
||||||
if (!dupFirstEntry) {
|
|
||||||
glyphZeroId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When `cssFontInfo` is set, the font is used to render text in the HTML
|
// 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.
|
// view (e.g. with Xfa) so nothing must be moved in the private use area.
|
||||||
@ -3248,10 +3245,7 @@ class Font {
|
|||||||
// Type 1 fonts have a notdef inserted at the beginning, so glyph 0
|
// 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
|
// becomes glyph 1. In a CFF font glyph 0 is appended to the end of the
|
||||||
// char strings.
|
// char strings.
|
||||||
let glyphZeroId = 1;
|
const glyphZeroId = font instanceof CFFFont ? font.numGlyphs - 1 : 1;
|
||||||
if (font instanceof CFFFont) {
|
|
||||||
glyphZeroId = font.numGlyphs - 1;
|
|
||||||
}
|
|
||||||
const mapping = font.getGlyphMapping(properties);
|
const mapping = font.getGlyphMapping(properties);
|
||||||
let newMapping = null;
|
let newMapping = null;
|
||||||
let newCharCodeToGlyphId = mapping;
|
let newCharCodeToGlyphId = mapping;
|
||||||
|
|||||||
@ -830,24 +830,29 @@ class OperatorList {
|
|||||||
* operations require being drawn in isolation (i.e. on a separate canvas).
|
* operations require being drawn in isolation (i.e. on a separate canvas).
|
||||||
* A group/pattern needs isolation when it uses non-default compositing
|
* A group/pattern needs isolation when it uses non-default compositing
|
||||||
* (blend mode) or a soft mask. The result is exposed via `needsIsolation`.
|
* (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 {
|
class CheckedOperatorList extends OperatorList {
|
||||||
needsIsolation = false;
|
needsIsolation = false;
|
||||||
|
|
||||||
|
hasSoftMask = false;
|
||||||
|
|
||||||
addOp(fn, args) {
|
addOp(fn, args) {
|
||||||
if (!this.needsIsolation) {
|
if (!this.needsIsolation || !this.hasSoftMask) {
|
||||||
if (fn === OPS.beginGroup) {
|
if (fn === OPS.beginGroup) {
|
||||||
// Propagate isolation only if the nested group itself needs it.
|
// Propagate isolation only if the nested group itself needs it.
|
||||||
this.needsIsolation = args[0].needsIsolation;
|
this.needsIsolation ||= args[0].needsIsolation;
|
||||||
|
this.hasSoftMask ||= args[0].hasSoftMask;
|
||||||
} else if (fn === OPS.setGState) {
|
} else if (fn === OPS.setGState) {
|
||||||
for (const [key, val] of args[0]) {
|
for (const [key, val] of args[0]) {
|
||||||
if (key === "BM" && val !== "source-over") {
|
if (key === "BM" && val !== "source-over") {
|
||||||
this.needsIsolation = true;
|
this.needsIsolation = true;
|
||||||
break;
|
} else if (key === "SMask" && val !== false) {
|
||||||
}
|
|
||||||
if (key === "SMask" && val !== false) {
|
|
||||||
this.needsIsolation = true;
|
this.needsIsolation = true;
|
||||||
break;
|
this.hasSoftMask = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -285,10 +285,9 @@ class RadialAxialShading extends BaseShading {
|
|||||||
}
|
}
|
||||||
colorStops.push([1, Util.makeHexColor(rPrev, gPrev, bPrev)]);
|
colorStops.push([1, Util.makeHexColor(rPrev, gPrev, bPrev)]);
|
||||||
|
|
||||||
let background = "transparent";
|
const background = dict.has("Background")
|
||||||
if (dict.has("Background")) {
|
? cs.getRgbHex(dict.get("Background"), 0)
|
||||||
background = cs.getRgbHex(dict.get("Background"), 0);
|
: "transparent";
|
||||||
}
|
|
||||||
|
|
||||||
if (!extendStart) {
|
if (!extendStart) {
|
||||||
// Insert a color stop at the front and offset the first real color stop
|
// Insert a color stop at the front and offset the first real color stop
|
||||||
|
|||||||
@ -116,6 +116,19 @@ class BasePdfManager {
|
|||||||
return this.ensure(this.pdfDocument.catalog, prop, args);
|
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) {
|
getPage(pageIndex) {
|
||||||
return this.pdfDocument.getPage(pageIndex);
|
return this.pdfDocument.getPage(pageIndex);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,9 +20,10 @@ import {
|
|||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
import { Dict, isDict, isName, Name, Ref, RefSetCache } from "./primitives.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 { stringToAsciiOrUTF16BE, stringToPDFString } from "./string_utils.js";
|
||||||
import { BaseStream } from "./base_stream.js";
|
import { BaseStream } from "./base_stream.js";
|
||||||
import { lookupNormalRect } from "./core_utils.js";
|
import { FileSpec } from "./file_spec.js";
|
||||||
import { NumberTree } from "./name_number_tree.js";
|
import { NumberTree } from "./name_number_tree.js";
|
||||||
|
|
||||||
const MAX_DEPTH = 40;
|
const MAX_DEPTH = 40;
|
||||||
@ -594,24 +595,18 @@ class StructElementNode {
|
|||||||
}
|
}
|
||||||
for (let af of AFs) {
|
for (let af of AFs) {
|
||||||
af = this.xref.fetchIfRef(af);
|
af = this.xref.fetchIfRef(af);
|
||||||
if (!isDict(af, "Filespec")) {
|
if (
|
||||||
|
!isDict(af, "Filespec") ||
|
||||||
|
!isName(af.get("AFRelationship"), "Supplement")
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!isName(af.get("AFRelationship"), "Supplement")) {
|
const fileStream = FileSpec.pickPlatformItem(af.get("EF"));
|
||||||
continue;
|
if (
|
||||||
}
|
!(fileStream instanceof BaseStream) ||
|
||||||
const ef = af.get("EF");
|
!isDict(fileStream.dict, "EmbeddedFile") ||
|
||||||
if (!(ef instanceof Dict)) {
|
!isName(fileStream.dict.get("Subtype"), "application/mathml+xml")
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
// The default encoding for xml files is UTF-8.
|
// The default encoding for xml files is UTF-8.
|
||||||
@ -909,9 +904,16 @@ class StructTreePage {
|
|||||||
obj.alt = stringToPDFString(alt);
|
obj.alt = stringToPDFString(alt);
|
||||||
}
|
}
|
||||||
if (obj.role === "Formula") {
|
if (obj.role === "Formula") {
|
||||||
const { mathML } = node;
|
try {
|
||||||
if (mathML) {
|
const { mathML } = node;
|
||||||
obj.mathML = mathML;
|
if (mathML) {
|
||||||
|
obj.mathML = mathML;
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
if (ex instanceof MissingDataException) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
warn(`Ignoring mathML: "${ex}".`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -691,13 +691,10 @@ class Type1Parser {
|
|||||||
subrs,
|
subrs,
|
||||||
this.seacAnalysisEnabled
|
this.seacAnalysisEnabled
|
||||||
);
|
);
|
||||||
let output = charString.output;
|
// It seems when FreeType encounters an error while evaluating a glyph
|
||||||
if (error) {
|
// that it completely ignores the glyph so we'll mimic that behaviour
|
||||||
// It seems when FreeType encounters an error while evaluating a glyph
|
// here and put an endchar to make the validator happy.
|
||||||
// that it completely ignores the glyph so we'll mimic that behaviour
|
const output = !error ? charString.output : [14];
|
||||||
// here and put an endchar to make the validator happy.
|
|
||||||
output = [14];
|
|
||||||
}
|
|
||||||
const charStringObject = {
|
const charStringObject = {
|
||||||
glyphName: glyph,
|
glyphName: glyph,
|
||||||
charstring: output,
|
charstring: output,
|
||||||
|
|||||||
@ -164,16 +164,7 @@ class WorkerMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadDocument(recoveryMode) {
|
async function loadDocument(recoveryMode) {
|
||||||
await pdfManager.ensureDoc("checkHeader");
|
await pdfManager.initDocument(recoveryMode);
|
||||||
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");
|
const isPureXfa = await pdfManager.ensureDoc("isPureXfa");
|
||||||
if (isPureXfa) {
|
if (isPureXfa) {
|
||||||
@ -519,20 +510,17 @@ class WorkerMessageHandler {
|
|||||||
startWorkerTask(task);
|
startWorkerTask(task);
|
||||||
}
|
}
|
||||||
pagePromises.push(
|
pagePromises.push(
|
||||||
pdfManager.getPage(i).then(async page => {
|
pdfManager
|
||||||
if (!page) {
|
.getPage(i)
|
||||||
return [];
|
.then(page =>
|
||||||
}
|
|
||||||
return (
|
|
||||||
page.collectAnnotationsByType(
|
page.collectAnnotationsByType(
|
||||||
handler,
|
handler,
|
||||||
task,
|
task,
|
||||||
types,
|
types,
|
||||||
annotationPromises,
|
annotationPromises,
|
||||||
annotationGlobals
|
annotationGlobals
|
||||||
) || []
|
)
|
||||||
);
|
)
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await Promise.all(pagePromises);
|
await Promise.all(pagePromises);
|
||||||
@ -629,9 +617,7 @@ class WorkerMessageHandler {
|
|||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
await manager.requestLoadedStream();
|
await manager.requestLoadedStream();
|
||||||
await manager.ensureDoc("checkHeader");
|
await manager.initDocument(recoveryMode);
|
||||||
await manager.ensureDoc("parseStartXRef");
|
|
||||||
await manager.ensureDoc("parse", [recoveryMode]);
|
|
||||||
break;
|
break;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof XRefParseException) {
|
if (e instanceof XRefParseException) {
|
||||||
|
|||||||
@ -101,7 +101,6 @@ class XFAFactory {
|
|||||||
const missingFonts = [];
|
const missingFonts = [];
|
||||||
for (let typeface of this.form[$globalData].usedTypefaces) {
|
for (let typeface of this.form[$globalData].usedTypefaces) {
|
||||||
typeface = stripQuotes(typeface);
|
typeface = stripQuotes(typeface);
|
||||||
// eslint-disable-next-line unicorn/prefer-array-some
|
|
||||||
const font = this.form[$globalData].fontFinder.find(typeface);
|
const font = this.form[$globalData].fontFinder.find(typeface);
|
||||||
if (!font) {
|
if (!font) {
|
||||||
missingFonts.push(typeface);
|
missingFonts.push(typeface);
|
||||||
|
|||||||
@ -51,7 +51,7 @@ class XMLParserBase {
|
|||||||
return s.replaceAll(/&([^;]+);/g, (all, entity) => {
|
return s.replaceAll(/&([^;]+);/g, (all, entity) => {
|
||||||
if (entity.substring(0, 2) === "#x") {
|
if (entity.substring(0, 2) === "#x") {
|
||||||
return String.fromCodePoint(parseInt(entity.substring(2), 16));
|
return String.fromCodePoint(parseInt(entity.substring(2), 16));
|
||||||
} else if (entity.substring(0, 1) === "#") {
|
} else if (entity.at(0) === "#") {
|
||||||
return String.fromCodePoint(parseInt(entity.substring(1), 10));
|
return String.fromCodePoint(parseInt(entity.substring(1), 10));
|
||||||
}
|
}
|
||||||
switch (entity) {
|
switch (entity) {
|
||||||
|
|||||||
@ -17,6 +17,10 @@ import {
|
|||||||
CanvasNestedDependencyTracker,
|
CanvasNestedDependencyTracker,
|
||||||
Dependencies,
|
Dependencies,
|
||||||
} from "./canvas_dependency_tracker.js";
|
} from "./canvas_dependency_tracker.js";
|
||||||
|
import {
|
||||||
|
convertBlackAndWhiteToRGBA,
|
||||||
|
convertRGBToRGBA,
|
||||||
|
} from "../shared/image_utils.js";
|
||||||
import {
|
import {
|
||||||
F32_BBOX_INIT,
|
F32_BBOX_INIT,
|
||||||
FeatureTest,
|
FeatureTest,
|
||||||
@ -45,7 +49,6 @@ import {
|
|||||||
PathType,
|
PathType,
|
||||||
TilingPattern,
|
TilingPattern,
|
||||||
} from "./pattern_helper.js";
|
} from "./pattern_helper.js";
|
||||||
import { convertBlackAndWhiteToRGBA } from "../shared/image_utils.js";
|
|
||||||
import { MathClamp } from "../shared/math_clamp.js";
|
import { MathClamp } from "../shared/math_clamp.js";
|
||||||
|
|
||||||
// <canvas> contexts store most of the state we need natively.
|
// <canvas> contexts store most of the state we need natively.
|
||||||
@ -317,11 +320,6 @@ class CanvasExtraState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function putBinaryImageData(ctx, imgData) {
|
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
|
// 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
|
// whole image at once. This saves JS memory, because the ImageData object
|
||||||
// is smaller. It also possibly saves C++ memory within the implementation
|
// is smaller. It also possibly saves C++ memory within the implementation
|
||||||
@ -333,40 +331,36 @@ function putBinaryImageData(ctx, imgData) {
|
|||||||
// will (conceptually) put pixels past the bounds of the canvas. But
|
// will (conceptually) put pixels past the bounds of the canvas. But
|
||||||
// that's ok; any such pixels are ignored.
|
// that's ok; any such pixels are ignored.
|
||||||
|
|
||||||
const height = imgData.height,
|
const { width, height, kind } = imgData;
|
||||||
width = imgData.width;
|
|
||||||
const partialChunkHeight = height % FULL_CHUNK_HEIGHT;
|
const partialChunkHeight = height % FULL_CHUNK_HEIGHT;
|
||||||
const fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
|
const fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
|
||||||
const totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
|
const totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
|
||||||
|
|
||||||
const chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT);
|
const chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT);
|
||||||
let srcPos = 0,
|
let srcPos = 0;
|
||||||
destPos;
|
|
||||||
const src = imgData.data;
|
const src = imgData.data;
|
||||||
const dest = chunkImgData.data;
|
const dest = chunkImgData.data;
|
||||||
let i, j, thisChunkHeight, elemsInThisChunk;
|
let i;
|
||||||
|
|
||||||
// There are multiple forms in which the pixel data can be passed, and
|
// There are multiple forms in which the pixel data can be passed, and
|
||||||
// imgData.kind tells us which one this is.
|
// imgData.kind tells us which one this is.
|
||||||
if (imgData.kind === ImageKind.GRAYSCALE_1BPP) {
|
if (kind === ImageKind.GRAYSCALE_1BPP) {
|
||||||
// Grayscale, 1 bit per pixel (i.e. black-and-white).
|
// Grayscale, 1 bit per pixel (i.e. black-and-white).
|
||||||
for (i = 0; i < totalChunks; i++) {
|
for (i = 0; i < totalChunks; i++) {
|
||||||
thisChunkHeight = i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight;
|
|
||||||
|
|
||||||
({ srcPos } = convertBlackAndWhiteToRGBA({
|
({ srcPos } = convertBlackAndWhiteToRGBA({
|
||||||
src,
|
src,
|
||||||
srcPos,
|
srcPos,
|
||||||
dest,
|
dest,
|
||||||
width,
|
width,
|
||||||
height: thisChunkHeight,
|
height: i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
|
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
|
||||||
}
|
}
|
||||||
} else if (imgData.kind === ImageKind.RGBA_32BPP) {
|
} else if (kind === ImageKind.RGBA_32BPP) {
|
||||||
// RGBA, 32-bits per pixel.
|
// RGBA, 32-bits per pixel.
|
||||||
j = 0;
|
let j = 0;
|
||||||
elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4;
|
let elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4;
|
||||||
for (i = 0; i < fullChunks; i++) {
|
for (i = 0; i < fullChunks; i++) {
|
||||||
dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
|
dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
|
||||||
srcPos += elemsInThisChunk;
|
srcPos += elemsInThisChunk;
|
||||||
@ -380,28 +374,21 @@ function putBinaryImageData(ctx, imgData) {
|
|||||||
|
|
||||||
ctx.putImageData(chunkImgData, 0, j);
|
ctx.putImageData(chunkImgData, 0, j);
|
||||||
}
|
}
|
||||||
} else if (imgData.kind === ImageKind.RGB_24BPP) {
|
} else if (kind === ImageKind.RGB_24BPP) {
|
||||||
// RGB, 24-bits per pixel.
|
// RGB, 24-bits per pixel.
|
||||||
thisChunkHeight = FULL_CHUNK_HEIGHT;
|
|
||||||
elemsInThisChunk = width * thisChunkHeight;
|
|
||||||
for (i = 0; i < totalChunks; i++) {
|
for (i = 0; i < totalChunks; i++) {
|
||||||
if (i >= fullChunks) {
|
({ srcPos } = convertRGBToRGBA({
|
||||||
thisChunkHeight = partialChunkHeight;
|
src,
|
||||||
elemsInThisChunk = width * thisChunkHeight;
|
srcPos,
|
||||||
}
|
dest: new Uint32Array(dest.buffer),
|
||||||
|
width,
|
||||||
destPos = 0;
|
height: i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight,
|
||||||
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);
|
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`bad image kind: ${imgData.kind}`);
|
throw new Error(`bad image kind: ${kind}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,8 +400,7 @@ function putBinaryImageMask(ctx, imgData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Slow path: OffscreenCanvas isn't available in the worker.
|
// Slow path: OffscreenCanvas isn't available in the worker.
|
||||||
const height = imgData.height,
|
const { width, height } = imgData;
|
||||||
width = imgData.width;
|
|
||||||
const partialChunkHeight = height % FULL_CHUNK_HEIGHT;
|
const partialChunkHeight = height % FULL_CHUNK_HEIGHT;
|
||||||
const fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
|
const fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
|
||||||
const totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
|
const totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
|
||||||
@ -425,18 +411,14 @@ function putBinaryImageMask(ctx, imgData) {
|
|||||||
const dest = chunkImgData.data;
|
const dest = chunkImgData.data;
|
||||||
|
|
||||||
for (let i = 0; i < totalChunks; i++) {
|
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
|
// Expand the mask so it can be used by the canvas. Any required
|
||||||
// inversion has already been handled.
|
// inversion has already been handled.
|
||||||
|
|
||||||
({ srcPos } = convertBlackAndWhiteToRGBA({
|
({ srcPos } = convertBlackAndWhiteToRGBA({
|
||||||
src,
|
src,
|
||||||
srcPos,
|
srcPos,
|
||||||
dest,
|
dest,
|
||||||
width,
|
width,
|
||||||
height: thisChunkHeight,
|
height: i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight,
|
||||||
nonBlackColor: 0,
|
nonBlackColor: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -2688,13 +2670,13 @@ class CanvasGraphics {
|
|||||||
this.showType3Text(opIdx, glyphs);
|
this.showType3Text(opIdx, glyphs);
|
||||||
this.dependencyTracker?.recordShowTextOperation(opIdx);
|
this.dependencyTracker?.recordShowTextOperation(opIdx);
|
||||||
this.#endKnockoutElement(started);
|
this.#endKnockoutElement(started);
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontSize = current.fontSize;
|
const fontSize = current.fontSize;
|
||||||
if (fontSize === 0) {
|
if (fontSize === 0) {
|
||||||
this.dependencyTracker?.recordOperation(opIdx);
|
this.dependencyTracker?.recordOperation(opIdx);
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const started = this.#beginKnockoutElement(current.fillAlpha);
|
const started = this.#beginKnockoutElement(current.fillAlpha);
|
||||||
@ -2812,7 +2794,7 @@ class CanvasGraphics {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
this.compose();
|
this.compose();
|
||||||
this.#endKnockoutElement(started);
|
this.#endKnockoutElement(started);
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let x = 0,
|
let x = 0,
|
||||||
@ -2928,7 +2910,6 @@ class CanvasGraphics {
|
|||||||
|
|
||||||
this.dependencyTracker?.recordShowTextOperation(opIdx);
|
this.dependencyTracker?.recordShowTextOperation(opIdx);
|
||||||
this.#endKnockoutElement(started);
|
this.#endKnockoutElement(started);
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showType3Text(opIdx, glyphs) {
|
showType3Text(opIdx, glyphs) {
|
||||||
@ -3223,12 +3204,13 @@ class CanvasGraphics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentCtx = this.ctx;
|
const currentCtx = this.ctx;
|
||||||
if (!group.isolated && !group.knockout && this.#knockoutGroupLevel === 0) {
|
|
||||||
info("TODO: Fully support non-isolated non-knockout groups.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!group.needsIsolation &&
|
// 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.knockout &&
|
!group.knockout &&
|
||||||
!group.isGray &&
|
!group.isGray &&
|
||||||
this.#knockoutGroupLevel === 0 &&
|
this.#knockoutGroupLevel === 0 &&
|
||||||
@ -3247,12 +3229,24 @@ class CanvasGraphics {
|
|||||||
}
|
}
|
||||||
currentCtx.clip(clip);
|
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.groupStack.push(null); // null = no intermediate canvas
|
||||||
this.#groupStackMeta.push(null);
|
this.#groupStackMeta.push(null);
|
||||||
this.groupLevel++;
|
this.groupLevel++;
|
||||||
return;
|
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);
|
const currentTransform = getCurrentTransform(currentCtx);
|
||||||
if (group.matrix) {
|
if (group.matrix) {
|
||||||
currentCtx.transform(...group.matrix);
|
currentCtx.transform(...group.matrix);
|
||||||
@ -3996,12 +3990,6 @@ class CanvasGraphics {
|
|||||||
const result = this.applyTransferMapsToBitmap(imgData);
|
const result = this.applyTransferMapsToBitmap(imgData);
|
||||||
imgToPaint = result.img;
|
imgToPaint = result.img;
|
||||||
inlineImgCanvas = result.canvasEntry;
|
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 {
|
} else {
|
||||||
const tmpCanvas = this.canvasFactory.create(width, height);
|
const tmpCanvas = this.canvasFactory.create(width, height);
|
||||||
putBinaryImageData(tmpCanvas.context, imgData);
|
putBinaryImageData(tmpCanvas.context, imgData);
|
||||||
|
|||||||
@ -343,10 +343,6 @@ class HighlightOutline extends Outline {
|
|||||||
get box() {
|
get box() {
|
||||||
return this.#box;
|
return this.#box;
|
||||||
}
|
}
|
||||||
|
|
||||||
get classNamesForOutlining() {
|
|
||||||
return ["highlightOutline"];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FreeHighlightOutliner extends FreeDrawOutliner {
|
class FreeHighlightOutliner extends FreeDrawOutliner {
|
||||||
|
|||||||
@ -2545,14 +2545,6 @@ class AnnotationEditorUIManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the editor is selected.
|
|
||||||
* @param {AnnotationEditor} editor
|
|
||||||
*/
|
|
||||||
isSelected(editor) {
|
|
||||||
return this.#selectedEditors.has(editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
get firstSelectedEditor() {
|
get firstSelectedEditor() {
|
||||||
return this.#selectedEditors.values().next().value;
|
return this.#selectedEditors.values().next().value;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,13 +51,6 @@ if (isNodeJS) {
|
|||||||
warn("Cannot polyfill `DOMMatrix`, rendering may be broken.");
|
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 (!globalThis.Path2D) {
|
||||||
if (canvas?.Path2D) {
|
if (canvas?.Path2D) {
|
||||||
globalThis.Path2D = canvas.Path2D;
|
globalThis.Path2D = canvas.Path2D;
|
||||||
|
|||||||
@ -255,11 +255,9 @@ class EventDispatcher {
|
|||||||
this.runCalculate(source, event);
|
this.runCalculate(source, event);
|
||||||
|
|
||||||
const savedValue = (event.value = source.obj._getValue());
|
const savedValue = (event.value = source.obj._getValue());
|
||||||
let formattedValue = null;
|
const formattedValue = this.runActions(source, source, event, "Format")
|
||||||
|
? event.value?.toString?.()
|
||||||
if (this.runActions(source, source, event, "Format")) {
|
: null;
|
||||||
formattedValue = event.value?.toString?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
source.obj._send({
|
source.obj._send({
|
||||||
id: source.obj._id,
|
id: source.obj._id,
|
||||||
@ -365,10 +363,9 @@ class EventDispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
savedValue = target.obj._getValue();
|
savedValue = target.obj._getValue();
|
||||||
let formattedValue = null;
|
const formattedValue = this.runActions(target, target, event, "Format")
|
||||||
if (this.runActions(target, target, event, "Format")) {
|
? event.value?.toString?.()
|
||||||
formattedValue = event.value?.toString?.();
|
: null;
|
||||||
}
|
|
||||||
|
|
||||||
target.obj._send({
|
target.obj._send({
|
||||||
id: target.obj._id,
|
id: target.obj._id,
|
||||||
|
|||||||
@ -139,4 +139,9 @@ function grayToRGBA(src, dest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { convertBlackAndWhiteToRGBA, convertToRGBA, grayToRGBA };
|
export {
|
||||||
|
convertBlackAndWhiteToRGBA,
|
||||||
|
convertRGBToRGBA,
|
||||||
|
convertToRGBA,
|
||||||
|
grayToRGBA,
|
||||||
|
};
|
||||||
|
|||||||
@ -169,6 +169,43 @@ 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 () {
|
describe("PR 19470", function () {
|
||||||
let pages;
|
let pages;
|
||||||
|
|
||||||
|
|||||||
@ -537,7 +537,11 @@ describe("Comment", () => {
|
|||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
|
|
||||||
const steps = 20;
|
const steps = 20;
|
||||||
await page.mouse.move(startX - extraWidth, startY, { steps });
|
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.up();
|
await page.mouse.up();
|
||||||
|
|
||||||
const rectAfter = await getRect(page, sidebarSelector);
|
const rectAfter = await getRect(page, sidebarSelector);
|
||||||
|
|||||||
@ -127,9 +127,11 @@ async function waitForHavingContents(page, expected) {
|
|||||||
});
|
});
|
||||||
return page.waitForFunction(
|
return page.waitForFunction(
|
||||||
ex => {
|
ex => {
|
||||||
|
const textLayers = document.querySelectorAll(".textLayer");
|
||||||
const buffer = [];
|
const buffer = [];
|
||||||
for (const textLayer of document.querySelectorAll(".textLayer")) {
|
for (const [i, textLayer] of textLayers.entries()) {
|
||||||
buffer.push(parseInt(textLayer.textContent.trim(), 10));
|
const text = textLayer.textContent.trim();
|
||||||
|
buffer.push(typeof ex[i] === "string" ? text : parseInt(text, 10));
|
||||||
}
|
}
|
||||||
return ex.length === buffer.length && ex.every((v, i) => v === buffer[i]);
|
return ex.length === buffer.length && ex.every((v, i) => v === buffer[i]);
|
||||||
},
|
},
|
||||||
@ -3396,6 +3398,119 @@ 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", () => {
|
describe("Drag-and-drop PDF merge", () => {
|
||||||
|
|||||||
@ -2745,4 +2745,60 @@ 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,19 +151,6 @@ function closePages(pages) {
|
|||||||
|
|
||||||
async function closeSinglePage(page) {
|
async function closeSinglePage(page) {
|
||||||
const coverage = await page.evaluate(async () => {
|
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
|
// Close the viewer gracefully, and clear local storage to avoid state
|
||||||
// leaking from one test to another.
|
// leaking from one test to another.
|
||||||
await window.PDFViewerApplication.testingClose();
|
await window.PDFViewerApplication.testingClose();
|
||||||
@ -175,16 +162,14 @@ async function closeSinglePage(page) {
|
|||||||
// logic kicks in (see https://github.com/puppeteer/puppeteer/issues/2427).
|
// logic kicks in (see https://github.com/puppeteer/puppeteer/issues/2427).
|
||||||
return {
|
return {
|
||||||
page: window.__coverage__ ? JSON.stringify(window.__coverage__) : null,
|
page: window.__coverage__ ? JSON.stringify(window.__coverage__) : null,
|
||||||
worker: workerCoverage ? JSON.stringify(workerCoverage) : null,
|
workers: window.__worker_coverage__?.map(c => JSON.stringify(c)) ?? null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (coverage.page) {
|
if (coverage.page) {
|
||||||
mergeCoverageIntoGlobal(JSON.parse(coverage.page));
|
mergeCoverageIntoGlobal(JSON.parse(coverage.page));
|
||||||
}
|
}
|
||||||
if (coverage.worker) {
|
coverage.workers?.map(c => mergeCoverageIntoGlobal(JSON.parse(c)));
|
||||||
mergeCoverageIntoGlobal(JSON.parse(coverage.worker));
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.close({ runBeforeUnload: false });
|
await page.close({ runBeforeUnload: false });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1455,6 +1455,72 @@ 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", () => {
|
describe("Pinch-zoom", () => {
|
||||||
let pages;
|
let pages;
|
||||||
|
|
||||||
|
|||||||
3
test/pdfs/.gitignore
vendored
3
test/pdfs/.gitignore
vendored
@ -935,3 +935,6 @@
|
|||||||
!text_field_own_canvas_calc.pdf
|
!text_field_own_canvas_calc.pdf
|
||||||
!bug1802506.pdf
|
!bug1802506.pdf
|
||||||
!checkbox_no_appearance.pdf
|
!checkbox_no_appearance.pdf
|
||||||
|
!opt_demo.pdf
|
||||||
|
!bug1873345.pdf
|
||||||
|
!cff_bluescale_small_zones.pdf
|
||||||
|
|||||||
BIN
test/pdfs/bug1873345.pdf
Normal file
BIN
test/pdfs/bug1873345.pdf
Normal file
Binary file not shown.
BIN
test/pdfs/cff_bluescale_small_zones.pdf
Normal file
BIN
test/pdfs/cff_bluescale_small_zones.pdf
Normal file
Binary file not shown.
1
test/pdfs/issue21458.pdf.link
Normal file
1
test/pdfs/issue21458.pdf.link
Normal file
@ -0,0 +1 @@
|
|||||||
|
https://github.com/user-attachments/files/28985267/Gumbel.Distillation.pdf
|
||||||
133
test/pdfs/opt_demo.pdf
Normal file
133
test/pdfs/opt_demo.pdf
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
%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,10 +555,11 @@ window.onload = function () {
|
|||||||
window.addEventListener("keydown", function keydown(event) {
|
window.addEventListener("keydown", function keydown(event) {
|
||||||
if (event.which === 84) {
|
if (event.which === 84) {
|
||||||
// 't' switch test/ref images
|
// 't' switch test/ref images
|
||||||
let val = 0;
|
const val = document.querySelector(
|
||||||
if (document.querySelector('input[name="which"][value="0"]:checked')) {
|
'input[name="which"][value="0"]:checked'
|
||||||
val = 1;
|
)
|
||||||
}
|
? 1
|
||||||
|
: 0;
|
||||||
document
|
document
|
||||||
.querySelector('input[name="which"][value="' + val + '"]')
|
.querySelector('input[name="which"][value="' + val + '"]')
|
||||||
.click();
|
.click();
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import {
|
|||||||
downloadManifestFiles,
|
downloadManifestFiles,
|
||||||
verifyManifestFiles,
|
verifyManifestFiles,
|
||||||
} from "./downloadutils.mjs";
|
} from "./downloadutils.mjs";
|
||||||
|
import { execSync } from "child_process";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import istanbulCoverage from "istanbul-lib-coverage";
|
import istanbulCoverage from "istanbul-lib-coverage";
|
||||||
import istanbulReportGenerator from "istanbul-reports";
|
import istanbulReportGenerator from "istanbul-reports";
|
||||||
@ -1095,9 +1096,13 @@ async function startBrowser({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startBrowsers({ baseUrl, initializeSession, numSessions = 1 }) {
|
async function startBrowsers({ baseUrl, initializeSession, numSessions = 1 }) {
|
||||||
// Remove old browser revisions from Puppeteer's cache. Updating Puppeteer can
|
// Install the browsers.
|
||||||
// cause new browser revisions to be downloaded, so trimming the cache will
|
for (const browser of ["firefox@nightly", "chrome@stable"]) {
|
||||||
// prevent the disk from filling up over time.
|
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.
|
||||||
await puppeteer.trimCache();
|
await puppeteer.trimCache();
|
||||||
|
|
||||||
const browserNames = ["firefox", "chrome"];
|
const browserNames = ["firefox", "chrome"];
|
||||||
|
|||||||
@ -31,6 +31,14 @@
|
|||||||
"link": true,
|
"link": true,
|
||||||
"type": "other"
|
"type": "other"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "issue21458",
|
||||||
|
"file": "pdfs/issue21458.pdf",
|
||||||
|
"md5": "875754beca276ab63568e06fd49e8375",
|
||||||
|
"rounds": 1,
|
||||||
|
"link": true,
|
||||||
|
"type": "other"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "filled-background-range",
|
"id": "filled-background-range",
|
||||||
"file": "pdfs/filled-background.pdf",
|
"file": "pdfs/filled-background.pdf",
|
||||||
@ -3574,6 +3582,15 @@
|
|||||||
"rounds": 1,
|
"rounds": 1,
|
||||||
"type": "eq"
|
"type": "eq"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "cmykjpeg-disable-isOffscreenCanvasSupported",
|
||||||
|
"file": "pdfs/cmykjpeg.pdf",
|
||||||
|
"md5": "85d162b48ce98503a382d96f574f70a2",
|
||||||
|
"link": false,
|
||||||
|
"rounds": 1,
|
||||||
|
"type": "eq",
|
||||||
|
"isOffscreenCanvasSupported": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "cmykjpeg_nowasm",
|
"id": "cmykjpeg_nowasm",
|
||||||
"file": "pdfs/cmykjpeg.pdf",
|
"file": "pdfs/cmykjpeg.pdf",
|
||||||
@ -6777,6 +6794,13 @@
|
|||||||
"type": "eq",
|
"type": "eq",
|
||||||
"about": "Every blend mode that PDF supports."
|
"about": "Every blend mode that PDF supports."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "bug1873345",
|
||||||
|
"file": "pdfs/bug1873345.pdf",
|
||||||
|
"md5": "03483b94a2c02ac5f98c17ccf14de8ea",
|
||||||
|
"rounds": 1,
|
||||||
|
"type": "eq"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "transparency_group",
|
"id": "transparency_group",
|
||||||
"file": "pdfs/transparency_group.pdf",
|
"file": "pdfs/transparency_group.pdf",
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
AnnotationFieldFlag,
|
AnnotationFieldFlag,
|
||||||
AnnotationFlag,
|
AnnotationFlag,
|
||||||
AnnotationType,
|
AnnotationType,
|
||||||
|
bytesToString,
|
||||||
DrawOPS,
|
DrawOPS,
|
||||||
OPS,
|
OPS,
|
||||||
RenderingIntentFlag,
|
RenderingIntentFlag,
|
||||||
@ -41,6 +42,7 @@ import {
|
|||||||
} from "./test_utils.js";
|
} from "./test_utils.js";
|
||||||
import { Dict, Name, Ref, RefSetCache } from "../../src/core/primitives.js";
|
import { Dict, Name, Ref, RefSetCache } from "../../src/core/primitives.js";
|
||||||
import { Lexer, Parser } from "../../src/core/parser.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 { FlateStream } from "../../src/core/flate_stream.js";
|
||||||
import { PartialEvaluator } from "../../src/core/evaluator.js";
|
import { PartialEvaluator } from "../../src/core/evaluator.js";
|
||||||
import { StringStream } from "../../src/core/stream.js";
|
import { StringStream } from "../../src/core/stream.js";
|
||||||
@ -52,9 +54,10 @@ describe("annotation", function () {
|
|||||||
constructor(params) {
|
constructor(params) {
|
||||||
this.pdfDocument = {
|
this.pdfDocument = {
|
||||||
catalog: {
|
catalog: {
|
||||||
attachmentDictById: new Map(),
|
|
||||||
attachmentIdByRef: new RefSetCache(),
|
|
||||||
baseUrl: params.docBaseUrl || null,
|
baseUrl: params.docBaseUrl || null,
|
||||||
|
getAttachmentIdForAnnotation(ref) {
|
||||||
|
return `attachmentRef:${ref.toString()}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.evaluatorOptions = {
|
this.evaluatorOptions = {
|
||||||
@ -930,6 +933,55 @@ 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 () {
|
it("should correctly parse a GoTo action", async function () {
|
||||||
const actionDict = new Dict();
|
const actionDict = new Dict();
|
||||||
actionDict.set("Type", Name.get("Action"));
|
actionDict.set("Type", Name.get("Action"));
|
||||||
@ -2051,7 +2103,7 @@ describe("annotation", function () {
|
|||||||
annotationStorage
|
annotationStorage
|
||||||
);
|
);
|
||||||
expect(appearance).toEqual(
|
expect(appearance).toEqual(
|
||||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 3.07 Tm" +
|
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 3.21 Tm" +
|
||||||
" 2.61 0 Td (a) Tj 8 0 Td (a) Tj 8.56 0 Td (\\() Tj" +
|
" 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" +
|
" 7.44 0 Td (a) Tj 8 0 Td (a) Tj" +
|
||||||
" 8.56 0 Td (\\)) Tj 7.44 0 Td (a) Tj" +
|
" 8.56 0 Td (\\)) Tj 7.44 0 Td (a) Tj" +
|
||||||
@ -2092,7 +2144,7 @@ describe("annotation", function () {
|
|||||||
annotationStorage
|
annotationStorage
|
||||||
);
|
);
|
||||||
expect(appearance).toEqual(
|
expect(appearance).toEqual(
|
||||||
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 3.07 Tm" +
|
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 2.5 Tm" +
|
||||||
" 1.5 0 Td (\x30\x53) Tj 8 0 Td (\x30\x93) Tj 8 0 Td (\x30\x6b) Tj" +
|
" 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 (\x30\x61) Tj 8 0 Td (\x30\x6f) Tj" +
|
||||||
" 8 0 Td (\x4e\x16) Tj 8 0 Td (\x75\x4c) Tj" +
|
" 8 0 Td (\x4e\x16) Tj 8 0 Td (\x75\x4c) Tj" +
|
||||||
@ -2494,6 +2546,80 @@ describe("annotation", function () {
|
|||||||
expect(data.exportValue).toEqual("Checked");
|
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 () {
|
it("should handle checkboxes without export value", async function () {
|
||||||
buttonWidgetDict.set("V", Name.get("Checked"));
|
buttonWidgetDict.set("V", Name.get("Checked"));
|
||||||
buttonWidgetDict.set("DV", Name.get("Off"));
|
buttonWidgetDict.set("DV", Name.get("Off"));
|
||||||
@ -2982,6 +3108,126 @@ describe("annotation", function () {
|
|||||||
expect(data.buttonValue).toEqual("2");
|
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 () {
|
it("should handle radio buttons with a field value that's not an ASCII string", async function () {
|
||||||
const parentDict = new Dict();
|
const parentDict = new Dict();
|
||||||
const name = "\x91I=\x91\xf0\x93\xe0\x97e3";
|
const name = "\x91I=\x91\xf0\x93\xe0\x97e3";
|
||||||
@ -3256,6 +3502,113 @@ describe("annotation", function () {
|
|||||||
expect(changes.size).toEqual(0);
|
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 () {
|
it("should save radio buttons without a field value", async function () {
|
||||||
const appearanceStatesDict = new Dict();
|
const appearanceStatesDict = new Dict();
|
||||||
const normalAppearanceDict = new Dict();
|
const normalAppearanceDict = new Dict();
|
||||||
@ -4102,20 +4455,17 @@ describe("annotation", function () {
|
|||||||
idFactoryMock
|
idFactoryMock
|
||||||
);
|
);
|
||||||
expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT);
|
expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT);
|
||||||
expect(data.fileId.startsWith("annotation:")).toEqual(true);
|
// 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.file).toEqual({
|
expect(data.file).toEqual({
|
||||||
rawFilename: "Test.txt",
|
rawFilename: "Test.txt",
|
||||||
filename: "Test.txt",
|
filename: "Test.txt",
|
||||||
description: "abc",
|
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 reuse the attachment NameTree id for referenced files", async function () {
|
it("should re-derive an inline file attachment from its embedded stream", async function () {
|
||||||
const fileStream = new StringStream(
|
const fileStream = new StringStream(
|
||||||
"<<\n" +
|
"<<\n" +
|
||||||
"/Type /EmbeddedFile\n" +
|
"/Type /EmbeddedFile\n" +
|
||||||
@ -4131,41 +4481,36 @@ describe("annotation", function () {
|
|||||||
allowStreams: true,
|
allowStreams: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileStreamRef = Ref.get(28, 0);
|
const fileStreamRef = Ref.get(18, 0);
|
||||||
const fileStreamDict = parser.getObj();
|
const fileStreamDict = parser.getObj();
|
||||||
|
|
||||||
const embeddedFileDict = new Dict();
|
const embeddedFileDict = new Dict();
|
||||||
embeddedFileDict.set("F", fileStreamRef);
|
embeddedFileDict.set("F", fileStreamRef);
|
||||||
|
|
||||||
const fileSpecRef = Ref.get(29, 0);
|
// The file-spec is inline (not an indirect object), so the embedded-file
|
||||||
|
// stream's reference is encoded in the id instead.
|
||||||
const fileSpecDict = new Dict();
|
const fileSpecDict = new Dict();
|
||||||
fileSpecDict.set("Type", Name.get("Filespec"));
|
fileSpecDict.set("Type", Name.get("Filespec"));
|
||||||
fileSpecDict.set("Desc", "abc");
|
fileSpecDict.set("Desc", "abc");
|
||||||
fileSpecDict.set("EF", embeddedFileDict);
|
fileSpecDict.set("EF", embeddedFileDict);
|
||||||
fileSpecDict.set("UF", "Test.txt");
|
fileSpecDict.set("UF", "Test.txt");
|
||||||
|
|
||||||
const fileAttachmentRef = Ref.get(30, 0);
|
const fileAttachmentRef = Ref.get(20, 0);
|
||||||
const fileAttachmentDict = new Dict();
|
const fileAttachmentDict = new Dict();
|
||||||
fileAttachmentDict.set("Type", Name.get("Annot"));
|
fileAttachmentDict.set("Type", Name.get("Annot"));
|
||||||
fileAttachmentDict.set("Subtype", Name.get("FileAttachment"));
|
fileAttachmentDict.set("Subtype", Name.get("FileAttachment"));
|
||||||
fileAttachmentDict.set("FS", fileSpecRef);
|
fileAttachmentDict.set("FS", fileSpecDict);
|
||||||
fileAttachmentDict.set("T", "Topic");
|
fileAttachmentDict.set("T", "Topic");
|
||||||
fileAttachmentDict.set("Contents", "Test.txt");
|
fileAttachmentDict.set("Contents", "Test.txt");
|
||||||
|
|
||||||
const xref = new XRefMock([
|
const xref = new XRefMock([
|
||||||
{ ref: fileStreamRef, data: fileStreamDict },
|
{ ref: fileStreamRef, data: fileStreamDict },
|
||||||
{ ref: fileSpecRef, data: fileSpecDict },
|
|
||||||
{ ref: fileAttachmentRef, data: fileAttachmentDict },
|
{ ref: fileAttachmentRef, data: fileAttachmentDict },
|
||||||
]);
|
]);
|
||||||
embeddedFileDict.assignXref(xref);
|
embeddedFileDict.assignXref(xref);
|
||||||
fileSpecDict.assignXref(xref);
|
fileSpecDict.assignXref(xref);
|
||||||
fileAttachmentDict.assignXref(xref);
|
fileAttachmentDict.assignXref(xref);
|
||||||
|
|
||||||
pdfManagerMock.pdfDocument.catalog.attachmentIdByRef.put(
|
|
||||||
fileSpecRef,
|
|
||||||
"Test.txt"
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = await AnnotationFactory.create(
|
const { data } = await AnnotationFactory.create(
|
||||||
xref,
|
xref,
|
||||||
fileAttachmentRef,
|
fileAttachmentRef,
|
||||||
@ -4173,17 +4518,81 @@ describe("annotation", function () {
|
|||||||
idFactoryMock
|
idFactoryMock
|
||||||
);
|
);
|
||||||
expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT);
|
expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT);
|
||||||
expect(data.fileId).toEqual("Test.txt");
|
expect(data.fileId).toEqual("attachmentRef:18R");
|
||||||
expect(data.file).toEqual({
|
expect(data.file).toEqual({
|
||||||
rawFilename: "Test.txt",
|
rawFilename: "Test.txt",
|
||||||
filename: "Test.txt",
|
filename: "Test.txt",
|
||||||
description: "abc",
|
description: "abc",
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// File should not be added as it’s already referenced in the `NameTree`.
|
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");
|
||||||
expect(
|
expect(
|
||||||
pdfManagerMock.pdfDocument.catalog.attachmentDictById.has(data.fileId)
|
bytesToString(catalog.attachmentContent("attachmentRef:18R"))
|
||||||
).toEqual(false);
|
).toEqual("Named attachment");
|
||||||
|
expect(bytesToString(catalog.attachmentContent(annotationId))).toEqual(
|
||||||
|
"Annotation attachment"
|
||||||
|
);
|
||||||
|
// An unknown id resolves to no content.
|
||||||
|
expect(catalog.attachmentContent("nonexistent")).toEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3915,6 +3915,34 @@ describe("api", function () {
|
|||||||
await loadingTask.destroy();
|
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 () {
|
it("gets annotations containing /Launch action with /FileSpec dictionary (issue 17846)", async function () {
|
||||||
const loadingTask = getDocument(buildGetDocumentParams("issue17846.pdf"));
|
const loadingTask = getDocument(buildGetDocumentParams("issue17846.pdf"));
|
||||||
const pdfDoc = await loadingTask.promise;
|
const pdfDoc = await loadingTask.promise;
|
||||||
|
|||||||
@ -22,6 +22,9 @@ import {
|
|||||||
CFFStrings,
|
CFFStrings,
|
||||||
CFFTopDict,
|
CFFTopDict,
|
||||||
} from "../../src/core/cff_parser.js";
|
} 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 { SEAC_ANALYSIS_ENABLED } from "../../src/core/fonts_utils.js";
|
||||||
import { Stream } from "../../src/core/stream.js";
|
import { Stream } from "../../src/core/stream.js";
|
||||||
|
|
||||||
@ -357,6 +360,58 @@ 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 () {
|
it("refuses to add topDict key with invalid value (bug 1068432)", function () {
|
||||||
const topDict = cff.topDict;
|
const topDict = cff.topDict;
|
||||||
const defaultValue = topDict.getByName("UnderlinePosition");
|
const defaultValue = topDict.getByName("UnderlinePosition");
|
||||||
|
|||||||
@ -37,7 +37,6 @@ describe("util", function () {
|
|||||||
expect(exception.message).toEqual("Something went wrong");
|
expect(exception.message).toEqual("Something went wrong");
|
||||||
expect(exception.name).toEqual("DerivedException");
|
expect(exception.name).toEqual("DerivedException");
|
||||||
expect(exception.foo).toEqual("bar");
|
expect(exception.foo).toEqual("bar");
|
||||||
expect(exception.stack).toContain("BaseExceptionClosure");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -332,10 +332,7 @@ class AnnotationLayerBuilder {
|
|||||||
let linkAreaRects;
|
let linkAreaRects;
|
||||||
|
|
||||||
for (const annotation of this.#annotations) {
|
for (const annotation of this.#annotations) {
|
||||||
if (
|
if (annotation.annotationType !== AnnotationType.LINK) {
|
||||||
annotation.annotationType !== AnnotationType.LINK ||
|
|
||||||
!annotation.url
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// TODO: Add a test case to verify that we can find the intersection
|
// TODO: Add a test case to verify that we can find the intersection
|
||||||
|
|||||||
53
web/app.js
53
web/app.js
@ -385,6 +385,7 @@ const PDFViewerApplication = {
|
|||||||
maxCanvasPixels: x => parseInt(x, 10),
|
maxCanvasPixels: x => parseInt(x, 10),
|
||||||
spreadModeOnLoad: x => parseInt(x, 10),
|
spreadModeOnLoad: x => parseInt(x, 10),
|
||||||
supportsCaretBrowsingMode: x => x === "true",
|
supportsCaretBrowsingMode: x => x === "true",
|
||||||
|
supportsDownloading: x => x === "true",
|
||||||
viewerCssTheme: x => parseInt(x, 10),
|
viewerCssTheme: x => parseInt(x, 10),
|
||||||
forcePageColors: x => x === "true",
|
forcePageColors: x => x === "true",
|
||||||
pageColorsBackground: x => x,
|
pageColorsBackground: x => x,
|
||||||
@ -434,7 +435,16 @@ const PDFViewerApplication = {
|
|||||||
ignoreDestinationZoom: AppOptions.get("ignoreDestinationZoom"),
|
ignoreDestinationZoom: AppOptions.get("ignoreDestinationZoom"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const downloadManager = (this.downloadManager = new DownloadManager());
|
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 findController = (this.findController = new PDFFindController({
|
const findController = (this.findController = new PDFFindController({
|
||||||
linkService,
|
linkService,
|
||||||
@ -1165,6 +1175,23 @@ const PDFViewerApplication = {
|
|||||||
// Ignoring errors, to ensure that document closing won't break.
|
// 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 = [];
|
const promises = [];
|
||||||
|
|
||||||
promises.push(this.pdfLoadingTask.destroy());
|
promises.push(this.pdfLoadingTask.destroy());
|
||||||
@ -1294,6 +1321,13 @@ const PDFViewerApplication = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async download() {
|
async download() {
|
||||||
|
if (!this.downloadManager) {
|
||||||
|
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||||
|
this.eventBus.dispatch("downloadskipped", { source: this });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await (this.pdfDocument
|
data = await (this.pdfDocument
|
||||||
@ -1306,6 +1340,13 @@ const PDFViewerApplication = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
|
if (!this.downloadManager) {
|
||||||
|
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||||
|
this.eventBus.dispatch("downloadskipped", { source: this });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._saveInProgress) {
|
if (this._saveInProgress) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1337,6 +1378,13 @@ const PDFViewerApplication = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async downloadOrSave() {
|
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.
|
// In the Firefox case, this method MUST always trigger a download.
|
||||||
// When the user is closing a modified and unsaved document, we display a
|
// 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
|
// prompt asking for saving or not. In case they save, we must wait for
|
||||||
@ -2425,6 +2473,9 @@ const PDFViewerApplication = {
|
|||||||
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.downloadManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.pdfDocument) {
|
if (!this.pdfDocument) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,6 +102,11 @@ const defaultOptions = {
|
|||||||
value: true,
|
value: true,
|
||||||
kind: OptionKind.BROWSER,
|
kind: OptionKind.BROWSER,
|
||||||
},
|
},
|
||||||
|
supportsDownloading: {
|
||||||
|
/** @type {boolean} */
|
||||||
|
value: true,
|
||||||
|
kind: OptionKind.BROWSER,
|
||||||
|
},
|
||||||
supportsIntegratedFind: {
|
supportsIntegratedFind: {
|
||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
value: false,
|
value: false,
|
||||||
|
|||||||
@ -57,6 +57,7 @@ class PasswordPrompt {
|
|||||||
this.input.addEventListener("keydown", e => {
|
this.input.addEventListener("keydown", e => {
|
||||||
if (e.keyCode === /* Enter = */ 13) {
|
if (e.keyCode === /* Enter = */ 13) {
|
||||||
this.#verify();
|
this.#verify();
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -126,7 +126,7 @@ class PDFAttachmentViewer extends BaseTreeViewer {
|
|||||||
: fallbackContent;
|
: fallbackContent;
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
this.downloadManager.openOrDownloadData(content, filename);
|
this.downloadManager?.openOrDownloadData(content, filename);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -155,7 +155,10 @@ class PDFOutlineViewer extends BaseTreeViewer {
|
|||||||
const content = await linkService.getAttachmentContent(attachmentId);
|
const content = await linkService.getAttachmentContent(attachmentId);
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
this.downloadManager.openOrDownloadData(content, attachment.filename);
|
this.downloadManager?.openOrDownloadData(
|
||||||
|
content,
|
||||||
|
attachment.filename
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user