Compare commits

..

64 Commits

Author SHA1 Message Date
calixteman
1ddf6449ac
Merge pull request #21478 from calixteman/comb-field-vertical-centering
Vertically center the glyphs in comb text fields
2026-06-22 09:57:55 +02:00
Jonas Jenwald
28a7606c14
Merge pull request #21480 from Snuffleupagus/mathML-FileSpec
A couple of small tweaks of the `StructElementNode.prototype.mathML` getter
2026-06-21 23:44:28 +02:00
Jonas Jenwald
9c9b465fd2 A couple of small tweaks of the StructElementNode.prototype.mathML getter
- Use `FileSpec.pickPlatformItem` when getting the fileStream, to ensure that /EF-entries are handled in a consistent way across the code-base.

 - Combine a couple of the data-validation steps, to reduce a tiny bit of duplication. Also, use the `isDict` helper a little more.

 - Finally, avoid using a temporary variable when returning data in the `Page.prototype.getStructTree` method.
2026-06-21 22:47:13 +02:00
Tim van der Meij
8ebc2382e3
Merge pull request #21479 from Snuffleupagus/Annotation-#setOptionalContent-MissingDataException
Don't swallow `MissingDataException`s in the `Annotation.prototype.#setOptionalContent` method (PR 21313 follow-up)
2026-06-21 19:11:54 +02:00
Tim van der Meij
38daede697
Merge pull request #21481 from Snuffleupagus/metadata-isDict
Use the `isDict` helper in the `Catalog.prototype.metadata` getter
2026-06-21 19:10:54 +02:00
Tim van der Meij
86b901fcde
Merge pull request #21470 from Snuffleupagus/AnnotationEditorUIManager-rm-isSelected
Remove the unused `AnnotationEditorUIManager.prototype.isSelected` method
2026-06-21 19:09:32 +02:00
Tim van der Meij
1d8e952062
Merge pull request #21482 from Snuffleupagus/password-input-Enter-preventDefault
Stop event propagation, for the `Enter` key, in the passwordPrompt input
2026-06-21 19:05:50 +02:00
Tim van der Meij
018ba66228
Merge pull request #21472 from mozilla/dependabot/npm_and_yarn/undici-7.28.0
Bump undici from 7.24.3 to 7.28.0
2026-06-21 19:04:29 +02:00
Jonas Jenwald
a911ce22e5 Stop event propagation, for the Enter key, in the passwordPrompt input
**Steps to reproduce:**
 1. Open the viewer.
 2. Show the sidebar, and switch to the "Pages" view if necessary.
 3. Click on the "Add file" button.
 4. Choose a password-protected PDF, e.g. the `issue6010_1.pdf` file, via the "File Upload" dialog opened by the browser.
 5. Enter the password, i.e. `abc`, and press the <kbd>Enter</kbd> key.

**Expected result:**
That the new PDF document is merged into the existing one, without UI side-effects.

**Actual result:**
Merging works, *however* the "File Upload" dialog is re-opened.

---

It seems that when the passwordPrompt dialog closes, the <kbd>Enter</kbd> key press (from the input) is forwarded to the previously focused element which naturally is the "Add file" button.

*Note:* This doesn't seem (easily) possible to test, since the integration-tests directly populate the `viewsManagerAddFilePicker` and doesn't actually "click" on the `viewsManagerAddFileButton` first.
2026-06-21 15:30:05 +02:00
Jonas Jenwald
a46ee2b647 Use the isDict helper in the Catalog.prototype.metadata getter 2026-06-21 12:26:21 +02:00
Jonas Jenwald
bd6541864b Don't swallow MissingDataExceptions in the Annotation.prototype.#setOptionalContent method (PR 21313 follow-up)
Unless the entire document has been loaded, the dictionary lookups in `parseMarkedContentProps` may throw `MissingDataException`s and in that case we need to re-parse the current Annotation rather than ignoring the optionalContent.
2026-06-21 09:13:33 +02:00
Jonas Jenwald
124228e318
Merge pull request #21473 from Snuffleupagus/showText-rm-return-undefined
Remove unnecessary explicit return statements in `CanvasGraphics.prototype.showText`
2026-06-20 22:31:58 +02:00
Jonas Jenwald
bff30726fa
Merge pull request #21476 from Snuffleupagus/relative-URI-action-test
Add a unit-test for relative URI actions specified as /Name instances
2026-06-20 22:31:02 +02:00
Calixte Denizet
34516bcec3 Vertically center the glyphs in comb text fields 2026-06-20 18:47:24 +02:00
Jonas Jenwald
bade1f3190 Add a unit-test for relative URI actions specified as /Name instances
The following branch was added to fix issue 4159, however looking at the coverage data it's not actually tested; see 59df671552/src/core/catalog.js (L1866-L1869) and 59df671552/blob/src/core/catalog.js (L1866)
2026-06-19 23:58:50 +02:00
Jonas Jenwald
00e1aabe93 Remove unnecessary explicit return statements in CanvasGraphics.prototype.showText 2026-06-19 10:58:14 +02:00
dependabot[bot]
dfa673290b
Bump undici from 7.24.3 to 7.28.0
Bumps [undici](https://github.com/nodejs/undici) from 7.24.3 to 7.28.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.24.3...v7.28.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.28.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 07:33:31 +00:00
calixteman
59df671552
Merge pull request #21471 from mozilla/update-locales
l10n: Update locale files
2026-06-19 08:10:32 +02:00
github-actions[bot]
87361aa094 l10n: Update locale files 2026-06-19 01:04:57 +00:00
Jonas Jenwald
786019eb0d Remove the unused AnnotationEditorUIManager.prototype.isSelected method
According to the coverage data this method is unused, see e20c810dd4/blob/src/display/editor/tools.js (L2552), and searching through the entire code-base reveals no call-site invoking an `isSelected` method.
2026-06-18 23:21:53 +02:00
calixteman
e20c810dd4
Merge pull request #21469 from calixteman/issue21466
Avoid too long BlueScale value when rewriting a CFF font
2026-06-18 21:43:05 +02:00
calixteman
07d4c1018a
Avoid too long BlueScale value when rewriting a CFF font
It fixes #21466.
2026-06-18 20:48:13 +02:00
Jonas Jenwald
e74be44919
Merge pull request #21467 from Snuffleupagus/canvas-rm-unused
Remove unused branches in the `src/display/canvas.js` file
2026-06-18 19:57:51 +02:00
Jonas Jenwald
b4b0a3fa04 Remove the unused ImageData branch in the putBinaryImageData function
This branch isn't covered by any tests, and looking at the two existing call-sites we only ever pass in a `CanvasRenderingContext2D` interface to this function.
Based on the git history this branch was added in PR 3312, however as far as I can tell it doesn't actually appear to have been necessary even back then!?
2026-06-18 18:22:55 +02:00
Jonas Jenwald
a443a635a1 Remove the unused HTMLElement branch in the paintInlineImageXObject method
This branch isn't covered by any tests, and as far as I can tell it's been unused ever since PR 11601 which simplified the JPEG image handling.
Prior to that we'd create an `Image` instance in one case, see [this code](https://github.com/mozilla/pdf.js/pull/11601/changes#diff-082d6b37ad01db7ac97cc07c6ddb0dc52040484c5ef91b110b072f50144d9f39L2312-L2314), which is why that branch was necessary since `new Image()` creates a `HTMLImageElement` instance which in itself is an instance of `HTMLElement`; note [this](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image) respectively [this](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement).
2026-06-18 17:47:26 +02:00
calixteman
3956ac1b39
Merge pull request #21465 from calixteman/fix_attachments
Re-derive annotation attachment content from the xref after cleanup
2026-06-18 16:48:13 +02:00
Calixte Denizet
9d9fb06d7f Re-derive annotation attachment content from the xref after cleanup
Annotation-local attachments (those not in the catalog `/Names` tree) were
resolved through a dictionary cache that `Catalog.cleanup` clears, so their
content became unreachable once the idle cleanup had run.

Encode the reference of the embedded content in the attachment id and re-fetch
it from the xref on demand instead of caching the dictionary, so the content
stays reachable without anything having to survive cleanup.

It fixes a regression introduced by #21351.
2026-06-18 16:03:32 +02:00
calixteman
187c22126a
Merge pull request #21310 from calixteman/dont_save
Add a 'supportsDownloading' browser option to gate saving/downloading
2026-06-18 15:36:41 +02:00
Calixte Denizet
ece1e2ed0c Add a 'supportsDownloading' browser option to gate saving/downloading
Introduces a 'supportsDownloading' browser option (defaulting to false)
that lets embedders disable the save/download paths entirely. When
disabled:
  - the toolbar and secondary-toolbar download buttons are hidden;
  - PDFViewerApplication.{download,save,downloadOrSave} and the
    "beforeunload" save prompt bail out early;
  - the BaseDownloadManager helpers (download, downloadData,
    openOrDownloadData) and the Firefox/generic _triggerDownload
    implementations no-op.
2026-06-18 14:51:32 +02:00
Jonas Jenwald
2ed018ec2d
Merge pull request #21460 from Snuffleupagus/autolinking-check-every-LinkAnnotation
Check every LinkAnnotation when testing if inferred links overlap (issue 21458)
2026-06-16 22:32:07 +02:00
calixteman
eae42379f2
Merge pull request #21462 from calixteman/bluescale-small-zones
Don't clamp BlueScale up when a font genuinely has small zones
2026-06-16 21:52:25 +02:00
calixteman
d28030f838
Merge pull request #21463 from calixteman/fix_unit_test
Adjust the 'BaseException' unit-test for the 'Error.stack' changes in Firefox
2026-06-16 21:43:49 +02:00
Jonas Jenwald
cbefb334fd Check every LinkAnnotation when testing if inferred links overlap (issue 21458)
Currently we only check LinkAnnotations with URLs, but completely ignore e.g. internal destinations, named actions, attachments, SetOCGState actions, JS actions, and ResetForm actions when testing if inferred links overlap any existing annotation.
This seems conceptually wrong, since it may easily break intended functionality by overlaying the *correct* DOM element with an inferred link (as was the case in issue 21458).
2026-06-16 21:35:44 +02:00
Calixte Denizet
5432642250 Adjust the 'BaseException' unit-test for the 'Error.stack' changes in Firefox
Firefox 154 no longer walks the prototype chain in the `Error.stack`
getter, so `BaseException`-derived instances return an empty string
rather than the prototype `Error`'s stack (see bug 1946559).
2026-06-16 21:30:41 +02:00
Tim van der Meij
f0dc2166ab
Merge pull request #21464 from mozilla/dependabot/npm_and_yarn/markdown-it-14.2.0
Bump markdown-it from 14.1.1 to 14.2.0
2026-06-16 21:16:19 +02:00
dependabot[bot]
9ed97a859f
Bump markdown-it from 14.1.1 to 14.2.0
Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 14.1.1 to 14.2.0.
- [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md)
- [Commits](https://github.com/markdown-it/markdown-it/compare/14.1.1...14.2.0)

---
updated-dependencies:
- dependency-name: markdown-it
  dependency-version: 14.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 19:01:51 +00:00
Tim van der Meij
8bc13d502b
Merge pull request #21459 from Snuffleupagus/PDFEditor-ternary-Dict-set
Use ternary expressions to shorten code in `src/core/editor/pdf_editor.js`
2026-06-16 20:50:05 +02:00
Tim van der Meij
36eae2c978
Merge pull request #21461 from mozilla/dependabot/github_actions/github/codeql-action-4.36.2
Bump github/codeql-action from 4.36.1 to 4.36.2
2026-06-16 20:48:22 +02:00
calixteman
bc99fc0678
Don't clamp BlueScale up when a font genuinely has small zones
The lower BlueScale clamp from #21343 guarded foundry fonts via
`blueScale < DEFAULT_BLUE_SCALE`, but that lets a near-default value
(e.g. 0.037) with small zones get raised to `0.5 / maxZoneHeight`. On
macOS' Core Text rasterizer this collapses the overshooting glyphs, so
most text disappears (not reproducible on Linux/Windows).
2026-06-16 19:34:48 +02:00
calixteman
fdeed2af5e
Merge pull request #21455 from calixteman/bug1873345
Draw non-isolated blend-mode groups against their backdrop (bug 1873345)
2026-06-16 15:26:00 +02:00
Calixte Denizet
082ad21387 Draw non-isolated blend-mode groups against their backdrop (bug 1873345)
A non-isolated transparency group must blend with its backdrop, but a group
containing a blend mode was forced onto a transparent intermediate canvas;
e.g. a /Multiply highlight then painted opaquely over the text behind it,
 making that text invisible.
2026-06-16 15:09:39 +02:00
dependabot[bot]
6cdd3c19fd
Bump github/codeql-action from 4.36.1 to 4.36.2
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.36.1 to 4.36.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](87557b9c84...8aad20d150)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 12:14:06 +00:00
Jonas Jenwald
6c0ad865c6 Use ternary expressions to shorten code in src/core/editor/pdf_editor.js
This makes setting a few Dictionary entries a little bit shorter, which shouldn't hurt.
(Part of this code isn't fully covered by tests, so it improves overall code coverage as well.)
2026-06-16 11:50:53 +02:00
Tim van der Meij
7f7b38b424
Merge pull request #21457 from Snuffleupagus/GetAnnotationsByType-rm-page-check
Remove unneeded check in the "GetAnnotationsByType" worker-thread handler
2026-06-15 21:41:10 +02:00
Tim van der Meij
7d3ec9da1e
Merge pull request #21456 from Snuffleupagus/rm-classNamesForOutlining
Remove the unused `HighlightOutline.prototype.classNamesForOutlining` getter
2026-06-15 20:46:32 +02:00
calixteman
67e6ff0090
Merge pull request #21428 from calixteman/checkubtton_opt
Resolve checkbox/radio export values from the 'Opt' entry
2026-06-15 19:43:28 +02:00
Calixte Denizet
2550b91be9 Resolve checkbox/radio export values from the 'Opt' entry
For checkbox and radio button fields, the export value can differ from the
appearance-state name: the field's inheritable `Opt` array holds the real
export values (used for non-Latin text, or values shared between buttons).
We previously exposed the appearance-state name as the export value.
2026-06-15 18:23:26 +02:00
Jonas Jenwald
d9e4cc5f65 Remove unneeded check in the "GetAnnotationsByType" worker-thread handler
Given that the Promise returned by the `PDFDocument.prototype.getPage` method *always* resolves with a `Page` instance, checking that the page is defined isn't necessary; note 3a09329113/src/core/document.js (L1702-L1723)

Furthermore the `Page.prototype.collectAnnotationsByType` method is asynchronous, and thus it always returns a Promise, hence it's "pointless" to fallback to return an empty Array.
2026-06-15 13:12:47 +02:00
Jonas Jenwald
f781ac33da Remove the unused HighlightOutline.prototype.classNamesForOutlining getter
This was added in PR 18972 and it became unused in PR 19085, however it was accidentally left behind.
2026-06-15 12:02:46 +02:00
Tim van der Meij
3a09329113
Merge pull request #21454 from timvandermeij/eslint-plugin-unicorn
Upgrade `eslint-plugin-unicorn` to version 66.0.0
2026-06-14 21:51:42 +02:00
Tim van der Meij
0ea67ed96f
Upgrade eslint-plugin-unicorn to version 66.0.0
This is a major version bump, but the changelog at
https://github.com/sindresorhus/eslint-plugin-unicorn/releases/tag/v66.0.0
doesn't indicate any breaking changes that should impact us.

However, improved rules do require a small number of changes here:

- The `prefer-array-some` rule no longer reports a false positive after
  https://github.com/sindresorhus/eslint-plugin-unicorn/issues/3198 got
  fixed, so the ignore line that was added in commit 68a5ec1 is removed.

- The `prefer-ternary` rule triggers on more cases now, in particular
  `let` declarations with `if` reassignments, so a number of changes are
  made to make it pass again.

- The `prefer-at` rule triggers on more cases now, in particular
  `substring` calls that just extract a single character, so one change
  is made to make it pass again.
2026-06-14 20:20:34 +02:00
calixteman
92bd2dbb38
Merge pull request #21453 from timvandermeij/puppeteer-skip-download
Don't download Puppeteer browsers on `npm install`
2026-06-14 19:25:32 +02:00
Tim van der Meij
26b4206d87
Configure Puppeteeer to not download Chrome/Firefox by default
We currently download Chrome/Firefox immediately on `npm install`
invocations because Puppeteer's postinstall script does that by default.
However, this is wasteful if the user/workflow doesn't actually need to
run Puppeteer or its browsers, for example in GitHub Actions workflows
that do linting, static analysis or other tasks like updating locales or
publishing artifacts.

This commit therefore makes sure no browser binaries get pulled in by
default anymore, and defers doing that until it's actually necessary,
which is when we want to start the browsers in the `startBrowsers`
function of `test.mjs`.

Locally this brings the `npm install` runtime down from 8.998 to 1.800
seconds, and as a bonus it results in better log output too because it
now shows which browser versions were used in the run (whereas
previously with `npm install` this information was not sent to stdout).
2026-06-14 17:15:47 +02:00
Tim van der Meij
6bfefa53da
Configure Puppeteer to use the stable version of Chrome
We currently use the pinned version of Chrome as hardcoded in the
Puppeteer release, which is based on the version of Chrome that was
deemed stable at the time of the Puppeteer release.

However, this is not ideal because it means that Chrome updates are
strongly tied to Puppeteer releases, so if Puppeteer releases are slow
we could be missing out on e.g. (security) patches being applied on the
stable channel. It's also not consistent with Firefox where we don't
use a hardcoded pinned version either.

This commit therefore configures Puppeteer to use (resolve) the most
recent stable version of Chrome at the time of the installation so that
determining the browser version to use is fully decoupled from the
Puppeteer release we're running.

The effect of this change can be seen in the output of running
`npx puppeteer browsers list`:

Before:

`chrome@149.0.7827.22 (linux) <path>`

After (note the slightly newer version):

`chrome@149.0.7827.115 (linux)` <path>`
2026-06-14 17:12:43 +02:00
Tim van der Meij
66c22b1fc5
Configure Puppeteer to not download Chrome headless shell
Nowadays Chrome has a built-in (new) headless mode in the regular
binary, but before that time there was an old headless mode that was
essentially a separate binary [1]. We don't use the latter, but it turns
out that Puppeteer downloads it automatically if it's not explicitly
skipped, which is wasteful because it costs extra time and resources for
each `npm install` invocation.

This commit therefore skips downloading Chrome headless shell explictly,
which results in the local runtime of `npm install` going from 10.125
seconds to 8.998 seconds (which can't hurt in e.g. GitHub Actions).

[1] https://developer.chrome.com/blog/chrome-headless-shell.
2026-06-14 16:59:13 +02:00
Tim van der Meij
8560125056
Merge pull request #21448 from timvandermeij/comment-intermittent
Fix intermittent failure in the `must check that the comment sidebar is resizable` comment integration test
2026-06-14 16:35:20 +02:00
Tim van der Meij
ed1b2f91be
Merge pull request #21440 from Snuffleupagus/putBinaryImageData-convertRGBToRGBA
Use the `convertRGBToRGBA` helper with RGB images in `putBinaryImageData`
2026-06-14 14:20:11 +02:00
Tim van der Meij
bfcafbc004
Merge pull request #21444 from Snuffleupagus/merge-test-password
Add an integration-test for merging a password-protected PDF
2026-06-14 14:16:35 +02:00
Jonas Jenwald
2dc73ad2a7 Collect coverage data from all workers when closing integration-tests
The "Merge PDF" integration-tests will (indirectly) invoke `PDFViewerApplication.open` as part of loading the new PDF document, which will end up creating a new `PDFWorker` instance.
Currently worker coverage is only collected at the end of each integration-test, which means that in these cases we miss the coverage data from any "previous" workers.
2026-06-14 13:27:06 +02:00
Jonas Jenwald
feec28583d Add an integration-test for merging a password-protected PDF
Looking at the coverage data the password-handling part of the merge functionality wasn't being tested; see e75a7cfd62/blob/src/core/worker.js (L652)
2026-06-14 13:17:33 +02:00
Jonas Jenwald
1373aa4a48
Merge pull request #21452 from Snuffleupagus/merge-test-corrupt
Add an integration-test for merging a corrupt PDF
2026-06-14 13:13:47 +02:00
Jonas Jenwald
e1c930adfe Add an integration-test for merging a corrupt PDF
Currently when opening a PDF document the following code is used, where `checkFirstPage`/`checkLastPage` helps detect XRef corruption; note 86a18bd5fe/src/core/worker.js (L167-L176)

However when merging a PDF into an existing document the parsing is only "partial"; note 86a18bd5fe/src/core/worker.js (L632-L634)

It seems a little strange to not support corrupt PDFs in a consistent manner in the code-base, hence this patch adds a new `BasePdfManager` helper that handles all the relevant parsing/checking and re-uses that when merging PDFs.
2026-06-14 09:49:23 +02:00
Tim van der Meij
d305b542df
Fix intermittent failure in the must check that the comment sidebar is resizable comment integration test
We use the generic `page.mouse.move(x, y, { steps }` API, but that purely
performs the mouse move steps without having knowledge about if/how the
application handles any events caused by it, so it doesn't wait for the
sidebar to render before moving on. This causes intermittent failures if
the sidebar didn't get enough time to render before the next mouse move
is initiated (which can happen in slower environments).

This commit fixes the issue by doing the mouse move steps ourselves and
by waiting for a browser trip between each of them to make sure that the
sidebar got a chance to render.

Fixes #21447.
Relates to #21044 / #21045 / 24e5377.
2026-06-13 21:32:00 +02:00
Jonas Jenwald
55c8516944 Use the convertRGBToRGBA helper with RGB images in putBinaryImageData
This removes a little bit of code duplication, which only exist since the `src/display/canvas.js` code pre-dates the helper function by many years.

Note: Given that `OffscreenCanvas` is enabled by default there's currently not a lot of test coverage for this code-path, hence the added browser-test.
2026-06-13 13:14:50 +02:00
54 changed files with 1553 additions and 419 deletions

View File

@ -24,13 +24,13 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
queries: security-and-quality
- 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
uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2

View File

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

View File

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

View File

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

View File

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

73
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -119,18 +119,12 @@ function fetchRemoteDest(action) {
class Catalog {
#actualNumPages = null;
/** @type {RefSetCache | null} */
#attachmentIdByRef = null;
#annotationAttachmentIdByRef = new RefSetCache();
#annotationAttachmentRefById = new Map();
#catDict = null;
/**
* Attachment dictionaries keyed by attachment id.
*
* @type {Map<string, Dict>}
*/
attachmentDictById = new Map();
builtInCMapCache = new Map();
fontCache = new RefSetCache();
@ -164,33 +158,44 @@ class Catalog {
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() {
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() {
const version = this.#catDict.get("Version");
if (version instanceof Name) {
@ -274,19 +279,18 @@ class Catalog {
/* suppressEncryption = */ !this.xref.encrypt?.encryptMetadata
);
if (stream instanceof BaseStream && stream.dict instanceof Dict) {
const type = stream.dict.get("Type");
const subtype = stream.dict.get("Subtype");
if (isName(type, "Metadata") && isName(subtype, "XML")) {
// XXX: This should examine the charset the XML document defines,
// however since there are currently no real means to decode arbitrary
// charsets, let's just hope that the author of the PDF was reasonable
// enough to stick with the XML default charset, which is UTF-8.
const data = stringToUTF8String(stream.getString());
if (data) {
metadata = new MetadataParser(data).serializable;
}
if (
stream instanceof BaseStream &&
isDict(stream.dict, "Metadata") &&
isName(stream.dict.get("Subtype"), "XML")
) {
// XXX: This should examine the charset the XML document defines,
// however since there are currently no real means to decode arbitrary
// charsets, let's just hope that the author of the PDF was reasonable
// enough to stick with the XML default charset, which is UTF-8.
const data = stringToUTF8String(stream.getString());
if (data) {
metadata = new MetadataParser(data).serializable;
}
}
} catch (ex) {
@ -1157,19 +1161,12 @@ class Catalog {
}
/**
* Get content for an attachment.
*
* @param {string} id
* Unique attachment identifier (required).
* @returns {CatalogAttachmentContent}
* Content.
* Unique attachment identifier.
* @returns {CatalogAttachmentContent | undefined}
* Content, or `undefined` when no named attachment exists for the id.
*/
attachmentContent(id) {
const dict = this.attachmentDictById.get(id);
if (dict) {
return FileSpec.readContent(dict);
}
#attachmentContentByName(id) {
const obj = this.#catDict.get("Names");
if (obj instanceof Dict && obj.has("EmbeddedFiles")) {
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;
}
@ -1280,9 +1307,6 @@ class Catalog {
async cleanup(manuallyTriggered = false) {
clearGlobalCaches();
this.#attachmentIdByRef?.clear();
this.#attachmentIdByRef = null;
this.attachmentDictById.clear();
this.globalColorSpaceCache.clear();
this.globalImageCache.clear(/* onlyData = */ manuallyTriggered);
this.pageKidsCountCache.clear();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 () {
let pages;

View File

@ -537,7 +537,11 @@ describe("Comment", () => {
await page.mouse.down();
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();
const rectAfter = await getRect(page, sidebarSelector);

View File

@ -127,9 +127,11 @@ async function waitForHavingContents(page, expected) {
});
return page.waitForFunction(
ex => {
const textLayers = document.querySelectorAll(".textLayer");
const buffer = [];
for (const textLayer of document.querySelectorAll(".textLayer")) {
buffer.push(parseInt(textLayer.textContent.trim(), 10));
for (const [i, textLayer] of textLayers.entries()) {
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]);
},
@ -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 12 come from the original document, then the page of
// the merged PDF, then page 3 of the original shifted to the end.
await waitForHavingContents(page, [1, 2, "Issue 6010", 3]);
await waitForTextToBe(
page,
"#viewsManagerStatusActionLabel",
`${FSI}1${PDI} selected`
);
})
);
});
it("should merge a corrupt PDF (with invalid pages /Count) after the current page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
// Navigate to page 2 so the merged PDF is inserted after it.
await page.evaluate(() => {
window.PDFViewerApplication.page = 2;
});
await page.waitForFunction(
() => window.PDFViewerApplication.page === 2
);
await waitAndClick(page, getThumbnailSelector(2));
const handleMerged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"thumbnailsloaded",
resolve,
{ once: true }
);
});
const picker = await page.$("#viewsManagerAddFilePicker");
await picker.uploadFile(
path.join(__dirname, "../pdfs/poppler-91414-0-53.pdf")
);
await awaitPromise(handleMerged);
// Original 3 pages + 1 merged page = 4 pages total.
await page.waitForFunction(
() => parseInt(document.getElementById("pageNumber").max, 10) === 4
);
// Focus must move to the first newly inserted page (page 3, since
// we merged after page 2).
await page.waitForFunction(
() => window.PDFViewerApplication.page === 3
);
// Pages 12 come from the original document, then the page of
// the merged PDF, then page 3 of the original shifted to the end.
await waitForHavingContents(page, [1, 2, "foobar", 3]);
await waitForTextToBe(
page,
"#viewsManagerStatusActionLabel",
`${FSI}1${PDI} selected`
);
})
);
});
});
describe("Drag-and-drop PDF merge", () => {

View File

@ -2745,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]"`
);
})
);
});
});
});

View File

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

View File

@ -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", () => {
let pages;

View File

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

BIN
test/pdfs/bug1873345.pdf Normal file

Binary file not shown.

Binary file not shown.

View File

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

133
test/pdfs/opt_demo.pdf Normal file
View 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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import {
AnnotationFieldFlag,
AnnotationFlag,
AnnotationType,
bytesToString,
DrawOPS,
OPS,
RenderingIntentFlag,
@ -41,6 +42,7 @@ import {
} from "./test_utils.js";
import { Dict, Name, Ref, RefSetCache } from "../../src/core/primitives.js";
import { Lexer, Parser } from "../../src/core/parser.js";
import { Catalog } from "../../src/core/catalog.js";
import { FlateStream } from "../../src/core/flate_stream.js";
import { PartialEvaluator } from "../../src/core/evaluator.js";
import { StringStream } from "../../src/core/stream.js";
@ -52,9 +54,10 @@ describe("annotation", function () {
constructor(params) {
this.pdfDocument = {
catalog: {
attachmentDictById: new Map(),
attachmentIdByRef: new RefSetCache(),
baseUrl: params.docBaseUrl || null,
getAttachmentIdForAnnotation(ref) {
return `attachmentRef:${ref.toString()}`;
},
},
};
this.evaluatorOptions = {
@ -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 () {
const actionDict = new Dict();
actionDict.set("Type", Name.get("Action"));
@ -2051,7 +2103,7 @@ describe("annotation", function () {
annotationStorage
);
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" +
" 7.44 0 Td (a) Tj 8 0 Td (a) Tj" +
" 8.56 0 Td (\\)) Tj 7.44 0 Td (a) Tj" +
@ -2092,7 +2144,7 @@ describe("annotation", function () {
annotationStorage
);
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" +
" 8 0 Td (\x30\x61) Tj 8 0 Td (\x30\x6f) 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");
});
it("should handle checkboxes with an Opt export value", async function () {
// The appearance state is the index "0"; the real export value lives in
// the "Opt" array (e.g. for values that aren't valid Name objects).
buttonWidgetDict.set("V", Name.get("0"));
buttonWidgetDict.set("DV", Name.get("0"));
buttonWidgetDict.set("Opt", ["I Agree to terms"]);
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
normalAppearanceDict.set("Off", 0);
normalAppearanceDict.set("0", 1);
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
const buttonWidgetRef = Ref.get(124, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);
const { data } = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.checkBox).toEqual(true);
expect(data.radioButton).toEqual(false);
expect(data.exportValue).toEqual("I Agree to terms");
expect(data.fieldValue).toEqual("I Agree to terms");
expect(data.defaultFieldValue).toEqual("I Agree to terms");
});
it("should handle checkboxes with an Opt export value in the parent", async function () {
const buttonWidgetRef = Ref.get(124, 0);
const parentRef = Ref.get(125, 0);
const parentDict = new Dict();
parentDict.set("V", Name.get("CheckedState"));
parentDict.set("DV", Name.get("CheckedState"));
parentDict.set("Kids", [buttonWidgetRef]);
parentDict.set("T", "CheckboxGroup");
parentDict.set("Opt", ["I Agree to terms"]);
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
normalAppearanceDict.set("Off", 0);
normalAppearanceDict.set("CheckedState", 1);
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
buttonWidgetDict.set("Parent", parentRef);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
{ ref: parentRef, data: parentDict },
]);
buttonWidgetDict.xref = parentDict.xref = xref;
const { data } = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.checkBox).toEqual(true);
expect(data.radioButton).toEqual(false);
expect(data.exportValue).toEqual("I Agree to terms");
expect(data.fieldValue).toEqual("I Agree to terms");
expect(data.defaultFieldValue).toEqual("I Agree to terms");
});
it("should handle checkboxes without export value", async function () {
buttonWidgetDict.set("V", Name.get("Checked"));
buttonWidgetDict.set("DV", Name.get("Off"));
@ -2982,6 +3108,126 @@ describe("annotation", function () {
expect(data.buttonValue).toEqual("2");
});
it("should handle radio buttons with an Opt export value", async function () {
const parentDict = new Dict();
parentDict.set("V", Name.get("1"));
parentDict.set("Opt", ["Apple", "Banane", "Cherry"]);
const normalAppearanceStateDict = new Dict();
normalAppearanceStateDict.set("2", null);
const appearanceStatesDict = new Dict();
appearanceStatesDict.set("N", normalAppearanceStateDict);
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
buttonWidgetDict.set("Parent", parentDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
const buttonWidgetRef = Ref.get(124, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);
const { data } = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.radioButton).toEqual(true);
// The field value (parent "V" = "1") and this widget's own on-state ("2")
// are both mapped through "Opt" to their real export values.
expect(data.fieldValue).toEqual("Banane");
expect(data.buttonValue).toEqual("Cherry");
});
it("should not map non-canonical numeric radio states through Opt", async function () {
const parentDict = new Dict();
parentDict.set("V", Name.get("02"));
parentDict.set("Opt", ["Apple", "Banane", "Cherry"]);
const normalAppearanceStateDict = new Dict();
normalAppearanceStateDict.set("02", null);
const appearanceStatesDict = new Dict();
appearanceStatesDict.set("N", normalAppearanceStateDict);
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
buttonWidgetDict.set("Parent", parentDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
const buttonWidgetRef = Ref.get(124, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);
const { data } = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.radioButton).toEqual(true);
expect(data.fieldValue).toEqual("02");
expect(data.buttonValue).toEqual("02");
});
it("should handle radio buttons with non-numeric Opt export values", async function () {
const appleRef = Ref.get(122, 0);
const bananaRef = Ref.get(123, 0);
const cherryRef = Ref.get(124, 0);
const parentRef = Ref.get(125, 0);
const parentDict = new Dict();
parentDict.set("V", Name.get("BananaState"));
parentDict.set("Kids", [appleRef, bananaRef, cherryRef]);
parentDict.set("T", "Fruits");
parentDict.set("Opt", ["Apple", "Banane", "Cherry"]);
const createRadioDict = state => {
const radioDict = buttonWidgetDict.clone();
const normalAppearanceStateDict = new Dict();
normalAppearanceStateDict.set(state, null);
const appearanceStatesDict = new Dict();
appearanceStatesDict.set("N", normalAppearanceStateDict);
radioDict.set("Ff", AnnotationFieldFlag.RADIO);
radioDict.set("Parent", parentRef);
radioDict.set("AP", appearanceStatesDict);
return radioDict;
};
const appleDict = createRadioDict("AppleState");
const bananaDict = createRadioDict("BananaState");
const cherryDict = createRadioDict("CherryState");
const xref = new XRefMock([
{ ref: appleRef, data: appleDict },
{ ref: bananaRef, data: bananaDict },
{ ref: cherryRef, data: cherryDict },
{ ref: parentRef, data: parentDict },
]);
appleDict.xref =
bananaDict.xref =
cherryDict.xref =
parentDict.xref =
xref;
const { data } = await AnnotationFactory.create(
xref,
cherryRef,
annotationGlobalsMock,
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.radioButton).toEqual(true);
expect(data.fieldValue).toEqual("Banane");
expect(data.buttonValue).toEqual("Cherry");
});
it("should handle radio buttons with a field value that's not an ASCII string", async function () {
const parentDict = new Dict();
const name = "\x91I=\x91\xf0\x93\xe0\x97e3";
@ -3256,6 +3502,113 @@ describe("annotation", function () {
expect(changes.size).toEqual(0);
});
it("should save radio buttons with Opt export values", async function () {
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
normalAppearanceDict.set("CheckedState", Ref.get(314, 0));
normalAppearanceDict.set("Off", Ref.get(271, 0));
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
buttonWidgetDict.set("AP", appearanceStatesDict);
const buttonWidgetRef = Ref.get(123, 0);
const parentRef = Ref.get(456, 0);
const parentDict = new Dict();
parentDict.set("V", Name.get("Off"));
parentDict.set("Kids", [buttonWidgetRef]);
parentDict.set("T", "RadioGroup");
parentDict.set("Opt", ["I Agree to terms"]);
buttonWidgetDict.set("Parent", parentRef);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
{ ref: parentRef, data: parentDict },
]);
parentDict.xref = xref;
buttonWidgetDict.xref = xref;
partialEvaluator.xref = xref;
const task = new WorkerTask("test save");
const annotation = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(annotation.data.buttonValue).toEqual("I Agree to terms");
const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: true });
const changes = new RefSetCache();
await annotation.save(partialEvaluator, task, annotationStorage, changes);
const data = await writeChanges(changes, xref);
expect(data.length).toEqual(2);
const [radioData, parentData] = data;
radioData.data = radioData.data.replace(/\(D:\d+\)/, "(date)");
expect(radioData.ref).toEqual(Ref.get(123, 0));
expect(radioData.data).toEqual(
"123 0 obj\n" +
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 32768 " +
"/AP << /N << /CheckedState 314 0 R /Off 271 0 R>>>> " +
"/Parent 456 0 R /AS /CheckedState /M (date)>>\nendobj\n"
);
expect(parentData.ref).toEqual(Ref.get(456, 0));
expect(parentData.data).toEqual(
"456 0 obj\n<< /V /CheckedState /Kids [123 0 R] /T (RadioGroup) " +
"/Opt [(I Agree to terms)]>>\nendobj\n"
);
});
it("should save checkboxes with Opt export values", async function () {
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
normalAppearanceDict.set("CheckedState", Ref.get(314, 0));
normalAppearanceDict.set("Off", Ref.get(271, 0));
appearanceStatesDict.set("N", normalAppearanceDict);
buttonWidgetDict.set("AP", appearanceStatesDict);
buttonWidgetDict.set("V", Name.get("Off"));
buttonWidgetDict.set("Opt", ["I Agree to terms"]);
const buttonWidgetRef = Ref.get(123, 0);
const xref = new XRefMock([
{ ref: buttonWidgetRef, data: buttonWidgetDict },
]);
buttonWidgetDict.xref = xref;
partialEvaluator.xref = xref;
const task = new WorkerTask("test save");
const annotation = await AnnotationFactory.create(
xref,
buttonWidgetRef,
annotationGlobalsMock,
idFactoryMock
);
expect(annotation.data.exportValue).toEqual("I Agree to terms");
const annotationStorage = new Map();
annotationStorage.set(annotation.data.id, { value: true });
const changes = new RefSetCache();
await annotation.save(partialEvaluator, task, annotationStorage, changes);
const [data] = await writeChanges(changes, xref);
data.data = data.data.replace(/\(D:\d+\)/, "(date)");
expect(data.ref).toEqual(Ref.get(123, 0));
expect(data.data).toEqual(
"123 0 obj\n" +
"<< /Type /Annot /Subtype /Widget /FT /Btn " +
"/AP << /N << /CheckedState 314 0 R /Off 271 0 R>>>> " +
"/V /CheckedState /Opt [(I Agree to terms)] " +
"/AS /CheckedState /M (date)>>\nendobj\n"
);
});
it("should save radio buttons without a field value", async function () {
const appearanceStatesDict = new Dict();
const normalAppearanceDict = new Dict();
@ -4102,20 +4455,17 @@ describe("annotation", function () {
idFactoryMock
);
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({
rawFilename: "Test.txt",
filename: "Test.txt",
description: "abc",
});
// Content lookup and reading requires a bigger mock than used here.
expect(
pdfManagerMock.pdfDocument.catalog.attachmentDictById.has(data.fileId)
).toEqual(true);
});
it("should 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(
"<<\n" +
"/Type /EmbeddedFile\n" +
@ -4131,41 +4481,36 @@ describe("annotation", function () {
allowStreams: true,
});
const fileStreamRef = Ref.get(28, 0);
const fileStreamRef = Ref.get(18, 0);
const fileStreamDict = parser.getObj();
const embeddedFileDict = new Dict();
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();
fileSpecDict.set("Type", Name.get("Filespec"));
fileSpecDict.set("Desc", "abc");
fileSpecDict.set("EF", embeddedFileDict);
fileSpecDict.set("UF", "Test.txt");
const fileAttachmentRef = Ref.get(30, 0);
const fileAttachmentRef = Ref.get(20, 0);
const fileAttachmentDict = new Dict();
fileAttachmentDict.set("Type", Name.get("Annot"));
fileAttachmentDict.set("Subtype", Name.get("FileAttachment"));
fileAttachmentDict.set("FS", fileSpecRef);
fileAttachmentDict.set("FS", fileSpecDict);
fileAttachmentDict.set("T", "Topic");
fileAttachmentDict.set("Contents", "Test.txt");
const xref = new XRefMock([
{ ref: fileStreamRef, data: fileStreamDict },
{ ref: fileSpecRef, data: fileSpecDict },
{ ref: fileAttachmentRef, data: fileAttachmentDict },
]);
embeddedFileDict.assignXref(xref);
fileSpecDict.assignXref(xref);
fileAttachmentDict.assignXref(xref);
pdfManagerMock.pdfDocument.catalog.attachmentIdByRef.put(
fileSpecRef,
"Test.txt"
);
const { data } = await AnnotationFactory.create(
xref,
fileAttachmentRef,
@ -4173,17 +4518,81 @@ describe("annotation", function () {
idFactoryMock
);
expect(data.annotationType).toEqual(AnnotationType.FILEATTACHMENT);
expect(data.fileId).toEqual("Test.txt");
expect(data.fileId).toEqual("attachmentRef:18R");
expect(data.file).toEqual({
rawFilename: "Test.txt",
filename: "Test.txt",
description: "abc",
});
});
// File should not be added as its 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(
pdfManagerMock.pdfDocument.catalog.attachmentDictById.has(data.fileId)
).toEqual(false);
bytesToString(catalog.attachmentContent("attachmentRef:18R"))
).toEqual("Named attachment");
expect(bytesToString(catalog.attachmentContent(annotationId))).toEqual(
"Annotation attachment"
);
// An unknown id resolves to no content.
expect(catalog.attachmentContent("nonexistent")).toEqual(null);
});
});

View File

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

View File

@ -22,6 +22,9 @@ import {
CFFStrings,
CFFTopDict,
} from "../../src/core/cff_parser.js";
import { DefaultFileReaderFactory, TEST_PDFS_PATH } from "./test_utils.js";
import { PDFDocument } from "../../src/core/document.js";
import { Ref } from "../../src/core/primitives.js";
import { SEAC_ANALYSIS_ENABLED } from "../../src/core/fonts_utils.js";
import { Stream } from "../../src/core/stream.js";
@ -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 () {
const topDict = cff.topDict;
const defaultValue = topDict.getByName("UnderlinePosition");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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