diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 89d8b1bcf615d..0000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,17 +0,0 @@ -Attach (recommended) or Link to PDF file here: - -Configuration: -- Web browser and its version: -- Operating system and its version: -- PDF.js version: -- Is a browser extension: - -Steps to reproduce the problem: -1. -2. - -What is the expected behavior? (add screenshot) - -What went wrong? (add screenshot) - -Link to a viewer (if hosted on a site other than mozilla.github.io/pdf.js or as Firefox/Chrome extension): diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000000..9a55ac44e1f40 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,76 @@ +name: Bug Report +description: Report a bug in PDF.js +title: "[Bug]: " +body: + - type: textarea + attributes: + label: Attach (recommended) or Link to PDF file + validations: + required: true + + - type: markdown + attributes: { value: "---" } + + - type: input + attributes: + label: Web browser and its version + validations: + required: true + - type: input + attributes: + label: Operating system and its version + validations: + required: true + - type: input + attributes: + label: PDF.js version + validations: + required: true + - type: dropdown + attributes: + label: Is the bug present in the latest PDF.js version? + options: ["Yes", "No"] + default: 0 + validations: + required: true + - type: dropdown + attributes: + label: Is a browser extension + options: ["Yes", "No"] + default: 1 + validations: + required: true + + - type: markdown + attributes: { value: "---" } + + - type: textarea + attributes: + label: Steps to reproduce the problem + placeholder: "1.\n2." + validations: + required: true + + - type: textarea + attributes: + label: What is the expected behavior? + description: Also add a screenshot + validations: + required: true + + - type: textarea + attributes: + label: What went wrong? + description: Also add a screenshot + validations: + required: true + + - type: input + attributes: + label: Link to a viewer + description: Needed if hosted on a site other than mozilla.github.io/pdf.js or as Firefox/Chrome extension + + - type: textarea + attributes: + label: Additional context + description: Do you have anything to add that doesn't fit in the issue template? diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..39ba6a835d228 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Need help? + url: https://github.com/mozilla/pdf.js/discussions + about: If you need help on how to use PDF.js, please open a discussion where other community members can reply diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000000000..145f4edf6df2c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,23 @@ +name: Feature request +description: Propose a new feature or enhancement for PDF.js +title: "[Feature]: " +body: + - type: dropdown + attributes: + label: Is the feature relevant to the Firefox PDF Viewer? + options: ["Yes", "No"] + default: 1 + validations: + required: true + + - type: textarea + attributes: + label: Feature description + description: What new feature would you like PDF.js to have? Why would it be useful? What are the current workarounds? + validations: + required: true + + - type: textarea + attributes: + label: Other PDF viewers + description: Do other PDF viewers implement similar functionality? Add descriptions, links, and/or screenshots. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000000000..281ecb67daf4f --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,13 @@ +# Security policy + +Mozilla takes the security of our software seriously. If you believe you have found a security vulnerability in PDF.js, please report it to us as described below. + +## Reporting security vulnerabilities + +**Please don't report security vulnerabilities through public GitHub issues.** + +Instead, please report security vulnerabilities in [Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=PDF%20Viewer&groups=firefox-core-security) and make sure that the checkbox in the "Security" section is checked so the required access controls are automatically configured: + +![Security checkbox](https://github.com/mozilla/pdf.js/blob/master/.github/security.png) + +The Mozilla security team will process the bug as described in [Mozilla's security bugs policy](https://www.mozilla.org/en-US/about/governance/policies/security-group/bugs). diff --git a/.github/security.png b/.github/security.png new file mode 100644 index 0000000000000..040128166b6bc Binary files /dev/null and b/.github/security.png differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c9e46938312f..c48286c92ad41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies - run: npm install + run: npm ci - name: Run external tests run: npx gulp externaltest diff --git a/.github/workflows/font_tests.yml b/.github/workflows/font_tests.yml index 0399c2ded0857..ff8353b371ae8 100644 --- a/.github/workflows/font_tests.yml +++ b/.github/workflows/font_tests.yml @@ -46,7 +46,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies - run: npm install + run: npm ci - name: Use Python 3.12 uses: actions/setup-python@v5 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a24b0bad5dd07..99b224c6ab9d3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,7 +25,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies - run: npm install + run: npm ci - name: Run lint run: npx gulp lint diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 0000000000000..9672c615a4a33 --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,39 @@ +name: Publish release +on: + release: + types: [published] +permissions: + contents: read + id-token: write + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [lts/*] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + + - name: Build the `pdfjs-dist` library + run: npx gulp dist + + - name: Publish the `pdfjs-dist` library to NPM + run: npm publish ./build/dist --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/publish_website.yml b/.github/workflows/publish_website.yml index 086bc8f5087b7..3e51ebff5c3fe 100644 --- a/.github/workflows/publish_website.yml +++ b/.github/workflows/publish_website.yml @@ -27,7 +27,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies - run: npm install + run: npm ci - name: Build the website run: npx gulp web diff --git a/.github/workflows/types_tests.yml b/.github/workflows/types_tests.yml index 98e96242ff58f..f0b96a3be1a1f 100644 --- a/.github/workflows/types_tests.yml +++ b/.github/workflows/types_tests.yml @@ -25,7 +25,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies - run: npm install + run: npm ci - name: Run types tests run: npx gulp typestest diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index ec8654581065e..23b37fb7f58a2 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -51,7 +51,15 @@ "type": "boolean", "default": true }, - "enableML": { + "enableAltText": { + "type": "boolean", + "default": false + }, + "altTextLearnMoreUrl": { + "type": "string", + "default": "" + }, + "enableUpdatedAddImage": { "type": "boolean", "default": false }, @@ -86,10 +94,6 @@ "type": "string", "default": "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F" }, - "enableStampEditor": { - "type": "boolean", - "default": true - }, "disableRange": { "title": "Disable range requests", "description": "Whether to disable range requests (not recommended).", diff --git a/extensions/firefox/.eslintrc b/extensions/firefox/.eslintrc deleted file mode 100644 index e5ae68046fee7..0000000000000 --- a/extensions/firefox/.eslintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - // Note: The root .eslintrc file will define the base rules, - // but mozilla/recommended will override them for the rules it sets. Finally, - // the rules in this file will take precedence. - "extends": [ - "plugin:mozilla/recommended", - ], - - "plugins": [ - "mozilla" - ], - - "rules": { - // Other rules mozilla/recommended hasn't enabled yet. - "no-shadow": "error", - "arrow-body-style": ["error", "as-needed"], - "arrow-parens": ["error", "always"], - "constructor-super": "error", - "no-confusing-arrow": "error", - "no-useless-constructor": "error", - }, -} diff --git a/extensions/firefox/content/PdfJsDefaultPreferences.sys.mjs b/extensions/firefox/content/PdfJsDefaultPreferences.sys.mjs deleted file mode 100644 index 738adad80b6da..0000000000000 --- a/extensions/firefox/content/PdfJsDefaultPreferences.sys.mjs +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright 2018 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const PdfJsDefaultPreferences = Object.freeze( - PDFJSDev.eval("DEFAULT_PREFERENCES") -); diff --git a/gulpfile.mjs b/gulpfile.mjs index 73cbf6a2c0211..211d4ae95a7a7 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -53,7 +53,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const BUILD_DIR = "build/"; const L10N_DIR = "l10n/"; const TEST_DIR = "test/"; -const EXTENSION_SRC_DIR = "extensions/"; const BASELINE_DIR = BUILD_DIR + "baseline/"; const MOZCENTRAL_BASELINE_DIR = BUILD_DIR + "mozcentral.baseline/"; @@ -78,8 +77,6 @@ const COMMON_WEB_FILES = [ ]; const MOZCENTRAL_DIFF_FILE = "mozcentral.diff"; -const DIST_REPO_URL = "https://github.com/mozilla/pdfjs-dist"; - const CONFIG_FILE = "pdfjs.config"; const config = JSON.parse(fs.readFileSync(CONFIG_FILE).toString()); @@ -285,9 +282,6 @@ function createWebpackConfig( BUNDLE_VERSION: versionInfo.version, BUNDLE_BUILD: versionInfo.commit, TESTING: defines.TESTING ?? process.env.TESTING === "true", - BROWSER_PREFERENCES: defaultPreferencesDir - ? getBrowserPreferences(defaultPreferencesDir) - : {}, DEFAULT_PREFERENCES: defaultPreferencesDir ? getDefaultPreferences(defaultPreferencesDir) : {}, @@ -450,21 +444,10 @@ function checkChromePreferencesFile(chromePrefsPath, webPrefs) { } function tweakWebpackOutput(jsName) { - const replacer = [ - " __webpack_exports__ = {};", // Normal builds. - ",__webpack_exports__={};", // Minified builds. - ]; - const regex = new RegExp(`(${replacer.join("|")})`, "gm"); - - return replace(regex, match => { - switch (match) { - case " __webpack_exports__ = {};": - return ` __webpack_exports__ = globalThis.${jsName} = {};`; - case ",__webpack_exports__={};": - return `,__webpack_exports__=globalThis.${jsName}={};`; - } - return match; - }); + return replace( + /((?:\s|,)__webpack_exports__)(?:\s?)=(?:\s?)({};)/gm, + (match, p1, p2) => `${p1} = globalThis.${jsName} = ${p2}` + ); } function createMainBundle(defines) { @@ -868,13 +851,6 @@ async function parseDefaultPreferences(dir) { "./" + DEFAULT_PREFERENCES_DIR + dir + "app_options.mjs" ); - const browserPrefs = AppOptions.getAll( - OptionKind.BROWSER, - /* defaultOnly = */ true - ); - if (Object.keys(browserPrefs).length === 0) { - throw new Error("No browser preferences found."); - } const prefs = AppOptions.getAll( OptionKind.PREFERENCE, /* defaultOnly = */ true @@ -883,23 +859,12 @@ async function parseDefaultPreferences(dir) { throw new Error("No default preferences found."); } - fs.writeFileSync( - DEFAULT_PREFERENCES_DIR + dir + "browser_preferences.json", - JSON.stringify(browserPrefs) - ); fs.writeFileSync( DEFAULT_PREFERENCES_DIR + dir + "default_preferences.json", JSON.stringify(prefs) ); } -function getBrowserPreferences(dir) { - const str = fs - .readFileSync(DEFAULT_PREFERENCES_DIR + dir + "browser_preferences.json") - .toString(); - return JSON.parse(str); -} - function getDefaultPreferences(dir) { const str = fs .readFileSync(DEFAULT_PREFERENCES_DIR + dir + "default_preferences.json") @@ -1289,26 +1254,31 @@ gulp.task( ) ); -function preprocessDefaultPreferences(content) { +function createDefaultPrefsFile() { + const defaultFileName = "PdfJsDefaultPrefs.js", + overrideFileName = "PdfJsOverridePrefs.js"; const licenseHeader = fs.readFileSync("./src/license_header.js").toString(); const MODIFICATION_WARNING = - "//\n// THIS FILE IS GENERATED AUTOMATICALLY, DO NOT EDIT MANUALLY!\n//\n"; + "// THIS FILE IS GENERATED AUTOMATICALLY, DO NOT EDIT MANUALLY!\n//\n" + + `// Any overrides should be placed in \`${overrideFileName}\`.\n`; - const bundleDefines = { - ...DEFINES, - DEFAULT_PREFERENCES: getDefaultPreferences("mozcentral/"), - }; + const prefs = getDefaultPreferences("mozcentral/"); + const buf = []; - content = preprocessPDFJSCode( - { - rootPath: __dirname, - defines: bundleDefines, - }, - content - ); + for (const name in prefs) { + let value = prefs[name]; - return licenseHeader + "\n" + MODIFICATION_WARNING + "\n" + content + "\n"; + if (typeof value === "string") { + value = `"${value}"`; + } + buf.push(`pref("pdfjs.${name}", ${value});`); + } + buf.sort(); + buf.unshift(licenseHeader, MODIFICATION_WARNING); + buf.push(`\n#include ${overrideFileName}\n`); + + return createStringSource(defaultFileName, buf.join("\n")); } function replaceMozcentralCSS() { @@ -1336,8 +1306,7 @@ gulp.task( MOZCENTRAL_EXTENSION_DIR = MOZCENTRAL_DIR + "browser/extensions/pdfjs/", MOZCENTRAL_CONTENT_DIR = MOZCENTRAL_EXTENSION_DIR + "content/", MOZCENTRAL_L10N_DIR = - MOZCENTRAL_DIR + "browser/locales/en-US/pdfviewer/", - FIREFOX_CONTENT_DIR = EXTENSION_SRC_DIR + "/firefox/content/"; + MOZCENTRAL_DIR + "browser/locales/en-US/pdfviewer/"; const MOZCENTRAL_WEB_FILES = [ ...COMMON_WEB_FILES, @@ -1412,12 +1381,7 @@ gulp.task( gulp .src("LICENSE", { encoding: false }) .pipe(gulp.dest(MOZCENTRAL_EXTENSION_DIR)), - gulp - .src(FIREFOX_CONTENT_DIR + "PdfJsDefaultPreferences.sys.mjs", { - encoding: false, - }) - .pipe(transform("utf8", preprocessDefaultPreferences)) - .pipe(gulp.dest(MOZCENTRAL_CONTENT_DIR)), + createDefaultPrefsFile().pipe(gulp.dest(MOZCENTRAL_EXTENSION_DIR)), ]); } ) @@ -1594,9 +1558,6 @@ function buildLib(defines, dir) { BUNDLE_VERSION: versionInfo.version, BUNDLE_BUILD: versionInfo.commit, TESTING: defines.TESTING ?? process.env.TESTING === "true", - BROWSER_PREFERENCES: getBrowserPreferences( - defines.SKIP_BABEL ? "lib/" : "lib-legacy/" - ), DEFAULT_PREFERENCES: getDefaultPreferences( defines.SKIP_BABEL ? "lib/" : "lib-legacy/" ), @@ -2087,8 +2048,19 @@ gulp.task( console.log(); console.log("### Starting local server"); + let port = 8888; + const i = process.argv.indexOf("--port"); + if (i >= 0 && i + 1 < process.argv.length) { + const p = parseInt(process.argv[i + 1], 10); + if (!isNaN(p)) { + port = p; + } else { + console.error("Invalid port number: using default (8888)"); + } + } + const { WebServer } = await import("./test/webserver.mjs"); - const server = new WebServer({ port: 8888 }); + const server = new WebServer({ port }); server.start(); } ) @@ -2199,8 +2171,9 @@ function packageJson() { const DIST_NAME = "pdfjs-dist"; const DIST_DESCRIPTION = "Generic build of Mozilla's PDF.js library."; const DIST_KEYWORDS = ["Mozilla", "pdf", "pdf.js"]; - const DIST_HOMEPAGE = "http://mozilla.github.io/pdf.js/"; + const DIST_HOMEPAGE = "https://mozilla.github.io/pdf.js/"; const DIST_BUGS_URL = "https://github.com/mozilla/pdf.js/issues"; + const DIST_GIT_URL = "https://github.com/mozilla/pdf.js.git"; const DIST_LICENSE = "Apache-2.0"; const npmManifest = { @@ -2215,7 +2188,7 @@ function packageJson() { license: DIST_LICENSE, optionalDependencies: { canvas: "^2.11.2", - path2d: "^0.2.0", + path2d: "^0.2.1", }, browser: { canvas: false, @@ -2226,11 +2199,12 @@ function packageJson() { }, repository: { type: "git", - url: DIST_REPO_URL, + url: `git+${DIST_GIT_URL}`, }, engines: { node: ">=18", }, + scripts: {}, }; return createStringSource( @@ -2240,7 +2214,7 @@ function packageJson() { } gulp.task( - "dist-pre", + "dist", gulp.series( "generic", "generic-legacy", @@ -2252,23 +2226,8 @@ gulp.task( "minified-legacy", "types", function createDist() { - console.log(); - console.log("### Cloning baseline distribution"); - fs.rmSync(DIST_DIR, { recursive: true, force: true }); fs.mkdirSync(DIST_DIR, { recursive: true }); - safeSpawnSync("git", ["clone", "--depth", "1", DIST_REPO_URL, DIST_DIR]); - - console.log(); - console.log("### Overwriting all files"); - - // Remove all files/folders, except for `.git` because it needs to be a - // valid Git repository for the Git commands in the `dist` target to work. - for (const entry of fs.readdirSync(DIST_DIR)) { - if (entry !== ".git") { - fs.rmSync(DIST_DIR + entry, { recursive: true, force: true }); - } - } return ordered([ packageJson().pipe(gulp.dest(DIST_DIR)), @@ -2368,7 +2327,7 @@ gulp.task( gulp.task( "dist-install", - gulp.series("dist-pre", function createDistInstall(done) { + gulp.series("dist", function createDistInstall(done) { let distPath = DIST_DIR; const opts = {}; const installPath = process.env.PDFJS_INSTALL_PATH; @@ -2381,47 +2340,6 @@ gulp.task( }) ); -gulp.task( - "dist", - gulp.series("dist-pre", function createDist(done) { - const VERSION = getVersionJSON().version; - - console.log(); - console.log("### Committing changes"); - - let reason = process.env.PDFJS_UPDATE_REASON; - // Attempt to work-around the broken link, see https://github.com/mozilla/pdf.js/issues/10391 - if (typeof reason === "string") { - const reasonParts = - /^(See )(mozilla\/pdf\.js)@tags\/(v\d+\.\d+\.\d+)\s*$/.exec(reason); - - if (reasonParts) { - reason = - reasonParts[1] + - "https://github.com/" + - reasonParts[2] + - "/releases/tag/" + - reasonParts[3]; - } - } - const message = - "PDF.js version " + VERSION + (reason ? " - " + reason : ""); - safeSpawnSync("git", ["add", "*"], { cwd: DIST_DIR }); - safeSpawnSync("git", ["commit", "-am", message], { cwd: DIST_DIR }); - safeSpawnSync("git", ["tag", "-a", "v" + VERSION, "-m", message], { - cwd: DIST_DIR, - }); - - console.log(); - console.log("Done. Push with"); - console.log( - " cd " + DIST_DIR + "; git push --tags " + DIST_REPO_URL + " master" - ); - console.log(); - done(); - }) -); - gulp.task( "mozcentralbaseline", gulp.series(createBaseline, function createMozcentralBaseline(done) { diff --git a/l10n/is/viewer.ftl b/l10n/is/viewer.ftl index d3afef3eae02b..620c0fc2156a5 100644 --- a/l10n/is/viewer.ftl +++ b/l10n/is/viewer.ftl @@ -51,12 +51,6 @@ pdfjs-download-button-label = Sækja pdfjs-bookmark-button = .title = Núverandi síða (Skoða vefslóð frá núverandi síðu) pdfjs-bookmark-button-label = Núverandi síða -# Used in Firefox for Android. -pdfjs-open-in-app-button = - .title = Opna í smáforriti -# Used in Firefox for Android. -# Length of the translation matters since we are in a mobile context, with limited screen estate. -pdfjs-open-in-app-button-label = Opna í smáforriti ## Secondary toolbar and context menu @@ -284,7 +278,7 @@ pdfjs-text-annotation-type = ## Password -pdfjs-password-label = Sláðu inn lykilorð til að opna þessa PDF skrá. +pdfjs-password-label = Settu inn lykilorð til að opna þessa PDF-skrá. pdfjs-password-invalid = Ógilt lykilorð. Reyndu aftur. pdfjs-password-ok-button = Í lagi pdfjs-password-cancel-button = Hætta við @@ -304,8 +298,6 @@ pdfjs-editor-stamp-button-label = Bæta við eða breyta myndum pdfjs-editor-highlight-button = .title = Áherslulita pdfjs-editor-highlight-button-label = Áherslulita -pdfjs-highlight-floating-button = - .title = Áherslulita pdfjs-highlight-floating-button1 = .title = Áherslulita .aria-label = Áherslulita diff --git a/l10n/ja/viewer.ftl b/l10n/ja/viewer.ftl index 9fd0d5b043dd4..a9c90fe9b936f 100644 --- a/l10n/ja/viewer.ftl +++ b/l10n/ja/viewer.ftl @@ -279,7 +279,7 @@ pdfjs-text-annotation-type = ## Password pdfjs-password-label = この PDF ファイルを開くためのパスワードを入力してください。 -pdfjs-password-invalid = 無効なパスワードです。もう一度やり直してください。 +pdfjs-password-invalid = パスワードが正しくありません。もう一度試してください。 pdfjs-password-ok-button = OK pdfjs-password-cancel-button = キャンセル pdfjs-web-fonts-disabled = ウェブフォントが無効になっています: 埋め込まれた PDF のフォントを使用できません。 diff --git a/l10n/zh-TW/viewer.ftl b/l10n/zh-TW/viewer.ftl index f8614a9f35f7d..98ef060abc8f0 100644 --- a/l10n/zh-TW/viewer.ftl +++ b/l10n/zh-TW/viewer.ftl @@ -51,12 +51,6 @@ pdfjs-download-button-label = 下載 pdfjs-bookmark-button = .title = 目前頁面(含目前檢視頁面的網址) pdfjs-bookmark-button-label = 目前頁面 -# Used in Firefox for Android. -pdfjs-open-in-app-button = - .title = 在應用程式中開啟 -# Used in Firefox for Android. -# Length of the translation matters since we are in a mobile context, with limited screen estate. -pdfjs-open-in-app-button-label = 用程式開啟 ## Secondary toolbar and context menu @@ -82,8 +76,8 @@ pdfjs-cursor-hand-tool-button = .title = 開啟頁面移動工具 pdfjs-cursor-hand-tool-button-label = 頁面移動工具 pdfjs-scroll-page-button = - .title = 使用頁面捲動功能 -pdfjs-scroll-page-button-label = 頁面捲動功能 + .title = 使用單頁捲動版面 +pdfjs-scroll-page-button-label = 單頁捲動 pdfjs-scroll-vertical-button = .title = 使用垂直捲動版面 pdfjs-scroll-vertical-button-label = 垂直捲動 @@ -108,8 +102,8 @@ pdfjs-spread-even-button-label = 偶數跨頁 pdfjs-document-properties-button = .title = 文件內容… pdfjs-document-properties-button-label = 文件內容… -pdfjs-document-properties-file-name = 檔案名稱: -pdfjs-document-properties-file-size = 檔案大小: +pdfjs-document-properties-file-name = 檔案名稱: +pdfjs-document-properties-file-size = 檔案大小: # Variables: # $size_kb (Number) - the PDF file size in kilobytes # $size_b (Number) - the PDF file size in bytes @@ -118,21 +112,21 @@ pdfjs-document-properties-kb = { $size_kb } KB({ $size_b } 位元組) # $size_mb (Number) - the PDF file size in megabytes # $size_b (Number) - the PDF file size in bytes pdfjs-document-properties-mb = { $size_mb } MB({ $size_b } 位元組) -pdfjs-document-properties-title = 標題: -pdfjs-document-properties-author = 作者: -pdfjs-document-properties-subject = 主旨: -pdfjs-document-properties-keywords = 關鍵字: -pdfjs-document-properties-creation-date = 建立日期: -pdfjs-document-properties-modification-date = 修改日期: +pdfjs-document-properties-title = 標題: +pdfjs-document-properties-author = 作者: +pdfjs-document-properties-subject = 主旨: +pdfjs-document-properties-keywords = 關鍵字: +pdfjs-document-properties-creation-date = 建立日期: +pdfjs-document-properties-modification-date = 修改日期: # Variables: # $date (Date) - the creation/modification date of the PDF file # $time (Time) - the creation/modification time of the PDF file pdfjs-document-properties-date-string = { $date } { $time } -pdfjs-document-properties-creator = 建立者: -pdfjs-document-properties-producer = PDF 產生器: -pdfjs-document-properties-version = PDF 版本: -pdfjs-document-properties-page-count = 頁數: -pdfjs-document-properties-page-size = 頁面大小: +pdfjs-document-properties-creator = 建立者: +pdfjs-document-properties-producer = PDF 產生器: +pdfjs-document-properties-version = PDF 版本: +pdfjs-document-properties-page-count = 頁數: +pdfjs-document-properties-page-size = 頁面大小: pdfjs-document-properties-page-size-unit-inches = in pdfjs-document-properties-page-size-unit-millimeters = mm pdfjs-document-properties-page-size-orientation-portrait = 垂直 @@ -156,7 +150,7 @@ pdfjs-document-properties-page-size-dimension-name-string = { $width } × { $hei # The linearization status of the document; usually called "Fast Web View" in # English locales of Adobe software. -pdfjs-document-properties-linearized = 快速 Web 檢視: +pdfjs-document-properties-linearized = 快速 Web 檢視: pdfjs-document-properties-linearized-yes = 是 pdfjs-document-properties-linearized-no = 否 pdfjs-document-properties-close-button = 關閉 @@ -296,8 +290,6 @@ pdfjs-editor-stamp-button-label = 新增或編輯圖片 pdfjs-editor-highlight-button = .title = 強調 pdfjs-editor-highlight-button-label = 強調 -pdfjs-highlight-floating-button = - .title = 強調 pdfjs-highlight-floating-button1 = .title = 強調 .aria-label = 強調 @@ -331,7 +323,7 @@ pdfjs-editor-free-highlight-thickness-title = .title = 更改強調文字以外的項目時的線條粗細 pdfjs-free-text = .aria-label = 文本編輯器 -pdfjs-free-text-default-content = 開始打字… +pdfjs-free-text-default-content = 在此打字… pdfjs-ink = .aria-label = 圖形編輯器 pdfjs-ink-canvas = diff --git a/package-lock.json b/package-lock.json index 465c4a1ddab4f..60019134a03ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,17 +8,17 @@ "hasInstallScript": true, "license": "Apache-2.0", "devDependencies": { - "@babel/core": "^7.24.7", - "@babel/preset-env": "^7.24.7", - "@babel/runtime": "^7.24.7", + "@babel/core": "^7.24.8", + "@babel/preset-env": "^7.24.8", + "@babel/runtime": "^7.24.8", "@fluent/bundle": "^0.18.0", - "@fluent/dom": "^0.9.0", + "@fluent/dom": "^0.10.0", "@jazzer.js/core": "^2.1.0", "@metalsmith/layouts": "^2.7.0", - "@metalsmith/markdown": "^1.3.0", + "@metalsmith/markdown": "^1.10.0", "autoprefixer": "^10.4.19", "babel-loader": "^9.1.3", - "caniuse-lite": "^1.0.30001632", + "caniuse-lite": "^1.0.30001641", "canvas": "^2.11.2", "core-js": "^3.37.1", "cross-env": "^7.0.3", @@ -27,45 +27,45 @@ "eslint-plugin-fetch-options": "^0.0.5", "eslint-plugin-html": "^8.1.1", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jasmine": "^4.1.3", + "eslint-plugin-jasmine": "^4.2.0", "eslint-plugin-json": "^3.1.0", "eslint-plugin-mozilla": "^3.7.4", "eslint-plugin-no-unsanitized": "^4.0.2", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-sort-exports": "^0.9.1", - "eslint-plugin-unicorn": "^53.0.0", - "globals": "^15.4.0", + "eslint-plugin-unicorn": "^54.0.0", + "globals": "^15.8.0", "gulp": "^5.0.0", "gulp-cli": "^3.0.0", "gulp-postcss": "^10.0.0", "gulp-rename": "^2.0.0", "gulp-replace": "^1.1.4", "gulp-zip": "^6.0.0", - "highlight.js": "^11.9.0", + "highlight.js": "^11.10.0", "jasmine": "^5.1.0", "jsdoc": "^4.0.3", "jstransformer-nunjucks": "^1.2.0", "metalsmith": "^2.6.3", "metalsmith-html-relative": "^2.0.1", "ordered-read-streams": "^2.0.0", - "path2d": "^0.2.0", + "path2d": "^0.2.1", "pngjs": "^7.0.0", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-dark-theme-class": "^1.3.0", "postcss-dir-pseudo-class": "^8.0.1", - "postcss-discard-comments": "^7.0.0", + "postcss-discard-comments": "^7.0.1", "postcss-nesting": "^12.1.5", - "prettier": "^3.3.2", - "puppeteer": "^22.10.1", + "prettier": "^3.3.3", + "puppeteer": "^22.13.0", "streamqueue": "^1.1.2", - "stylelint": "^16.6.1", + "stylelint": "^16.7.0", "stylelint-prettier": "^5.0.0", "terser-webpack-plugin": "^5.3.10", "tsc-alias": "^1.8.10", "ttest": "^4.0.0", - "typescript": "^5.4.5", + "typescript": "^5.5.3", "vinyl": "^3.0.0", - "webpack": "^5.91.0", + "webpack": "^5.93.0", "webpack-stream": "^7.0.0", "yargs": "^17.7.2" }, @@ -123,9 +123,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.8.tgz", + "integrity": "sha512-c4IM7OTg6k1Q+AJ153e2mc2QVTezTwnb4VzquwcyiEzGnW0Kedv4do/TrkU98qPeC5LNiMt/QXwIjzYXLBpyZg==", "dev": true, "license": "MIT", "engines": { @@ -133,22 +133,22 @@ } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.8.tgz", + "integrity": "sha512-6AWcmZC/MZCO0yKys4uhg5NlxL0ESF3K6IAaoQ+xSXvPyPyxNWRafP+GDbI88Oh68O7QkJgmEtedWPM9U0pZNg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -163,29 +163,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -196,13 +173,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.8.tgz", + "integrity": "sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7", + "@babel/types": "^7.24.8", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -239,15 +216,15 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -350,29 +327,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@babel/helper-environment-visitor": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", @@ -442,9 +396,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.8.tgz", + "integrity": "sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==", "dev": true, "license": "MIT", "dependencies": { @@ -475,9 +429,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, "license": "MIT", "engines": { @@ -562,9 +516,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "license": "MIT", "engines": { @@ -582,9 +536,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "license": "MIT", "engines": { @@ -608,14 +562,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/types": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -638,9 +592,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", "dev": true, "license": "MIT", "bin": { @@ -1100,17 +1054,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", - "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz", + "integrity": "sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.8", "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-replace-supers": "^7.24.7", "@babel/helper-split-export-declaration": "^7.24.7", "globals": "^11.1.0" @@ -1150,13 +1104,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", - "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1368,14 +1322,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", - "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-simple-access": "^7.24.7" }, "engines": { @@ -1542,13 +1496,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", - "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, @@ -1726,13 +1680,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", - "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1809,16 +1763,16 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", - "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.8.tgz", + "integrity": "sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", + "@babel/compat-data": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", @@ -1849,9 +1803,9 @@ "@babel/plugin-transform-block-scoping": "^7.24.7", "@babel/plugin-transform-class-properties": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.8", "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-dotall-regex": "^7.24.7", "@babel/plugin-transform-duplicate-keys": "^7.24.7", "@babel/plugin-transform-dynamic-import": "^7.24.7", @@ -1864,7 +1818,7 @@ "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-member-expression-literals": "^7.24.7", "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-modules-systemjs": "^7.24.7", "@babel/plugin-transform-modules-umd": "^7.24.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", @@ -1874,7 +1828,7 @@ "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-object-super": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", @@ -1885,7 +1839,7 @@ "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", "@babel/plugin-transform-unicode-escapes": "^7.24.7", "@babel/plugin-transform-unicode-property-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7", @@ -1894,7 +1848,7 @@ "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.4", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.31.0", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "engines": { @@ -1934,9 +1888,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", "dev": true, "license": "MIT", "dependencies": { @@ -1969,20 +1923,20 @@ } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", + "@babel/generator": "^7.24.8", "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-function-name": "^7.24.7", "@babel/helper-hoist-variables": "^7.24.7", "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2018,21 +1972,14 @@ "node": ">=4" } }, - "node_modules/@babel/traverse/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.8.tgz", + "integrity": "sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, @@ -2050,9 +1997,9 @@ } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.3.tgz", - "integrity": "sha512-xI/tL2zxzEbESvnSxwFgwvy5HS00oCXxL4MLs6HUiDcYfwowsoQaABKxUElp1ARITrINzBnsECOc1q0eg2GOrA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", "dev": true, "funding": [ { @@ -2069,13 +2016,13 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.3.1" + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.3.1.tgz", - "integrity": "sha512-iMNHTyxLbBlWIfGtabT157LH9DUx9X8+Y3oymFEuMj8HNc+rpE3dPFGFgHjpKfjeFDjLjYIAIhXPGvS2lKxL9g==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", "dev": true, "funding": [ { @@ -2093,9 +2040,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.11.tgz", - "integrity": "sha512-uox5MVhvNHqitPP+SynrB1o8oPxPMt2JLgp5ghJOWf54WGQ5OKu47efne49r1SWqs3wRP8xSWjnO9MBKxhB1dA==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", + "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", "dev": true, "funding": [ { @@ -2112,8 +2059,8 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.6.3", - "@csstools/css-tokenizer": "^2.3.1" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/selector-resolve-nested": { @@ -2224,23 +2171,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -2268,12 +2198,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@eslint/eslintrc/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -2306,15 +2230,16 @@ } }, "node_modules/@fluent/dom": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@fluent/dom/-/dom-0.9.0.tgz", - "integrity": "sha512-KElkUkHhFuliHeQaL4bDMin3MEJlXm3Mgh1lDE5JdmdO+5VW1bFZGjxpFS1qNzz8XZtsa71lL5zDPVg5vOgYtQ==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@fluent/dom/-/dom-0.10.0.tgz", + "integrity": "sha512-31a+GJRg6Xhpw9IQ8yNiHhegd10g1DvC30TMSO52bFpjJVJqfQHTuLKFSORNR5xI1oyP4RU4lGLho9+HaC/pVQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "cached-iterable": "^0.3" }, "engines": { - "node": ">=14.0.0", + "node": ">=18.0.0", "npm": ">=7.0.0" } }, @@ -2354,29 +2279,6 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2828,31 +2730,6 @@ "node": ">=18" } }, - "node_modules/@puppeteer/browsers/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@puppeteer/browsers/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/@puppeteer/browsers/node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -3164,11 +3041,12 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^8" } @@ -3194,29 +3072,6 @@ "node": ">= 6.0.0" } }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3783,9 +3638,9 @@ } }, "node_modules/bare-os": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.3.0.tgz", - "integrity": "sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.0.tgz", + "integrity": "sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==", "dev": true, "license": "Apache-2.0", "optional": true @@ -3802,9 +3657,9 @@ } }, "node_modules/bare-stream": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.2.tgz", - "integrity": "sha512-az/7TFOh4Gk9Tqs1/xMFq5FuFoeZ9hZ3orsM2x69u8NXVUDXZnpdhG8mZY/Pv6DF954MGn+iIt4rFrG34eQsvg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.1.3.tgz", + "integrity": "sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -3912,9 +3767,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", "dev": true, "funding": [ { @@ -3930,11 +3785,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -4014,9 +3870,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001632", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz", - "integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==", + "version": "1.0.30001641", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001641.tgz", + "integrity": "sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA==", "dev": true, "funding": [ { @@ -4170,9 +4026,9 @@ } }, "node_modules/chromium-bidi": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.23.tgz", - "integrity": "sha512-1o/gLU9wDqbN5nL2MtfjykjOuighGXc3/hnWueO1haiEoFgX8h5vbvcA4tgdQfjw1mkZ1OEF4x/+HVeqEX6NoA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.0.tgz", + "integrity": "sha512-VnxVrpGojAjkiGFN2I+KtsDILFAjiGWVEDizOEnKzEDkT93eQT1cqTfUkqmOyLq33i1q4a1KDYbH+52CUe4Ufw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4295,23 +4151,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/cmake-js/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/cmake-js/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -4354,12 +4193,6 @@ "node": ">=8" } }, - "node_modules/cmake-js/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/cmake-js/node_modules/npmlog": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", @@ -4793,6 +4626,24 @@ "node": ">= 14" } }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decompress-response": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", @@ -4937,9 +4788,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1286932", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1286932.tgz", - "integrity": "sha512-wu58HMQll9voDjR4NlPyoDEw1syfzaBNHymMMZ/QOXiHRNluOnDgu9hp1yHOKYoMlxCh4lSSiugLITe6Fvu1eA==", + "version": "0.0.1299070", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1299070.tgz", + "integrity": "sha512-+qtL3eX50qsJ7c+qVyagqi7AWMoQCBGNfoyJZMwm/NSXVqLYbuitrWEEIzxfUmTNy7//Xe8yhMmQ+elj3uAqSg==", "dev": true, "license": "BSD-3-Clause" }, @@ -5101,10 +4952,11 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.708", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.708.tgz", - "integrity": "sha512-iWgEEvREL4GTXXHKohhh33+6Y8XkPI5eHihDmm8zUk5Zo7HICEW+wI/j5kJ2tbuNUCXJ/sNXa03ajW635DiJXA==", - "dev": true + "version": "1.4.827", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.827.tgz", + "integrity": "sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -5123,10 +4975,11 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -5278,10 +5131,11 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5573,10 +5427,11 @@ } }, "node_modules/eslint-plugin-jasmine": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jasmine/-/eslint-plugin-jasmine-4.1.3.tgz", - "integrity": "sha512-q8j8KnLH/4uwmPELFZvEyfEcuCuGxXScJaRdqHjOjz064GcfX6aoFbzy5VohZ5QYk2+WvoqMoqDSb9nRLf89GQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jasmine/-/eslint-plugin-jasmine-4.2.0.tgz", + "integrity": "sha512-zSCsnP4gMqBSt8jApExP0ja43nAI1fpAD5kY+knrIJylBxC/LEth25PkqcKJqW32GjesjsiA1SSSR3Z5qIranA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8", "npm": ">=6" @@ -5844,10 +5699,11 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "53.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz", - "integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==", + "version": "54.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", + "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", @@ -5917,23 +5773,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-unicorn/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", @@ -6014,12 +5853,6 @@ "node": ">=6" } }, - "node_modules/eslint-plugin-unicorn/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/eslint-plugin-unicorn/node_modules/regjsparser": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", @@ -6162,23 +5995,6 @@ "node": ">=7.0.0" } }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6320,12 +6136,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/eslint/node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -6611,24 +6421,6 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/extract-zip/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/extract-zip/node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -6645,13 +6437,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -7323,31 +7108,6 @@ "node": ">= 14" } }, - "node_modules/get-uri/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/get-uri/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -7638,9 +7398,9 @@ } }, "node_modules/globals": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.4.0.tgz", - "integrity": "sha512-unnwvMZpv0eDUyjNyh9DH/yxUaRYrEjW/qK4QcdrHg3oO11igUQrCSgODHEqxlKg8v2CD2Sd7UkqqEBoz5U7TQ==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", + "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", "dev": true, "license": "MIT", "engines": { @@ -8168,9 +7928,9 @@ } }, "node_modules/highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -8271,31 +8031,6 @@ "node": ">= 14" } }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8309,29 +8044,6 @@ "node": ">= 6" } }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -9620,9 +9332,9 @@ } }, "node_modules/known-css-properties": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.31.0.tgz", - "integrity": "sha512-sBPIUGTNF0czz0mwGGUoKKJC8Q7On1GPbCSFPfyEsfHb2DyBG0Y4QtV+EVWpINSaiGKZblDNuF5AezxSgOhesQ==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz", + "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", "dev": true, "license": "MIT" }, @@ -10206,13 +9918,6 @@ "node": ">=8.6" } }, - "node_modules/metalsmith/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/metalsmith/node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -10362,6 +10067,13 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, "node_modules/mute-stdout": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", @@ -10796,9 +10508,9 @@ } }, "node_modules/pac-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", - "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", "dev": true, "license": "MIT", "dependencies": { @@ -10807,9 +10519,9 @@ "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "pac-resolver": "^7.0.0", - "socks-proxy-agent": "^8.0.2" + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" }, "engines": { "node": ">= 14" @@ -10828,28 +10540,10 @@ "node": ">= 14" } }, - "node_modules/pac-proxy-agent/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "license": "MIT", "dependencies": { @@ -10860,13 +10554,6 @@ "node": ">= 14" } }, - "node_modules/pac-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/pac-resolver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", @@ -11054,10 +10741,11 @@ } }, "node_modules/path2d": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.0.tgz", - "integrity": "sha512-KdPAykQX6kmLSOO6Jpu2KNcCED7CKjmaBNGGNuctOsG0hgYO1OdYQaan6cYXJiG0WmXOwZZPILPBimu5QAIw3A==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.1.tgz", + "integrity": "sha512-Fl2z/BHvkTNvkuBzYTpTuirHZg6wW9z8+4SND/3mDTEcYbbNKWAy21dz9D3ePNNwrrK8pqZO5vLPZ1hLF6T7XA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -11146,9 +10834,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "dev": true, "funding": [ { @@ -11164,9 +10852,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -11224,10 +10913,14 @@ } }, "node_modules/postcss-discard-comments": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.0.tgz", - "integrity": "sha512-xpSdzRqYmy4YIVmjfGyYXKaI1SRnK6CTr+4Zmvyof8ANwvgfZgGdVtmgAvzh59gJm808mJCWQC9tFN0KF5dEXA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.1.tgz", + "integrity": "sha512-GVrQxUOhmle1W6jX2SvNLt4kmN+JYhV7mzI6BMnkAWR9DtVvg8e67rrV0NfdWhn7x1zxvzdWkMBPdBDCls+uwQ==", "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.0" + }, "engines": { "node": "^18.12.0 || ^20.9.0 || >=22.0" }, @@ -11480,9 +11173,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -11588,28 +11281,10 @@ "node": ">= 14" } }, - "node_modules/proxy-agent/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "license": "MIT", "dependencies": { @@ -11630,13 +11305,6 @@ "node": ">=12" } }, - "node_modules/proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -11679,17 +11347,17 @@ } }, "node_modules/puppeteer": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.10.1.tgz", - "integrity": "sha512-yFT7BlyIa4B1SnE6ccBVt+nYpDNTtZtwNh9tL79d6eWlGh94YrdzQDH+WtG88mCuQIcjjcJgvTRfm8uljuMgUQ==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.13.0.tgz", + "integrity": "sha512-nmICzeHTBtZiu+y4vs0fboe/NKIFwH5W8RZuxmEVAKNfBQg/8u5FEQAvPlWmyVpJoAVM5kXD5PEl3GlK3F9pPA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.2.3", - "cosmiconfig": "9.0.0", - "devtools-protocol": "0.0.1286932", - "puppeteer-core": "22.10.1" + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1299070", + "puppeteer-core": "22.13.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" @@ -11699,17 +11367,17 @@ } }, "node_modules/puppeteer-core": { - "version": "22.10.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.10.1.tgz", - "integrity": "sha512-t98x55ohn6eioM/e1dmhy6bG9iuQ+mvpNXyr+UppB789uvNKKOvy9YnWpKp4mVyKxi9BGsceHAFuovQGqQKyCQ==", + "version": "22.13.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.13.0.tgz", + "integrity": "sha512-ZkpRX8nm/S39BnpcCverMzIc6oGWBPOUeOeaWRLKHqiKVCZ1l28HxPTYLitJlDiB16xZATSKpjul+sl+ZEm0HQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.2.3", - "chromium-bidi": "0.5.23", - "debug": "4.3.5", - "devtools-protocol": "0.0.1286932", - "ws": "8.17.0" + "chromium-bidi": "0.6.0", + "debug": "^4.3.5", + "devtools-protocol": "0.0.1299070", + "ws": "^8.18.0" }, "engines": { "node": ">=18" @@ -11733,13 +11401,6 @@ } } }, - "node_modules/puppeteer-core/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/queue-lit": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.0.tgz", @@ -12613,15 +12274,15 @@ } }, "node_modules/socks-proxy-agent": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", - "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.1", "debug": "^4.3.4", - "socks": "^2.7.1" + "socks": "^2.8.3" }, "engines": { "node": ">= 14" @@ -12640,31 +12301,6 @@ "node": ">= 14" } }, - "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socks-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12998,9 +12634,9 @@ } }, "node_modules/stylelint": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.6.1.tgz", - "integrity": "sha512-yNgz2PqWLkhH2hw6X9AweV9YvoafbAD5ZsFdKN9BvSDVwGvPh+AUIrn7lYwy1S7IHmtFin75LLfX1m0D2tHu8Q==", + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.7.0.tgz", + "integrity": "sha512-Q1ATiXlz+wYr37a7TGsfvqYn2nSR3T/isw3IWlZQzFzCNoACHuGBb6xBplZXz56/uDRJHIygxjh7jbV/8isewA==", "dev": true, "funding": [ { @@ -13014,9 +12650,9 @@ ], "license": "MIT", "dependencies": { - "@csstools/css-parser-algorithms": "^2.6.3", - "@csstools/css-tokenizer": "^2.3.1", - "@csstools/media-query-list-parser": "^2.1.11", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/media-query-list-parser": "^2.1.13", "@csstools/selector-specificity": "^3.1.1", "@dual-bundle/import-meta-resolve": "^4.1.0", "balanced-match": "^2.0.0", @@ -13024,7 +12660,7 @@ "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.2", "css-tree": "^2.3.1", - "debug": "^4.3.4", + "debug": "^4.3.5", "fast-glob": "^3.3.2", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^9.0.0", @@ -13035,13 +12671,13 @@ "ignore": "^5.3.1", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.31.0", + "known-css-properties": "^0.34.0", "mathml-tag-names": "^2.1.3", "meow": "^13.2.0", "micromatch": "^4.0.7", "normalize-path": "^3.0.0", "picocolors": "^1.0.1", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-resolve-nested-selector": "^0.1.1", "postcss-safe-parser": "^7.0.0", "postcss-selector-parser": "^6.1.0", @@ -13109,10 +12745,11 @@ } }, "node_modules/stylelint/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -13233,12 +12870,6 @@ "node": ">=8.6" } }, - "node_modules/stylelint/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/stylelint/node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -13512,6 +13143,7 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -14087,10 +13719,11 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14233,9 +13866,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -14251,9 +13884,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -14543,10 +14177,11 @@ "dev": true }, "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, + "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -14554,10 +14189,10 @@ "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", + "enhanced-resolve": "^5.17.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -14973,9 +14608,9 @@ } }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 591f9d8dce13c..ee056e60c2071 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,17 @@ "name": "pdf.js", "type": "module", "devDependencies": { - "@babel/core": "^7.24.7", - "@babel/preset-env": "^7.24.7", - "@babel/runtime": "^7.24.7", + "@babel/core": "^7.24.8", + "@babel/preset-env": "^7.24.8", + "@babel/runtime": "^7.24.8", "@fluent/bundle": "^0.18.0", - "@fluent/dom": "^0.9.0", + "@fluent/dom": "^0.10.0", "@jazzer.js/core": "^2.1.0", "@metalsmith/layouts": "^2.7.0", - "@metalsmith/markdown": "^1.3.0", + "@metalsmith/markdown": "^1.10.0", "autoprefixer": "^10.4.19", "babel-loader": "^9.1.3", - "caniuse-lite": "^1.0.30001632", + "caniuse-lite": "^1.0.30001641", "canvas": "^2.11.2", "core-js": "^3.37.1", "cross-env": "^7.0.3", @@ -21,45 +21,45 @@ "eslint-plugin-fetch-options": "^0.0.5", "eslint-plugin-html": "^8.1.1", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jasmine": "^4.1.3", + "eslint-plugin-jasmine": "^4.2.0", "eslint-plugin-json": "^3.1.0", "eslint-plugin-mozilla": "^3.7.4", "eslint-plugin-no-unsanitized": "^4.0.2", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-sort-exports": "^0.9.1", - "eslint-plugin-unicorn": "^53.0.0", - "globals": "^15.4.0", + "eslint-plugin-unicorn": "^54.0.0", + "globals": "^15.8.0", "gulp": "^5.0.0", "gulp-cli": "^3.0.0", "gulp-postcss": "^10.0.0", "gulp-rename": "^2.0.0", "gulp-replace": "^1.1.4", "gulp-zip": "^6.0.0", - "highlight.js": "^11.9.0", + "highlight.js": "^11.10.0", "jasmine": "^5.1.0", "jsdoc": "^4.0.3", "jstransformer-nunjucks": "^1.2.0", "metalsmith": "^2.6.3", "metalsmith-html-relative": "^2.0.1", "ordered-read-streams": "^2.0.0", - "path2d": "^0.2.0", + "path2d": "^0.2.1", "pngjs": "^7.0.0", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-dark-theme-class": "^1.3.0", "postcss-dir-pseudo-class": "^8.0.1", - "postcss-discard-comments": "^7.0.0", + "postcss-discard-comments": "^7.0.1", "postcss-nesting": "^12.1.5", - "prettier": "^3.3.2", - "puppeteer": "^22.10.1", + "prettier": "^3.3.3", + "puppeteer": "^22.13.0", "streamqueue": "^1.1.2", - "stylelint": "^16.6.1", + "stylelint": "^16.7.0", "stylelint-prettier": "^5.0.0", "terser-webpack-plugin": "^5.3.10", "tsc-alias": "^1.8.10", "ttest": "^4.0.0", - "typescript": "^5.4.5", + "typescript": "^5.5.3", "vinyl": "^3.0.0", - "webpack": "^5.91.0", + "webpack": "^5.93.0", "webpack-stream": "^7.0.0", "yargs": "^17.7.2" }, diff --git a/pdfjs.config b/pdfjs.config index 5d402b9712acc..0f48eda4929cc 100644 --- a/pdfjs.config +++ b/pdfjs.config @@ -1,5 +1,5 @@ { - "stableVersion": "4.3.136", - "baseVersion": "fdb3617e0f7ebbefce9a65a87971251e92653d07", - "versionPrefix": "4.4." + "stableVersion": "4.4.168", + "baseVersion": "bdcc4a0febc02aec5be79da67507972e3c7be280", + "versionPrefix": "4.5." } diff --git a/src/core/annotation.js b/src/core/annotation.js index 4e821b98730c3..7ac9a3eed0722 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -680,6 +680,7 @@ class Annotation { hasOwnCanvas: false, noRotate: !!(this.flags & AnnotationFlag.NOROTATE), noHTML: isLocked && isContentLocked, + isEditable: false, }; if (params.collectFields) { @@ -776,6 +777,10 @@ class Annotation { return this.printable; } + mustBeViewedWhenEditing(isEditing, modifiedIds = null) { + return isEditing ? !this.data.isEditable : !modifiedIds?.has(this.data.id); + } + /** * @type {boolean} */ @@ -1100,13 +1105,7 @@ class Annotation { }); } - async getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ) { + async getOperatorList(evaluator, task, intent, annotationStorage) { const { hasOwnCanvas, id, rect } = this.data; let appearance = this.appearance; const isUsingOwnCanvas = !!( @@ -1712,18 +1711,28 @@ class MarkupAnnotation extends Annotation { } static async createNewAnnotation(xref, annotation, dependencies, params) { - const annotationRef = (annotation.ref ||= xref.getNewTemporaryRef()); + let oldAnnotation; + if (annotation.ref) { + oldAnnotation = (await xref.fetchIfRefAsync(annotation.ref)).clone(); + } else { + annotation.ref = xref.getNewTemporaryRef(); + } + + const annotationRef = annotation.ref; const ap = await this.createNewAppearanceStream(annotation, xref, params); const buffer = []; let annotationDict; if (ap) { const apRef = xref.getNewTemporaryRef(); - annotationDict = this.createNewDict(annotation, xref, { apRef }); + annotationDict = this.createNewDict(annotation, xref, { + apRef, + oldAnnotation, + }); await writeObject(apRef, ap, buffer, xref); dependencies.push({ ref: apRef, data: buffer.join("") }); } else { - annotationDict = this.createNewDict(annotation, xref, {}); + annotationDict = this.createNewDict(annotation, xref, { oldAnnotation }); } if (Number.isInteger(annotation.parentTreeId)) { annotationDict.set("StructParent", annotation.parentTreeId); @@ -1954,17 +1963,11 @@ class WidgetAnnotation extends Annotation { return str; } - async getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ) { + async getOperatorList(evaluator, task, intent, annotationStorage) { // Do not render form elements on the canvas when interactive forms are // enabled. The display layer is responsible for rendering them instead. if ( - renderForms && + intent & RenderingIntentFlag.ANNOTATIONS_FORMS && !(this instanceof SignatureWidgetAnnotation) && !this.data.noHTML && !this.data.hasOwnCanvas @@ -1977,13 +1980,7 @@ class WidgetAnnotation extends Annotation { } if (!this._hasText) { - return super.getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ); + return super.getOperatorList(evaluator, task, intent, annotationStorage); } const content = await this._getAppearance( @@ -1993,13 +1990,7 @@ class WidgetAnnotation extends Annotation { annotationStorage ); if (this.appearance && content === null) { - return super.getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ); + return super.getOperatorList(evaluator, task, intent, annotationStorage); } const opList = new OperatorList(); @@ -2929,13 +2920,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { } } - async getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ) { + async getOperatorList(evaluator, task, intent, annotationStorage) { if (this.data.pushButton) { return super.getOperatorList( evaluator, @@ -2957,13 +2942,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { if (value === null && this.appearance) { // Nothing in the annotationStorage. // But we've a default appearance so use it. - return super.getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ); + return super.getOperatorList(evaluator, task, intent, annotationStorage); } if (value === null || value === undefined) { @@ -2996,7 +2975,6 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { evaluator, task, intent, - renderForms, annotationStorage ); this.appearance = savedAppearance; @@ -3802,7 +3780,8 @@ class FreeTextAnnotation extends MarkupAnnotation { // It uses its own canvas in order to be hidden if edited. // But if it has the noHTML flag, it means that we don't want to be able // to modify it so we can just draw it on the main canvas. - this.data.hasOwnCanvas = !this.data.noHTML; + this.data.hasOwnCanvas = this.data.noRotate; + this.data.isEditable = !this.data.noHTML; // We want to be able to add mouse listeners to the annotation. this.data.noHTML = false; @@ -3857,12 +3836,19 @@ class FreeTextAnnotation extends MarkupAnnotation { return this._hasAppearance; } - static createNewDict(annotation, xref, { apRef, ap }) { + static createNewDict(annotation, xref, { apRef, ap, oldAnnotation }) { const { color, fontSize, rect, rotation, user, value } = annotation; - const freetext = new Dict(xref); + const freetext = oldAnnotation || new Dict(xref); freetext.set("Type", Name.get("Annot")); freetext.set("Subtype", Name.get("FreeText")); - freetext.set("CreationDate", `D:${getModificationDate()}`); + if (oldAnnotation) { + freetext.set("M", `D:${getModificationDate()}`); + // TODO: We should try to generate a new RC from the content we've. + // For now we can just remove it to avoid any issues. + freetext.delete("RC"); + } else { + freetext.set("CreationDate", `D:${getModificationDate()}`); + } freetext.set("Rect", rect); const da = `/Helv ${fontSize} Tf ${getPdfColor(color, /* isFill */ true)}`; freetext.set("DA", da); diff --git a/src/core/catalog.js b/src/core/catalog.js index 47d2812de73bb..2c4551d3ab2c7 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -64,26 +64,27 @@ function isValidExplicitDest(dest) { if (!(zoom instanceof Name)) { return false; } + const argsLen = args.length; let allowNull = true; switch (zoom.name) { case "XYZ": - if (args.length !== 3) { + if (argsLen < 2 || argsLen > 3) { return false; } break; case "Fit": case "FitB": - return args.length === 0; + return argsLen === 0; case "FitH": case "FitBH": case "FitV": case "FitBV": - if (args.length !== 1) { + if (argsLen > 1) { return false; } break; case "FitR": - if (args.length !== 4) { + if (argsLen !== 4) { return false; } allowNull = false; diff --git a/src/core/colorspace.js b/src/core/colorspace.js index 74b43a1c43c6f..1b66ab6d501bf 100644 --- a/src/core/colorspace.js +++ b/src/core/colorspace.js @@ -396,7 +396,9 @@ class ColorSpace { } } } - throw new FormatError(`Unrecognized ColorSpace: ${cs.name}`); + // Fallback to the default gray color space. + warn(`Unrecognized ColorSpace: ${cs.name}`); + return this.singletons.gray; } } if (Array.isArray(cs)) { @@ -474,10 +476,14 @@ class ColorSpace { const range = params.getArray("Range"); return new LabCS(whitePoint, blackPoint, range); default: - throw new FormatError(`Unimplemented ColorSpace object: ${mode}`); + // Fallback to the default gray color space. + warn(`Unimplemented ColorSpace object: ${mode}`); + return this.singletons.gray; } } - throw new FormatError(`Unrecognized ColorSpace object: ${cs}`); + // Fallback to the default gray color space. + warn(`Unrecognized ColorSpace object: ${cs}`); + return this.singletons.gray; } /** diff --git a/src/core/document.js b/src/core/document.js index 505cd9a75834b..edfc1cc629751 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -411,6 +411,7 @@ class Page { intent, cacheKey, annotationStorage = null, + modifiedIds = null, }) { const contentStreamPromise = this.getContentStream(); const resourcesPromise = this.loadResources([ @@ -568,6 +569,7 @@ class Page { return { length: pageOpList.totalLength }; } const renderForms = !!(intent & RenderingIntentFlag.ANNOTATIONS_FORMS), + isEditing = !!(intent & RenderingIntentFlag.IS_EDITING), intentAny = !!(intent & RenderingIntentFlag.ANY), intentDisplay = !!(intent & RenderingIntentFlag.DISPLAY), intentPrint = !!(intent & RenderingIntentFlag.PRINT); @@ -579,7 +581,8 @@ class Page { if ( intentAny || (intentDisplay && - annotation.mustBeViewed(annotationStorage, renderForms)) || + annotation.mustBeViewed(annotationStorage, renderForms) && + annotation.mustBeViewedWhenEditing(isEditing, modifiedIds)) || (intentPrint && annotation.mustBePrinted(annotationStorage)) ) { opListPromises.push( @@ -588,7 +591,6 @@ class Page { partialEvaluator, task, intent, - renderForms, annotationStorage ) .catch(function (reason) { diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 41595b4a36b0f..3896ed47e406c 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -3852,6 +3852,11 @@ class PartialEvaluator { map[charCode] = String.fromCodePoint(token); return; } + // Add back omitted leading zeros on odd length tokens + // (fixes issue #18099) + if (token.length % 2 !== 0) { + token = "\u0000" + token; + } const str = []; for (let k = 0; k < token.length; k += 2) { const w1 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1); @@ -3905,7 +3910,7 @@ class PartialEvaluator { let defaultVMetrics; if (properties.composite) { const dw = dict.get("DW"); - defaultWidth = Number.isInteger(dw) ? dw : 1000; + defaultWidth = typeof dw === "number" ? Math.ceil(dw) : 1000; const widths = dict.get("W"); if (Array.isArray(widths)) { diff --git a/src/core/flate_stream.js b/src/core/flate_stream.js index e660f39dbef33..f1753d01bd912 100644 --- a/src/core/flate_stream.js +++ b/src/core/flate_stream.js @@ -296,10 +296,15 @@ class FlateStream extends DecodeStream { } readBlock() { - let buffer, len; + let buffer, hdr, len; const str = this.str; // read block header - let hdr = this.getBits(3); + try { + hdr = this.getBits(3); + } catch (ex) { + this.#endsStreamOnError(ex.message); + return; + } if (hdr & 1) { this.eof = true; } diff --git a/src/core/fonts.js b/src/core/fonts.js index 7959f8ffc118d..29f186fa361b7 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -26,6 +26,7 @@ import { import { CFFCompiler, CFFParser } from "./cff_parser.js"; import { FontFlags, + getVerticalPresentationForm, MacStandardGlyphOrdering, normalizeFontName, recoverGlyphName, @@ -799,7 +800,7 @@ function createOS2Table(properties, charstrings, override) { const unitsPerEm = override.unitsPerEm || (properties.fontMatrix - ? 1 / Math.max(...properties.fontMatrix.slice(0, 4)) + ? 1 / Math.max(...properties.fontMatrix.slice(0, 4).map(Math.abs)) : 1000); // if the font units differ to the PDF glyph space units @@ -3199,7 +3200,7 @@ class Font { } const unitsPerEm = properties.fontMatrix - ? 1 / Math.max(...properties.fontMatrix.slice(0, 4)) + ? 1 / Math.max(...properties.fontMatrix.slice(0, 4).map(Math.abs)) : 1000; const builder = new OpenTypeFileBuilder("\x4F\x54\x54\x4F"); @@ -3292,6 +3293,47 @@ class Font { return builder.toArray(); } + /** + * @private + */ + get _spaceWidth() { + // trying to estimate space character width + const possibleSpaceReplacements = ["space", "minus", "one", "i", "I"]; + let width; + for (const glyphName of possibleSpaceReplacements) { + // if possible, getting width by glyph name + if (glyphName in this.widths) { + width = this.widths[glyphName]; + break; + } + const glyphsUnicodeMap = getGlyphsUnicode(); + const glyphUnicode = glyphsUnicodeMap[glyphName]; + // finding the charcode via unicodeToCID map + let charcode = 0; + if (this.composite && this.cMap.contains(glyphUnicode)) { + charcode = this.cMap.lookup(glyphUnicode); + + if (typeof charcode === "string") { + charcode = convertCidString(glyphUnicode, charcode); + } + } + // ... via toUnicode map + if (!charcode && this.toUnicode) { + charcode = this.toUnicode.charCodeOf(glyphUnicode); + } + // setting it to unicode if negative or undefined + if (charcode <= 0) { + charcode = glyphUnicode; + } + // trying to get width via charcode + width = this.widths[charcode]; + if (width) { + break; // the non-zero width found + } + } + return shadow(this, "_spaceWidth", width || this.defaultWidth); + } + /** * @private */ @@ -3337,6 +3379,13 @@ class Font { // .notdef glyphs should be invisible in non-embedded Type1 fonts, so // replace them with spaces. fontCharCode = 0x20; + + if (glyphName === "") { + // Ensure that other relevant glyph properties are also updated + // (fixes issue18059.pdf). + width ||= this._spaceWidth; + unicode = String.fromCharCode(fontCharCode); + } } fontCharCode = mapSpecialUnicodeValues(fontCharCode); } @@ -3366,6 +3415,13 @@ class Font { } } + if (this.missingFile && this.vertical && fontChar.length === 1) { + const vertical = getVerticalPresentationForm()[fontChar.charCodeAt(0)]; + if (vertical) { + fontChar = unicode = String.fromCharCode(vertical); + } + } + glyph = new Glyph( charcode, fontChar, diff --git a/src/core/fonts_utils.js b/src/core/fonts_utils.js index e5067d8e6ca8f..20c8e87e81905 100644 --- a/src/core/fonts_utils.js +++ b/src/core/fonts_utils.js @@ -15,6 +15,7 @@ import { getEncoding, StandardEncoding } from "./encodings.js"; import { getGlyphsUnicode } from "./glyphlist.js"; +import { getLookupTableFactory } from "./core_utils.js"; import { getUnicodeForGlyph } from "./unicode.js"; import { info } from "../shared/util.js"; @@ -168,8 +169,47 @@ function normalizeFontName(name) { return name.replaceAll(/[,_]/g, "-").replaceAll(/\s/g, ""); } +const getVerticalPresentationForm = getLookupTableFactory(t => { + // This table has been found at + // https://searchfox.org/mozilla-central/rev/cbdfa503a87597b20719aae5f6a1efccd6cb3b7b/gfx/thebes/gfxHarfBuzzShaper.cpp#251-294 + t[0x2013] = 0xfe32; // EN DASH + t[0x2014] = 0xfe31; // EM DASH + t[0x2025] = 0xfe30; // TWO DOT LEADER + t[0x2026] = 0xfe19; // HORIZONTAL ELLIPSIS + t[0x3001] = 0xfe11; // IDEOGRAPHIC COMMA + t[0x3002] = 0xfe12; // IDEOGRAPHIC FULL STOP + t[0x3008] = 0xfe3f; // LEFT ANGLE BRACKET + t[0x3009] = 0xfe40; // RIGHT ANGLE BRACKET + t[0x300a] = 0xfe3d; // LEFT DOUBLE ANGLE BRACKET + t[0x300b] = 0xfe3e; // RIGHT DOUBLE ANGLE BRACKET + t[0x300c] = 0xfe41; // LEFT CORNER BRACKET + t[0x300d] = 0xfe42; // RIGHT CORNER BRACKET + t[0x300e] = 0xfe43; // LEFT WHITE CORNER BRACKET + t[0x300f] = 0xfe44; // RIGHT WHITE CORNER BRACKET + t[0x3010] = 0xfe3b; // LEFT BLACK LENTICULAR BRACKET + t[0x3011] = 0xfe3c; // RIGHT BLACK LENTICULAR BRACKET + t[0x3014] = 0xfe39; // LEFT TORTOISE SHELL BRACKET + t[0x3015] = 0xfe3a; // RIGHT TORTOISE SHELL BRACKET + t[0x3016] = 0xfe17; // LEFT WHITE LENTICULAR BRACKET + t[0x3017] = 0xfe18; // RIGHT WHITE LENTICULAR BRACKET + t[0xfe4f] = 0xfe34; // WAVY LOW LINE + t[0xff01] = 0xfe15; // FULLWIDTH EXCLAMATION MARK + t[0xff08] = 0xfe35; // FULLWIDTH LEFT PARENTHESIS + t[0xff09] = 0xfe36; // FULLWIDTH RIGHT PARENTHESIS + t[0xff0c] = 0xfe10; // FULLWIDTH COMMA + t[0xff1a] = 0xfe13; // FULLWIDTH COLON + t[0xff1b] = 0xfe14; // FULLWIDTH SEMICOLON + t[0xff1f] = 0xfe16; // FULLWIDTH QUESTION MARK + t[0xff3b] = 0xfe47; // FULLWIDTH LEFT SQUARE BRACKET + t[0xff3d] = 0xfe48; // FULLWIDTH RIGHT SQUARE BRACKET + t[0xff3f] = 0xfe33; // FULLWIDTH LOW LINE + t[0xff5b] = 0xfe37; // FULLWIDTH LEFT CURLY BRACKET + t[0xff5d] = 0xfe38; // FULLWIDTH RIGHT CURLY BRACKET +}); + export { FontFlags, + getVerticalPresentationForm, MacStandardGlyphOrdering, normalizeFontName, recoverGlyphName, diff --git a/src/core/primitives.js b/src/core/primitives.js index e1bc4798b65de..a6801935de2fe 100644 --- a/src/core/primitives.js +++ b/src/core/primitives.js @@ -270,6 +270,10 @@ class Dict { } return dict; } + + delete(key) { + delete this._map[key]; + } } class Ref { diff --git a/src/core/worker.js b/src/core/worker.js index 804936229bcc9..3764b12edcda6 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -709,7 +709,7 @@ class WorkerMessageHandler { fileIds: xref.trailer.get("ID") || null, startXRef: linearization ? startXRef - : xref.lastXRefStreamPos ?? startXRef, + : (xref.lastXRefStreamPos ?? startXRef), filename, }; } @@ -752,6 +752,7 @@ class WorkerMessageHandler { intent: data.intent, cacheKey: data.cacheKey, annotationStorage: data.annotationStorage, + modifiedIds: data.modifiedIds, }) .then( function (operatorListInfo) { diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 57c074ab4db80..dff737b6ca6d7 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -198,6 +198,10 @@ class AnnotationElement { return !!(titleObj?.str || contentsObj?.str || richText?.str); } + get _isEditable() { + return this.data.isEditable; + } + get hasPopupData() { return AnnotationElement._hasPopupData(this.data); } @@ -734,10 +738,6 @@ class AnnotationElement { } } - get _isEditable() { - return false; - } - _editOnDoubleClick() { if (!this._isEditable) { return; @@ -2530,10 +2530,6 @@ class FreeTextAnnotationElement extends AnnotationElement { return this.container; } - - get _isEditable() { - return this.data.hasOwnCanvas; - } } class LineAnnotationElement extends AnnotationElement { @@ -3107,6 +3103,10 @@ class AnnotationLayer { } } + hasEditableAnnotations() { + return this.#editableAnnotations.size > 0; + } + #appendElement(element, id) { const contentElement = element.firstChild || element; contentElement.id = `${AnnotationPrefix}${id}`; @@ -3188,7 +3188,7 @@ class AnnotationLayer { } this.#appendElement(rendered, data.id); - if (element.annotationEditorType > 0) { + if (element._isEditable) { this.#editableAnnotations.set(element.data.id, element); this._annotationEditorUIManager?.renderAnnotationElement(element); } diff --git a/src/display/annotation_storage.js b/src/display/annotation_storage.js index 8154453c3b48b..9999f3b52b025 100644 --- a/src/display/annotation_storage.js +++ b/src/display/annotation_storage.js @@ -13,7 +13,7 @@ * limitations under the License. */ -import { objectFromMap, unreachable } from "../shared/util.js"; +import { objectFromMap, shadow, unreachable } from "../shared/util.js"; import { AnnotationEditor } from "./editor/editor.js"; import { MurmurHash3_64 } from "../shared/murmurhash3.js"; @@ -29,6 +29,8 @@ const SerializableEmpty = Object.freeze({ class AnnotationStorage { #modified = false; + #modifiedIds = null; + #storage = new Map(); constructor() { @@ -248,6 +250,34 @@ class AnnotationStorage { } return stats; } + + resetModifiedIds() { + this.#modifiedIds = null; + } + + /** + * @returns {{ids: Set, hash: string}} + */ + get modifiedIds() { + if (this.#modifiedIds) { + return this.#modifiedIds; + } + const ids = []; + for (const value of this.#storage.values()) { + if ( + !(value instanceof AnnotationEditor) || + !value.annotationElementId || + !value.serialize() + ) { + continue; + } + ids.push(value.annotationElementId); + } + return (this.#modifiedIds = { + ids: new Set(ids), + hash: ids.join(","), + }); + } } /** @@ -282,6 +312,13 @@ class PrintAnnotationStorage extends AnnotationStorage { get serializable() { return this.#serializable; } + + get modifiedIds() { + return shadow(this, "modifiedIds", { + ids: new Set(), + hash: "", + }); + } } export { AnnotationStorage, PrintAnnotationStorage, SerializableEmpty }; diff --git a/src/display/api.js b/src/display/api.js index 3b1ca159c6f2f..02e2bad9995ed 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -294,7 +294,7 @@ function getDocument(src = {}) { const enableHWA = src.enableHWA === true; // Parameters whose default values depend on other parameters. - const length = rangeTransport ? rangeTransport.length : src.length ?? NaN; + const length = rangeTransport ? rangeTransport.length : (src.length ?? NaN); const useSystemFonts = typeof src.useSystemFonts === "boolean" ? src.useSystemFonts @@ -1227,6 +1227,7 @@ class PDFDocumentProxy { * @property {Map} [annotationCanvasMap] - Map some * annotation ids with canvases used to render them. * @property {PrintAnnotationStorage} [printAnnotationStorage] + * @property {boolean} [isEditing] - Render the page in editing mode. */ /** @@ -1248,6 +1249,7 @@ class PDFDocumentProxy { * from the {@link AnnotationStorage}-instance; useful e.g. for printing. * The default value is `AnnotationMode.ENABLE`. * @property {PrintAnnotationStorage} [printAnnotationStorage] + * @property {boolean} [isEditing] - Render the page in editing mode. */ /** @@ -1420,13 +1422,15 @@ class PDFPageProxy { annotationCanvasMap = null, pageColors = null, printAnnotationStorage = null, + isEditing = false, }) { this._stats?.time("Overall"); const intentArgs = this._transport.getRenderingIntent( intent, annotationMode, - printAnnotationStorage + printAnnotationStorage, + isEditing ); const { renderingIntent, cacheKey } = intentArgs; // If there was a pending destroy, cancel it so no cleanup happens during @@ -1560,6 +1564,7 @@ class PDFPageProxy { intent = "display", annotationMode = AnnotationMode.ENABLE, printAnnotationStorage = null, + isEditing = false, } = {}) { if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) { throw new Error("Not implemented: getOperatorList"); @@ -1576,6 +1581,7 @@ class PDFPageProxy { intent, annotationMode, printAnnotationStorage, + isEditing, /* isOpList = */ true ); let intentState = this._intentStates.get(intentArgs.cacheKey); @@ -1812,6 +1818,7 @@ class PDFPageProxy { renderingIntent, cacheKey, annotationStorageSerializable, + modifiedIds, }) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { assert( @@ -1828,6 +1835,7 @@ class PDFPageProxy { intent: renderingIntent, cacheKey, annotationStorage: map, + modifiedIds, }, transfer ); @@ -2420,6 +2428,7 @@ class WorkerTransport { intent, annotationMode = AnnotationMode.ENABLE, printAnnotationStorage = null, + isEditing = false, isOpList = false ) { let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value. @@ -2438,6 +2447,12 @@ class WorkerTransport { warn(`getRenderingIntent - invalid intent: ${intent}`); } + const annotationStorage = + renderingIntent & RenderingIntentFlag.PRINT && + printAnnotationStorage instanceof PrintAnnotationStorage + ? printAnnotationStorage + : this.annotationStorage; + switch (annotationMode) { case AnnotationMode.DISABLE: renderingIntent += RenderingIntentFlag.ANNOTATIONS_DISABLE; @@ -2450,26 +2465,33 @@ class WorkerTransport { case AnnotationMode.ENABLE_STORAGE: renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE; - const annotationStorage = - renderingIntent & RenderingIntentFlag.PRINT && - printAnnotationStorage instanceof PrintAnnotationStorage - ? printAnnotationStorage - : this.annotationStorage; - annotationStorageSerializable = annotationStorage.serializable; break; default: warn(`getRenderingIntent - invalid annotationMode: ${annotationMode}`); } + if (isEditing) { + renderingIntent += RenderingIntentFlag.IS_EDITING; + } if (isOpList) { renderingIntent += RenderingIntentFlag.OPLIST; } + const { ids: modifiedIds, hash: modifiedIdsHash } = + annotationStorage.modifiedIds; + + const cacheKeyBuf = [ + renderingIntent, + annotationStorageSerializable.hash, + modifiedIdsHash, + ]; + return { renderingIntent, - cacheKey: `${renderingIntent}_${annotationStorageSerializable.hash}`, + cacheKey: cacheKeyBuf.join("_"), annotationStorageSerializable, + modifiedIds, }; } @@ -3254,6 +3276,8 @@ class RenderTask { * @ignore */ class InternalRenderTask { + #rAF = null; + static #canvasInUse = new WeakSet(); constructor({ @@ -3353,6 +3377,10 @@ class InternalRenderTask { this.running = false; this.cancelled = true; this.gfx?.endDrawing(); + if (this.#rAF) { + window.cancelAnimationFrame(this.#rAF); + this.#rAF = null; + } InternalRenderTask.#canvasInUse.delete(this._canvas); this.callback( @@ -3391,7 +3419,8 @@ class InternalRenderTask { _scheduleNext() { if (this._useRequestAnimationFrame) { - window.requestAnimationFrame(() => { + this.#rAF = window.requestAnimationFrame(() => { + this.#rAF = null; this._nextBound().catch(this._cancelBound); }); } else { diff --git a/src/display/canvas.js b/src/display/canvas.js index 811e625a72df6..247870f3b2d3c 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -1006,6 +1006,7 @@ class CanvasGraphics { this.restore(); } + this.current.activeSMask = null; this.ctx.restore(); if (this.transparentCanvas) { diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 459df1f7c6b55..31fd390234be9 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -49,6 +49,8 @@ class PixelsPerInch { * does the magic for us. */ class DOMFilterFactory extends BaseFilterFactory { + #baseUrl; + #_cache; #_defs; @@ -121,6 +123,23 @@ class DOMFilterFactory extends BaseFilterFactory { return [bufferR.join(","), bufferG.join(","), bufferB.join(",")]; } + #createUrl(id) { + if (this.#baseUrl === undefined) { + // Unless a ``-element is present a relative URL should work. + this.#baseUrl = ""; + + const url = this.#document.URL; + if (url !== this.#document.baseURI) { + if (isDataScheme(url)) { + warn('#createUrl: ignore "data:"-URL for performance reasons.'); + } else { + this.#baseUrl = url.split("#", 1)[0]; + } + } + } + return `url(${this.#baseUrl}#${id})`; + } + addFilter(maps) { if (!maps) { return "none"; @@ -146,7 +165,7 @@ class DOMFilterFactory extends BaseFilterFactory { // https://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement const id = `g_${this.#docId}_transfer_map_${this.#id++}`; - const url = `url(#${id})`; + const url = this.#createUrl(id); this.#cache.set(maps, url); this.#cache.set(key, url); @@ -232,7 +251,7 @@ class DOMFilterFactory extends BaseFilterFactory { filter ); - info.url = `url(#${id})`; + info.url = this.#createUrl(id); return info.url; } @@ -254,7 +273,7 @@ class DOMFilterFactory extends BaseFilterFactory { } const id = `g_${this.#docId}_alpha_map_${this.#id++}`; - const url = `url(#${id})`; + const url = this.#createUrl(id); this.#cache.set(map, url); this.#cache.set(key, url); @@ -287,7 +306,7 @@ class DOMFilterFactory extends BaseFilterFactory { } const id = `g_${this.#docId}_luminosity_map_${this.#id++}`; - const url = `url(#${id})`; + const url = this.#createUrl(id); this.#cache.set(map, url); this.#cache.set(key, url); @@ -389,7 +408,7 @@ class DOMFilterFactory extends BaseFilterFactory { filter ); - info.url = `url(#${id})`; + info.url = this.#createUrl(id); return info.url; } diff --git a/src/display/editor/alt_text.js b/src/display/editor/alt_text.js index 428f2f20dad7e..fececf29269cf 100644 --- a/src/display/editor/alt_text.js +++ b/src/display/editor/alt_text.js @@ -49,20 +49,27 @@ class AltText { altText.textContent = msg; altText.setAttribute("aria-label", msg); altText.tabIndex = "0"; - altText.addEventListener("contextmenu", noContextMenu); - altText.addEventListener("pointerdown", event => event.stopPropagation()); + const signal = this.#editor._uiManager._signal; + altText.addEventListener("contextmenu", noContextMenu, { signal }); + altText.addEventListener("pointerdown", event => event.stopPropagation(), { + signal, + }); const onClick = event => { event.preventDefault(); this.#editor._uiManager.editAltText(this.#editor); }; - altText.addEventListener("click", onClick, { capture: true }); - altText.addEventListener("keydown", event => { - if (event.target === altText && event.key === "Enter") { - this.#altTextWasFromKeyBoard = true; - onClick(event); - } - }); + altText.addEventListener("click", onClick, { capture: true, signal }); + altText.addEventListener( + "keydown", + event => { + if (event.target === altText && event.key === "Enter") { + this.#altTextWasFromKeyBoard = true; + onClick(event); + } + }, + { signal } + ); await this.#setState(); return altText; @@ -142,22 +149,39 @@ class AltText { button.setAttribute("aria-describedby", id); const DELAY_TO_SHOW_TOOLTIP = 100; - button.addEventListener("mouseenter", () => { - this.#altTextTooltipTimeout = setTimeout(() => { - this.#altTextTooltipTimeout = null; - this.#altTextTooltip.classList.add("show"); - this.#editor._reportTelemetry({ - action: "alt_text_tooltip", - }); - }, DELAY_TO_SHOW_TOOLTIP); - }); - button.addEventListener("mouseleave", () => { - if (this.#altTextTooltipTimeout) { + const signal = this.#editor._uiManager._signal; + signal.addEventListener( + "abort", + () => { clearTimeout(this.#altTextTooltipTimeout); this.#altTextTooltipTimeout = null; - } - this.#altTextTooltip?.classList.remove("show"); - }); + }, + { once: true } + ); + button.addEventListener( + "mouseenter", + () => { + this.#altTextTooltipTimeout = setTimeout(() => { + this.#altTextTooltipTimeout = null; + this.#altTextTooltip.classList.add("show"); + this.#editor._reportTelemetry({ + action: "alt_text_tooltip", + }); + }, DELAY_TO_SHOW_TOOLTIP); + }, + { signal } + ); + button.addEventListener( + "mouseleave", + () => { + if (this.#altTextTooltipTimeout) { + clearTimeout(this.#altTextTooltipTimeout); + this.#altTextTooltipTimeout = null; + } + this.#altTextTooltip?.classList.remove("show"); + }, + { signal } + ); } tooltip.innerText = this.#altTextDecorative ? await AltText._l10nPromise.get( diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 1e95423d09f15..52d76d3560946 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -230,6 +230,10 @@ class AnnotationEditorLayer { this.#uiManager.addCommands(params); } + toggleDrawing(enabled = false) { + this.div.classList.toggle("drawing", !enabled); + } + togglePointerEvents(enabled = false) { this.div.classList.toggle("disabled", !enabled); } @@ -365,7 +369,8 @@ class AnnotationEditorLayer { this.#boundTextLayerPointerDown = this.#textLayerPointerDown.bind(this); this.#textLayer.div.addEventListener( "pointerdown", - this.#boundTextLayerPointerDown + this.#boundTextLayerPointerDown, + { signal: this.#uiManager._signal } ); this.#textLayer.div.classList.add("highlighting"); } @@ -387,7 +392,12 @@ class AnnotationEditorLayer { // Unselect all the editors in order to let the user select some text // without being annoyed by an editor toolbar. this.#uiManager.unselectAll(); - if (event.target === this.#textLayer.div) { + const { target } = event; + if ( + target === this.#textLayer.div || + (target.classList.contains("endOfContent") && + this.#textLayer.div.contains(target)) + ) { const { isMac } = FeatureTest.platform; if (event.button !== 0 || (event.ctrlKey && isMac)) { // Do nothing on right click. @@ -399,6 +409,7 @@ class AnnotationEditorLayer { /* updateButton = */ true ); this.#textLayer.div.classList.add("free"); + this.toggleDrawing(); HighlightEditor.startHighlighting( this, this.#uiManager.direction === "ltr", @@ -408,8 +419,9 @@ class AnnotationEditorLayer { "pointerup", () => { this.#textLayer.div.classList.remove("free"); + this.toggleDrawing(true); }, - { once: true } + { once: true, signal: this.#uiManager._signal } ); event.preventDefault(); } @@ -419,10 +431,13 @@ class AnnotationEditorLayer { if (this.#boundPointerdown) { return; } + const signal = this.#uiManager._signal; this.#boundPointerdown = this.pointerdown.bind(this); this.#boundPointerup = this.pointerup.bind(this); - this.div.addEventListener("pointerdown", this.#boundPointerdown); - this.div.addEventListener("pointerup", this.#boundPointerup); + this.div.addEventListener("pointerdown", this.#boundPointerdown, { + signal, + }); + this.div.addEventListener("pointerup", this.#boundPointerup, { signal }); } disableClick() { @@ -540,7 +555,7 @@ class AnnotationEditorLayer { () => { editor._focusEventsAllowed = true; }, - { once: true } + { once: true, signal: this.#uiManager._signal } ); activeElement.focus(); } else { @@ -596,6 +611,10 @@ class AnnotationEditorLayer { return AnnotationEditorLayer.#editorTypes.get(this.#uiManager.getMode()); } + get _signal() { + return this.#uiManager._signal; + } + /** * Create a new editor * @param {Object} params diff --git a/src/display/editor/color_picker.js b/src/display/editor/color_picker.js index ab69299c7af1d..9550145205674 100644 --- a/src/display/editor/color_picker.js +++ b/src/display/editor/color_picker.js @@ -89,8 +89,9 @@ class ColorPicker { button.tabIndex = "0"; button.setAttribute("data-l10n-id", "pdfjs-editor-colorpicker-button"); button.setAttribute("aria-haspopup", true); - button.addEventListener("click", this.#openDropdown.bind(this)); - button.addEventListener("keydown", this.#boundKeyDown); + const signal = this.#uiManager._signal; + button.addEventListener("click", this.#openDropdown.bind(this), { signal }); + button.addEventListener("keydown", this.#boundKeyDown, { signal }); const swatch = (this.#buttonSwatch = document.createElement("span")); swatch.className = "swatch"; swatch.setAttribute("aria-hidden", true); @@ -109,7 +110,8 @@ class ColorPicker { #getDropdownRoot() { const div = document.createElement("div"); - div.addEventListener("contextmenu", noContextMenu); + const signal = this.#uiManager._signal; + div.addEventListener("contextmenu", noContextMenu, { signal }); div.className = "dropdown"; div.role = "listbox"; div.setAttribute("aria-multiselectable", false); @@ -127,11 +129,13 @@ class ColorPicker { swatch.className = "swatch"; swatch.style.backgroundColor = color; button.setAttribute("aria-selected", color === this.#defaultColor); - button.addEventListener("click", this.#colorSelect.bind(this, color)); + button.addEventListener("click", this.#colorSelect.bind(this, color), { + signal, + }); div.append(button); } - div.addEventListener("keydown", this.#boundKeyDown); + div.addEventListener("keydown", this.#boundKeyDown, { signal }); return div; } @@ -211,7 +215,9 @@ class ColorPicker { return; } this.#dropdownWasFromKeyboard = event.detail === 0; - window.addEventListener("pointerdown", this.#boundPointerDown); + window.addEventListener("pointerdown", this.#boundPointerDown, { + signal: this.#uiManager._signal, + }); if (this.#dropdown) { this.#dropdown.classList.remove("hidden"); return; diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 3a42786d3374a..69dee6f15cfb7 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -40,6 +40,8 @@ import { noContextMenu } from "../display_utils.js"; * Base class for editors. */ class AnnotationEditor { + #accessibilityData = null; + #allResizerDivs = null; #altText = null; @@ -715,6 +717,7 @@ class AnnotationEditor { "bottomLeft", "middleLeft", ]; + const signal = this._uiManager._signal; for (const name of classes) { const div = document.createElement("div"); this.#resizersDiv.append(div); @@ -722,9 +725,10 @@ class AnnotationEditor { div.setAttribute("data-resizer-name", name); div.addEventListener( "pointerdown", - this.#resizerPointerdown.bind(this, name) + this.#resizerPointerdown.bind(this, name), + { signal } ); - div.addEventListener("contextmenu", noContextMenu); + div.addEventListener("contextmenu", noContextMenu, { signal }); div.tabIndex = -1; } this.div.prepend(this.#resizersDiv); @@ -742,14 +746,15 @@ class AnnotationEditor { const boundResizerPointermove = this.#resizerPointermove.bind(this, name); const savedDraggable = this._isDraggable; this._isDraggable = false; - const pointerMoveOptions = { passive: true, capture: true }; + const signal = this._uiManager._signal; + const pointerMoveOptions = { passive: true, capture: true, signal }; this.parent.togglePointerEvents(false); window.addEventListener( "pointermove", boundResizerPointermove, pointerMoveOptions ); - window.addEventListener("contextmenu", noContextMenu); + window.addEventListener("contextmenu", noContextMenu, { signal }); const savedX = this.x; const savedY = this.y; const savedWidth = this.width; @@ -776,10 +781,10 @@ class AnnotationEditor { this.#addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight); }; - window.addEventListener("pointerup", pointerUpCallback); + window.addEventListener("pointerup", pointerUpCallback, { signal }); // If the user switches to another window (with alt+tab), then we end the // resize session. - window.addEventListener("blur", pointerUpCallback); + window.addEventListener("blur", pointerUpCallback, { signal }); } #addResizeToUndoStack(savedX, savedY, savedWidth, savedHeight) { @@ -990,6 +995,10 @@ class AnnotationEditor { } AltText.initialize(AnnotationEditor._l10nPromise); this.#altText = new AltText(this); + if (this.#accessibilityData) { + this.#altText.data = this.#accessibilityData; + this.#accessibilityData = null; + } await this.addEditToolbar(); } @@ -1027,8 +1036,9 @@ class AnnotationEditor { this.setInForeground(); - this.div.addEventListener("focusin", this.#boundFocusin); - this.div.addEventListener("focusout", this.#boundFocusout); + const signal = this._uiManager._signal; + this.div.addEventListener("focusin", this.#boundFocusin, { signal }); + this.div.addEventListener("focusout", this.#boundFocusout, { signal }); const [parentWidth, parentHeight] = this.parentDimensions; if (this.parentRotation % 180 !== 0) { @@ -1089,9 +1099,10 @@ class AnnotationEditor { this._uiManager.setUpDragSession(); let pointerMoveOptions, pointerMoveCallback; + const signal = this._uiManager._signal; if (isSelected) { this.div.classList.add("moving"); - pointerMoveOptions = { passive: true, capture: true }; + pointerMoveOptions = { passive: true, capture: true, signal }; this.#prevDragX = event.clientX; this.#prevDragY = event.clientY; pointerMoveCallback = e => { @@ -1128,11 +1139,11 @@ class AnnotationEditor { this.#selectOnPointerEvent(event); } }; - window.addEventListener("pointerup", pointerUpCallback); + window.addEventListener("pointerup", pointerUpCallback, { signal }); // If the user is using alt+tab during the dragging session, the pointerup // event could be not fired, but a blur event is fired so we can use it in // order to interrupt the dragging session. - window.addEventListener("blur", pointerUpCallback); + window.addEventListener("blur", pointerUpCallback, { signal }); } moveInDOM() { @@ -1284,8 +1295,9 @@ class AnnotationEditor { * To implement in subclasses. */ rebuild() { - this.div?.addEventListener("focusin", this.#boundFocusin); - this.div?.addEventListener("focusout", this.#boundFocusout); + const signal = this._uiManager._signal; + this.div?.addEventListener("focusin", this.#boundFocusin, { signal }); + this.div?.addEventListener("focusout", this.#boundFocusout, { signal }); } /** @@ -1324,6 +1336,7 @@ class AnnotationEditor { uiManager, }); editor.rotation = data.rotation; + editor.#accessibilityData = data.accessibilityData; const [pageWidth, pageHeight] = editor.pageDimensions; const [x, y, width, height] = editor.getRectInCurrentCoords( @@ -1429,12 +1442,15 @@ class AnnotationEditor { this.#allResizerDivs = Array.from(children); const boundResizerKeydown = this.#resizerKeydown.bind(this); const boundResizerBlur = this.#resizerBlur.bind(this); + const signal = this._uiManager._signal; for (const div of this.#allResizerDivs) { const name = div.getAttribute("data-resizer-name"); div.setAttribute("role", "spinbutton"); - div.addEventListener("keydown", boundResizerKeydown); - div.addEventListener("blur", boundResizerBlur); - div.addEventListener("focus", this.#resizerFocus.bind(this, name)); + div.addEventListener("keydown", boundResizerKeydown, { signal }); + div.addEventListener("blur", boundResizerBlur, { signal }); + div.addEventListener("focus", this.#resizerFocus.bind(this, name), { + signal, + }); AnnotationEditor._l10nPromise .get(`pdfjs-editor-resizer-label-${name}`) .then(msg => div.setAttribute("aria-label", msg)); diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 2280021d8c88c..fcd28f50b286d 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -307,11 +307,22 @@ class FreeTextEditor extends AnnotationEditor { this.editorDiv.contentEditable = true; this._isDraggable = false; this.div.removeAttribute("aria-activedescendant"); - this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown); - this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus); - this.editorDiv.addEventListener("blur", this.#boundEditorDivBlur); - this.editorDiv.addEventListener("input", this.#boundEditorDivInput); - this.editorDiv.addEventListener("paste", this.#boundEditorDivPaste); + const signal = this._uiManager._signal; + this.editorDiv.addEventListener("keydown", this.#boundEditorDivKeydown, { + signal, + }); + this.editorDiv.addEventListener("focus", this.#boundEditorDivFocus, { + signal, + }); + this.editorDiv.addEventListener("blur", this.#boundEditorDivBlur, { + signal, + }); + this.editorDiv.addEventListener("input", this.#boundEditorDivInput, { + signal, + }); + this.editorDiv.addEventListener("paste", this.#boundEditorDivPaste, { + signal, + }); } /** @inheritdoc */ diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index 58638d800c18b..36ca75cc51463 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -568,7 +568,9 @@ class HighlightEditor extends AnnotationEditor { if (this.#isFreeHighlight) { div.classList.add("free"); } else { - this.div.addEventListener("keydown", this.#boundKeydown); + this.div.addEventListener("keydown", this.#boundKeydown, { + signal: this._uiManager._signal, + }); } const highlightDiv = (this.#highlightDiv = document.createElement("div")); div.append(highlightDiv); @@ -669,12 +671,13 @@ class HighlightEditor extends AnnotationEditor { return null; } const [pageWidth, pageHeight] = this.pageDimensions; + const [pageX, pageY] = this.pageTranslation; const boxes = this.#boxes; const quadPoints = new Float32Array(boxes.length * 8); let i = 0; for (const { x, y, width, height } of boxes) { - const sx = x * pageWidth; - const sy = (1 - y - height) * pageHeight; + const sx = x * pageWidth + pageX; + const sy = (1 - y - height) * pageHeight + pageY; // The specifications say that the rectangle should start from the bottom // left corner and go counter-clockwise. // But when opening the file in Adobe Acrobat it appears that this isn't @@ -702,7 +705,8 @@ class HighlightEditor extends AnnotationEditor { const pointerMove = e => { this.#highlightMove(parent, e); }; - const pointerDownOptions = { capture: true, passive: false }; + const signal = parent._signal; + const pointerDownOptions = { capture: true, passive: false, signal }; const pointerDown = e => { // Avoid to have undesired clicks during the drawing. e.preventDefault(); @@ -720,12 +724,12 @@ class HighlightEditor extends AnnotationEditor { window.removeEventListener("contextmenu", noContextMenu); this.#endHighlight(parent, e); }; - window.addEventListener("blur", pointerUpCallback); - window.addEventListener("pointerup", pointerUpCallback); + window.addEventListener("blur", pointerUpCallback, { signal }); + window.addEventListener("pointerup", pointerUpCallback, { signal }); window.addEventListener("pointerdown", pointerDown, pointerDownOptions); - window.addEventListener("contextmenu", noContextMenu); + window.addEventListener("contextmenu", noContextMenu, { signal }); - textLayer.addEventListener("pointermove", pointerMove); + textLayer.addEventListener("pointermove", pointerMove, { signal }); this._freeHighlight = new FreeOutliner( { x, y }, [layerX, layerY, parentWidth, parentHeight], diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index f0794763421eb..3db72632eaeb1 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -261,7 +261,7 @@ class InkEditor extends AnnotationEditor { this.#canvasContextMenuTimeoutId = null; } - this.#observer.disconnect(); + this.#observer?.disconnect(); this.#observer = null; super.remove(); @@ -296,7 +296,9 @@ class InkEditor extends AnnotationEditor { super.enableEditMode(); this._isDraggable = false; - this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown); + this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown, { + signal: this._uiManager._signal, + }); } /** @inheritdoc */ @@ -363,10 +365,19 @@ class InkEditor extends AnnotationEditor { * @param {number} y */ #startDrawing(x, y) { - this.canvas.addEventListener("contextmenu", noContextMenu); - this.canvas.addEventListener("pointerleave", this.#boundCanvasPointerleave); - this.canvas.addEventListener("pointermove", this.#boundCanvasPointermove); - this.canvas.addEventListener("pointerup", this.#boundCanvasPointerup); + const signal = this._uiManager._signal; + this.canvas.addEventListener("contextmenu", noContextMenu, { signal }); + this.canvas.addEventListener( + "pointerleave", + this.#boundCanvasPointerleave, + { signal } + ); + this.canvas.addEventListener("pointermove", this.#boundCanvasPointermove, { + signal, + }); + this.canvas.addEventListener("pointerup", this.#boundCanvasPointerup, { + signal, + }); this.canvas.removeEventListener( "pointerdown", this.#boundCanvasPointerdown @@ -706,7 +717,9 @@ class InkEditor extends AnnotationEditor { this.#boundCanvasPointermove ); this.canvas.removeEventListener("pointerup", this.#boundCanvasPointerup); - this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown); + this.canvas.addEventListener("pointerdown", this.#boundCanvasPointerdown, { + signal: this._uiManager._signal, + }); // Slight delay to avoid the context menu to appear (it can happen on a long // tap with a pen). @@ -751,6 +764,14 @@ class InkEditor extends AnnotationEditor { } }); this.#observer.observe(this.div); + this._uiManager._signal.addEventListener( + "abort", + () => { + this.#observer?.disconnect(); + this.#observer = null; + }, + { once: true } + ); } /** @inheritdoc */ diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index a57b522f27b69..c84078336e784 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -36,6 +36,8 @@ class StampEditor extends AnnotationEditor { #canvas = null; + #hasMLBeenQueried = false; + #observer = null; #resizeTimeoutId = null; @@ -160,26 +162,35 @@ class StampEditor extends AnnotationEditor { } input.type = "file"; input.accept = StampEditor.supportedTypesStr; + const signal = this._uiManager._signal; this.#bitmapPromise = new Promise(resolve => { - input.addEventListener("change", async () => { - if (!input.files || input.files.length === 0) { + input.addEventListener( + "change", + async () => { + if (!input.files || input.files.length === 0) { + this.remove(); + } else { + this._uiManager.enableWaiting(true); + const data = await this._uiManager.imageManager.getFromFile( + input.files[0] + ); + this.#getBitmapFetched(data); + } + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { + input.remove(); + } + resolve(); + }, + { signal } + ); + input.addEventListener( + "cancel", + () => { this.remove(); - } else { - this._uiManager.enableWaiting(true); - const data = await this._uiManager.imageManager.getFromFile( - input.files[0] - ); - this.#getBitmapFetched(data); - } - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { - input.remove(); - } - resolve(); - }); - input.addEventListener("cancel", () => { - this.remove(); - resolve(); - }); + resolve(); + }, + { signal } + ); }).finally(() => this.#getBitmapDone()); if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("TESTING")) { input.click(); @@ -414,6 +425,43 @@ class StampEditor extends AnnotationEditor { return bitmap; } + async #mlGuessAltText(bitmap, width, height) { + if (this.#hasMLBeenQueried) { + return; + } + this.#hasMLBeenQueried = true; + const isMLEnabled = await this._uiManager.isMLEnabledFor("altText"); + if (!isMLEnabled || this.hasAltText()) { + return; + } + const offscreen = new OffscreenCanvas(width, height); + const ctx = offscreen.getContext("2d", { willReadFrequently: true }); + ctx.drawImage( + bitmap, + 0, + 0, + bitmap.width, + bitmap.height, + 0, + 0, + width, + height + ); + const response = await this._uiManager.mlGuess({ + service: "moz-image-to-text", + request: { + data: ctx.getImageData(0, 0, width, height).data, + width, + height, + channels: 4, + }, + }); + const altText = response?.output || ""; + if (this.parent && altText && !this.hasAltText()) { + this.altTextData = { altText, decorative: false }; + } + } + #drawBitmap(width, height) { width = Math.ceil(width); height = Math.ceil(height); @@ -427,37 +475,8 @@ class StampEditor extends AnnotationEditor { ? this.#bitmap : this.#scaleBitmap(width, height); - if (this._uiManager.hasMLManager && !this.hasAltText()) { - const offscreen = new OffscreenCanvas(width, height); - const ctx = offscreen.getContext("2d"); - ctx.drawImage( - bitmap, - 0, - 0, - bitmap.width, - bitmap.height, - 0, - 0, - width, - height - ); - this._uiManager - .mlGuess({ - service: "image-to-text", - request: { - data: ctx.getImageData(0, 0, width, height).data, - width, - height, - channels: 4, - }, - }) - .then(response => { - const altText = response?.output || ""; - if (this.parent && altText && !this.hasAltText()) { - this.altTextData = { altText, decorative: false }; - } - }); - } + this.#mlGuessAltText(bitmap, width, height); + const ctx = canvas.getContext("2d"); ctx.filter = this._uiManager.hcmFilter; ctx.drawImage( @@ -529,6 +548,11 @@ class StampEditor extends AnnotationEditor { * Create the resize observer. */ #createObserver() { + if (!this._uiManager._signal) { + // This method is called after the canvas has been created but the canvas + // creation is async, so it's possible that the viewer has been closed. + return; + } this.#observer = new ResizeObserver(entries => { const rect = entries[0].contentRect; if (rect.width && rect.height) { @@ -536,6 +560,14 @@ class StampEditor extends AnnotationEditor { } }); this.#observer.observe(this.div); + this._uiManager._signal.addEventListener( + "abort", + () => { + this.#observer?.disconnect(); + this.#observer = null; + }, + { once: true } + ); } /** @inheritdoc */ diff --git a/src/display/editor/toolbar.js b/src/display/editor/toolbar.js index ea493004537c1..66c0b93ede31b 100644 --- a/src/display/editor/toolbar.js +++ b/src/display/editor/toolbar.js @@ -32,8 +32,11 @@ class EditorToolbar { const editToolbar = (this.#toolbar = document.createElement("div")); editToolbar.className = "editToolbar"; editToolbar.setAttribute("role", "toolbar"); - editToolbar.addEventListener("contextmenu", noContextMenu); - editToolbar.addEventListener("pointerdown", EditorToolbar.#pointerDown); + const signal = this.#editor._uiManager._signal; + editToolbar.addEventListener("contextmenu", noContextMenu, { signal }); + editToolbar.addEventListener("pointerdown", EditorToolbar.#pointerDown, { + signal, + }); const buttons = (this.#buttons = document.createElement("div")); buttons.className = "buttons"; @@ -77,13 +80,16 @@ class EditorToolbar { // If we're clicking on a button with the keyboard or with // the mouse, we don't want to trigger any focus events on // the editor. + const signal = this.#editor._uiManager._signal; element.addEventListener("focusin", this.#focusIn.bind(this), { capture: true, + signal, }); element.addEventListener("focusout", this.#focusOut.bind(this), { capture: true, + signal, }); - element.addEventListener("contextmenu", noContextMenu); + element.addEventListener("contextmenu", noContextMenu, { signal }); } hide() { @@ -104,9 +110,13 @@ class EditorToolbar { `pdfjs-editor-remove-${this.#editor.editorType}-button` ); this.#addListenersToElement(button); - button.addEventListener("click", e => { - this.#editor._uiManager.delete(); - }); + button.addEventListener( + "click", + e => { + this.#editor._uiManager.delete(); + }, + { signal: this.#editor._uiManager._signal } + ); this.#buttons.append(button); } @@ -150,7 +160,9 @@ class HighlightToolbar { const editToolbar = (this.#toolbar = document.createElement("div")); editToolbar.className = "editToolbar"; editToolbar.setAttribute("role", "toolbar"); - editToolbar.addEventListener("contextmenu", noContextMenu); + editToolbar.addEventListener("contextmenu", noContextMenu, { + signal: this.#uiManager._signal, + }); const buttons = (this.#buttons = document.createElement("div")); buttons.className = "buttons"; @@ -207,10 +219,15 @@ class HighlightToolbar { button.append(span); span.className = "visuallyHidden"; span.setAttribute("data-l10n-id", "pdfjs-highlight-floating-button-label"); - button.addEventListener("contextmenu", noContextMenu); - button.addEventListener("click", () => { - this.#uiManager.highlightSelection("floating_button"); - }); + const signal = this.#uiManager._signal; + button.addEventListener("contextmenu", noContextMenu, { signal }); + button.addEventListener( + "click", + () => { + this.#uiManager.highlightSelection("floating_button"); + }, + { signal } + ); this.#buttons.append(button); } } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 14444cf9461fa..fa4be1fd1ea60 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -534,6 +534,8 @@ class ColorManager { * some action like copy/paste, undo/redo, ... */ class AnnotationEditorUIManager { + #abortController = new AbortController(); + #activeEditor = null; #allEditors = new Map(); @@ -560,6 +562,8 @@ class AnnotationEditorUIManager { #enableHighlightFloatingButton = false; + #enableUpdatedAddImage = false; + #filterFactory = null; #focusMainContainerTimeoutId = null; @@ -600,10 +604,6 @@ class AnnotationEditorUIManager { #boundCut = this.cut.bind(this); - #boundDragOver = this.dragOver.bind(this); - - #boundDrop = this.drop.bind(this); - #boundPaste = this.paste.bind(this); #boundKeydown = this.keydown.bind(this); @@ -616,8 +616,6 @@ class AnnotationEditorUIManager { #boundOnScaleChanging = this.onScaleChanging.bind(this); - #boundSelectionChange = this.#selectionChange.bind(this); - #boundOnRotationChanging = this.onRotationChanging.bind(this); #previousStates = { @@ -783,8 +781,10 @@ class AnnotationEditorUIManager { pageColors, highlightColors, enableHighlightFloatingButton, + enableUpdatedAddImage, mlManager ) { + this._signal = this.#abortController.signal; this.#container = container; this.#viewer = viewer; this.#altTextManager = altTextManager; @@ -801,6 +801,7 @@ class AnnotationEditorUIManager { this.#pageColors = pageColors; this.#highlightColors = highlightColors || null; this.#enableHighlightFloatingButton = enableHighlightFloatingButton; + this.#enableUpdatedAddImage = enableUpdatedAddImage; this.#mlManager = mlManager || null; this.viewParameters = { realScale: PixelsPerInch.PDF_TO_CSS_UNITS, @@ -820,9 +821,10 @@ class AnnotationEditorUIManager { } destroy() { - this.#removeDragAndDropListeners(); - this.#removeKeyboardManager(); - this.#removeFocusManager(); + this.#abortController?.abort(); + this.#abortController = null; + this._signal = null; + this._eventBus._off("editingaction", this.#boundOnEditingAction); this._eventBus._off("pagechanging", this.#boundOnPageChanging); this._eventBus._off("scalechanging", this.#boundOnScaleChanging); @@ -847,15 +849,18 @@ class AnnotationEditorUIManager { clearTimeout(this.#translationTimeoutId); this.#translationTimeoutId = null; } - this.#removeSelectionListener(); } async mlGuess(data) { return this.#mlManager?.guess(data) || null; } - get hasMLManager() { - return !!this.#mlManager; + async isMLEnabledFor(name) { + return !!(await this.#mlManager?.isEnabledFor(name)); + } + + get useNewAltTextFlow() { + return this.#enableUpdatedAddImage; } get hcmFilter() { @@ -911,6 +916,26 @@ class AnnotationEditorUIManager { this.#altTextManager?.editAltText(this, editor); } + switchToMode(mode, callback) { + // Switching to a mode can be asynchronous. + this._eventBus.on("annotationeditormodechanged", callback, { + once: true, + signal: this._signal, + }); + this._eventBus.dispatch("showannotationeditorui", { + source: this, + mode, + }); + } + + setPreference(name, value) { + this._eventBus.dispatch("setpreference", { + source: this, + name, + value, + }); + } + onPageChanging({ pageNumber }) { this.#currentPageIndex = pageNumber - 1; } @@ -970,6 +995,19 @@ class AnnotationEditorUIManager { : anchorNode; } + #getLayerForTextLayer(textLayer) { + const { currentLayer } = this; + if (currentLayer.hasTextLayer(textLayer)) { + return currentLayer; + } + for (const layer of this.#allLayers.values()) { + if (layer.hasTextLayer(textLayer)) { + return layer; + } + } + return null; + } + highlightSelection(methodOfCreation = "") { const selection = document.getSelection(); if (!selection || selection.isCollapsed) { @@ -984,27 +1022,28 @@ class AnnotationEditorUIManager { return; } selection.empty(); - if (this.#mode === AnnotationEditorType.NONE) { - this._eventBus.dispatch("showannotationeditorui", { - source: this, - mode: AnnotationEditorType.HIGHLIGHT, + + const layer = this.#getLayerForTextLayer(textLayer); + const isNoneMode = this.#mode === AnnotationEditorType.NONE; + const callback = () => { + layer?.createAndAddNewEditor({ x: 0, y: 0 }, false, { + methodOfCreation, + boxes, + anchorNode, + anchorOffset, + focusNode, + focusOffset, + text, }); - this.showAllEditors("highlight", true, /* updateButton = */ true); - } - for (const layer of this.#allLayers.values()) { - if (layer.hasTextLayer(textLayer)) { - layer.createAndAddNewEditor({ x: 0, y: 0 }, false, { - methodOfCreation, - boxes, - anchorNode, - anchorOffset, - focusNode, - focusOffset, - text, - }); - break; + if (isNoneMode) { + this.showAllEditors("highlight", true, /* updateButton = */ true); } + }; + if (isNoneMode) { + this.switchToMode(AnnotationEditorType.HIGHLIGHT, callback); + return; } + callback(); } #displayHighlightToolbar() { @@ -1065,6 +1104,7 @@ class AnnotationEditorUIManager { } return; } + this.#highlightToolbar?.hide(); this.#selectedTextNode = anchorNode; this.#dispatchUpdateStates({ @@ -1084,19 +1124,27 @@ class AnnotationEditorUIManager { this.#highlightWhenShiftUp = this.isShiftKeyDown; if (!this.isShiftKeyDown) { + const activeLayer = + this.#mode === AnnotationEditorType.HIGHLIGHT + ? this.#getLayerForTextLayer(textLayer) + : null; + activeLayer?.toggleDrawing(); + + const signal = this._signal; const pointerup = e => { if (e.type === "pointerup" && e.button !== 0) { // Do nothing on right click. return; } + activeLayer?.toggleDrawing(true); window.removeEventListener("pointerup", pointerup); window.removeEventListener("blur", pointerup); if (e.type === "pointerup") { this.#onSelectEnd("main_toolbar"); } }; - window.addEventListener("pointerup", pointerup); - window.addEventListener("blur", pointerup); + window.addEventListener("pointerup", pointerup, { signal }); + window.addEventListener("blur", pointerup, { signal }); } } @@ -1109,16 +1157,19 @@ class AnnotationEditorUIManager { } #addSelectionListener() { - document.addEventListener("selectionchange", this.#boundSelectionChange); - } - - #removeSelectionListener() { - document.removeEventListener("selectionchange", this.#boundSelectionChange); + document.addEventListener( + "selectionchange", + this.#selectionChange.bind(this), + { + signal: this._signal, + } + ); } #addFocusManager() { - window.addEventListener("focus", this.#boundFocus); - window.addEventListener("blur", this.#boundBlur); + const signal = this._signal; + window.addEventListener("focus", this.#boundFocus, { signal }); + window.addEventListener("blur", this.#boundBlur, { signal }); } #removeFocusManager() { @@ -1160,16 +1211,17 @@ class AnnotationEditorUIManager { () => { lastEditor._focusEventsAllowed = true; }, - { once: true } + { once: true, signal: this._signal } ); lastActiveElement.focus(); } #addKeyboardManager() { + const signal = this._signal; // The keyboard events are caught at the container level in order to be able // to execute some callbacks even if the current page doesn't have focus. - window.addEventListener("keydown", this.#boundKeydown); - window.addEventListener("keyup", this.#boundKeyup); + window.addEventListener("keydown", this.#boundKeydown, { signal }); + window.addEventListener("keyup", this.#boundKeyup, { signal }); } #removeKeyboardManager() { @@ -1178,9 +1230,10 @@ class AnnotationEditorUIManager { } #addCopyPasteListeners() { - document.addEventListener("copy", this.#boundCopy); - document.addEventListener("cut", this.#boundCut); - document.addEventListener("paste", this.#boundPaste); + const signal = this._signal; + document.addEventListener("copy", this.#boundCopy, { signal }); + document.addEventListener("cut", this.#boundCut, { signal }); + document.addEventListener("paste", this.#boundPaste, { signal }); } #removeCopyPasteListeners() { @@ -1190,13 +1243,9 @@ class AnnotationEditorUIManager { } #addDragAndDropListeners() { - document.addEventListener("dragover", this.#boundDragOver); - document.addEventListener("drop", this.#boundDrop); - } - - #removeDragAndDropListeners() { - document.removeEventListener("dragover", this.#boundDragOver); - document.removeEventListener("drop", this.#boundDrop); + const signal = this._signal; + document.addEventListener("dragover", this.dragOver.bind(this), { signal }); + document.addEventListener("drop", this.drop.bind(this), { signal }); } addEditListeners() { diff --git a/src/display/text_layer.js b/src/display/text_layer.js index df35dfb4e5b73..bace7a87ea999 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -17,7 +17,7 @@ /** @typedef {import("./api").TextContent} TextContent */ import { AbortException, Util, warn } from "../shared/util.js"; -import { deprecated, setLayerDimensions } from "./display_utils.js"; +import { setLayerDimensions } from "./display_utils.js"; /** * @typedef {Object} TextLayerParameters @@ -83,6 +83,8 @@ class TextLayer { static #canvasContexts = new Map(); + static #minFontSize = null; + static #pendingTextLayers = new Set(); /** @@ -120,6 +122,8 @@ class TextLayer { this.#pageWidth = pageWidth; this.#pageHeight = pageHeight; + TextLayer.#ensureMinFontSizeComputed(); + setLayerDimensions(container, viewport); // Always clean-up the temporary canvas once rendering is no longer pending. @@ -242,7 +246,7 @@ class TextLayer { if (this.#disableProcessItems) { return; } - this.#layoutTextParams.ctx ||= TextLayer.#getCtx(this.#lang); + this.#layoutTextParams.ctx ??= TextLayer.#getCtx(this.#lang); const textDivs = this.#textDivs, textContentItemsStr = this.#textContentItemsStr; @@ -326,7 +330,11 @@ class TextLayer { divStyle.left = `${scaleFactorStr}${left.toFixed(2)}px)`; divStyle.top = `${scaleFactorStr}${top.toFixed(2)}px)`; } - divStyle.fontSize = `${scaleFactorStr}${fontHeight.toFixed(2)}px)`; + // We multiply the font size by #minFontSize, and then #layout will + // scale the element by 1/#minFontSize. This allows us to effectively + // ignore the minimum font size enforced by the browser, so that the text + // layer s can always match the size of the text in the canvas. + divStyle.fontSize = `${scaleFactorStr}${(TextLayer.#minFontSize * fontHeight).toFixed(2)}px)`; divStyle.fontFamily = fontFamily; textDivProperties.fontSize = fontHeight; @@ -388,7 +396,12 @@ class TextLayer { #layout(params) { const { div, properties, ctx, prevFontSize, prevFontFamily } = params; const { style } = div; + let transform = ""; + if (TextLayer.#minFontSize > 1) { + transform = `scale(${1 / TextLayer.#minFontSize})`; + } + if (properties.canvasWidth !== 0 && properties.hasText) { const { fontFamily } = style; const { canvasWidth, fontSize } = properties; @@ -403,7 +416,7 @@ class TextLayer { const { width } = ctx.measureText(div.textContent); if (width > 0) { - transform = `scaleX(${(canvasWidth * this.#scale) / width})`; + transform = `scaleX(${(canvasWidth * this.#scale) / width}) ${transform}`; } } if (properties.angle !== 0) { @@ -456,6 +469,27 @@ class TextLayer { return canvasContext; } + /** + * Compute the minimum font size enforced by the browser. + */ + static #ensureMinFontSizeComputed() { + if (this.#minFontSize !== null) { + return; + } + const div = document.createElement("div"); + div.style.opacity = 0; + div.style.lineHeight = 1; + div.style.fontSize = "1px"; + div.style.position = "absolute"; + div.textContent = "X"; + document.body.append(div); + // In `display:block` elements contain a single line of text, + // the height matches the line height (which, when set to 1, + // matches the actual font size). + this.#minFontSize = div.getBoundingClientRect().height; + div.remove(); + } + static #getAscent(fontFamily, lang) { const cachedAscent = this.#ascentCache.get(fontFamily); if (cachedAscent) { @@ -524,40 +558,4 @@ class TextLayer { } } -function renderTextLayer() { - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - return; - } - deprecated("`renderTextLayer`, please use `TextLayer` instead."); - - const { textContentSource, container, viewport, ...rest } = arguments[0]; - const restKeys = Object.keys(rest); - if (restKeys.length > 0) { - warn("Ignoring `renderTextLayer` parameters: " + restKeys.join(", ")); - } - - const textLayer = new TextLayer({ - textContentSource, - container, - viewport, - }); - - const { textDivs, textContentItemsStr } = textLayer; - const promise = textLayer.render(); - - // eslint-disable-next-line consistent-return - return { - promise, - textDivs, - textContentItemsStr, - }; -} - -function updateTextLayer() { - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - return; - } - deprecated("`updateTextLayer`, please use `TextLayer` instead."); -} - -export { renderTextLayer, TextLayer, updateTextLayer }; +export { TextLayer }; diff --git a/src/license_header.js b/src/license_header.js index 22d9d8a176bcb..c113cdc898a84 100644 --- a/src/license_header.js +++ b/src/license_header.js @@ -1,4 +1,4 @@ -/* Copyright 2023 Mozilla Foundation +/* Copyright 2024 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/license_header_libre.js b/src/license_header_libre.js index 4832396898ef3..90a33e6980cb8 100644 --- a/src/license_header_libre.js +++ b/src/license_header_libre.js @@ -2,7 +2,7 @@ * @licstart The following is the entire license notice for the * JavaScript code in this page * - * Copyright 2023 Mozilla Foundation + * Copyright 2024 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/pdf.js b/src/pdf.js index cb24267617494..064ae6c61f802 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -63,11 +63,6 @@ import { RenderingCancelledException, setLayerDimensions, } from "./display/display_utils.js"; -import { - renderTextLayer, - TextLayer, - updateTextLayer, -} from "./display/text_layer.js"; import { AnnotationEditorLayer } from "./display/editor/annotation_editor_layer.js"; import { AnnotationEditorUIManager } from "./display/editor/tools.js"; import { AnnotationLayer } from "./display/annotation_layer.js"; @@ -75,6 +70,7 @@ import { ColorPicker } from "./display/editor/color_picker.js"; import { DrawLayer } from "./display/draw_layer.js"; import { GlobalWorkerOptions } from "./display/worker_options.js"; import { Outliner } from "./display/editor/outliner.js"; +import { TextLayer } from "./display/text_layer.js"; import { XfaLayer } from "./display/xfa_layer.js"; /* eslint-disable-next-line no-unused-vars */ @@ -84,6 +80,12 @@ const pdfjsVersion = const pdfjsBuild = typeof PDFJSDev !== "undefined" ? PDFJSDev.eval("BUNDLE_BUILD") : void 0; +if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { + globalThis.pdfjsTestingUtils = { + Outliner, + }; +} + export { AbortException, AnnotationEditorLayer, @@ -113,7 +115,6 @@ export { noContextMenu, normalizeUnicode, OPS, - Outliner, PasswordResponses, PDFDataRangeTransport, PDFDateString, @@ -121,12 +122,10 @@ export { PermissionFlag, PixelsPerInch, RenderingCancelledException, - renderTextLayer, setLayerDimensions, shadow, TextLayer, UnexpectedResponseException, - updateTextLayer, Util, VerbosityLevel, version, diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index bbc4c4d9f16cb..a9cbe90d8d7a8 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -96,10 +96,11 @@ class Doc extends PDFObject { this._zoom = data.zoom || 100; this._actions = createActionsMap(data.actions); this._globalEval = data.globalEval; - this._pageActions = new Map(); + this._pageActions = null; this._userActivation = false; this._disablePrinting = false; this._disableSaving = false; + this._otherPageActions = null; } _initActions() { @@ -155,16 +156,19 @@ class Doc extends PDFObject { _dispatchPageEvent(name, actions, pageNumber) { if (name === "PageOpen") { + this._pageActions ||= new Map(); if (!this._pageActions.has(pageNumber)) { this._pageActions.set(pageNumber, createActionsMap(actions)); } this._pageNum = pageNumber - 1; } - actions = this._pageActions.get(pageNumber)?.get(name); - if (actions) { - for (const action of actions) { - this._globalEval(action); + for (const acts of [this._pageActions, this._otherPageActions]) { + actions = acts?.get(pageNumber)?.get(name); + if (actions) { + for (const action of actions) { + this._globalEval(action); + } } } } @@ -182,6 +186,34 @@ class Doc extends PDFObject { this._fields.set(name, field); this._fieldNames.push(name); this._numFields++; + + // Fields on a page can have PageOpen/PageClose actions. + const po = field.obj._actions.get("PageOpen"); + const pc = field.obj._actions.get("PageClose"); + if (po || pc) { + this._otherPageActions ||= new Map(); + let actions = this._otherPageActions.get(field.obj._page + 1); + if (!actions) { + actions = new Map(); + this._otherPageActions.set(field.obj._page + 1, actions); + } + if (po) { + let poActions = actions.get("PageOpen"); + if (!poActions) { + poActions = []; + actions.set("PageOpen", poActions); + } + poActions.push(...po); + } + if (pc) { + let pcActions = actions.get("PageClose"); + if (!pcActions) { + pcActions = []; + actions.set("PageClose", pcActions); + } + pcActions.push(...pc); + } + } } _getDate(date) { diff --git a/src/shared/util.js b/src/shared/util.js index 68d12cd4cd21c..292a66f52913e 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -41,10 +41,12 @@ const BASELINE_FACTOR = LINE_DESCENT_FACTOR / LINE_FACTOR; * how these flags are being used: * - ANY, DISPLAY, and PRINT are the normal rendering intents, note the * `PDFPageProxy.{render, getOperatorList, getAnnotations}`-methods. + * - SAVE is used, on the worker-thread, when saving modified annotations. * - ANNOTATIONS_FORMS, ANNOTATIONS_STORAGE, ANNOTATIONS_DISABLE control which * annotations are rendered onto the canvas (i.e. by being included in the * operatorList), note the `PDFPageProxy.{render, getOperatorList}`-methods * and their `annotationMode`-option. + * - IS_EDITING is used when editing is active in the viewer. * - OPLIST is used with the `PDFPageProxy.getOperatorList`-method, note the * `OperatorList`-constructor (on the worker-thread). */ @@ -56,6 +58,7 @@ const RenderingIntentFlag = { ANNOTATIONS_FORMS: 0x10, ANNOTATIONS_STORAGE: 0x20, ANNOTATIONS_DISABLE: 0x40, + IS_EDITING: 0x80, OPLIST: 0x100, }; diff --git a/test/driver.js b/test/driver.js index aaeb20d580dd1..8b26975f92aea 100644 --- a/test/driver.js +++ b/test/driver.js @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals pdfjsLib, pdfjsViewer */ +/* globals pdfjsLib, pdfjsTestingUtils, pdfjsViewer */ const { AnnotationLayer, @@ -20,12 +20,12 @@ const { DrawLayer, getDocument, GlobalWorkerOptions, - Outliner, PixelsPerInch, shadow, TextLayer, XfaLayer, } = pdfjsLib; +const { Outliner } = pdfjsTestingUtils; const { GenericL10n, parseQueryString, SimpleLinkService } = pdfjsViewer; const WAITING_TIME = 100; // ms diff --git a/test/integration-boot.mjs b/test/integration-boot.mjs index f38121e75de62..4e04bb6f1f5d6 100644 --- a/test/integration-boot.mjs +++ b/test/integration-boot.mjs @@ -44,6 +44,7 @@ async function runTests(results) { jasmineDone(suiteInfo) {}, jasmineStarted(suiteInfo) {}, specDone(result) { + // Report on the result of individual tests. ++results.runs; if (result.failedExpectations.length > 0) { ++results.failures; @@ -53,8 +54,20 @@ async function runTests(results) { } }, specStarted(result) {}, - suiteDone(result) {}, - suiteStarted(result) {}, + suiteDone(result) { + // Report on the result of `afterAll` invocations. + if (result.failedExpectations.length > 0) { + ++results.failures; + console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`); + } + }, + suiteStarted(result) { + // Report on the result of `beforeAll` invocations. + if (result.failedExpectations.length > 0) { + ++results.failures; + console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`); + } + }, }); return jasmine.execute(); diff --git a/test/integration/annotation_spec.mjs b/test/integration/annotation_spec.mjs index 579ee2d0ce1f9..a15a777e8a78d 100644 --- a/test/integration/annotation_spec.mjs +++ b/test/integration/annotation_spec.mjs @@ -503,6 +503,14 @@ describe("ResetForm action", () => { it("must check that the Ink annotation has a popup", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + if (browserName) { + // TODO + pending( + "Re-enable this test when the Ink annotation has been made editable." + ); + return; + } + await page.waitForFunction( `document.querySelector("[data-annotation-id='25R']").hidden === false` ); diff --git a/test/integration/copy_paste_spec.mjs b/test/integration/copy_paste_spec.mjs index 6c18929d913a0..cb5ae3ab9b906 100644 --- a/test/integration/copy_paste_spec.mjs +++ b/test/integration/copy_paste_spec.mjs @@ -15,7 +15,7 @@ import { closePages, - kbCopy, + copy, kbSelectAll, loadAndWait, mockClipboard, @@ -23,9 +23,11 @@ import { } from "./test_utils.mjs"; const selectAll = async page => { - const promise = waitForEvent(page, "selectionchange"); - await kbSelectAll(page); - await promise; + await waitForEvent({ + page, + eventName: "selectionchange", + action: () => kbSelectAll(page), + }); await page.waitForFunction(() => { const selection = document.getSelection(); @@ -55,10 +57,7 @@ describe("Copy and paste", () => { ); await selectAll(page); - const promise = waitForEvent(page, "copy"); - await kbCopy(page); - await promise; - + await copy(page); await page.waitForFunction( `document.querySelector('#viewerContainer').style.cursor !== "wait"` ); @@ -159,10 +158,7 @@ describe("Copy and paste", () => { ); await selectAll(page); - const promise = waitForEvent(page, "copy"); - await kbCopy(page); - await promise; - + await copy(page); await page.waitForFunction( `document.querySelector('#viewerContainer').style.cursor !== "wait"` ); diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index d7afe96ab52c4..d03df9fa69feb 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -16,6 +16,8 @@ import { awaitPromise, closePages, + copy, + copyToClipboard, createPromise, dragAndDropAnnotation, firstPageOnTop, @@ -30,21 +32,20 @@ import { kbBigMoveLeft, kbBigMoveRight, kbBigMoveUp, - kbCopy, kbGoToBegin, kbGoToEnd, kbModifierDown, kbModifierUp, - kbPaste, kbRedo, kbSelectAll, kbUndo, loadAndWait, + paste, pasteFromClipboard, scrollIntoView, switchToEditor, waitForAnnotationEditorLayer, - waitForEvent, + waitForAnnotationModeChanged, waitForSelectedEditor, waitForSerialized, waitForStorageEntries, @@ -53,16 +54,6 @@ import { } from "./test_utils.mjs"; import { PNG } from "pngjs"; -const copyPaste = async page => { - let promise = waitForEvent(page, "copy"); - await kbCopy(page); - await promise; - - promise = waitForEvent(page, "paste"); - await kbPaste(page); - await promise; -}; - const selectAll = async page => { await kbSelectAll(page); await page.waitForFunction( @@ -187,7 +178,8 @@ describe("FreeText Editor", () => { ); await waitForSelectedEditor(page, getEditorSelector(0)); - await copyPaste(page); + await copy(page); + await paste(page); await page.waitForSelector(getEditorSelector(1), { visible: true, }); @@ -203,7 +195,8 @@ describe("FreeText Editor", () => { expect(pastedContent).withContext(`In ${browserName}`).toEqual(content); - await copyPaste(page); + await copy(page); + await paste(page); await page.waitForSelector(getEditorSelector(2), { visible: true, }); @@ -263,7 +256,8 @@ describe("FreeText Editor", () => { ); await waitForSelectedEditor(page, getEditorSelector(3)); - await copyPaste(page); + await copy(page); + await paste(page); await page.waitForSelector(getEditorSelector(4), { visible: true, }); @@ -276,9 +270,7 @@ describe("FreeText Editor", () => { ); for (let i = 0; i < 2; i++) { - const promise = waitForEvent(page, "paste"); - await kbPaste(page); - await promise; + await paste(page); await page.waitForSelector(getEditorSelector(5 + i)); } @@ -597,7 +589,8 @@ describe("FreeText Editor", () => { .withContext(`In ${browserName}`) .toEqual([0, 1, 3]); - await copyPaste(page); + await copy(page); + await paste(page); await page.waitForSelector(getEditorSelector(6), { visible: true, }); @@ -995,6 +988,29 @@ describe("FreeText Editor", () => { pages.map(async ([browserName, page]) => { await switchToFreeText(page); + const isEditorWhite = editorRect => + page.evaluate(rect => { + const canvas = document.querySelector(".canvasWrapper canvas"); + const ctx = canvas.getContext("2d"); + rect ||= { + x: 0, + y: 0, + width: canvas.width, + height: canvas.height, + }; + const { data } = ctx.getImageData( + rect.x, + rect.y, + rect.width, + rect.height + ); + return data.every(x => x === 0xff); + }, editorRect); + + // The page has been re-rendered but with no freetext annotations. + let isWhite = await isEditorWhite(); + expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); + let editorIds = await getEditors(page, "freeText"); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6); @@ -1049,11 +1065,9 @@ describe("FreeText Editor", () => { // canvas. editorIds = await getEditors(page, "freeText"); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1); - const hidden = await page.$eval( - "[data-annotation-id='26R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeTrue(); + + isWhite = await isEditorWhite(editorRect); + expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); // Check we've now a div containing the text. const newDivText = await page.$eval( @@ -1275,7 +1289,8 @@ describe("FreeText Editor", () => { ); await waitForSelectedEditor(page, getEditorSelector(1)); - await copyPaste(page); + await copy(page); + await paste(page); await page.waitForSelector(getEditorSelector(6), { visible: true, }); @@ -1295,10 +1310,12 @@ describe("FreeText Editor", () => { await closePages(pages); }); - it("must move an annotation", async () => { + it("must edit an annotation", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + const modeChangedHandle = await waitForAnnotationModeChanged(page); await page.click("[data-annotation-id='26R']", { count: 2 }); + await awaitPromise(modeChangedHandle); await page.waitForSelector(`${getEditorSelector(0)}-editor`); const [focusedId, editable] = await page.evaluate(() => { @@ -1354,6 +1371,7 @@ describe("FreeText Editor", () => { // TODO: remove this when we switch to BiDi. await hover(page, "[data-annotation-id='23R']"); + // Wait for the popup to be displayed. await page.waitForFunction( () => @@ -1595,12 +1613,6 @@ describe("FreeText Editor", () => { it("must open an existing annotation and check that the position are good", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await switchToFreeText(page); - - await page.evaluate(() => { - document.getElementById("editorFreeTextParamsToolbar").remove(); - }); - const toBinary = buf => { for (let i = 0; i < buf.length; i += 4) { const gray = @@ -1653,8 +1665,12 @@ describe("FreeText Editor", () => { return null; }; - for (const n of [0, 1, 2, 3, 4]) { - const rect = await getRect(page, getEditorSelector(n)); + const firstPixelsAnnotations = new Map(); + + // [26, 32, ...] are the annotation ids + for (const n of [26, 32, 42, 57, 35, 1]) { + const id = `${n}R`; + const rect = await getRect(page, `[data-annotation-id="${id}"]`); const editorPng = await page.screenshot({ clip: rect, type: "png", @@ -1665,33 +1681,33 @@ describe("FreeText Editor", () => { editorImage.width, editorImage.height ); + firstPixelsAnnotations.set(id, { editorFirstPix, rect }); + } + + await switchToFreeText(page); + await page.evaluate(() => { + document.getElementById("editorFreeTextParamsToolbar").remove(); + }); + + for (const n of [0, 1, 2, 3, 4]) { const annotationId = await page.evaluate(N => { const editor = document.getElementById( `pdfjs_internal_editor_${N}` ); - const annId = editor.getAttribute("annotation-id"); - const annotation = document.querySelector( - `[data-annotation-id="${annId}"]` - ); - editor.hidden = true; - annotation.hidden = false; - return annId; + return editor.getAttribute("annotation-id"); }, n); - await page.waitForSelector(`${getEditorSelector(n)}[hidden]`); - await page.waitForSelector( - `[data-annotation-id="${annotationId}"]:not([hidden])` - ); - - const annotationPng = await page.screenshot({ + const { editorFirstPix: annotationFirstPix, rect } = + firstPixelsAnnotations.get(annotationId); + const editorPng = await page.screenshot({ clip: rect, type: "png", }); - const annotationImage = PNG.sync.read(annotationPng); - const annotationFirstPix = getFirstPixel( - annotationImage.data, - annotationImage.width, - annotationImage.height + const editorImage = PNG.sync.read(editorPng); + const editorFirstPix = getFirstPixel( + editorImage.data, + editorImage.width, + editorImage.height ); expect( @@ -1726,12 +1742,6 @@ describe("FreeText Editor", () => { it("must open an existing rotated annotation and check that the position are good", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await switchToFreeText(page); - - await page.evaluate(() => { - document.getElementById("editorFreeTextParamsToolbar").remove(); - }); - const toBinary = buf => { for (let i = 0; i < buf.length; i += 4) { const gray = @@ -1813,13 +1823,15 @@ describe("FreeText Editor", () => { return null; }; + const firstPixelsAnnotations = new Map(); for (const [n, start] of [ - [0, "BL"], - [1, "BR"], - [2, "TR"], - [3, "TL"], + [17, "BL"], + [18, "BR"], + [19, "TR"], + [20, "TL"], ]) { - const rect = await getRect(page, getEditorSelector(n)); + const id = `${n}R`; + const rect = await getRect(page, `[data-annotation-id="${id}"]`); const editorPng = await page.screenshot({ clip: rect, type: "png", @@ -1831,33 +1843,38 @@ describe("FreeText Editor", () => { editorImage.height, start ); + firstPixelsAnnotations.set(id, { editorFirstPix, rect }); + } + + await switchToFreeText(page); + await page.evaluate(() => { + document.getElementById("editorFreeTextParamsToolbar").remove(); + }); + + for (const [n, start] of [ + [0, "BL"], + [1, "BR"], + [2, "TR"], + [3, "TL"], + ]) { const annotationId = await page.evaluate(N => { const editor = document.getElementById( `pdfjs_internal_editor_${N}` ); - const annId = editor.getAttribute("annotation-id"); - const annotation = document.querySelector( - `[data-annotation-id="${annId}"]` - ); - editor.hidden = true; - annotation.hidden = false; - return annId; + return editor.getAttribute("annotation-id"); }, n); - await page.waitForSelector(`${getEditorSelector(n)}[hidden]`); - await page.waitForSelector( - `[data-annotation-id="${annotationId}"]:not([hidden])` - ); - - const annotationPng = await page.screenshot({ + const { editorFirstPix: annotationFirstPix, rect } = + firstPixelsAnnotations.get(annotationId); + const editorPng = await page.screenshot({ clip: rect, type: "png", }); - const annotationImage = PNG.sync.read(annotationPng); - const annotationFirstPix = getFirstPixel( - annotationImage.data, - annotationImage.width, - annotationImage.height, + const editorImage = PNG.sync.read(editorPng); + const editorFirstPix = getFirstPixel( + editorImage.data, + editorImage.width, + editorImage.height, start ); @@ -2256,6 +2273,7 @@ describe("FreeText Editor", () => { rect = await getRect(page, getEditorSelector(0)); + // Create a new editor. await page.mouse.click( rect.x + 5 * rect.width, rect.y + 5 * rect.height @@ -2271,11 +2289,12 @@ describe("FreeText Editor", () => { `${getEditorSelector(1)} .overlay.enabled` ); - rect = await getRect(page, getEditorSelector(0)); + // Select the second editor. + rect = await getRect(page, getEditorSelector(1)); await page.mouse.click( - rect.x + 5 * rect.width, - rect.y + 5 * rect.height + rect.x + 0.5 * rect.width, + rect.y + 0.5 * rect.height ); await waitForSelectedEditor(page, getEditorSelector(1)); @@ -3376,149 +3395,129 @@ describe("FreeText Editor", () => { }); it("must check that pasting html just keep the text", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - await switchToFreeText(page); - - const rect = await getRect(page, ".annotationEditorLayer"); + // Run sequentially to avoid clipboard issues. + for (const [browserName, page] of pages) { + await switchToFreeText(page); - let editorSelector = getEditorSelector(0); - const data = "Hello PDF.js World !!"; - await page.mouse.click(rect.x + 100, rect.y + 100); - await page.waitForSelector(editorSelector, { - visible: true, - }); - await page.type(`${editorSelector} .internal`, data); - const editorRect = await getRect(page, editorSelector); + const rect = await getRect(page, ".annotationEditorLayer"); - // Commit. - await page.keyboard.press("Escape"); - await page.waitForSelector(`${editorSelector} .overlay.enabled`); + let editorSelector = getEditorSelector(0); + const data = "Hello PDF.js World !!"; + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.waitForSelector(editorSelector, { + visible: true, + }); + await page.type(`${editorSelector} .internal`, data); + const editorRect = await getRect(page, editorSelector); - const waitForTextChange = (previous, edSelector) => - page.waitForFunction( - (prev, sel) => document.querySelector(sel).innerText !== prev, - {}, - previous, - `${edSelector} .internal` - ); - const getText = edSelector => - page.$eval(`${edSelector} .internal`, el => el.innerText.trimEnd()); + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector(`${editorSelector} .overlay.enabled`); - await page.mouse.click( - editorRect.x + editorRect.width / 2, - editorRect.y + editorRect.height / 2, - { count: 2 } - ); - await page.waitForSelector( - `${editorSelector} .overlay:not(.enabled)` + const waitForTextChange = (previous, edSelector) => + page.waitForFunction( + (prev, sel) => document.querySelector(sel).innerText !== prev, + {}, + previous, + `${edSelector} .internal` ); + const getText = edSelector => + page.$eval(`${edSelector} .internal`, el => el.innerText.trimEnd()); - const select = position => - page.evaluate( - (sel, pos) => { - const el = document.querySelector(sel); - document.getSelection().setPosition(el.firstChild, pos); - }, - `${editorSelector} .internal`, - position - ); + await page.mouse.click( + editorRect.x + editorRect.width / 2, + editorRect.y + editorRect.height / 2, + { count: 2 } + ); + await page.waitForSelector(`${editorSelector} .overlay:not(.enabled)`); - await select(0); - await pasteFromClipboard( - page, - { - "text/html": "Bold Foo", - "text/plain": "Foo", + const select = position => + page.evaluate( + (sel, pos) => { + const el = document.querySelector(sel); + document.getSelection().setPosition(el.firstChild, pos); }, - `${editorSelector} .internal` + `${editorSelector} .internal`, + position ); - let lastText = data; + await select(0); + await copyToClipboard(page, { + "text/html": "Bold Foo", + "text/plain": "Foo", + }); + await pasteFromClipboard(page, `${editorSelector} .internal`); - await waitForTextChange(lastText, editorSelector); - let text = await getText(editorSelector); - lastText = `Foo${data}`; - expect(text).withContext(`In ${browserName}`).toEqual(lastText); + let lastText = data; - await select(3); - await pasteFromClipboard( - page, - { - "text/html": "Bold Bar
Oof", - "text/plain": "Bar\nOof", - }, - `${editorSelector} .internal` - ); + await waitForTextChange(lastText, editorSelector); + let text = await getText(editorSelector); + lastText = `Foo${data}`; + expect(text).withContext(`In ${browserName}`).toEqual(lastText); - await waitForTextChange(lastText, editorSelector); - text = await getText(editorSelector); - lastText = `FooBar\nOof${data}`; - expect(text).withContext(`In ${browserName}`).toEqual(lastText); + await select(3); + await copyToClipboard(page, { + "text/html": "Bold Bar
Oof", + "text/plain": "Bar\nOof", + }); + await pasteFromClipboard(page, `${editorSelector} .internal`); - await select(0); - await pasteFromClipboard( - page, - { - "text/html": "basic html", - }, - `${editorSelector} .internal` - ); + await waitForTextChange(lastText, editorSelector); + text = await getText(editorSelector); + lastText = `FooBar\nOof${data}`; + expect(text).withContext(`In ${browserName}`).toEqual(lastText); - // Nothing should change, so it's hard to wait on something. - // eslint-disable-next-line no-restricted-syntax - await waitForTimeout(100); + await select(0); + await copyToClipboard(page, { "text/html": "basic html" }); + await pasteFromClipboard(page, `${editorSelector} .internal`); - text = await getText(editorSelector); - expect(text).withContext(`In ${browserName}`).toEqual(lastText); + // Nothing should change, so it's hard to wait on something. + // eslint-disable-next-line no-restricted-syntax + await waitForTimeout(100); - const getHTML = () => - page.$eval(`${editorSelector} .internal`, el => el.innerHTML); - const prevHTML = await getHTML(); + text = await getText(editorSelector); + expect(text).withContext(`In ${browserName}`).toEqual(lastText); - // Try to paste an image. - await pasteFromClipboard( - page, - { - "image/png": - // 1x1 transparent png. - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", - }, - `${editorSelector} .internal` - ); + const getHTML = () => + page.$eval(`${editorSelector} .internal`, el => el.innerHTML); + const prevHTML = await getHTML(); - // Nothing should change, so it's hard to wait on something. - // eslint-disable-next-line no-restricted-syntax - await waitForTimeout(100); + // Try to paste an image. + await copyToClipboard(page, { + "image/png": + // 1x1 transparent png. + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + }); + await pasteFromClipboard(page, `${editorSelector} .internal`); - const html = await getHTML(); - expect(html).withContext(`In ${browserName}`).toEqual(prevHTML); + // Nothing should change, so it's hard to wait on something. + // eslint-disable-next-line no-restricted-syntax + await waitForTimeout(100); - // Commit. - await page.keyboard.press("Escape"); - await page.waitForSelector(`${editorSelector} .overlay.enabled`); + const html = await getHTML(); + expect(html).withContext(`In ${browserName}`).toEqual(prevHTML); - editorSelector = getEditorSelector(1); - await page.mouse.click(rect.x + 200, rect.y + 200); - await page.waitForSelector(editorSelector, { - visible: true, - }); + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector(`${editorSelector} .overlay.enabled`); - const fooBar = "Foo\nBar\nOof"; - await pasteFromClipboard( - page, - { - "text/html": "html", - "text/plain": fooBar, - }, - `${editorSelector} .internal` - ); + editorSelector = getEditorSelector(1); + await page.mouse.click(rect.x + 200, rect.y + 200); + await page.waitForSelector(editorSelector, { + visible: true, + }); - await waitForTextChange("", editorSelector); - text = await getText(editorSelector); - expect(text).withContext(`In ${browserName}`).toEqual(fooBar); - }) - ); + const fooBar = "Foo\nBar\nOof"; + await copyToClipboard(page, { + "text/html": "html", + "text/plain": fooBar, + }); + await pasteFromClipboard(page, `${editorSelector} .internal`); + + await waitForTextChange("", editorSelector); + text = await getText(editorSelector); + expect(text).withContext(`In ${browserName}`).toEqual(fooBar); + } }); }); @@ -3579,13 +3578,6 @@ describe("FreeText Editor", () => { ); } - await page.waitForSelector("[data-annotation-id='998R'] canvas"); - let hidden = await page.$eval( - "[data-annotation-id='998R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeTrue(); - // Check we've now a div containing the text. await page.waitForSelector( "[data-annotation-id='998R'] div.annotationContent" @@ -3598,6 +3590,24 @@ describe("FreeText Editor", () => { .withContext(`In ${browserName}`) .toEqual("Hello World and edited in Firefox"); + // Check that the canvas has nothing drawn at the annotation position. + await page.$eval( + "[data-annotation-id='998R']", + el => (el.hidden = true) + ); + let editorPng = await page.screenshot({ + clip: editorRect, + type: "png", + }); + await page.$eval( + "[data-annotation-id='998R']", + el => (el.hidden = false) + ); + let editorImage = PNG.sync.read(editorPng); + expect(editorImage.data.every(x => x === 0xff)) + .withContext(`In ${browserName}`) + .toBeTrue(); + const oneToThirteen = Array.from(new Array(13).keys(), n => n + 2); for (const pageNumber of oneToThirteen) { await scrollIntoView( @@ -3614,6 +3624,19 @@ describe("FreeText Editor", () => { await switchToFreeText(page, /* disable = */ true); const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n); + const handlePromise = await createPromise(page, resolve => { + const callback = e => { + if (e.source.id === 1) { + window.PDFViewerApplication.eventBus.off( + "pagerendered", + callback + ); + resolve(); + } + }; + window.PDFViewerApplication.eventBus.on("pagerendered", callback); + }); + for (const pageNumber of thirteenToOne) { await scrollIntoView( page, @@ -3621,12 +3644,16 @@ describe("FreeText Editor", () => { ); } - await page.waitForSelector("[data-annotation-id='998R'] canvas"); - hidden = await page.$eval( - "[data-annotation-id='998R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeFalse(); + await awaitPromise(handlePromise); + + editorPng = await page.screenshot({ + clip: editorRect, + type: "png", + }); + editorImage = PNG.sync.read(editorPng); + expect(editorImage.data.every(x => x === 0xff)) + .withContext(`In ${browserName}`) + .toBeFalse(); }) ); }); diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index a3c6c6d084519..c61663e0330f4 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -30,6 +30,7 @@ import { kbUndo, loadAndWait, scrollIntoView, + setCaretAt, switchToEditor, waitForSerialized, } from "./test_utils.mjs"; @@ -1043,19 +1044,12 @@ describe("Highlight Editor", () => { `${getEditorSelector(0)}:not(.selectedEditor)` ); - await page.evaluate(() => { - const text = - "Dynamic languages such as JavaScript are more difficult to com-"; - for (const el of document.querySelectorAll( - `.page[data-page-number="${1}"] > .textLayer > span` - )) { - if (el.textContent === text) { - window.getSelection().setPosition(el.firstChild, 1); - break; - } - } - }); - + await setCaretAt( + page, + 1, + "Dynamic languages such as JavaScript are more difficult to com-", + 1 + ); await page.keyboard.press("ArrowUp"); const [text, offset] = await page.evaluate(() => { const selection = window.getSelection(); @@ -1073,19 +1067,12 @@ describe("Highlight Editor", () => { pages.map(async ([browserName, page]) => { await switchToHighlight(page); - await page.evaluate(() => { - const text = - "Dynamic languages such as JavaScript are more difficult to com-"; - for (const el of document.querySelectorAll( - `.page[data-page-number="${1}"] > .textLayer > span` - )) { - if (el.textContent === text) { - window.getSelection().setPosition(el.firstChild, 15); - break; - } - } - }); - + await setCaretAt( + page, + 1, + "Dynamic languages such as JavaScript are more difficult to com-", + 15 + ); await page.keyboard.down("Shift"); await page.keyboard.press("ArrowDown"); await page.keyboard.up("Shift"); @@ -1302,6 +1289,41 @@ describe("Highlight Editor", () => { }); }); + describe("Quadpoints must be correct when they're in a translated page", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue18360.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that the quadpoints for an highlight are almost correct", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Hello World"); + await page.mouse.click( + rect.x + rect.width / 4, + rect.y + rect.height / 2, + { count: 2, delay: 100 } + ); + + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + const quadPoints = await getFirstSerialized(page, e => e.quadPoints); + const expected = [148, 624, 176, 624, 148, 637, 176, 637]; + expect(quadPoints.every((x, i) => Math.abs(x - expected[i]) <= 5)) + .withContext(`In ${browserName} (got ${quadPoints})`) + .toBeTrue(); + }) + ); + }); + }); + describe("Editor must be unselected when the color picker is Escaped", () => { let pages; @@ -1667,4 +1689,247 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Use a toolbar overlapping an other highlight", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + ".annotationEditorLayer", + null, + null, + { + highlightEditorColors: + "yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0000", + } + ); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that the toolbar is usable", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + await setCaretAt( + page, + 1, + "Dynamic languages such as JavaScript are more difficult to com-", + 0 + ); + await page.keyboard.down("Shift"); + for (let i = 0; i < 3; i++) { + await page.keyboard.press("ArrowDown"); + } + await page.keyboard.up("Shift"); + + const editorSelector = getEditorSelector(0); + await page.waitForSelector(editorSelector); + + await setCaretAt( + page, + 1, + "handle all possible type combinations at runtime. We present an al-", + 0 + ); + await page.keyboard.down("Shift"); + for (let i = 0; i < 3; i++) { + await page.keyboard.press("ArrowDown"); + } + await page.keyboard.up("Shift"); + await page.waitForSelector(getEditorSelector(1)); + + const rect = await getRect(page, editorSelector); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y); + + await page.waitForSelector( + `${editorSelector} .editToolbar button.colorPicker` + ); + + await page.click(`${editorSelector} .editToolbar button.colorPicker`); + await page.waitForSelector( + `${editorSelector} .editToolbar button[title = "Green"]` + ); + await page.click( + `${editorSelector} .editToolbar button[title = "Green"]` + ); + await page.waitForSelector( + `.page[data-page-number = "1"] svg.highlight[fill = "#00FF00"]` + ); + }) + ); + }); + }); + + describe("Draw a free highlight with the pointer hovering an existing highlight", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that an existing highlight is ignored on hovering", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const editorSelector = getEditorSelector(0); + const x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + await page.keyboard.press("Escape"); + await page.waitForSelector(`${editorSelector}:not(.selectedEditor)`); + + const counterHandle = await page.evaluateHandle(sel => { + const el = document.querySelector(sel); + const counter = { count: 0 }; + el.addEventListener( + "pointerover", + () => { + counter.count += 1; + }, + { capture: true } + ); + return counter; + }, editorSelector); + + const clickHandle = await waitForPointerUp(page); + y = rect.y - rect.height; + await page.mouse.move(x, y); + await page.mouse.down(); + for ( + const endY = rect.y + 2 * rect.height; + y <= endY; + y += rect.height / 10 + ) { + await page.mouse.move(x, y); + } + await page.mouse.up(); + await awaitPromise(clickHandle); + + const { count } = await counterHandle.jsonValue(); + expect(count).withContext(`In ${browserName}`).toEqual(0); + }) + ); + }); + }); + + describe("Select text with the pointer hovering an existing highlight", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that an existing highlight is ignored on hovering", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText( + page, + 1, + "ternative compilation technique for dynamically-typed languages" + ); + const editorSelector = getEditorSelector(0); + const x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 3, delay: 100 }); + await page.waitForSelector(editorSelector); + await waitForSerialized(page, 1); + await page.keyboard.press("Escape"); + await page.waitForSelector(`${editorSelector}:not(.selectedEditor)`); + + const counterHandle = await page.evaluateHandle(sel => { + const el = document.querySelector(sel); + const counter = { count: 0 }; + el.addEventListener( + "pointerover", + () => { + counter.count += 1; + }, + { capture: true } + ); + return counter; + }, editorSelector); + + const clickHandle = await waitForPointerUp(page); + y = rect.y - 3 * rect.height; + await page.mouse.move(x, y); + await page.mouse.down(); + for ( + const endY = rect.y + 3 * rect.height; + y <= endY; + y += rect.height / 10 + ) { + await page.mouse.move(x, y); + } + await page.mouse.up(); + await awaitPromise(clickHandle); + + const { count } = await counterHandle.jsonValue(); + expect(count).withContext(`In ${browserName}`).toEqual(0); + }) + ); + }); + }); + + describe("Highlight with the floating button in a pdf containing a FreeText", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "file_pdfjs_test.pdf", + ".annotationEditorLayer", + null, + null, + { highlightEditorColors: "red=#AB0000" } + ); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that the highlight is created", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const rect = await getSpanRectFromText(page, 1, "In production"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 3, delay: 100 }); + + await page.waitForSelector(".textLayer .highlightButton"); + await page.click(".textLayer .highlightButton"); + + await page.waitForSelector(getEditorSelector(0)); + const usedColor = await page.evaluate(() => { + const highlight = document.querySelector( + `.page[data-page-number = "1"] .canvasWrapper > svg.highlight` + ); + return highlight.getAttribute("fill"); + }); + + expect(usedColor).withContext(`In ${browserName}`).toEqual("#AB0000"); + }) + ); + }); + }); }); diff --git a/test/integration/scripting_spec.mjs b/test/integration/scripting_spec.mjs index 50e2cfe961f42..f41c271b882ea 100644 --- a/test/integration/scripting_spec.mjs +++ b/test/integration/scripting_spec.mjs @@ -17,6 +17,7 @@ import { awaitPromise, clearInput, closePages, + closeSinglePage, getAnnotationStorage, getComputedStyleSelector, getFirstSerialized, @@ -31,6 +32,12 @@ import { waitForTimeout, } from "./test_utils.mjs"; +async function waitForScripting(page) { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); +} + describe("Interaction", () => { async function actAndWaitForInput(page, selector, action, clear = true) { await page.waitForSelector(selector, { @@ -60,9 +67,8 @@ describe("Interaction", () => { it("must check that first text field has focus", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); + await page.waitForFunction(`window.document.activeElement !== null`); // The document has an open action in order to give the focus to 401R. @@ -78,6 +84,8 @@ describe("Interaction", () => { it("must show a text field and then make in invisible when content is removed", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + let visibility = await page.$eval( getSelector("427R"), el => getComputedStyle(el).visibility @@ -120,6 +128,8 @@ describe("Interaction", () => { it("must format the field with 2 digits and leave field with a click", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + await page.type(getSelector("416R"), "3.14159", { delay: 200 }); await page.click(getSelector("419R")); @@ -138,6 +148,8 @@ describe("Interaction", () => { it("must format the field with 2 digits, leave field with a click and again", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + await page.type(getSelector("448R"), "61803", { delay: 200 }); await page.click(getSelector("419R")); @@ -177,6 +189,8 @@ describe("Interaction", () => { it("must format the field with 2 digits and leave field with a TAB", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const prevSum = await page.$eval(getSelector("427R"), el => el.value); await page.type(getSelector("422R"), "2.7182818", { delay: 200 }); @@ -202,6 +216,8 @@ describe("Interaction", () => { it("must format the field with 2 digits and hit ESC", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + let sum = await page.$eval(getSelector("471R"), el => el.value); expect(sum).withContext(`In ${browserName}`).toEqual("4,24"); @@ -224,6 +240,8 @@ describe("Interaction", () => { it("must format the field with 2 digits on key ENTER", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const prevSum = await page.$eval(getSelector("427R"), el => el.value); await page.type(getSelector("419R"), "0.577215", { delay: 200 }); @@ -244,6 +262,8 @@ describe("Interaction", () => { it("must reset all", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + // click on a radio button await page.click("[data-annotation-id='449R']"); @@ -302,9 +322,7 @@ describe("Interaction", () => { it("must show values in a text input when clicking on radio buttons", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); const expected = [ ["81R", "Group1=Choice1::1"], @@ -330,6 +348,8 @@ describe("Interaction", () => { it("must show values in a text input when clicking on checkboxes", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const expected = [ ["85R", "Check1=Yes::5"], ["87R", "Check2=Yes::6"], @@ -358,6 +378,8 @@ describe("Interaction", () => { it("must show values in a text input when clicking on checkboxes in a group", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const expected = [ ["90R", "Check5=Yes1::9"], ["91R", "Check5=Yes2::10"], @@ -383,6 +405,8 @@ describe("Interaction", () => { it("must show values in a text input when clicking on checkboxes or radio with no actions", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const expected = [ ["", "Off;Off"], ["94R", "Yes;Off"], @@ -418,41 +442,35 @@ describe("Interaction", () => { pages = await loadAndWait("doc_actions.pdf", getSelector("47R")); }); - afterAll(async () => { - await closePages(pages); - }); - it("must execute WillPrint and DidPrint actions", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - if (process.platform === "win32" && browserName === "firefox") { - pending("Disabled in Firefox on Windows, because of bug 1662471."); - } - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); - - await clearInput(page, getSelector("47R")); - await page.evaluate(_ => { - window.document.activeElement.blur(); - }); - await page.waitForFunction(`${getQuerySelector("47R")}.value === ""`); + // Run the tests sequentially to avoid to use the same printer at the same + // time. + // And to make sure that a printer isn't locked by a process we close the + // page before running the next test. + for (const [browserName, page] of pages) { + await waitForScripting(page); - let text = await actAndWaitForInput( - page, - getSelector("47R"), - async () => { - await page.click("#print"); - } - ); - expect(text).withContext(`In ${browserName}`).toEqual("WillPrint"); + await clearInput(page, getSelector("47R")); + await page.evaluate(_ => { + window.document.activeElement.blur(); + }); + await page.waitForFunction(`${getQuerySelector("47R")}.value === ""`); - await page.waitForFunction(`${getQuerySelector("50R")}.value !== ""`); + const text = await actAndWaitForInput( + page, + getSelector("47R"), + async () => { + await page.click("#print"); + } + ); + expect(text).withContext(`In ${browserName}`).toEqual("WillPrint"); + await page.keyboard.press("Escape"); - text = await page.$eval(getSelector("50R"), el => el.value); - expect(text).withContext(`In ${browserName}`).toEqual("DidPrint"); - }) - ); + await page.waitForFunction( + `${getQuerySelector("50R")}.value === "DidPrint"` + ); + await closeSinglePage(page); + } }); }); @@ -470,9 +488,7 @@ describe("Interaction", () => { it("must execute WillSave and DidSave actions", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); try { // Disable download in chrome @@ -519,9 +535,7 @@ describe("Interaction", () => { it("must execute PageOpen and PageClose actions", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.waitForFunction(`${getQuerySelector("47R")}.value !== ""`); @@ -578,6 +592,8 @@ describe("Interaction", () => { it("must print authors in a text field", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const text = await actAndWaitForInput( page, getSelector("25R"), @@ -607,6 +623,8 @@ describe("Interaction", () => { it("must print selected value in a text field", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + for (const num of [7, 6, 4, 3, 2, 1]) { await clearInput(page, getSelector("33R")); await page.click(`option[value=Export${num}]`); @@ -625,6 +643,8 @@ describe("Interaction", () => { it("must clear and restore list elements", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + // Click on ClearItems button. await page.click("[data-annotation-id='34R']"); await page.waitForFunction( @@ -655,6 +675,8 @@ describe("Interaction", () => { it("must insert new elements", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + let len = 6; for (const num of [1, 3, 5, 6, 431, -1, 0]) { ++len; @@ -693,6 +715,8 @@ describe("Interaction", () => { it("must delete some element", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + let len = 6; // Click on Restore button. await clearInput(page, getSelector("33R")); @@ -744,6 +768,8 @@ describe("Interaction", () => { it("must change colors", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + for (const [name, ref] of [ ["Text1", "34R"], ["Check1", "35R"], @@ -817,9 +843,7 @@ describe("Interaction", () => { it("must compute sum of fields", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await scrollIntoView(page, getSelector("138R")); @@ -880,9 +904,7 @@ describe("Interaction", () => { it("must check page index", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("55R")); await page.type( @@ -906,6 +928,8 @@ describe("Interaction", () => { it("must check display", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + for (const [type, vis] of [ ["hidden", "hidden"], ["noPrint", "visible"], @@ -955,9 +979,7 @@ describe("Interaction", () => { it("must update fields with the same name from JS", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("27R"), "hello"); await page.keyboard.press("Enter"); @@ -991,6 +1013,8 @@ describe("Interaction", () => { it("must print securityHandler value in a text field", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const text = await actAndWaitForInput( page, getSelector("25R"), @@ -1024,9 +1048,7 @@ describe("Interaction", () => { // Run the tests sequentially to avoid any focus issues between the two // browsers when an alert is displayed. for (const [browserName, page] of pages) { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("29R")); await clearInput(page, getSelector("30R")); @@ -1081,9 +1103,7 @@ describe("Interaction", () => { // Run the tests sequentially to avoid any focus issues between the two // browsers when an alert is displayed. for (const [browserName, page] of pages) { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("29R")); await clearInput(page, getSelector("30R")); @@ -1138,9 +1158,7 @@ describe("Interaction", () => { // Run the tests sequentially to avoid any focus issues between the two // browsers when an alert is displayed. for (const [browserName, page] of pages) { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("29R")); await clearInput(page, getSelector("30R")); @@ -1194,9 +1212,7 @@ describe("Interaction", () => { it("must convert input to uppercase", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("27R"), "Hello", { delay: 200 }); await page.waitForFunction( @@ -1260,9 +1276,7 @@ describe("Interaction", () => { it("must check that an infinite loop is not triggered", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("28R")); await page.$eval(getSelector("28R"), el => @@ -1313,9 +1327,7 @@ describe("Interaction", () => { it("must check that field value is correctly updated", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("29R"), "Hello World", { delay: 200 }); await page.click(getSelector("27R")); @@ -1354,9 +1366,7 @@ describe("Interaction", () => { it("must check that field value is correctly formatted", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("75R"), el => el.value); expect(text).withContext(`In ${browserName}`).toEqual("150.32 €"); @@ -1388,9 +1398,7 @@ describe("Interaction", () => { it("must check that a button and text field with a border are hidden", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let visibility = await page.$eval( "[data-annotation-id='35R']", @@ -1443,9 +1451,7 @@ describe("Interaction", () => { it("must check that data-main-rotation is correct", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let base = 0; @@ -1491,9 +1497,7 @@ describe("Interaction", () => { it("must check that a values is correctly updated on a field and its siblings", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await clearInput(page, getSelector("39R")); await page.type(getSelector("39R"), "123", { delay: 10 }); @@ -1538,49 +1542,33 @@ describe("Interaction", () => { it("must check that charLimit is correctly set", async () => { await Promise.all( - pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); - - await clearInput(page, getSelector("7R")); - // By default the charLimit is 0 which means that the input - // length is unlimited. - await page.type(getSelector("7R"), "abcdefghijklmnopq", { - delay: 10, - }); - - let value = await page.$eval(getSelector("7R"), el => el.value); - expect(value) - .withContext(`In ${browserName}`) - .toEqual("abcdefghijklmnopq"); - - // charLimit is set to 1 - await page.click(getSelector("9R")); + pages.map(async ([, page]) => { + await waitForScripting(page); + // The default charLimit is 0, which indicates unlimited text length. + await page.type(getSelector("7R"), "abcdefghij", { delay: 10 }); await page.waitForFunction( - `document.querySelector('${getSelector( - "7R" - )}').value !== "abcdefgh"` + `${getQuerySelector("7R")}.value === "abcdefghij"` ); - value = await page.$eval(getSelector("7R"), el => el.value); - expect(value).withContext(`In ${browserName}`).toEqual("a"); + // Increase the charLimit to 1 (this truncates the existing text). + await page.click(getSelector("9R")); + await waitForSandboxTrip(page); + await page.waitForFunction(`${getQuerySelector("7R")}.value === "a"`); await clearInput(page, getSelector("7R")); await page.type(getSelector("7R"), "xyz", { delay: 10 }); + await page.waitForFunction(`${getQuerySelector("7R")}.value === "x"`); - value = await page.$eval(getSelector("7R"), el => el.value); - expect(value).withContext(`In ${browserName}`).toEqual("x"); - - // charLimit is set to 2 + // Increase the charLimit to 2. await page.click(getSelector("9R")); + await waitForSandboxTrip(page); await clearInput(page, getSelector("7R")); await page.type(getSelector("7R"), "xyz", { delay: 10 }); - - value = await page.$eval(getSelector("7R"), el => el.value); - expect(value).withContext(`In ${browserName}`).toEqual("xy"); + await page.waitForFunction( + `${getQuerySelector("7R")}.value === "xy"` + ); }) ); }); @@ -1600,9 +1588,7 @@ describe("Interaction", () => { it("must check field value is treated by default as a number", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("30R"), "123", { delay: 10, @@ -1636,9 +1622,7 @@ describe("Interaction", () => { it("must check field value is correctly updated when committed with ENTER key", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("27R"), "abc", { delay: 10, @@ -1687,9 +1671,7 @@ describe("Interaction", () => { it("must check field value is correctly updated when committed with ENTER key", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let value = "A"; for (const [displayValue, exportValue] of [ @@ -1736,9 +1718,7 @@ describe("Interaction", () => { it("must check the field value set when the document is open", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.waitForFunction(`${getQuerySelector("27R")}.value !== ""`); @@ -1751,9 +1731,7 @@ describe("Interaction", () => { it("must check the format action is called when setFocus is used", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.type(getSelector("30R"), "abc", { delay: 200 }); await page.waitForFunction( @@ -1789,17 +1767,19 @@ describe("Interaction", () => { pages = await loadAndWait( "autoprint.pdf", "", - null /* pageSetup = */, + null /* zoom = */, async page => { printHandles.set( page, - await page.evaluateHandle(() => [ + page.evaluateHandle(() => [ new Promise(resolve => { globalThis.printResolve = resolve; }), ]) ); await page.waitForFunction(() => { + // We don't really need to print the document. + window.print = () => {}; if (!window.PDFViewerApplication?.eventBus) { return false; } @@ -1826,7 +1806,9 @@ describe("Interaction", () => { it("must check if printing is triggered when the document is open", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await awaitPromise(printHandles.get(page)); + await waitForScripting(page); + + await awaitPromise(await printHandles.get(page)); }) ); }); @@ -1846,9 +1828,7 @@ describe("Interaction", () => { it("must check that a field value with a number isn't changed", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("25R")); await page.type(getSelector("25R"), "00000000123", { delay: 10 }); @@ -1880,9 +1860,7 @@ describe("Interaction", () => { it("must check that a field value with a number with a comma has the correct value", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("22R"), el => el.value); expect(text).withContext(`In ${browserName}`).toEqual("5,25"); @@ -1922,9 +1900,7 @@ describe("Interaction", () => { it("must check that a field has the correct value when a choice is changed", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("44R"), el => el.value); expect(text).withContext(`In ${browserName}`).toEqual(""); @@ -1959,9 +1935,7 @@ describe("Interaction", () => { it("must check that a field has the correct formatted value", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("23R"), el => el.value); expect(text) @@ -1982,9 +1956,7 @@ describe("Interaction", () => { it("must check that a field is empty", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let text = await page.$eval(getSelector("26R"), el => el.value); expect(text).withContext(`In ${browserName}`).toEqual(""); @@ -2017,6 +1989,8 @@ describe("Interaction", () => { it("must check that a field has the correct formatted value", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + await waitForScripting(page); + const hasVisibleCanvas = await page.$eval( `[data-annotation-id="9R"] > canvas`, elem => elem && !elem.hasAttribute("hidden") @@ -2073,9 +2047,7 @@ describe("Interaction", () => { it("must check that invisible fields are made visible", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let visibility = await page.$eval( getSelector("7R"), @@ -2131,9 +2103,7 @@ describe("Interaction", () => { it("must check that checkboxes are correctly resetted", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); let readonly = await page.$eval( getSelector("353R"), @@ -2203,9 +2173,7 @@ describe("Interaction", () => { it("must check that focus/blur callbacks aren't called", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("55R")); await page.type(getSelector("55R"), "Hello", { delay: 10 }); @@ -2249,9 +2217,7 @@ describe("Interaction", () => { it("must check that blur callback is called", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("25R")); await page.click(getSelector("26R")); @@ -2290,11 +2256,9 @@ describe("Interaction", () => { it("must check that only one radio is selected", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); - await scrollIntoView(page, getSelector("22R")); + await waitForScripting(page); + await scrollIntoView(page, getSelector("22R")); await page.click(getSelector("25R")); await waitForEntryInStorage(page, "25R", { value: true }); @@ -2356,9 +2320,7 @@ describe("Interaction", () => { it("must check the number has the correct number of decimals", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("15R")); await page.type(getSelector("15R"), "3"); @@ -2398,9 +2360,7 @@ describe("Interaction", () => { it("must check the zip code is correctly formatted", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); await page.click(getSelector("24R")); await page.type(getSelector("24R"), "01234", { delay: 10 }); @@ -2435,9 +2395,7 @@ describe("Interaction", () => { it("must check the properties of the event", async () => { await Promise.all( pages.map(async ([browserName, page], i) => { - await page.waitForFunction( - "window.PDFViewerApplication.scriptingReady === true" - ); + await waitForScripting(page); for (const [value, expected] of [ ["b", "change=B,changeEx=b,value=A"], @@ -2456,4 +2414,56 @@ describe("Interaction", () => { ); }); }); + + describe("PageOpen and PageClose actions in fields", () => { + let pages; + let otherPages; + + beforeAll(async () => { + otherPages = await Promise.all( + global.integrationSessions.map(async session => + session.browser.newPage() + ) + ); + pages = await loadAndWait("issue18305.pdf", getSelector("7R")); + }); + + afterAll(async () => { + await closePages(pages); + await Promise.all(otherPages.map(page => page.close())); + }); + + it("must check that PageOpen/PageClose actions are correctly executed", async () => { + await Promise.all( + pages.map(async ([browserName, page], i) => { + await waitForScripting(page); + + const buttonSelector = `[data-annotation-id="25R"`; + await page.waitForSelector(buttonSelector, { + timeout: 0, + }); + + const inputSelector = getSelector("7R"); + let text = await page.$eval(inputSelector, el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual(""); + + text = await actAndWaitForInput( + page, + inputSelector, + () => scrollIntoView(page, buttonSelector), + false + ); + expect(text).withContext(`In ${browserName}`).toEqual("PageOpen"); + + text = await actAndWaitForInput( + page, + inputSelector, + () => scrollIntoView(page, inputSelector), + false + ); + expect(text).withContext(`In ${browserName}`).toEqual("PageClose"); + }) + ); + }); + }); }); diff --git a/test/integration/stamp_editor_spec.mjs b/test/integration/stamp_editor_spec.mjs index 62438dc6dbf12..fed856d8627a2 100644 --- a/test/integration/stamp_editor_spec.mjs +++ b/test/integration/stamp_editor_spec.mjs @@ -14,19 +14,22 @@ */ import { + applyFunctionToEditor, awaitPromise, closePages, + copy, + copyToClipboard, getEditorDimensions, getEditorSelector, getFirstSerialized, getRect, + getSerialized, kbBigMoveDown, kbBigMoveRight, - kbCopy, - kbPaste, kbSelectAll, kbUndo, loadAndWait, + paste, pasteFromClipboard, scrollIntoView, serializeBitmapDimensions, @@ -76,12 +79,10 @@ const copyImage = async (page, imagePath, number) => { const data = fs .readFileSync(path.join(__dirname, imagePath)) .toString("base64"); - await pasteFromClipboard( - page, - { "image/png": `data:image/png;base64,${data}` }, - "", - 500 - ); + + await copyToClipboard(page, { "image/png": `data:image/png;base64,${data}` }); + await pasteFromClipboard(page); + await waitForImage(page, getEditorSelector(number)); }; @@ -257,160 +258,159 @@ describe("Stamp Editor", () => { }); it("must check that the alt-text flow is correctly implemented", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - await switchToStamp(page); - - await copyImage(page, "../images/firefox_logo.png", 0); - - // Wait for the alt-text button to be visible. - const buttonSelector = `${getEditorSelector(0)} button.altText`; - await page.waitForSelector(buttonSelector); - - // Click on the alt-text button. - await page.click(buttonSelector); - - // Wait for the alt-text dialog to be visible. - await page.waitForSelector("#altTextDialog", { visible: true }); - - // Click on the alt-text editor. - const textareaSelector = "#altTextDialog textarea"; - await page.click(textareaSelector); - await page.type(textareaSelector, "Hello World"); - - // Click on save button. - const saveButtonSelector = "#altTextDialog #altTextSave"; - await page.click(saveButtonSelector); - - // Check that the canvas has an aria-describedby attribute. - await page.waitForSelector( - `${getEditorSelector(0)} canvas[aria-describedby]` - ); - - // Wait for the alt-text button to have the correct icon. - await page.waitForSelector(`${buttonSelector}.done`); - - // Hover the button. - await page.hover(buttonSelector); - - // Wait for the tooltip to be visible. - const tooltipSelector = `${buttonSelector} .tooltip`; - await page.waitForSelector(tooltipSelector, { visible: true }); - - let tooltipText = await page.evaluate( - sel => document.querySelector(`${sel}`).innerText, - tooltipSelector - ); - expect(tooltipText).toEqual("Hello World"); - - // Now we change the alt-text and check that the tooltip is updated. - await page.click(buttonSelector); - await page.waitForSelector("#altTextDialog", { visible: true }); - await page.evaluate(sel => { - document.querySelector(`${sel}`).value = ""; - }, textareaSelector); - await page.click(textareaSelector); - await page.type(textareaSelector, "Dlrow Olleh"); - await page.click(saveButtonSelector); - await page.waitForSelector(`${buttonSelector}.done`); - await page.hover(buttonSelector); - await page.waitForSelector(tooltipSelector, { visible: true }); - tooltipText = await page.evaluate( - sel => document.querySelector(`${sel}`).innerText, - tooltipSelector - ); - expect(tooltipText).toEqual("Dlrow Olleh"); - - // Now we just check that cancel didn't change anything. - await page.click(buttonSelector); - await page.waitForSelector("#altTextDialog", { visible: true }); - await page.evaluate(sel => { - document.querySelector(`${sel}`).value = ""; - }, textareaSelector); - await page.click(textareaSelector); - await page.type(textareaSelector, "Hello PDF.js"); - const cancelButtonSelector = "#altTextDialog #altTextCancel"; - await page.click(cancelButtonSelector); - await page.waitForSelector(`${buttonSelector}.done`); - await page.hover(buttonSelector); - await page.waitForSelector(tooltipSelector, { visible: true }); - tooltipText = await page.evaluate( - sel => document.querySelector(`${sel}`).innerText, - tooltipSelector - ); - // The tooltip should still be "Dlrow Olleh". - expect(tooltipText).toEqual("Dlrow Olleh"); - - // Now we switch to decorative. - await page.click(buttonSelector); - await page.waitForSelector("#altTextDialog", { visible: true }); - const decorativeSelector = "#altTextDialog #decorativeButton"; - await page.click(decorativeSelector); - await page.click(saveButtonSelector); - await page.waitForSelector(`${buttonSelector}.done`); - await page.hover(buttonSelector); - await page.waitForSelector(tooltipSelector, { visible: true }); - tooltipText = await page.evaluate( - sel => document.querySelector(`${sel}`).innerText, - tooltipSelector - ); - expect(tooltipText).toEqual("Marked as decorative"); - - // Now we switch back to non-decorative. - await page.click(buttonSelector); - await page.waitForSelector("#altTextDialog", { visible: true }); - const descriptionSelector = "#altTextDialog #descriptionButton"; - await page.click(descriptionSelector); - await page.click(saveButtonSelector); - await page.waitForSelector(`${buttonSelector}.done`); - await page.hover(buttonSelector); - await page.waitForSelector(tooltipSelector, { visible: true }); - tooltipText = await page.evaluate( - sel => document.querySelector(`${sel}`).innerText, - tooltipSelector - ); - expect(tooltipText).toEqual("Dlrow Olleh"); - - // Now we remove the alt-text and check that the tooltip is removed. - await page.click(buttonSelector); - await page.waitForSelector("#altTextDialog", { visible: true }); - await page.evaluate(sel => { - document.querySelector(`${sel}`).value = ""; - }, textareaSelector); - await page.click(saveButtonSelector); - await page.waitForSelector(`${buttonSelector}:not(.done)`); - await page.hover(buttonSelector); - await page.evaluate( - sel => document.querySelector(sel) === null, - tooltipSelector - ); - - // We check that the alt-text button works correctly with the - // keyboard. - const handle = await page.evaluateHandle(sel => { - document.getElementById("viewerContainer").focus(); - return [ - new Promise(resolve => { - setTimeout(() => { - const el = document.querySelector(sel); - el.addEventListener("focus", resolve, { once: true }); - el.focus({ focusVisible: true }); - }, 0); - }), - ]; - }, buttonSelector); - await awaitPromise(handle); - await (browserName === "chrome" - ? page.waitForSelector(`${buttonSelector}:focus`) - : page.waitForSelector(`${buttonSelector}:focus-visible`)); - await page.keyboard.press("Enter"); - await page.waitForSelector("#altTextDialog", { visible: true }); - await page.keyboard.press("Escape"); - await (browserName === "chrome" - ? page.waitForSelector(`${buttonSelector}:focus`) - : page.waitForSelector(`${buttonSelector}:focus-visible`)); - }) - ); + // Run sequentially to avoid clipboard issues. + for (const [browserName, page] of pages) { + await switchToStamp(page); + + await copyImage(page, "../images/firefox_logo.png", 0); + + // Wait for the alt-text button to be visible. + const buttonSelector = `${getEditorSelector(0)} button.altText`; + await page.waitForSelector(buttonSelector); + + // Click on the alt-text button. + await page.click(buttonSelector); + + // Wait for the alt-text dialog to be visible. + await page.waitForSelector("#altTextDialog", { visible: true }); + + // Click on the alt-text editor. + const textareaSelector = "#altTextDialog textarea"; + await page.click(textareaSelector); + await page.type(textareaSelector, "Hello World"); + + // Click on save button. + const saveButtonSelector = "#altTextDialog #altTextSave"; + await page.click(saveButtonSelector); + + // Check that the canvas has an aria-describedby attribute. + await page.waitForSelector( + `${getEditorSelector(0)} canvas[aria-describedby]` + ); + + // Wait for the alt-text button to have the correct icon. + await page.waitForSelector(`${buttonSelector}.done`); + + // Hover the button. + await page.hover(buttonSelector); + + // Wait for the tooltip to be visible. + const tooltipSelector = `${buttonSelector} .tooltip`; + await page.waitForSelector(tooltipSelector, { visible: true }); + + let tooltipText = await page.evaluate( + sel => document.querySelector(`${sel}`).innerText, + tooltipSelector + ); + expect(tooltipText).toEqual("Hello World"); + + // Now we change the alt-text and check that the tooltip is updated. + await page.click(buttonSelector); + await page.waitForSelector("#altTextDialog", { visible: true }); + await page.evaluate(sel => { + document.querySelector(`${sel}`).value = ""; + }, textareaSelector); + await page.click(textareaSelector); + await page.type(textareaSelector, "Dlrow Olleh"); + await page.click(saveButtonSelector); + await page.waitForSelector(`${buttonSelector}.done`); + await page.hover(buttonSelector); + await page.waitForSelector(tooltipSelector, { visible: true }); + tooltipText = await page.evaluate( + sel => document.querySelector(`${sel}`).innerText, + tooltipSelector + ); + expect(tooltipText).toEqual("Dlrow Olleh"); + + // Now we just check that cancel didn't change anything. + await page.click(buttonSelector); + await page.waitForSelector("#altTextDialog", { visible: true }); + await page.evaluate(sel => { + document.querySelector(`${sel}`).value = ""; + }, textareaSelector); + await page.click(textareaSelector); + await page.type(textareaSelector, "Hello PDF.js"); + const cancelButtonSelector = "#altTextDialog #altTextCancel"; + await page.click(cancelButtonSelector); + await page.waitForSelector(`${buttonSelector}.done`); + await page.hover(buttonSelector); + await page.waitForSelector(tooltipSelector, { visible: true }); + tooltipText = await page.evaluate( + sel => document.querySelector(`${sel}`).innerText, + tooltipSelector + ); + // The tooltip should still be "Dlrow Olleh". + expect(tooltipText).toEqual("Dlrow Olleh"); + + // Now we switch to decorative. + await page.click(buttonSelector); + await page.waitForSelector("#altTextDialog", { visible: true }); + const decorativeSelector = "#altTextDialog #decorativeButton"; + await page.click(decorativeSelector); + await page.click(saveButtonSelector); + await page.waitForSelector(`${buttonSelector}.done`); + await page.hover(buttonSelector); + await page.waitForSelector(tooltipSelector, { visible: true }); + tooltipText = await page.evaluate( + sel => document.querySelector(`${sel}`).innerText, + tooltipSelector + ); + expect(tooltipText).toEqual("Marked as decorative"); + + // Now we switch back to non-decorative. + await page.click(buttonSelector); + await page.waitForSelector("#altTextDialog", { visible: true }); + const descriptionSelector = "#altTextDialog #descriptionButton"; + await page.click(descriptionSelector); + await page.click(saveButtonSelector); + await page.waitForSelector(`${buttonSelector}.done`); + await page.hover(buttonSelector); + await page.waitForSelector(tooltipSelector, { visible: true }); + tooltipText = await page.evaluate( + sel => document.querySelector(`${sel}`).innerText, + tooltipSelector + ); + expect(tooltipText).toEqual("Dlrow Olleh"); + + // Now we remove the alt-text and check that the tooltip is removed. + await page.click(buttonSelector); + await page.waitForSelector("#altTextDialog", { visible: true }); + await page.evaluate(sel => { + document.querySelector(`${sel}`).value = ""; + }, textareaSelector); + await page.click(saveButtonSelector); + await page.waitForSelector(`${buttonSelector}:not(.done)`); + await page.hover(buttonSelector); + await page.evaluate( + sel => document.querySelector(sel) === null, + tooltipSelector + ); + + // We check that the alt-text button works correctly with the + // keyboard. + const handle = await page.evaluateHandle(sel => { + document.getElementById("viewerContainer").focus(); + return [ + new Promise(resolve => { + setTimeout(() => { + const el = document.querySelector(sel); + el.addEventListener("focus", resolve, { once: true }); + el.focus({ focusVisible: true }); + }, 0); + }), + ]; + }, buttonSelector); + await awaitPromise(handle); + await (browserName === "chrome" + ? page.waitForSelector(`${buttonSelector}:focus`) + : page.waitForSelector(`${buttonSelector}:focus-visible`)); + await page.keyboard.press("Enter"); + await page.waitForSelector("#altTextDialog", { visible: true }); + await page.keyboard.press("Escape"); + await (browserName === "chrome" + ? page.waitForSelector(`${buttonSelector}:focus`) + : page.waitForSelector(`${buttonSelector}:focus-visible`)); + } }); }); @@ -426,125 +426,124 @@ describe("Stamp Editor", () => { }); it("must check that the dimensions change", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - await switchToStamp(page); - - await copyImage(page, "../images/firefox_logo.png", 0); + // Run sequentially to avoid clipboard issues. + for (const [browserName, page] of pages) { + await switchToStamp(page); - const editorSelector = getEditorSelector(0); + await copyImage(page, "../images/firefox_logo.png", 0); - await page.click(editorSelector); - await waitForSelectedEditor(page, editorSelector); + const editorSelector = getEditorSelector(0); - await page.waitForSelector( - `${editorSelector} .resizer.topLeft[tabindex="-1"]` - ); - - const getDims = async () => { - const [blX, blY, trX, trY] = await getFirstSerialized( - page, - x => x.rect - ); - return [trX - blX, trY - blY]; - }; + await page.click(editorSelector); + await waitForSelectedEditor(page, editorSelector); - const [width, height] = await getDims(); + await page.waitForSelector( + `${editorSelector} .resizer.topLeft[tabindex="-1"]` + ); - // Press Enter to enter in resize-with-keyboard mode. - await page.keyboard.press("Enter"); - - // The resizer must become keyboard focusable. - await page.waitForSelector( - `${editorSelector} .resizer.topLeft[tabindex="0"]` + const getDims = async () => { + const [blX, blY, trX, trY] = await getFirstSerialized( + page, + x => x.rect ); + return [trX - blX, trY - blY]; + }; - let prevWidth = width; - let prevHeight = height; - - const waitForDimsChange = async (w, h) => { - await page.waitForFunction( - (prevW, prevH) => { - const [x1, y1, x2, y2] = - window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.map - .values() - .next().value.rect; - const newWidth = x2 - x1; - const newHeight = y2 - y1; - return newWidth !== prevW || newHeight !== prevH; - }, - {}, - w, - h - ); - }; - - for (let i = 0; i < 40; i++) { - await page.keyboard.press("ArrowLeft"); - await waitForDimsChange(prevWidth, prevHeight); - [prevWidth, prevHeight] = await getDims(); - } + const [width, height] = await getDims(); - let [newWidth, newHeight] = await getDims(); - expect(newWidth > width + 30) - .withContext(`In ${browserName}`) - .toEqual(true); - expect(newHeight > height + 30) - .withContext(`In ${browserName}`) - .toEqual(true); + // Press Enter to enter in resize-with-keyboard mode. + await page.keyboard.press("Enter"); - for (let i = 0; i < 4; i++) { - await kbBigMoveRight(page); - await waitForDimsChange(prevWidth, prevHeight); - [prevWidth, prevHeight] = await getDims(); - } + // The resizer must become keyboard focusable. + await page.waitForSelector( + `${editorSelector} .resizer.topLeft[tabindex="0"]` + ); - [newWidth, newHeight] = await getDims(); - expect(Math.abs(newWidth - width) < 2) - .withContext(`In ${browserName}`) - .toEqual(true); - expect(Math.abs(newHeight - height) < 2) - .withContext(`In ${browserName}`) - .toEqual(true); + let prevWidth = width; + let prevHeight = height; - // Move the focus to the next resizer. - await page.keyboard.press("Tab"); + const waitForDimsChange = async (w, h) => { await page.waitForFunction( - () => !!document.activeElement?.classList.contains("topMiddle") + (prevW, prevH) => { + const [x1, y1, x2, y2] = + window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.map + .values() + .next().value.rect; + const newWidth = x2 - x1; + const newHeight = y2 - y1; + return newWidth !== prevW || newHeight !== prevH; + }, + {}, + w, + h ); + }; - for (let i = 0; i < 40; i++) { - await page.keyboard.press("ArrowUp"); - await waitForDimsChange(prevWidth, prevHeight); - [prevWidth, prevHeight] = await getDims(); - } + for (let i = 0; i < 40; i++) { + await page.keyboard.press("ArrowLeft"); + await waitForDimsChange(prevWidth, prevHeight); + [prevWidth, prevHeight] = await getDims(); + } - [, newHeight] = await getDims(); - expect(newHeight > height + 50) - .withContext(`In ${browserName}`) - .toEqual(true); + let [newWidth, newHeight] = await getDims(); + expect(newWidth > width + 30) + .withContext(`In ${browserName}`) + .toEqual(true); + expect(newHeight > height + 30) + .withContext(`In ${browserName}`) + .toEqual(true); + + for (let i = 0; i < 4; i++) { + await kbBigMoveRight(page); + await waitForDimsChange(prevWidth, prevHeight); + [prevWidth, prevHeight] = await getDims(); + } - for (let i = 0; i < 4; i++) { - await kbBigMoveDown(page); - await waitForDimsChange(prevWidth, prevHeight); - [prevWidth, prevHeight] = await getDims(); - } + [newWidth, newHeight] = await getDims(); + expect(Math.abs(newWidth - width) < 2) + .withContext(`In ${browserName}`) + .toEqual(true); + expect(Math.abs(newHeight - height) < 2) + .withContext(`In ${browserName}`) + .toEqual(true); + + // Move the focus to the next resizer. + await page.keyboard.press("Tab"); + await page.waitForFunction( + () => !!document.activeElement?.classList.contains("topMiddle") + ); + + for (let i = 0; i < 40; i++) { + await page.keyboard.press("ArrowUp"); + await waitForDimsChange(prevWidth, prevHeight); + [prevWidth, prevHeight] = await getDims(); + } - [, newHeight] = await getDims(); - expect(Math.abs(newHeight - height) < 2) - .withContext(`In ${browserName}`) - .toEqual(true); + [, newHeight] = await getDims(); + expect(newHeight > height + 50) + .withContext(`In ${browserName}`) + .toEqual(true); - // Escape should remove the focus from the resizer. - await page.keyboard.press("Escape"); - await page.waitForSelector( - `${editorSelector} .resizer.topLeft[tabindex="-1"]` - ); - await page.waitForFunction( - () => !document.activeElement?.classList.contains("resizer") - ); - }) - ); + for (let i = 0; i < 4; i++) { + await kbBigMoveDown(page); + await waitForDimsChange(prevWidth, prevHeight); + [prevWidth, prevHeight] = await getDims(); + } + + [, newHeight] = await getDims(); + expect(Math.abs(newHeight - height) < 2) + .withContext(`In ${browserName}`) + .toEqual(true); + + // Escape should remove the focus from the resizer. + await page.keyboard.press("Escape"); + await page.waitForSelector( + `${editorSelector} .resizer.topLeft[tabindex="-1"]` + ); + await page.waitForFunction( + () => !document.activeElement?.classList.contains("resizer") + ); + } }); }); @@ -564,20 +563,19 @@ describe("Stamp Editor", () => { it("must check that the alt-text button is here when pasting in the second tab", async () => { for (let i = 0; i < pages1.length; i++) { const [, page1] = pages1[i]; - page1.bringToFront(); - await page1.click("#editorStamp"); + await page1.bringToFront(); + await switchToStamp(page1); await copyImage(page1, "../images/firefox_logo.png", 0); - await kbCopy(page1); + await copy(page1); const [, page2] = pages2[i]; - page2.bringToFront(); - await page2.click("#editorStamp"); + await page2.bringToFront(); + await switchToStamp(page2); - await kbPaste(page2); + await paste(page2); await waitForImage(page2, getEditorSelector(0)); - await page2.waitForSelector(`${getEditorSelector(0)} .altText`); } }); }); @@ -594,23 +592,23 @@ describe("Stamp Editor", () => { }); it("must check that a stamp can be undone", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - await switchToStamp(page); - - await copyImage(page, "../images/firefox_logo.png", 0); - await page.waitForSelector(getEditorSelector(0)); - await waitForSerialized(page, 1); - - await page.waitForSelector(`${getEditorSelector(0)} button.delete`); - await page.click(`${getEditorSelector(0)} button.delete`); - await waitForSerialized(page, 0); - - await kbUndo(page); - await waitForSerialized(page, 1); - await page.waitForSelector(getEditorSelector(0)); - }) - ); + // Run sequentially to avoid clipboard issues. + for (const [, page] of pages) { + await switchToStamp(page); + const selector = getEditorSelector(0); + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(selector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${selector} button.delete`); + await page.click(`${selector} button.delete`); + await waitForSerialized(page, 0); + + await kbUndo(page); + await waitForSerialized(page, 1); + await page.waitForSelector(`${selector} canvas`); + } }); }); @@ -626,36 +624,36 @@ describe("Stamp Editor", () => { }); it("must check that a stamp can be undone", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - await switchToStamp(page); - - await copyImage(page, "../images/firefox_logo.png", 0); - await page.waitForSelector(getEditorSelector(0)); - await waitForSerialized(page, 1); - - await page.waitForSelector(`${getEditorSelector(0)} button.delete`); - await page.click(`${getEditorSelector(0)} button.delete`); - await waitForSerialized(page, 0); - - const twoToFourteen = Array.from(new Array(13).keys(), n => n + 2); - for (const pageNumber of twoToFourteen) { - const pageSelector = `.page[data-page-number = "${pageNumber}"]`; - await scrollIntoView(page, pageSelector); - } + // Run sequentially to avoid clipboard issues. + for (const [, page] of pages) { + await switchToStamp(page); + const selector = getEditorSelector(0); + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(selector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${selector} button.delete`); + await page.click(`${selector} button.delete`); + await waitForSerialized(page, 0); + + const twoToFourteen = Array.from(new Array(13).keys(), n => n + 2); + for (const pageNumber of twoToFourteen) { + const pageSelector = `.page[data-page-number = "${pageNumber}"]`; + await scrollIntoView(page, pageSelector); + } - await kbUndo(page); - await waitForSerialized(page, 1); + await kbUndo(page); + await waitForSerialized(page, 1); - const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n); - for (const pageNumber of thirteenToOne) { - const pageSelector = `.page[data-page-number = "${pageNumber}"]`; - await scrollIntoView(page, pageSelector); - } + const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n); + for (const pageNumber of thirteenToOne) { + const pageSelector = `.page[data-page-number = "${pageNumber}"]`; + await scrollIntoView(page, pageSelector); + } - await page.waitForSelector(getEditorSelector(0)); - }) - ); + await page.waitForSelector(`${selector} canvas`); + } }); }); @@ -671,31 +669,31 @@ describe("Stamp Editor", () => { }); it("must check that a stamp can be undone", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - await switchToStamp(page); - - await copyImage(page, "../images/firefox_logo.png", 0); - await page.waitForSelector(getEditorSelector(0)); - await waitForSerialized(page, 1); - - await page.waitForSelector(`${getEditorSelector(0)} button.delete`); - await page.click(`${getEditorSelector(0)} button.delete`); - await waitForSerialized(page, 0); - - const twoToOne = Array.from(new Array(13).keys(), n => n + 2).concat( - Array.from(new Array(13).keys(), n => 13 - n) - ); - for (const pageNumber of twoToOne) { - const pageSelector = `.page[data-page-number = "${pageNumber}"]`; - await scrollIntoView(page, pageSelector); - } + // Run sequentially to avoid clipboard issues. + for (const [, page] of pages) { + await switchToStamp(page); + const selector = getEditorSelector(0); + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(selector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${selector} button.delete`); + await page.click(`${selector} button.delete`); + await waitForSerialized(page, 0); + + const twoToOne = Array.from(new Array(13).keys(), n => n + 2).concat( + Array.from(new Array(13).keys(), n => 13 - n) + ); + for (const pageNumber of twoToOne) { + const pageSelector = `.page[data-page-number = "${pageNumber}"]`; + await scrollIntoView(page, pageSelector); + } - await kbUndo(page); - await waitForSerialized(page, 1); - await page.waitForSelector(getEditorSelector(0)); - }) - ); + await kbUndo(page); + await waitForSerialized(page, 1); + await page.waitForSelector(`${selector} canvas`); + } }); }); @@ -711,44 +709,43 @@ describe("Stamp Editor", () => { }); it("must check that a resized stamp has its canvas at the right position", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - await switchToStamp(page); - - await copyImage(page, "../images/firefox_logo.png", 0); - await page.waitForSelector(getEditorSelector(0)); - await waitForSerialized(page, 1); - - const serializedRect = await getFirstSerialized(page, x => x.rect); - const rect = await getRect(page, ".resizer.bottomRight"); - const centerX = rect.x + rect.width / 2; - const centerY = rect.y + rect.height / 2; - - await page.mouse.move(centerX, centerY); - await page.mouse.down(); - await page.mouse.move(centerX - 500, centerY - 500); - await page.mouse.up(); - - await waitForEntryInStorage( - page, - "rect", - serializedRect, - (x, y) => x !== y - ); - - const canvasRect = await getRect( - page, - `${getEditorSelector(0)} canvas` - ); - const stampRect = await getRect(page, getEditorSelector(0)); - - expect( - ["x", "y", "width", "height"].every( - key => Math.abs(canvasRect[key] - stampRect[key]) <= 10 - ) - ).toBeTrue(); - }) - ); + // Run sequentially to avoid clipboard issues. + for (const [, page] of pages) { + await switchToStamp(page); + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + const serializedRect = await getFirstSerialized(page, x => x.rect); + const rect = await getRect(page, ".resizer.bottomRight"); + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + + await page.mouse.move(centerX, centerY); + await page.mouse.down(); + await page.mouse.move(centerX - 500, centerY - 500); + await page.mouse.up(); + + await waitForEntryInStorage( + page, + "rect", + serializedRect, + (x, y) => x !== y + ); + + const canvasRect = await getRect( + page, + `${getEditorSelector(0)} canvas` + ); + const stampRect = await getRect(page, getEditorSelector(0)); + + expect( + ["x", "y", "width", "height"].every( + key => Math.abs(canvasRect[key] - stampRect[key]) <= 10 + ) + ).toBeTrue(); + } }); }); @@ -772,27 +769,70 @@ describe("Stamp Editor", () => { }); it("must check that the stamp has its canvas at the right position", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - await switchToStamp(page); + // Run sequentially to avoid clipboard issues. + for (const [, page] of pages) { + await switchToStamp(page); + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + const canvasRect = await getRect( + page, + `${getEditorSelector(0)} canvas` + ); + const stampRect = await getRect(page, getEditorSelector(0)); + + expect( + ["x", "y", "width", "height"].every( + key => Math.abs(canvasRect[key] - stampRect[key]) <= 10 + ) + ).toBeTrue(); + } + }); + }); - await copyImage(page, "../images/firefox_logo.png", 0); - await page.waitForSelector(getEditorSelector(0)); - await waitForSerialized(page, 1); + describe("Copy and paste a stamp with an alt text", () => { + let pages; - const canvasRect = await getRect( - page, - `${getEditorSelector(0)} canvas` - ); - const stampRect = await getRect(page, getEditorSelector(0)); + beforeAll(async () => { + pages = await loadAndWait("empty.pdf", ".annotationEditorLayer"); + }); - expect( - ["x", "y", "width", "height"].every( - key => Math.abs(canvasRect[key] - stampRect[key]) <= 10 - ) - ).toBeTrue(); - }) - ); + afterAll(async () => { + await closePages(pages); + }); + + it("must check that the pasted image has an alt text", async () => { + // Run sequentially to avoid clipboard issues. + for (const [browserName, page] of pages) { + await switchToStamp(page); + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + await applyFunctionToEditor(page, "pdfjs_internal_editor_0", editor => { + editor.altTextData = { + altText: "Hello World", + decorative: false, + }; + }); + await page.waitForSelector(`${getEditorSelector(0)} .altText.done`); + + await copy(page); + await paste(page); + await page.waitForSelector(`${getEditorSelector(1)} .altText.done`); + await waitForSerialized(page, 2); + + const serialized = await getSerialized( + page, + x => x.accessibilityData?.alt + ); + + expect(serialized) + .withContext(`In ${browserName}`) + .toEqual(["Hello World", "Hello World"]); + } }); }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index ce05d346aec10..207007fa41ef4 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -38,8 +38,13 @@ function loadAndWait(filename, selector, zoom, pageSetup, options) { let app_options = ""; if (options) { + const optionsObject = + typeof options === "function" + ? await options(page, session.name) + : options; + // Options must be handled in app.js::_parseHashParams. - for (const [key, value] of Object.entries(options)) { + for (const [key, value] of Object.entries(optionsObject)) { app_options += `&${key}=${encodeURIComponent(value)}`; } } @@ -76,19 +81,16 @@ function awaitPromise(promise) { } function closePages(pages) { - return Promise.all( - pages.map(async ([_, page]) => { - // Avoid to keep something from a previous test. - await page.evaluate(async () => { - const viewer = window.PDFViewerApplication; - viewer.unbindWindowEvents(); - viewer.unbindEvents(); - await viewer.close(); - window.localStorage.clear(); - }); - await page.close({ runBeforeUnload: false }); - }) - ); + return Promise.all(pages.map(([_, page]) => closeSinglePage(page))); +} + +async function closeSinglePage(page) { + // Avoid to keep something from a previous test. + await page.evaluate(async () => { + await window.PDFViewerApplication.testingClose(); + window.localStorage.clear(); + }); + await page.close({ runBeforeUnload: false }); } async function waitForSandboxTrip(page) { @@ -133,6 +135,7 @@ function getSelector(id) { async function getRect(page, selector) { // In Chrome something is wrong when serializing a `DomRect`, // so we extract the values and return them ourselves. + await page.waitForSelector(selector, { visible: true }); return page.$eval(selector, el => { const { x, y, width, height } = el.getBoundingClientRect(); return { x, y, width, height }; @@ -184,32 +187,56 @@ async function getSpanRectFromText(page, pageNumber, text) { ); } -async function waitForEvent(page, eventName, timeout = 5000) { +async function waitForEvent({ + page, + eventName, + action, + selector = null, + validator = null, + timeout = 5000, +}) { const handle = await page.evaluateHandle( - (name, timeOut) => { - let callback = null; + (name, sel, validate, timeOut) => { + let callback = null, + timeoutId = null; + const element = sel ? document.querySelector(sel) : document; return [ Promise.race([ new Promise(resolve => { - // add event listener and wait for event to fire before returning - callback = () => resolve(false); - document.addEventListener(name, callback, { once: true }); + // The promise is resolved if the event fired in the context of the + // selector and, if a validator is defined, the event data satisfies + // the conditions of the validator function. + callback = e => { + if (timeoutId) { + clearTimeout(timeoutId); + } + // eslint-disable-next-line no-eval + resolve(validate ? eval(`(${validate})`)(e) : true); + }; + element.addEventListener(name, callback, { once: true }); }), new Promise(resolve => { - setTimeout(() => { - document.removeEventListener(name, callback); - resolve(true); + timeoutId = setTimeout(() => { + element.removeEventListener(name, callback); + resolve(null); }, timeOut); }), ]), ]; }, eventName, + selector, + validator ? validator.toString() : null, timeout ); - const hasTimedout = await awaitPromise(handle); - if (hasTimedout === true) { - console.log(`waitForEvent: timeout waiting for ${eventName}`); + + await action(); + + const success = await awaitPromise(handle); + if (success === null) { + console.log(`waitForEvent: ${eventName} didn't trigger within the timeout`); + } else if (!success) { + console.log(`waitForEvent: ${eventName} triggered, but validation failed`); } } @@ -231,6 +258,21 @@ async function waitForSerialized(page, nEntries) { ); } +async function applyFunctionToEditor(page, editorId, func) { + return page.evaluate( + (id, f) => { + const editor = + window.PDFViewerApplication.pdfDocument.annotationStorage.getRawValue( + id + ); + // eslint-disable-next-line no-eval + eval(`(${f})`)(editor); + }, + editorId, + func.toString() + ); +} + async function waitForSelectedEditor(page, selector) { return page.waitForSelector(`${selector}.selectedEditor`); } @@ -254,7 +296,15 @@ async function mockClipboard(pages) { ); } -async function pasteFromClipboard(page, data, selector, timeout = 100) { +async function copy(page) { + await waitForEvent({ + page, + eventName: "copy", + action: () => kbCopy(page), + }); +} + +async function copyToClipboard(page, data) { await page.evaluate(async dat => { const items = Object.create(null); for (const [type, value] of Object.entries(dat)) { @@ -267,37 +317,25 @@ async function pasteFromClipboard(page, data, selector, timeout = 100) { } await navigator.clipboard.write([new ClipboardItem(items)]); }, data); +} - let hasPasteEvent = false; - while (!hasPasteEvent) { - // We retry to paste if nothing has been pasted before the timeout. - const handle = await page.evaluateHandle( - (sel, timeOut) => { - let callback = null; - const element = sel ? document.querySelector(sel) : document; - return [ - Promise.race([ - new Promise(resolve => { - callback = e => resolve(e.clipboardData.items.length !== 0); - element.addEventListener("paste", callback, { - once: true, - }); - }), - new Promise(resolve => { - setTimeout(() => { - element.removeEventListener("paste", callback); - resolve(false); - }, timeOut); - }), - ]), - ]; - }, - selector, - timeout - ); - await kbPaste(page); - hasPasteEvent = await awaitPromise(handle); - } +async function paste(page) { + await waitForEvent({ + page, + eventName: "paste", + action: () => kbPaste(page), + }); +} + +async function pasteFromClipboard(page, selector = null) { + const validator = e => e.clipboardData.items.length !== 0; + await waitForEvent({ + page, + eventName: "paste", + action: () => kbPaste(page), + selector, + validator, + }); } async function getSerialized(page, filter = undefined) { @@ -410,11 +448,30 @@ function waitForAnnotationEditorLayer(page) { return createPromise(page, resolve => { window.PDFViewerApplication.eventBus.on( "annotationeditorlayerrendered", - resolve + resolve, + { once: true } ); }); } +function waitForAnnotationModeChanged(page) { + return createPromise(page, resolve => { + window.PDFViewerApplication.eventBus.on( + "annotationeditormodechanged", + resolve, + { once: true } + ); + }); +} + +function waitForPageRendered(page) { + return createPromise(page, resolve => { + window.PDFViewerApplication.eventBus.on("pagerendered", resolve, { + once: true, + }); + }); +} + async function scrollIntoView(page, selector) { const handle = await page.evaluateHandle( sel => [ @@ -454,6 +511,24 @@ async function hover(page, selector) { await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2); } +async function setCaretAt(page, pageNumber, text, position) { + await page.evaluate( + (pageN, string, pos) => { + for (const el of document.querySelectorAll( + `.page[data-page-number="${pageN}"] > .textLayer > span` + )) { + if (el.textContent === string) { + window.getSelection().setPosition(el.firstChild, pos); + break; + } + } + }, + pageNumber, + text, + position + ); +} + const modifier = isMac ? "Meta" : "Control"; async function kbCopy(page) { await page.keyboard.down(modifier); @@ -613,9 +688,13 @@ async function switchToEditor(name, page, disable = false) { } export { + applyFunctionToEditor, awaitPromise, clearInput, closePages, + closeSinglePage, + copy, + copyToClipboard, createPromise, dragAndDropAnnotation, firstPageOnTop, @@ -636,7 +715,6 @@ export { kbBigMoveLeft, kbBigMoveRight, kbBigMoveUp, - kbCopy, kbDeleteLastWord, kbFocusNext, kbFocusPrevious, @@ -644,19 +722,22 @@ export { kbGoToEnd, kbModifierDown, kbModifierUp, - kbPaste, kbRedo, kbSelectAll, kbUndo, loadAndWait, mockClipboard, + paste, pasteFromClipboard, scrollIntoView, serializeBitmapDimensions, + setCaretAt, switchToEditor, waitForAnnotationEditorLayer, + waitForAnnotationModeChanged, waitForEntryInStorage, waitForEvent, + waitForPageRendered, waitForSandboxTrip, waitForSelectedEditor, waitForSerialized, diff --git a/test/integration/text_layer_spec.mjs b/test/integration/text_layer_spec.mjs index 1fd8296b808ba..83779da82f9ff 100644 --- a/test/integration/text_layer_spec.mjs +++ b/test/integration/text_layer_spec.mjs @@ -13,7 +13,12 @@ * limitations under the License. */ -import { closePages, getSpanRectFromText, loadAndWait } from "./test_utils.mjs"; +import { + closePages, + closeSinglePage, + getSpanRectFromText, + loadAndWait, +} from "./test_utils.mjs"; import { startBrowser } from "../test.mjs"; describe("Text layer", () => { @@ -227,6 +232,7 @@ describe("Text layer", () => { ); }); afterAll(async () => { + await closeSinglePage(page); await browser.close(); }); @@ -290,4 +296,47 @@ describe("Text layer", () => { }); }); }); + + describe("when the browser enforces a minimum font size", () => { + let browser; + let page; + + beforeAll(async () => { + // Only testing in Firefox because, while Chrome has a setting similar to + // font.minimum-size.x-western, it is not exposed through its API. + browser = await startBrowser({ + browserName: "firefox", + startUrl: "", + extraPrefsFirefox: { "font.minimum-size.x-western": 40 }, + }); + page = await browser.newPage(); + await page.goto( + `${global.integrationBaseUrl}?file=/test/pdfs/tracemonkey.pdf#zoom=100` + ); + await page.bringToFront(); + await page.waitForSelector( + `.page[data-page-number = "1"] .endOfContent`, + { timeout: 0 } + ); + }); + + afterAll(async () => { + await closeSinglePage(page); + await browser.close(); + }); + + it("renders spans with the right size", async () => { + const rect = await getSpanRectFromText( + page, + 1, + "Dynamic languages such as JavaScript are more difficult to com-" + ); + + // The difference between `a` and `b`, as a percentage of the lower one + const getPercentDiff = (a, b) => Math.max(a, b) / Math.min(a, b) - 1; + + expect(getPercentDiff(rect.width, 315)).toBeLessThan(0.03); + expect(getPercentDiff(rect.height, 12)).toBeLessThan(0.03); + }); + }); }); diff --git a/test/integration/viewer_spec.mjs b/test/integration/viewer_spec.mjs index 5f780684ab5fe..279e1784f2421 100644 --- a/test/integration/viewer_spec.mjs +++ b/test/integration/viewer_spec.mjs @@ -174,24 +174,6 @@ describe("PDF viewer", () => { }); describe("CSS-only zoom", () => { - let pages; - - beforeAll(async () => { - pages = await loadAndWait( - "tracemonkey.pdf", - ".textLayer .endOfContent", - null, - null, - { - maxCanvasPixels: 0, - } - ); - }); - - afterAll(async () => { - await closePages(pages); - }); - function createPromiseForFirstPageRendered(page) { return createPromise(page, (resolve, reject) => { const controller = new AbortController(); @@ -209,50 +191,184 @@ describe("PDF viewer", () => { }); } - it("respects drawing delay when zooming out", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - const promise = await createPromiseForFirstPageRendered(page); + describe("forced (maxCanvasPixels: 0)", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + ".textLayer .endOfContent", + null, + null, + { maxCanvasPixels: 0 } + ); + }); - const start = await page.evaluate(() => { - const startTime = performance.now(); - window.PDFViewerApplication.pdfViewer.decreaseScale({ - drawingDelay: 100, - scaleFactor: 0.9, + afterAll(async () => { + await closePages(pages); + }); + + it("respects drawing delay when zooming out", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const promise = await createPromiseForFirstPageRendered(page); + + const start = await page.evaluate(() => { + const startTime = performance.now(); + window.PDFViewerApplication.pdfViewer.decreaseScale({ + drawingDelay: 100, + scaleFactor: 0.9, + }); + return startTime; }); - return startTime; - }); - const end = await awaitPromise(promise); + const end = await awaitPromise(promise); - expect(end - start) - .withContext(`In ${browserName}`) - .toBeGreaterThanOrEqual(100); - }) - ); + expect(end - start) + .withContext(`In ${browserName}`) + .toBeGreaterThanOrEqual(100); + }) + ); + }); + + it("respects drawing delay when zooming in", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const promise = await createPromiseForFirstPageRendered(page); + + const start = await page.evaluate(() => { + const startTime = performance.now(); + window.PDFViewerApplication.pdfViewer.increaseScale({ + drawingDelay: 100, + scaleFactor: 1.1, + }); + return startTime; + }); + + const end = await awaitPromise(promise); + + expect(end - start) + .withContext(`In ${browserName}`) + .toBeGreaterThanOrEqual(100); + }) + ); + }); }); - it("respects drawing delay when zooming in", async () => { - await Promise.all( - pages.map(async ([browserName, page]) => { - const promise = await createPromiseForFirstPageRendered(page); + describe("triggers when going bigger than maxCanvasPixels", () => { + let pages; + + const MAX_CANVAS_PIXELS = new Map(); + + beforeAll(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + ".textLayer .endOfContent", + null, + null, + async (page, browserName) => { + const ratio = await page.evaluate(() => window.devicePixelRatio); + const maxCanvasPixels = 1_000_000 * ratio ** 2; + MAX_CANVAS_PIXELS.set(browserName, maxCanvasPixels); + + return { maxCanvasPixels }; + } + ); + }); - const start = await page.evaluate(() => { - const startTime = performance.now(); - window.PDFViewerApplication.pdfViewer.increaseScale({ - drawingDelay: 100, - scaleFactor: 1.1, + beforeEach(async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.evaluate(() => { + window.PDFViewerApplication.pdfViewer.currentScale = 0.5; }); - return startTime; - }); + }) + ); + }); - const end = await awaitPromise(promise); + afterAll(async () => { + await closePages(pages); + }); - expect(end - start) - .withContext(`In ${browserName}`) - .toBeGreaterThanOrEqual(100); - }) - ); + function getCanvasSize(page) { + return page.evaluate(() => { + const canvas = window.document.querySelector(".canvasWrapper canvas"); + return canvas.width * canvas.height; + }); + } + + // MAX_CANVAS_PIXELS must be big enough that the originally rendered + // canvas still has enough space to grow before triggering CSS-only zoom + it("test correctly configured", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const canvasSize = await getCanvasSize(page); + + expect(canvasSize) + .withContext(`In ${browserName}`) + .toBeLessThan(MAX_CANVAS_PIXELS.get(browserName) / 4); + expect(canvasSize) + .withContext(`In ${browserName}`) + .toBeGreaterThan(MAX_CANVAS_PIXELS.get(browserName) / 16); + }) + ); + }); + + it("does not trigger CSS-only zoom below maxCanvasPixels", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const originalCanvasSize = await getCanvasSize(page); + const factor = 2; + + await page.evaluate(scaleFactor => { + window.PDFViewerApplication.pdfViewer.increaseScale({ + drawingDelay: 0, + scaleFactor, + }); + }, factor); + + const canvasSize = await getCanvasSize(page); + + expect(canvasSize) + .withContext(`In ${browserName}`) + .toBe(originalCanvasSize * factor ** 2); + + expect(canvasSize) + .withContext(`In ${browserName}, MAX_CANVAS_PIXELS`) + .toBeLessThan(MAX_CANVAS_PIXELS.get(browserName)); + }) + ); + }); + + it("triggers CSS-only zoom above maxCanvasPixels", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const originalCanvasSize = await getCanvasSize(page); + const factor = 4; + + await page.evaluate(scaleFactor => { + window.PDFViewerApplication.pdfViewer.increaseScale({ + drawingDelay: 0, + scaleFactor, + }); + }, factor); + + const canvasSize = await getCanvasSize(page); + + expect(canvasSize) + .withContext(`In ${browserName}`) + .toBeLessThan(originalCanvasSize * factor ** 2); + + expect(canvasSize) + .withContext(`In ${browserName}, <= MAX_CANVAS_PIXELS`) + .toBeLessThanOrEqual(MAX_CANVAS_PIXELS.get(browserName)); + + expect(canvasSize) + .withContext(`In ${browserName}, > MAX_CANVAS_PIXELS * 0.99`) + .toBeGreaterThan(MAX_CANVAS_PIXELS.get(browserName) * 0.99); + }) + ); + }); }); }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index a0991c7193413..dde1c01660e87 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -13,6 +13,8 @@ !issue1155r.pdf !issue2017r.pdf !bug1727053.pdf +!issue18408_reduced.pdf +!bug1907000_reduced.pdf !issue11913.pdf !issue2391-1.pdf !issue2391-2.pdf @@ -56,6 +58,7 @@ !issue17679_2.pdf !issue18030.pdf !issue18042.pdf +!issue18059.pdf !issue14953.pdf !issue15367.pdf !issue15372.pdf @@ -125,6 +128,7 @@ !issue7891_bc0.pdf !issue11242_reduced.pdf !issue16176.pdf +!issue16287.pdf !issue17064_readonly.pdf !issue11279.pdf !issue11362.pdf @@ -650,3 +654,8 @@ !issue17998.pdf !pdfjs_wikipedia.pdf !bug1539074.pdf +!bug1539074.1.pdf +!issue18305.pdf +!issue18360.pdf +!issue18099_reduced.pdf +!file_pdfjs_test.pdf diff --git a/test/pdfs/bug1539074.1.pdf b/test/pdfs/bug1539074.1.pdf new file mode 100755 index 0000000000000..d99f1de37db05 Binary files /dev/null and b/test/pdfs/bug1539074.1.pdf differ diff --git a/test/pdfs/bug1903731.pdf.link b/test/pdfs/bug1903731.pdf.link new file mode 100644 index 0000000000000..d7524dc9bcdc3 --- /dev/null +++ b/test/pdfs/bug1903731.pdf.link @@ -0,0 +1,2 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9408642 + diff --git a/test/pdfs/bug1905623.pdf.link b/test/pdfs/bug1905623.pdf.link new file mode 100644 index 0000000000000..0c249d74b4846 --- /dev/null +++ b/test/pdfs/bug1905623.pdf.link @@ -0,0 +1 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9410429 diff --git a/test/pdfs/bug1907000_reduced.pdf b/test/pdfs/bug1907000_reduced.pdf new file mode 100644 index 0000000000000..60e63076570ba Binary files /dev/null and b/test/pdfs/bug1907000_reduced.pdf differ diff --git a/test/pdfs/file_pdfjs_test.pdf b/test/pdfs/file_pdfjs_test.pdf new file mode 100755 index 0000000000000..7ad87e3c2e328 Binary files /dev/null and b/test/pdfs/file_pdfjs_test.pdf differ diff --git a/test/pdfs/issue16287.pdf b/test/pdfs/issue16287.pdf new file mode 100644 index 0000000000000..2a910231a8643 Binary files /dev/null and b/test/pdfs/issue16287.pdf differ diff --git a/test/pdfs/issue18059.pdf b/test/pdfs/issue18059.pdf new file mode 100644 index 0000000000000..82416e2666631 Binary files /dev/null and b/test/pdfs/issue18059.pdf differ diff --git a/test/pdfs/issue18099_reduced.pdf b/test/pdfs/issue18099_reduced.pdf new file mode 100644 index 0000000000000..8fa6fd6a8d7e2 Binary files /dev/null and b/test/pdfs/issue18099_reduced.pdf differ diff --git a/test/pdfs/issue18298.pdf.link b/test/pdfs/issue18298.pdf.link new file mode 100644 index 0000000000000..afaf55127e35f --- /dev/null +++ b/test/pdfs/issue18298.pdf.link @@ -0,0 +1 @@ +https://github.com/user-attachments/files/15908428/example-malformed.pdf diff --git a/test/pdfs/issue18305.pdf b/test/pdfs/issue18305.pdf new file mode 100755 index 0000000000000..fee93b6e1d45a Binary files /dev/null and b/test/pdfs/issue18305.pdf differ diff --git a/test/pdfs/issue18360.pdf b/test/pdfs/issue18360.pdf new file mode 100755 index 0000000000000..43a96575ecf4f Binary files /dev/null and b/test/pdfs/issue18360.pdf differ diff --git a/test/pdfs/issue18408_reduced.pdf b/test/pdfs/issue18408_reduced.pdf new file mode 100644 index 0000000000000..a97f2d79d57aa Binary files /dev/null and b/test/pdfs/issue18408_reduced.pdf differ diff --git a/test/pdfs/issue18444.pdf.link b/test/pdfs/issue18444.pdf.link new file mode 100644 index 0000000000000..f1e437217b264 --- /dev/null +++ b/test/pdfs/issue18444.pdf.link @@ -0,0 +1 @@ +https://github.com/user-attachments/files/16264442/9.170.1.pdf diff --git a/test/test.mjs b/test/test.mjs index b46b789bd29e4..8aac63e1a82c4 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -883,12 +883,12 @@ async function startBrowser({ }) { const options = { product: browserName, - protocol: "cdp", - dumpio: true, + protocol: "webDriverBiDi", headless, + dumpio: true, defaultViewport: null, ignoreDefaultArgs: ["--disable-extensions"], - // The timeout for individual protocol (CDP) calls should always be lower + // The timeout for individual protocol (BiDi) calls should always be lower // than the Jasmine timeout. This way protocol errors are always raised in // the context of the tests that actually triggered them and don't leak // through to other tests (causing unrelated failures or tracebacks). The @@ -911,11 +911,9 @@ async function startBrowser({ } if (browserName === "firefox") { - // Run tests with the WebDriver BiDi protocol enabled only for Firefox for - // now given that for Chrome further fixes are needed first. - options.protocol = "webDriverBiDi"; - options.extraPrefsFirefox = { + // Disable system addon updates. + "extensions.systemAddon.update.enabled": false, // avoid to have a prompt when leaving a page with a form "dom.disable_beforeunload": true, // Disable dialog when saving a pdf @@ -944,6 +942,10 @@ async function startBrowser({ "dom.events.asyncClipboard.clipboardItem": true, // It's helpful to see where the caret is. "accessibility.browsewithcaret": true, + // Disable the newtabpage stuff. + "browser.newtabpage.enabled": false, + // Disable network connections to Contile. + "browser.topsites.contile.enabled": false, ...extraPrefsFirefox, }; } @@ -966,8 +968,6 @@ async function startBrowsers({ baseUrl, initializeSession }) { await puppeteer.trimCache(); const browserNames = options.noChrome ? ["firefox"] : ["firefox", "chrome"]; - - sessions = []; for (const browserName of browserNames) { // The session must be pushed first and augmented with the browser once // it's initialized. The reason for this is that browser initialization @@ -1074,25 +1074,33 @@ async function main() { stats = []; } - if (options.downloadOnly) { - await ensurePDFsDownloaded(); - } else if (options.unitTest) { - // Allows linked PDF files in unit-tests as well. - await ensurePDFsDownloaded(); - startUnitTest("/test/unit/unit_test.html", "unit"); - } else if (options.fontTest) { - startUnitTest("/test/font/font_test.html", "font"); - } else if (options.integration) { - // Allows linked PDF files in integration-tests as well. - await ensurePDFsDownloaded(); - startIntegrationTest(); - } else { - startRefTest(options.masterMode, options.reftest); + try { + if (options.downloadOnly) { + await ensurePDFsDownloaded(); + } else if (options.unitTest) { + // Allows linked PDF files in unit-tests as well. + await ensurePDFsDownloaded(); + await startUnitTest("/test/unit/unit_test.html", "unit"); + } else if (options.fontTest) { + await startUnitTest("/test/font/font_test.html", "font"); + } else if (options.integration) { + // Allows linked PDF files in integration-tests as well. + await ensurePDFsDownloaded(); + await startIntegrationTest(); + } else { + await startRefTest(options.masterMode, options.reftest); + } + } catch (e) { + // Close the browsers if uncaught exceptions occur, otherwise the spawned + // processes can become orphaned and keep running after `test.mjs` exits + // because the teardown logic of the tests did not get a chance to run. + console.error(e); + await Promise.all(sessions.map(session => closeSession(session.name))); } } var server; -var sessions; +var sessions = []; var onAllSessionsClosed; var host = "127.0.0.1"; var options = parseOptions(); diff --git a/test/test_manifest.json b/test/test_manifest.json index 98eb53829e0f7..5f1edf3727194 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -2983,6 +2983,27 @@ "rounds": 1, "type": "eq" }, + { + "id": "issue11403-text", + "file": "pdfs/issue11403_reduced.pdf", + "md5": "08287b64f442cb7c329b97c4774aa1cd", + "rounds": 1, + "type": "text" + }, + { + "id": "issue18059", + "file": "pdfs/issue18059.pdf", + "md5": "b70373894edfcd571a41caa1a0776b6f", + "rounds": 1, + "type": "eq" + }, + { + "id": "issue18059-text", + "file": "pdfs/issue18059.pdf", + "md5": "b70373894edfcd571a41caa1a0776b6f", + "rounds": 1, + "type": "text" + }, { "id": "issue11139", "file": "pdfs/issue11139.pdf", @@ -5438,6 +5459,15 @@ "lastPage": 1, "type": "eq" }, + { + "id": "issue18298", + "file": "pdfs/issue18298.pdf", + "md5": "30a6108220c41ec88fa92ff45924a6cb", + "rounds": 1, + "link": true, + "lastPage": 1, + "type": "eq" + }, { "id": "issue18138", "file": "pdfs/issue18138.pdf", @@ -5616,6 +5646,14 @@ "rounds": 1, "type": "eq" }, + { + "id": "issue16287", + "file": "pdfs/issue16287.pdf", + "md5": "cd3e0859140465ae8b8bde0c95cb4929", + "rounds": 1, + "type": "eq", + "about": "Please note that this file currently renders incorrectly." + }, { "id": "issue2006", "file": "pdfs/issue2006.pdf", @@ -6470,6 +6508,14 @@ "type": "eq", "about": "Has a 4 bit per component image with mask and decode." }, + { + "id": "issue2770-text", + "file": "pdfs/issue2770.pdf", + "md5": "36070d756d06eaa35c2227efb069fb1b", + "rounds": 1, + "link": true, + "type": "text" + }, { "id": "issue2984", "file": "pdfs/issue2984.pdf", @@ -10012,6 +10058,7 @@ "id": "issue17779", "file": "pdfs/issue17779.pdf", "md5": "764b72e8e56e22662b321b308254fd2b", + "talos": false, "rounds": 1, "link": true, "type": "eq" @@ -10088,5 +10135,40 @@ "md5": "73922be020083d54747af18a4d5e0768", "rounds": 1, "type": "eq" + }, + { + "id": "bug1539074_1", + "file": "pdfs/bug1539074.1.pdf", + "md5": "d15c49142fda433323d3d35f2762cd33", + "rounds": 1, + "type": "eq" + }, + { + "id": "bug1903731", + "file": "pdfs/bug1903731.pdf", + "md5": "c90d1b03a62d0221e5f5609e3db16a38", + "rounds": 1, + "link": true, + "type": "eq" + }, + { + "id": "bug1905623", + "file": "pdfs/bug1905623.pdf", + "md5": "6c180a4353bda6b3cb0c693c5e5b32c4", + "rounds": 1, + "link": true, + "firstPage": 2, + "lastPage": 2, + "type": "eq" + }, + { + "id": "issue18444", + "file": "pdfs/issue18444.pdf", + "md5": "233b7b72d67133044338a728d52d58a9", + "rounds": 1, + "link": true, + "firstPage": 1, + "lastPage": 1, + "type": "eq" } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index f4828f3d91096..8f91754741270 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1768,7 +1768,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(3); @@ -2523,7 +2522,6 @@ describe("annotation", function () { checkboxEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(5); @@ -2584,7 +2582,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList1.argsArray.length).toEqual(3); @@ -2608,7 +2605,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList2.argsArray.length).toEqual(3); @@ -2670,7 +2666,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(3); @@ -2732,7 +2727,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(3); @@ -2986,7 +2980,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList1.argsArray.length).toEqual(3); @@ -3010,7 +3003,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList2.argsArray.length).toEqual(3); @@ -3070,7 +3062,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, annotationStorage ); expect(opList.argsArray.length).toEqual(3); @@ -4242,7 +4233,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, null ); @@ -4267,6 +4257,46 @@ describe("annotation", function () { ]); }); + it("should update an existing FreeText annotation", async function () { + const freeTextDict = new Dict(); + freeTextDict.set("Type", Name.get("Annot")); + freeTextDict.set("Subtype", Name.get("FreeText")); + freeTextDict.set("CreationDate", "D:20190423"); + freeTextDict.set("Foo", Name.get("Bar")); + + const freeTextRef = Ref.get(143, 0); + partialEvaluator.xref = new XRefMock([ + { ref: freeTextRef, data: freeTextDict }, + ]); + + const task = new WorkerTask("test FreeText update"); + const data = await AnnotationFactory.saveNewAnnotations( + partialEvaluator, + task, + [ + { + annotationType: AnnotationEditorType.FREETEXT, + rect: [12, 34, 56, 78], + rotation: 0, + fontSize: 10, + color: [0, 0, 0], + value: "Hello PDF.js World !", + id: "143R", + ref: freeTextRef, + }, + ] + ); + + const base = data.annotations[0].data.replaceAll(/\(D:\d+\)/g, "(date)"); + expect(base).toEqual( + "143 0 obj\n" + + "<< /Type /Annot /Subtype /FreeText /CreationDate (date) /Foo /Bar /M (date) " + + "/Rect [12 34 56 78] /DA (/Helv 10 Tf 0 g) /Contents (Hello PDF.js World !) " + + "/F 4 /Border [0 0 0] /Rotate 0 /AP << /N 2 0 R>>>>\n" + + "endobj\n" + ); + }); + it("should extract the text from a FreeText annotation", async function () { partialEvaluator.xref = new XRefMock(); const task = new WorkerTask("test FreeText text extraction"); @@ -4503,7 +4533,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, null ); @@ -4672,7 +4701,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, null ); @@ -4791,7 +4819,6 @@ describe("annotation", function () { partialEvaluator, task, RenderingIntentFlag.PRINT, - false, null ); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 44f9de06b9373..680ec6a9eb080 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1901,6 +1901,92 @@ describe("api", function () { await loadingTask.destroy(); }); + it("gets outline, with /XYZ destinations that lack zoom parameter (issue 18408)", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("issue18408_reduced.pdf") + ); + const pdfDoc = await loadingTask.promise; + const outline = await pdfDoc.getOutline(); + + expect(outline).toEqual([ + { + action: null, + attachment: undefined, + dest: [{ num: 14, gen: 0 }, { name: "XYZ" }, 65, 705], + url: null, + unsafeUrl: undefined, + newWindow: undefined, + setOCGState: undefined, + title: "Page 1", + color: new Uint8ClampedArray([0, 0, 0]), + count: undefined, + bold: false, + italic: false, + items: [], + }, + { + action: null, + attachment: undefined, + dest: [{ num: 13, gen: 0 }, { name: "XYZ" }, 60, 710], + url: null, + unsafeUrl: undefined, + newWindow: undefined, + setOCGState: undefined, + title: "Page 2", + color: new Uint8ClampedArray([0, 0, 0]), + count: undefined, + bold: false, + italic: false, + items: [], + }, + ]); + + await loadingTask.destroy(); + }); + + it("gets outline, with /FitH destinations that lack coordinate parameter (bug 1907000)", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("bug1907000_reduced.pdf") + ); + const pdfDoc = await loadingTask.promise; + const outline = await pdfDoc.getOutline(); + + expect(outline).toEqual([ + { + action: null, + attachment: undefined, + dest: [{ num: 14, gen: 0 }, { name: "FitH" }], + url: null, + unsafeUrl: undefined, + newWindow: undefined, + setOCGState: undefined, + title: "Page 1", + color: new Uint8ClampedArray([0, 0, 0]), + count: undefined, + bold: false, + italic: false, + items: [], + }, + { + action: null, + attachment: undefined, + dest: [{ num: 13, gen: 0 }, { name: "FitH" }], + url: null, + unsafeUrl: undefined, + newWindow: undefined, + setOCGState: undefined, + title: "Page 2", + color: new Uint8ClampedArray([0, 0, 0]), + count: undefined, + bold: false, + italic: false, + items: [], + }, + ]); + + await loadingTask.destroy(); + }); + it("gets non-existent permissions", async function () { const permissions = await pdfDocument.getPermissions(); expect(permissions).toEqual(null); @@ -3419,6 +3505,21 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`) await loadingTask.destroy(); }); + it("gets text content, correctly handling documents with toUnicode cmaps that omit leading zeros on hex-encoded UTF-16", async function () { + const loadingTask = getDocument( + buildGetDocumentParams("issue18099_reduced.pdf") + ); + const pdfDoc = await loadingTask.promise; + const pdfPage = await pdfDoc.getPage(1); + const { items } = await pdfPage.getTextContent({ + disableNormalization: true, + }); + const text = mergeText(items); + expect(text).toEqual("Hello world!"); + + await loadingTask.destroy(); + }); + it("gets text content, and check that out-of-page text is not present (bug 1755201)", async function () { if (isNodeJS) { pending("Linked test-cases are not supported in Node.js."); @@ -4155,7 +4256,7 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`) checkedCopyLocalImage = true; // Ensure that the image was copied in the main-thread, rather // than being re-parsed in the worker-thread (which is slower). - expect(statsOverall).toBeLessThan(firstStatsOverall / 4); + expect(statsOverall).toBeLessThan(firstStatsOverall / 2); } } } diff --git a/test/unit/pdf_find_controller_spec.js b/test/unit/pdf_find_controller_spec.js index b89316e5b806b..a6b5c0bad4c2c 100644 --- a/test/unit/pdf_find_controller_spec.js +++ b/test/unit/pdf_find_controller_spec.js @@ -51,7 +51,8 @@ class MockLinkService extends SimpleLinkService { async function initPdfFindController( filename, - updateMatchesCountOnProgress = true + updateMatchesCountOnProgress = true, + matcher = undefined ) { const loadingTask = getDocument( buildGetDocumentParams(filename || tracemonkeyFileName, { @@ -69,6 +70,7 @@ async function initPdfFindController( linkService, eventBus, updateMatchesCountOnProgress, + matcher, }); pdfFindController.setDocument(pdfDocument); // Enable searching. @@ -1022,4 +1024,112 @@ describe("pdf_find_controller", function () { pageMatchesLength: [[1, 1, 1, 1, 1, 1, 1, 1, 1]], }); }); + + it("dispatches updatefindcontrolstate with correct properties", async function () { + const testOnFind = ({ eventBus }) => + new Promise(function (resolve) { + const eventState = { + source: this, + type: "", + query: "Foo", + caseSensitive: true, + entireWord: true, + findPrevious: false, + matchDiacritics: false, + }; + eventBus.dispatch("find", eventState); + + eventBus.on("updatefindcontrolstate", function (evt) { + expect(evt).toEqual( + jasmine.objectContaining({ + state: FindState.NOT_FOUND, + previous: false, + entireWord: true, + matchesCount: { current: 0, total: 0 }, + rawQuery: "Foo", + }) + ); + resolve(); + }); + }); + + const { eventBus } = await initPdfFindController(); + await testOnFind({ eventBus }); + }); + + describe("custom matcher", () => { + it("calls to the matcher with the right arguments", async () => { + const QUERY = "Foo bar"; + + const spy = jasmine + .createSpy("custom find matcher") + .and.callFake(() => [{ index: 0, length: 1 }]); + + const { eventBus, pdfFindController } = await initPdfFindController( + null, + false, + spy + ); + + const PAGES_COUNT = 14; + + await testSearch({ + eventBus, + pdfFindController, + state: { query: QUERY }, + selectedMatch: { pageIndex: 0, matchIndex: 0 }, + matchesPerPage: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + }); + + expect(spy).toHaveBeenCalledTimes(PAGES_COUNT); + + for (let i = 0; i < PAGES_COUNT; i++) { + const args = spy.calls.argsFor(i); + expect(args[0]).withContext(`page ${i}`).toBe(QUERY); + expect(args[2]).withContext(`page ${i}`).toBe(i); + } + + expect(spy.calls.argsFor(0)[1]).toMatch(/^Trace-based /); + expect(spy.calls.argsFor(1)[1]).toMatch(/^Hence, recording and /); + expect(spy.calls.argsFor(12)[1]).toMatch(/Figure 12. Fraction of time /); + expect(spy.calls.argsFor(13)[1]).toMatch(/^not be interpreted as /); + }); + + it("uses the results returned by the custom matcher", async () => { + const QUERY = "Foo bar"; + + // prettier-ignore + const spy = jasmine.createSpy("custom find matcher") + .and.returnValue(undefined) + .withArgs(QUERY, jasmine.anything(), 0) + .and.returnValue([ + { index: 20, length: 3 }, + { index: 50, length: 8 }, + ]) + .withArgs(QUERY, jasmine.anything(), 2) + .and.returnValue([ + { index: 7, length: 19 } + ]) + .withArgs(QUERY, jasmine.anything(), 13) + .and.returnValue([ + { index: 50, length: 2 }, + { index: 54, length: 9 }, + { index: 80, length: 4 }, + ]); + + const { eventBus, pdfFindController } = await initPdfFindController( + null, + false, + spy + ); + + await testSearch({ + eventBus, + pdfFindController, + state: { query: QUERY }, + selectedMatch: { pageIndex: 0, matchIndex: 0 }, + matchesPerPage: [2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3], + }); + }); + }); }); diff --git a/test/unit/pdf_spec.js b/test/unit/pdf_spec.js index 6dfc1584fc478..7e2224c8de772 100644 --- a/test/unit/pdf_spec.js +++ b/test/unit/pdf_spec.js @@ -55,18 +55,13 @@ import { RenderingCancelledException, setLayerDimensions, } from "../../src/display/display_utils.js"; -import { - renderTextLayer, - TextLayer, - updateTextLayer, -} from "../../src/display/text_layer.js"; import { AnnotationEditorLayer } from "../../src/display/editor/annotation_editor_layer.js"; import { AnnotationEditorUIManager } from "../../src/display/editor/tools.js"; import { AnnotationLayer } from "../../src/display/annotation_layer.js"; import { ColorPicker } from "../../src/display/editor/color_picker.js"; import { DrawLayer } from "../../src/display/draw_layer.js"; import { GlobalWorkerOptions } from "../../src/display/worker_options.js"; -import { Outliner } from "../../src/display/editor/outliner.js"; +import { TextLayer } from "../../src/display/text_layer.js"; import { XfaLayer } from "../../src/display/xfa_layer.js"; const expectedAPI = Object.freeze({ @@ -98,7 +93,6 @@ const expectedAPI = Object.freeze({ noContextMenu, normalizeUnicode, OPS, - Outliner, PasswordResponses, PDFDataRangeTransport, PDFDateString, @@ -106,12 +100,10 @@ const expectedAPI = Object.freeze({ PermissionFlag, PixelsPerInch, RenderingCancelledException, - renderTextLayer, setLayerDimensions, shadow, TextLayer, UnexpectedResponseException, - updateTextLayer, Util, VerbosityLevel, version, diff --git a/test/unit/testreporter.js b/test/unit/testreporter.js index 924f79bf5e835..05abc51847961 100644 --- a/test/unit/testreporter.js +++ b/test/unit/testreporter.js @@ -58,12 +58,7 @@ const TestReporter = function (browser) { }; this.suiteStarted = function (result) { - // Normally suite starts don't have to be reported because the individual - // specs inside them are reported, but it can happen that the suite cannot - // start, for instance due to an uncaught exception in `beforeEach`. This - // is problematic because the specs inside the suite will never be found - // and run, so if we don't report the suite start failure here it would be - // ignored silently, leading to passing tests even though some did not run. + // Report on the result of `beforeAll` invocations. if (result.failedExpectations.length > 0) { let failedMessages = ""; for (const item of result.failedExpectations) { @@ -76,6 +71,7 @@ const TestReporter = function (browser) { this.specStarted = function (result) {}; this.specDone = function (result) { + // Report on the result of individual tests. if (result.failedExpectations.length === 0) { sendResult("TEST-PASSED", result.description); } else { @@ -87,7 +83,16 @@ const TestReporter = function (browser) { } }; - this.suiteDone = function (result) {}; + this.suiteDone = function (result) { + // Report on the result of `afterAll` invocations. + if (result.failedExpectations.length > 0) { + let failedMessages = ""; + for (const item of result.failedExpectations) { + failedMessages += `${item.message} `; + } + sendResult("TEST-UNEXPECTED-FAIL", result.description, failedMessages); + } + }; this.jasmineDone = function () { // Give the test runner some time process any queued requests. diff --git a/test/webserver.mjs b/test/webserver.mjs index 7df0ca4dc3889..d597aa2512d7d 100644 --- a/test/webserver.mjs +++ b/test/webserver.mjs @@ -14,6 +14,9 @@ * limitations under the License. */ +// PLEASE NOTE: This code is intended for development purposes only and +// should NOT be used in production environments. + import fs from "fs"; import fsPromises from "fs/promises"; import http from "http"; diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 0fa433e82771f..eb13c7e46c05e 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -112,6 +112,14 @@ font-size: calc(100px * var(--scale-factor)); transform-origin: 0 0; cursor: auto; + + .selectedEditor { + z-index: 100000 !important; + } + + &.drawing * { + pointer-events: none !important; + } } .annotationEditorLayer.waiting { diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index 481a3e1e42c8a..2b56d506cd86f 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -182,6 +182,10 @@ class AnnotationLayerBuilder { this.div.hidden = true; } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + #updatePresentationModeState(state) { if (!this.div) { return; diff --git a/web/app.js b/web/app.js index fecfabe7e10b5..02436ff4a9d98 100644 --- a/web/app.js +++ b/web/app.js @@ -57,7 +57,7 @@ import { version, } from "pdfjs-lib"; import { AppOptions, OptionKind } from "./app_options.js"; -import { AutomationEventBus, EventBus } from "./event_utils.js"; +import { EventBus, FirefoxEventBus } from "./event_utils.js"; import { ExternalServices, initCom, MLManager } from "web-external_services"; import { LinkTarget, PDFLinkService } from "./pdf_link_service.js"; import { AltTextManager } from "web-alt_text_manager"; @@ -87,7 +87,6 @@ import { Toolbar } from "web-toolbar"; import { ViewHistory } from "./view_history.js"; const FORCE_PAGES_LOADED_TIMEOUT = 10000; // ms -const WHEEL_ZOOM_DISABLED_TIMEOUT = 1000; // ms const ViewOnLoad = { UNKNOWN: -1, @@ -140,7 +139,7 @@ const PDFViewerApplication = { /** @type {OverlayManager} */ overlayManager: null, /** @type {Preferences} */ - preferences: null, + preferences: new Preferences(), /** @type {Toolbar} */ toolbar: null, /** @type {SecondaryToolbar} */ @@ -152,10 +151,10 @@ const PDFViewerApplication = { /** @type {AnnotationEditorParams} */ annotationEditorParams: null, isInitialViewSet: false, - downloadComplete: false, isViewerEmbedded: window.parent !== window, url: "", baseUrl: "", + mlManager: null, _downloadUrl: "", _eventBusAbortController: null, _windowAbortController: null, @@ -175,28 +174,13 @@ const PDFViewerApplication = { _printAnnotationStoragePromise: null, _touchInfo: null, _isCtrlKeyDown: false, - _nimbusDataPromise: null, _caretBrowsing: null, _isScrolling: false, // Called once when the document is loaded. async initialize(appConfig) { - let l10nPromise; - // In the (various) extension builds, where the locale is set automatically, - // initialize the `L10n`-instance as soon as possible. - if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) { - l10nPromise = this.externalServices.createL10n(); - } this.appConfig = appConfig; - if ( - typeof PDFJSDev === "undefined" - ? window.isGECKOVIEW - : PDFJSDev.test("GECKOVIEW") - ) { - this._nimbusDataPromise = this.externalServices.getNimbusExperimentData(); - } - // Ensure that `Preferences`, and indirectly `AppOptions`, have initialized // before creating e.g. the various viewer components. try { @@ -221,14 +205,17 @@ const PDFViewerApplication = { if (mode) { document.documentElement.classList.add(mode); } + } else { + // We want to load the image-to-text AI engine as soon as possible. + this.mlManager = new MLManager({ + enableAltText: AppOptions.get("enableAltText"), + altTextLearnMoreUrl: AppOptions.get("altTextLearnMoreUrl"), + }); } // Ensure that the `L10n`-instance has been initialized before creating // e.g. the various viewer components. - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { - l10nPromise = this.externalServices.createL10n(); - } - this.l10n = await l10nPromise; + this.l10n = await this.externalServices.createL10n(); document.getElementsByTagName("html")[0].dir = this.l10n.getDirection(); // Connect Fluent, when necessary, and translate what we already have. if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { @@ -387,9 +374,19 @@ const PDFViewerApplication = { async _initializeViewerComponents() { const { appConfig, externalServices, l10n } = this; - const eventBus = AppOptions.get("isInAutomation") - ? new AutomationEventBus() - : new EventBus(); + let eventBus; + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + eventBus = + AppOptions.eventBus = + this.mlManager.eventBus = + new FirefoxEventBus( + AppOptions.get("allowedGlobalEvents"), + externalServices, + AppOptions.get("isInAutomation") + ); + } else { + eventBus = new EventBus(); + } this.eventBus = eventBus; this.overlayManager = new OverlayManager(); @@ -465,6 +462,7 @@ const PDFViewerApplication = { enableHighlightFloatingButton: AppOptions.get( "enableHighlightFloatingButton" ), + enableUpdatedAddImage: AppOptions.get("enableUpdatedAddImage"), imageResourcesPath: AppOptions.get("imageResourcesPath"), enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"), @@ -509,10 +507,6 @@ const PDFViewerApplication = { if (appConfig.annotationEditorParams) { if (annotationEditorMode !== AnnotationEditorType.DISABLE) { - if (AppOptions.get("enableStampEditor")) { - appConfig.toolbar?.editorStampButton?.classList.remove("hidden"); - } - const editorHighlightButton = appConfig.toolbar?.editorHighlightButton; if (editorHighlightButton && AppOptions.get("enableHighlightEditor")) { editorHighlightButton.hidden = false; @@ -555,13 +549,16 @@ const PDFViewerApplication = { ? window.isGECKOVIEW : PDFJSDev.test("GECKOVIEW") ) { + const nimbusData = JSON.parse( + AppOptions.get("nimbusDataStr") || "null" + ); + this.toolbar = new Toolbar(appConfig.toolbar, eventBus, nimbusData); + } else { this.toolbar = new Toolbar( appConfig.toolbar, eventBus, - await this._nimbusDataPromise + AppOptions.get("toolbarDensity") ); - } else { - this.toolbar = new Toolbar(appConfig.toolbar, eventBus); } } @@ -642,7 +639,6 @@ const PDFViewerApplication = { }, async run(config) { - this.preferences = new Preferences(); await this.initialize(config); const { appConfig, eventBus } = this; @@ -743,14 +739,6 @@ const PDFViewerApplication = { return shadow(this, "externalServices", new ExternalServices()); }, - get mlManager() { - return shadow( - this, - "mlManager", - AppOptions.get("enableML") === true ? new MLManager() : null - ); - }, - get initialized() { return this._initializedCapability.settled; }, @@ -870,14 +858,12 @@ const PDFViewerApplication = { let title = getPdfFilenameFromUrl(url, ""); if (!title) { try { - title = decodeURIComponent(getFilenameFromUrl(url)) || url; + title = decodeURIComponent(getFilenameFromUrl(url)); } catch { - // decodeURIComponent may throw URIError, - // fall back to using the unprocessed url in that case - title = url; + // decodeURIComponent may throw URIError. } } - this.setTitle(title); + this.setTitle(title || url); // Always fallback to the raw URL. }, setTitle(title = this._title) { @@ -953,7 +939,6 @@ const PDFViewerApplication = { this.pdfLinkService.externalLinkEnabled = true; this.store = null; this.isInitialViewSet = false; - this.downloadComplete = false; this.url = ""; this.baseUrl = ""; this._downloadUrl = ""; @@ -1019,16 +1004,6 @@ const PDFViewerApplication = { AppOptions.set("docBaseUrl", this.baseUrl); } - // On Android, there is almost no chance to have the font we want so we - // don't use the system fonts in this case. - if ( - typeof PDFJSDev === "undefined" - ? window.isGECKOVIEW - : PDFJSDev.test("GECKOVIEW") - ) { - args.useSystemFonts = false; - } - // Set the necessary API parameters, using all the available options. const apiParams = AppOptions.getAll(OptionKind.API); const loadingTask = getDocument({ @@ -1083,12 +1058,9 @@ const PDFViewerApplication = { async download(options = {}) { let data; try { - if (this.downloadComplete) { - data = await this.pdfDocument.getData(); - } + data = await this.pdfDocument.getData(); } catch { - // When the PDF document isn't ready, or the PDF file is still - // downloading, simply download using the URL. + // When the PDF document isn't ready, simply download using the URL. } this.downloadManager.download( data, @@ -1199,17 +1171,12 @@ const PDFViewerApplication = { }, progress(level) { - if (!this.loadingBar || this.downloadComplete) { - // Don't accidentally show the loading bar again when the entire file has - // already been fetched (only an issue when disableAutoFetch is enabled). - return; - } const percent = Math.round(level * 100); // When we transition from full request to range requests, it's possible // that we discard some of the loaded data. This can cause the loading // bar to move backwards. So prevent this by only updating the bar if it // increases. - if (percent <= this.loadingBar.percent) { + if (!this.loadingBar || percent <= this.loadingBar.percent) { return; } this.loadingBar.percent = percent; @@ -1232,7 +1199,6 @@ const PDFViewerApplication = { pdfDocument.getDownloadInfo().then(({ length }) => { this._contentLength = length; // Ensure that the correct length is used. - this.downloadComplete = true; this.loadingBar?.hide(); firstPagePromise.then(() => { @@ -1953,6 +1919,7 @@ const PDFViewerApplication = { { signal } ); eventBus._on("reporttelemetry", webViewerReportTelemetry, { signal }); + eventBus._on("setpreference", webViewerSetPreference, { signal }); } }, @@ -1982,9 +1949,6 @@ const PDFViewerApplication = { } addWindowResolutionChange(); - window.addEventListener("visibilitychange", webViewerVisibilityChange, { - signal, - }); window.addEventListener("wheel", webViewerWheel, { passive: false, signal, @@ -2105,14 +2069,21 @@ const PDFViewerApplication = { unbindWindowEvents() { this._windowAbortController?.abort(); this._windowAbortController = null; - if ( - (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) || - AppOptions.get("isInAutomation") - ) { - this._globalAbortController?.abort(); - this._globalAbortController = null; - this.l10n?.pause(); - } + }, + + /** + * @ignore + */ + async testingClose() { + this.unbindEvents(); + this.unbindWindowEvents(); + + this._globalAbortController?.abort(); + this._globalAbortController = null; + + this.findBar?.close(); + + await Promise.all([this.l10n?.destroy(), this.close()]); }, _accumulateTicks(ticks, prop) { @@ -2508,6 +2479,7 @@ function webViewerUpdateFindMatchesCount({ matchesCount }) { function webViewerUpdateFindControlState({ state, previous, + entireWord, matchesCount, rawQuery, }) { @@ -2515,6 +2487,7 @@ function webViewerUpdateFindControlState({ PDFViewerApplication.externalServices.updateFindControlState({ result: state, findPrevious: previous, + entireWord, matchesCount, rawQuery, }); @@ -2562,23 +2535,6 @@ function webViewerResolutionChange(evt) { PDFViewerApplication.pdfViewer.refresh(); } -function webViewerVisibilityChange(evt) { - if (document.visibilityState === "visible") { - // Ignore mouse wheel zooming during tab switches (bug 1503412). - setZoomDisabledTimeout(); - } -} - -let zoomDisabledTimeout = null; -function setZoomDisabledTimeout() { - if (zoomDisabledTimeout) { - clearTimeout(zoomDisabledTimeout); - } - zoomDisabledTimeout = setTimeout(function () { - zoomDisabledTimeout = null; - }, WHEEL_ZOOM_DISABLED_TIMEOUT); -} - function webViewerWheel(evt) { const { pdfViewer, @@ -2631,7 +2587,6 @@ function webViewerWheel(evt) { // NOTE: this check must be placed *after* preventDefault. if ( PDFViewerApplication._isScrolling || - zoomDisabledTimeout || document.visibilityState === "hidden" || PDFViewerApplication.overlayManager.active ) { @@ -3189,4 +3144,8 @@ function webViewerReportTelemetry({ details }) { PDFViewerApplication.externalServices.reportTelemetry(details); } +function webViewerSetPreference({ name, value }) { + PDFViewerApplication.preferences.set(name, value); +} + export { PDFViewerApplication }; diff --git a/web/app_options.js b/web/app_options.js index ac24bf1505873..001fa72ddb14c 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -15,7 +15,7 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // eslint-disable-next-line no-var - var compatibilityParams = Object.create(null); + var compatParams = new Map(); if ( typeof PDFJSDev !== "undefined" && PDFJSDev.test("LIB") && @@ -34,9 +34,17 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { // Limit canvas size to 5 mega-pixels on mobile. // Support: Android, iOS - (function checkCanvasSizeLimitation() { + (function () { if (isIOS || isAndroid) { - compatibilityParams.maxCanvasPixels = 5242880; + compatParams.set("maxCanvasPixels", 5242880); + } + })(); + + // Don't use system fonts on Android (issue 18210). + // Support: Android + (function () { + if (isAndroid) { + compatParams.set("useSystemFonts", false); } })(); } @@ -46,6 +54,8 @@ const OptionKind = { VIEWER: 0x02, API: 0x04, WORKER: 0x08, + EVENT_DISPATCH: 0x10, + UNDEF_ALLOWED: 0x20, PREFERENCE: 0x80, }; @@ -55,6 +65,11 @@ const OptionKind = { * primitive types and cannot rely on any imported types. */ const defaultOptions = { + allowedGlobalEvents: { + /** @type {Object} */ + value: null, + kind: OptionKind.BROWSER, + }, canvasMaxAreaInBytes: { /** @type {number} */ value: -1, @@ -65,6 +80,16 @@ const defaultOptions = { value: false, kind: OptionKind.BROWSER, }, + localeProperties: { + /** @type {Object} */ + value: null, + kind: OptionKind.BROWSER, + }, + nimbusDataStr: { + /** @type {string} */ + value: "", + kind: OptionKind.BROWSER, + }, supportsCaretBrowsingMode: { /** @type {boolean} */ value: false, @@ -95,7 +120,20 @@ const defaultOptions = { value: true, kind: OptionKind.BROWSER, }, + toolbarDensity: { + /** @type {number} */ + value: 0, // 0 = "normal", 1 = "compact", 2 = "touch" + kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH, + }, + altTextLearnMoreUrl: { + /** @type {string} */ + value: + typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL") + ? "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/pdf-alt-text" + : "", + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, annotationEditorMode: { /** @type {number} */ value: 0, @@ -136,6 +174,11 @@ const defaultOptions = { value: false, kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + enableAltText: { + /** @type {boolean} */ + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, enableHighlightEditor: { // We'll probably want to make some experiments before enabling this // in Firefox release, but it has to be temporary. @@ -152,11 +195,6 @@ const defaultOptions = { value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"), kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, - enableML: { - /** @type {boolean} */ - value: false, - kind: OptionKind.VIEWER + OptionKind.PREFERENCE, - }, enablePermissions: { /** @type {boolean} */ value: false, @@ -172,12 +210,12 @@ const defaultOptions = { value: typeof PDFJSDev === "undefined" || !PDFJSDev.test("CHROME"), kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, - enableStampEditor: { + enableUpdatedAddImage: { // We'll probably want to make some experiments before enabling this // in Firefox release, but it has to be temporary. // TODO: remove it when unnecessary. /** @type {boolean} */ - value: true, + value: false, kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, externalLinkRel: { @@ -356,6 +394,19 @@ const defaultOptions = { : "../web/standard_fonts/", kind: OptionKind.API, }, + useSystemFonts: { + // On Android, there is almost no chance to have the font we want so we + // don't use the system fonts in this case (bug 1882613). + /** @type {boolean|undefined} */ + value: ( + typeof PDFJSDev === "undefined" + ? window.isGECKOVIEW + : PDFJSDev.test("GECKOVIEW") + ) + ? false + : undefined, + kind: OptionKind.API + OptionKind.UNDEF_ALLOWED, + }, verbosity: { /** @type {number} */ value: 1, @@ -421,13 +472,12 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { }; } -const userOptions = Object.create(null); +const userOptions = new Map(); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { - // Apply any compatibility-values to the user-options, - // see also `AppOptions.remove` below. - for (const name in compatibilityParams) { - userOptions[name] = compatibilityParams[name]; + // Apply any compatibility-values to the user-options. + for (const [name, value] of compatParams) { + userOptions.set(name, value); } } @@ -443,10 +493,12 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) { if (kind & OptionKind.BROWSER) { throw new Error(`Cannot mix "PREFERENCE" and "BROWSER" kind: ${name}`); } - if ( - typeof compatibilityParams === "object" && - compatibilityParams[name] !== undefined - ) { + if (kind & OptionKind.UNDEF_ALLOWED) { + throw new Error( + `Cannot allow \`undefined\` value for "PREFERENCE" kind: ${name}` + ); + } + if (typeof compatParams === "object" && compatParams.has(name)) { throw new Error( `Should not have compatibility-value for "PREFERENCE" kind: ${name}` ); @@ -459,74 +511,127 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) { ) { throw new Error(`Invalid value for "PREFERENCE" kind: ${name}`); } + } else if (kind & OptionKind.BROWSER) { + if (kind & OptionKind.UNDEF_ALLOWED) { + throw new Error( + `Cannot allow \`undefined\` value for "BROWSER" kind: ${name}` + ); + } + if (typeof compatParams === "object" && compatParams.has(name)) { + throw new Error( + `Should not have compatibility-value for "BROWSER" kind: ${name}` + ); + } + if (value === undefined) { + throw new Error(`Invalid value for "BROWSER" kind: ${name}`); + } } } } class AppOptions { + static eventBus; + constructor() { throw new Error("Cannot initialize AppOptions."); } static get(name) { - return userOptions[name] ?? defaultOptions[name]?.value ?? undefined; + return userOptions.has(name) + ? userOptions.get(name) + : defaultOptions[name]?.value; } static getAll(kind = null, defaultOnly = false) { const options = Object.create(null); for (const name in defaultOptions) { - const defaultOption = defaultOptions[name]; + const defaultOpt = defaultOptions[name]; - if (kind && !(kind & defaultOption.kind)) { + if (kind && !(kind & defaultOpt.kind)) { continue; } - options[name] = defaultOnly - ? defaultOption.value - : userOptions[name] ?? defaultOption.value; + options[name] = + !defaultOnly && userOptions.has(name) + ? userOptions.get(name) + : defaultOpt.value; } return options; } static set(name, value) { - userOptions[name] = value; + const defaultOpt = defaultOptions[name]; + + if ( + !defaultOpt || + !( + typeof value === typeof defaultOpt.value || + (defaultOpt.kind & OptionKind.UNDEF_ALLOWED && + (value === undefined || defaultOpt.value === undefined)) + ) + ) { + return; + } + userOptions.set(name, value); } - static setAll(options, init = false) { - if ((typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && init) { - if (this.get("disablePreferences")) { - // Give custom implementations of the default viewer a simpler way to - // opt-out of having the `Preferences` override existing `AppOptions`. - return; + static setAll(options, prefs = false) { + let events; + + for (const name in options) { + const defaultOpt = defaultOptions[name], + userOpt = options[name]; + + if ( + !defaultOpt || + !( + typeof userOpt === typeof defaultOpt.value || + (defaultOpt.kind & OptionKind.UNDEF_ALLOWED && + (userOpt === undefined || defaultOpt.value === undefined)) + ) + ) { + continue; } - for (const name in userOptions) { - // Ignore any compatibility-values in the user-options. - if (compatibilityParams[name] !== undefined) { + if (prefs) { + const { kind } = defaultOpt; + + if (!(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)) { continue; } - console.warn( - "setAll: The Preferences may override manually set AppOptions; " + - 'please use the "disablePreferences"-option in order to prevent that.' - ); - break; + if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) { + (events ||= new Map()).set(name, userOpt); + } } + userOptions.set(name, userOpt); } - for (const name in options) { - userOptions[name] = options[name]; + if (events) { + for (const [name, value] of events) { + this.eventBus.dispatch(name.toLowerCase(), { source: this, value }); + } } } +} - static remove(name) { - delete userOptions[name]; - - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { - // Re-apply a compatibility-value, if it exists, to the user-options. - const val = compatibilityParams[name]; - if (val !== undefined) { - userOptions[name] = val; +if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { + AppOptions._checkDisablePreferences = () => { + if (AppOptions.get("disablePreferences")) { + // Give custom implementations of the default viewer a simpler way to + // opt-out of having the `Preferences` override existing `AppOptions`. + return true; + } + for (const [name] of userOptions) { + // Ignore any compatibility-values in the user-options. + if (compatParams.has(name)) { + continue; } + console.warn( + "The Preferences may override manually set AppOptions; " + + 'please use the "disablePreferences"-option to prevent that.' + ); + break; } - } + return false; + }; } export { AppOptions, OptionKind }; diff --git a/web/chromecom.js b/web/chromecom.js index 7f26b70dbccc3..b33e445377cf0 100644 --- a/web/chromecom.js +++ b/web/chromecom.js @@ -436,6 +436,10 @@ class ExternalServices extends BaseExternalServices { } class MLManager { + isEnabledFor(_name) { + return false; + } + async guess() { return null; } diff --git a/web/event_utils.js b/web/event_utils.js index 29c30166e9f3c..dfca497d53db9 100644 --- a/web/event_utils.js +++ b/web/event_utils.js @@ -170,35 +170,57 @@ class EventBus { } /** - * NOTE: Only used to support various PDF viewer tests in `mozilla-central`. + * NOTE: Only used in the Firefox build-in pdf viewer. */ -class AutomationEventBus extends EventBus { +class FirefoxEventBus extends EventBus { + #externalServices; + + #globalEventNames; + + #isInAutomation; + + constructor(globalEventNames, externalServices, isInAutomation) { + super(); + this.#globalEventNames = globalEventNames; + this.#externalServices = externalServices; + this.#isInAutomation = isInAutomation; + } + dispatch(eventName, data) { if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("MOZCENTRAL")) { - throw new Error("Not implemented: AutomationEventBus.dispatch"); + throw new Error("Not implemented: FirefoxEventBus.dispatch"); } super.dispatch(eventName, data); - const detail = Object.create(null); - if (data) { - for (const key in data) { - const value = data[key]; - if (key === "source") { - if (value === window || value === document) { - return; // No need to re-dispatch (already) global events. + if (this.#isInAutomation) { + const detail = Object.create(null); + if (data) { + for (const key in data) { + const value = data[key]; + if (key === "source") { + if (value === window || value === document) { + return; // No need to re-dispatch (already) global events. + } + continue; // Ignore the `source` property. } - continue; // Ignore the `source` property. + detail[key] = value; } - detail[key] = value; } + const event = new CustomEvent(eventName, { + bubbles: true, + cancelable: true, + detail, + }); + document.dispatchEvent(event); + } + + if (this.#globalEventNames?.has(eventName)) { + this.#externalServices.dispatchGlobalEvent({ + eventName, + detail: data, + }); } - const event = new CustomEvent(eventName, { - bubbles: true, - cancelable: true, - detail, - }); - document.dispatchEvent(event); } } -export { AutomationEventBus, EventBus, waitOnEventOrTimeout, WaitOnType }; +export { EventBus, FirefoxEventBus, waitOnEventOrTimeout, WaitOnType }; diff --git a/web/external_services.js b/web/external_services.js index fa1f086b484f1..55fbb707d1d1d 100644 --- a/web/external_services.js +++ b/web/external_services.js @@ -45,7 +45,7 @@ class BaseExternalServices { throw new Error("Not implemented: updateEditorStates"); } - async getNimbusExperimentData() {} + dispatchGlobalEvent(_event) {} } export { BaseExternalServices }; diff --git a/web/firefoxcom.js b/web/firefoxcom.js index babd142ee0eea..8f84d6b9f5c04 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -14,6 +14,7 @@ */ import { isPdfFile, PDFDataRangeTransport } from "pdfjs-lib"; +import { AppOptions } from "./app_options.js"; import { BaseExternalServices } from "./external_services.js"; import { BasePreferences } from "./preferences.js"; import { DEFAULT_SCALE_VALUE } from "./ui_utils.js"; @@ -150,6 +151,10 @@ class Preferences extends BasePreferences { async _readFromStorage(prefObj) { return FirefoxCom.requestAsync("getPreferences", prefObj); } + + async _writeToStorage(prefObj) { + return FirefoxCom.requestAsync("setPreferences", prefObj); + } } (function listenFindEvents() { @@ -303,9 +308,64 @@ class FirefoxScripting { } class MLManager { + #enabled = null; + + eventBus = null; + + constructor(options) { + this.enable({ ...options, listenToProgress: false }); + } + + async isEnabledFor(name) { + return !!(await this.#enabled?.get(name)); + } + + deleteModel(service) { + return FirefoxCom.requestAsync("mlDelete", service); + } + guess(data) { return FirefoxCom.requestAsync("mlGuess", data); } + + enable({ altTextLearnMoreUrl, enableAltText, listenToProgress }) { + if (enableAltText) { + this.#loadAltTextEngine(listenToProgress); + } + // The `altTextLearnMoreUrl` is used to provide a link to the user to learn + // more about the "alt text" feature. + // The link is used in the Alt Text dialog or in the Image Settings. + this.altTextLearnMoreUrl = altTextLearnMoreUrl; + } + + async #loadAltTextEngine(listenToProgress) { + if (this.#enabled?.has("altText")) { + // We already have a promise for the "altText" service. + return; + } + const promise = FirefoxCom.requestAsync("loadAIEngine", { + service: "moz-image-to-text", + listenToProgress, + }); + (this.#enabled ||= new Map()).set("altText", promise); + if (listenToProgress) { + const callback = ({ detail }) => { + this.eventBus.dispatch("loadaiengineprogress", { + source: this, + detail, + }); + if (detail.finished) { + window.removeEventListener("loadAIEngineProgress", callback); + } + }; + window.addEventListener("loadAIEngineProgress", callback); + promise.then(ok => { + if (!ok) { + window.removeEventListener("loadAIEngineProgress", callback); + } + }); + } + } } class ExternalServices extends BaseExternalServices { @@ -386,26 +446,16 @@ class ExternalServices extends BaseExternalServices { } async createL10n() { - const [localeProperties] = await Promise.all([ - FirefoxCom.requestAsync("getLocaleProperties", null), - document.l10n.ready, - ]); - return new L10n(localeProperties, document.l10n); + await document.l10n.ready; + return new L10n(AppOptions.get("localeProperties"), document.l10n); } createScripting() { return FirefoxScripting; } - async getNimbusExperimentData() { - if (!PDFJSDev.test("GECKOVIEW")) { - return null; - } - const nimbusData = await FirefoxCom.requestAsync( - "getNimbusExperimentData", - null - ); - return nimbusData && JSON.parse(nimbusData); + dispatchGlobalEvent(event) { + FirefoxCom.request("dispatchGlobalEvent", event); } } diff --git a/web/genericcom.js b/web/genericcom.js index 996051018f6f8..0e224dc247508 100644 --- a/web/genericcom.js +++ b/web/genericcom.js @@ -48,6 +48,14 @@ class ExternalServices extends BaseExternalServices { } class MLManager { + async isEnabledFor(_name) { + return false; + } + + async deleteModel(_service) { + return null; + } + async guess() { return null; } diff --git a/web/l10n.js b/web/l10n.js index 7909e43ca31c3..f86cb0fcc0182 100644 --- a/web/l10n.js +++ b/web/l10n.js @@ -23,6 +23,8 @@ class L10n { #dir; + #elements = new Set(); + #lang; #l10n; @@ -30,7 +32,7 @@ class L10n { constructor({ lang, isRTL }, l10n = null) { this.#lang = L10n.#fixupLangCode(lang); this.#l10n = l10n; - this.#dir = isRTL ?? L10n.#isRTL(this.#lang) ? "rtl" : "ltr"; + this.#dir = (isRTL ?? L10n.#isRTL(this.#lang)) ? "rtl" : "ltr"; } _setL10n(l10n) { @@ -69,6 +71,7 @@ class L10n { /** @inheritdoc */ async translate(element) { + this.#elements.add(element); try { this.#l10n.connectRoot(element); await this.#l10n.translateRoots(); @@ -77,6 +80,15 @@ class L10n { } } + /** @inheritdoc */ + async destroy() { + for (const element of this.#elements) { + this.#l10n.disconnectRoot(element); + } + this.#elements.clear(); + this.#l10n.pauseObserving(); + } + /** @inheritdoc */ pause() { this.#l10n.pauseObserving(); diff --git a/web/pdf_find_controller.js b/web/pdf_find_controller.js index 8b9f3d9aeda13..ecbee5bf7bb36 100644 --- a/web/pdf_find_controller.js +++ b/web/pdf_find_controller.js @@ -368,6 +368,23 @@ function getOriginalIndex(diffs, pos, len) { return [oldStart, oldLen]; } +/** + * @callback PDFFindMatcher + * @this {PDFFindController} + * @param {string | string[]} query - The search query. + * @param {string} pageContent - The text content of the page to search in. + * @param {number} pageIndex - The index of the page that is being processed. + * @returns {Promise | SingleFindMatch[] | undefined} An + * array of matches in the provided page. + */ + +/** + * @typedef {Object} SingleFindMatch + * @property {number} index - The start of the matched text in the page's string + * contents. + * @property {number} length - The length of the matched text. + */ + /** * @typedef {Object} PDFFindControllerOptions * @property {IPDFLinkService} linkService - The navigation/linking service. @@ -375,6 +392,8 @@ function getOriginalIndex(diffs, pos, len) { * @property {boolean} [updateMatchesCountOnProgress] - True if the matches * count must be updated on progress or only when the last page is reached. * The default value is `true`. + * @property {PDFFindMatcher} [matcher] - The function that will be used to + * run the search queries. */ /** @@ -387,13 +406,22 @@ class PDFFindController { #visitedPagesCount = 0; + /** @type {PDFFindMatcher} */ + #matcher = null; + /** * @param {PDFFindControllerOptions} options */ - constructor({ linkService, eventBus, updateMatchesCountOnProgress = true }) { + constructor({ + linkService, + eventBus, + updateMatchesCountOnProgress = true, + matcher = this.#defaultFindMatcher, + }) { this._linkService = linkService; this._eventBus = eventBus; this.#updateMatchesCountOnProgress = updateMatchesCountOnProgress; + this.#matcher = matcher; /** * Callback used to check if a `pageNumber` is currently visible. @@ -670,37 +698,6 @@ class PDFFindController { return true; } - #calculateRegExpMatch(query, entireWord, pageIndex, pageContent) { - const matches = (this._pageMatches[pageIndex] = []); - const matchesLength = (this._pageMatchesLength[pageIndex] = []); - if (!query) { - // The query can be empty because some chars like diacritics could have - // been stripped out. - return; - } - const diffs = this._pageDiffs[pageIndex]; - let match; - while ((match = query.exec(pageContent)) !== null) { - if ( - entireWord && - !this.#isEntireWord(pageContent, match.index, match[0].length) - ) { - continue; - } - - const [matchPos, matchLen] = getOriginalIndex( - diffs, - match.index, - match[0].length - ); - - if (matchLen) { - matches.push(matchPos); - matchesLength.push(matchLen); - } - } - } - #convertToRegExpString(query, hasDiacritics) { const { matchDiacritics } = this.#state; let isUnicode = false; @@ -771,13 +768,56 @@ class PDFFindController { return [isUnicode, query]; } - #calculateMatch(pageIndex) { - let query = this.#query; + async #calculateMatch(pageIndex) { + const query = this.#query; if (query.length === 0) { return; // Do nothing: the matches should be wiped out already. } - const { caseSensitive, entireWord } = this.#state; - const pageContent = this._pageContents[pageIndex]; + + const matcherResult = await this.#matcher( + query, + this._pageContents[pageIndex], + pageIndex + ); + + const matches = (this._pageMatches[pageIndex] = []); + const matchesLength = (this._pageMatchesLength[pageIndex] = []); + const diffs = this._pageDiffs[pageIndex]; + + matcherResult?.forEach(({ index, length }) => { + const [matchPos, matchLen] = getOriginalIndex(diffs, index, length); + + if (matchLen) { + matches.push(matchPos); + matchesLength.push(matchLen); + } + }); + + // When `highlightAll` is set, ensure that the matches on previously + // rendered (and still active) pages are correctly highlighted. + if (this.#state.highlightAll) { + this.#updatePage(pageIndex); + } + if (this._resumePageIdx === pageIndex) { + this._resumePageIdx = null; + this.#nextPageMatch(); + } + + // Update the match count. + const pageMatchesCount = this._pageMatches[pageIndex].length; + this._matchesCountTotal += pageMatchesCount; + if (this.#updateMatchesCountOnProgress) { + if (pageMatchesCount > 0) { + this.#updateUIResultsCount(); + } + } else if (++this.#visitedPagesCount === this._linkService.pagesCount) { + // For example, in GeckoView we want to have only the final update because + // the Java side provides only one object to update the counts. + this.#updateUIResultsCount(); + } + } + + #defaultFindMatcher(query, pageContent, pageIndex) { const hasDiacritics = this._hasDiacritics[pageIndex]; let isUnicode = false; @@ -799,34 +839,25 @@ class PDFFindController { }) .join("|"); } + if (!query) { + return; + } + const { caseSensitive, entireWord } = this.#state; const flags = `g${isUnicode ? "u" : ""}${caseSensitive ? "" : "i"}`; - query = query ? new RegExp(query, flags) : null; + query = new RegExp(query, flags); - this.#calculateRegExpMatch(query, entireWord, pageIndex, pageContent); + const matches = []; + for (const { index, 0: text } of pageContent.matchAll(query)) { + const { length } = text; + if (entireWord && !this.#isEntireWord(pageContent, index, length)) { + continue; + } - // When `highlightAll` is set, ensure that the matches on previously - // rendered (and still active) pages are correctly highlighted. - if (this.#state.highlightAll) { - this.#updatePage(pageIndex); - } - if (this._resumePageIdx === pageIndex) { - this._resumePageIdx = null; - this.#nextPageMatch(); + matches.push({ index, length }); } - // Update the match count. - const pageMatchesCount = this._pageMatches[pageIndex].length; - this._matchesCountTotal += pageMatchesCount; - if (this.#updateMatchesCountOnProgress) { - if (pageMatchesCount > 0) { - this.#updateUIResultsCount(); - } - } else if (++this.#visitedPagesCount === this._linkService.pagesCount) { - // For example, in GeckoView we want to have only the final update because - // the Java side provides only one object to update the counts. - this.#updateUIResultsCount(); - } + return matches; } #extractText() { @@ -930,10 +961,9 @@ class PDFFindController { continue; } this._pendingFindMatches.add(i); - this._extractTextPromises[i].then(() => { - this._pendingFindMatches.delete(i); - this.#calculateMatch(i); - }); + this._extractTextPromises[i] + .then(() => this.#calculateMatch(i)) + .finally(() => this._pendingFindMatches.delete(i)); } } @@ -1133,6 +1163,7 @@ class PDFFindController { source: this, state, previous, + entireWord: this.#state?.entireWord ?? null, matchesCount: this.#requestMatchesCount(), rawQuery: this.#state?.query ?? null, }); diff --git a/web/pdf_link_service.js b/web/pdf_link_service.js index 7dd8398053ff3..14ffb9c10134b 100644 --- a/web/pdf_link_service.js +++ b/web/pdf_link_service.js @@ -505,26 +505,27 @@ class PDFLinkService { if (!(typeof zoom === "object" && typeof zoom?.name === "string")) { return false; } + const argsLen = args.length; let allowNull = true; switch (zoom.name) { case "XYZ": - if (args.length !== 3) { + if (argsLen < 2 || argsLen > 3) { return false; } break; case "Fit": case "FitB": - return args.length === 0; + return argsLen === 0; case "FitH": case "FitBH": case "FitV": case "FitBV": - if (args.length !== 1) { + if (argsLen > 1) { return false; } break; case "FitR": - if (args.length !== 4) { + if (argsLen !== 4) { return false; } allowNull = false; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 00e550e836717..ddeca8d0b75ff 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -34,9 +34,9 @@ import { import { approximateFraction, DEFAULT_SCALE, + floorToDivide, OutputScale, RenderingStates, - roundToDivide, TextLayerMode, } from "./ui_utils.js"; import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js"; @@ -119,6 +119,8 @@ class PDFPageView { #hasRestrictedScaling = false; + #isEditing = false; + #layerProperties = null; #loadingId = null; @@ -354,6 +356,10 @@ class PDFPageView { this.pdfPage?.cleanup(); } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + get _textHighlighter() { return shadow( this, @@ -582,6 +588,20 @@ class PDFPageView { } } + toggleEditingMode(isEditing) { + if (!this.hasEditableAnnotations()) { + return; + } + this.#isEditing = isEditing; + this.reset({ + keepZoomLayer: true, + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + }); + } + /** * @typedef {Object} PDFPageViewUpdateParameters * @property {number} [scale] The new scale, if specified. @@ -1016,11 +1036,11 @@ class PDFPageView { const sfx = approximateFraction(outputScale.sx); const sfy = approximateFraction(outputScale.sy); - canvas.width = roundToDivide(width * outputScale.sx, sfx[0]); - canvas.height = roundToDivide(height * outputScale.sy, sfy[0]); + canvas.width = floorToDivide(width * outputScale.sx, sfx[0]); + canvas.height = floorToDivide(height * outputScale.sy, sfy[0]); const { style } = canvas; - style.width = roundToDivide(width, sfx[1]) + "px"; - style.height = roundToDivide(height, sfy[1]) + "px"; + style.width = floorToDivide(width, sfx[1]) + "px"; + style.height = floorToDivide(height, sfy[1]) + "px"; // Add the viewport so it's known what it was originally drawn with. this.#viewportMap.set(canvas, viewport); @@ -1037,6 +1057,7 @@ class PDFPageView { optionalContentConfigPromise: this._optionalContentConfigPromise, annotationCanvasMap: this._annotationCanvasMap, pageColors, + isEditing: this.#isEditing, }; const renderTask = (this.renderTask = pdfPage.render(renderContext)); renderTask.onContinue = renderContinueCallback; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 570fca03fdd56..7f3e46825fadc 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -219,10 +219,16 @@ class PDFViewer { #enablePermissions = false; + #enableUpdatedAddImage = false; + #eventAbortController = null; #mlManager = null; + #onPageRenderedCallback = null; + + #switchAnnotationEditorModeTimeoutId = null; + #getAllTextInProgress = false; #hiddenCopyElement = null; @@ -287,6 +293,7 @@ class PDFViewer { options.annotationEditorHighlightColors || null; this.#enableHighlightFloatingButton = options.enableHighlightFloatingButton === true; + this.#enableUpdatedAddImage = options.enableUpdatedAddImage === true; this.imageResourcesPath = options.imageResourcesPath || ""; this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { @@ -886,6 +893,7 @@ class PDFViewer { pageColors, this.#annotationEditorHighlightColors, this.#enableHighlightFloatingButton, + this.#enableUpdatedAddImage, this.#mlManager ); eventBus.dispatch("annotationeditoruimanager", { @@ -1117,6 +1125,8 @@ class PDFViewer { this.#hiddenCopyElement?.remove(); this.#hiddenCopyElement = null; + + this.#cleanupSwitchAnnotationEditorMode(); } #ensurePageViewVisible() { @@ -1653,6 +1663,32 @@ class PDFViewer { }); } + #switchToEditAnnotationMode() { + const visible = this._getVisiblePages(); + const pagesToRefresh = []; + const { ids, views } = visible; + for (const page of views) { + const { view } = page; + if (!view.hasEditableAnnotations()) { + ids.delete(view.id); + continue; + } + pagesToRefresh.push(page); + } + + if (pagesToRefresh.length === 0) { + return null; + } + this.renderingQueue.renderHighestPriority({ + first: pagesToRefresh[0], + last: pagesToRefresh.at(-1), + views: pagesToRefresh, + ids, + }); + + return ids; + } + containsElement(element) { return this.container.contains(element); } @@ -2229,6 +2265,17 @@ class PDFViewer { ]); } + #cleanupSwitchAnnotationEditorMode() { + if (this.#onPageRenderedCallback) { + this.eventBus._off("pagerendered", this.#onPageRenderedCallback); + this.#onPageRenderedCallback = null; + } + if (this.#switchAnnotationEditorModeTimeoutId !== null) { + clearTimeout(this.#switchAnnotationEditorModeTimeoutId); + this.#switchAnnotationEditorModeTimeoutId = null; + } + } + get annotationEditorMode() { return this.#annotationEditorUIManager ? this.#annotationEditorMode @@ -2259,13 +2306,48 @@ class PDFViewer { if (!this.pdfDocument) { return; } - this.#annotationEditorMode = mode; - this.eventBus.dispatch("annotationeditormodechanged", { - source: this, - mode, - }); - this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); + const { eventBus } = this; + const updater = () => { + this.#cleanupSwitchAnnotationEditorMode(); + this.#annotationEditorMode = mode; + this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode, + }); + }; + + if ( + mode === AnnotationEditorType.NONE || + this.#annotationEditorMode === AnnotationEditorType.NONE + ) { + const isEditing = mode !== AnnotationEditorType.NONE; + if (!isEditing) { + this.pdfDocument.annotationStorage.resetModifiedIds(); + } + for (const pageView of this._pages) { + pageView.toggleEditingMode(isEditing); + } + // We must call #switchToEditAnnotationMode unconditionally to ensure that + // page is rendered if it's useful or not. + const idsToRefresh = this.#switchToEditAnnotationMode(); + if (isEditing && idsToRefresh) { + // We're editing so we must switch to editing mode when the rendering is + // done. + this.#cleanupSwitchAnnotationEditorMode(); + this.#onPageRenderedCallback = ({ pageNumber }) => { + idsToRefresh.delete(pageNumber); + if (idsToRefresh.size === 0) { + this.#switchAnnotationEditorModeTimeoutId = setTimeout(updater, 0); + } + }; + const { signal } = this.#eventAbortController; + eventBus._on("pagerendered", this.#onPageRenderedCallback, { signal }); + return; + } + } + updater(); } // eslint-disable-next-line accessor-pairs diff --git a/web/pdfjs.js b/web/pdfjs.js index dd85896fd4d57..ad709d93676a1 100644 --- a/web/pdfjs.js +++ b/web/pdfjs.js @@ -42,7 +42,6 @@ const { noContextMenu, normalizeUnicode, OPS, - Outliner, PasswordResponses, PDFDataRangeTransport, PDFDateString, @@ -50,12 +49,10 @@ const { PermissionFlag, PixelsPerInch, RenderingCancelledException, - renderTextLayer, setLayerDimensions, shadow, TextLayer, UnexpectedResponseException, - updateTextLayer, Util, VerbosityLevel, version, @@ -91,7 +88,6 @@ export { noContextMenu, normalizeUnicode, OPS, - Outliner, PasswordResponses, PDFDataRangeTransport, PDFDateString, @@ -99,12 +95,10 @@ export { PermissionFlag, PixelsPerInch, RenderingCancelledException, - renderTextLayer, setLayerDimensions, shadow, TextLayer, UnexpectedResponseException, - updateTextLayer, Util, VerbosityLevel, version, diff --git a/web/preferences.js b/web/preferences.js index 15da2cc584440..d44e4a39a74f0 100644 --- a/web/preferences.js +++ b/web/preferences.js @@ -21,20 +21,12 @@ import { AppOptions, OptionKind } from "./app_options.js"; * or every time the viewer is loaded. */ class BasePreferences { - #browserDefaults = Object.freeze( - typeof PDFJSDev === "undefined" - ? AppOptions.getAll(OptionKind.BROWSER, /* defaultOnly = */ true) - : PDFJSDev.eval("BROWSER_PREFERENCES") - ); - #defaults = Object.freeze( typeof PDFJSDev === "undefined" ? AppOptions.getAll(OptionKind.PREFERENCE, /* defaultOnly = */ true) : PDFJSDev.eval("DEFAULT_PREFERENCES") ); - #prefs = Object.create(null); - #initializedPromise = null; constructor() { @@ -52,27 +44,25 @@ class BasePreferences { this.#initializedPromise = this._readFromStorage(this.#defaults).then( ({ browserPrefs, prefs }) => { - const options = Object.create(null); - - for (const [name, val] of Object.entries(this.#browserDefaults)) { - const prefVal = browserPrefs?.[name]; - options[name] = typeof prefVal === typeof val ? prefVal : val; - } - for (const [name, val] of Object.entries(this.#defaults)) { - const prefVal = prefs?.[name]; - // Ignore preferences whose types don't match the default values. - options[name] = this.#prefs[name] = - typeof prefVal === typeof val ? prefVal : val; - } - AppOptions.setAll(options, /* init = */ true); - - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - window.addEventListener("updatedPreference", evt => { - this.#updatePref(evt.detail); - }); + if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && + AppOptions._checkDisablePreferences() + ) { + return; } + AppOptions.setAll({ ...browserPrefs, ...prefs }, /* prefs = */ true); } ); + + if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { + window.addEventListener( + "updatedPreference", + async ({ detail: { name, value } }) => { + await this.#initializedPromise; + AppOptions.setAll({ [name]: value }, /* prefs = */ true); + } + ); + } } /** @@ -95,26 +85,6 @@ class BasePreferences { throw new Error("Not implemented: _readFromStorage"); } - #updatePref({ name, value }) { - if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { - throw new Error("Not implemented: #updatePref"); - } - - if (name in this.#browserDefaults) { - if (typeof value !== typeof this.#browserDefaults[name]) { - return; // Invalid preference value. - } - } else if (name in this.#defaults) { - if (typeof value !== typeof this.#defaults[name]) { - return; // Invalid preference value. - } - this.#prefs[name] = value; - } else { - return; // Invalid preference. - } - AppOptions.set(name, value); - } - /** * Reset the preferences to their default values and update storage. * @returns {Promise} A promise that is resolved when the preference values @@ -125,16 +95,9 @@ class BasePreferences { throw new Error("Please use `about:config` to change preferences."); } await this.#initializedPromise; - const oldPrefs = structuredClone(this.#prefs); - - this.#prefs = Object.create(null); - try { - await this._writeToStorage(this.#defaults); - } catch (reason) { - // Revert all preference values, since writing to storage failed. - this.#prefs = oldPrefs; - throw reason; - } + AppOptions.setAll(this.#defaults, /* prefs = */ true); + + await this._writeToStorage(this.#defaults); } /** @@ -145,41 +108,14 @@ class BasePreferences { * provided that the preference exists and the types match. */ async set(name, value) { - if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { - throw new Error("Please use `about:config` to change preferences."); - } await this.#initializedPromise; - const defaultValue = this.#defaults[name], - oldPrefs = structuredClone(this.#prefs); + AppOptions.setAll({ [name]: value }, /* prefs = */ true); - if (defaultValue === undefined) { - throw new Error(`Set preference: "${name}" is undefined.`); - } else if (value === undefined) { - throw new Error("Set preference: no value is specified."); - } - const valueType = typeof value, - defaultType = typeof defaultValue; - - if (valueType !== defaultType) { - if (valueType === "number" && defaultType === "string") { - value = value.toString(); - } else { - throw new Error( - `Set preference: "${value}" is a ${valueType}, expected a ${defaultType}.` - ); - } - } else if (valueType === "number" && !Number.isInteger(value)) { - throw new Error(`Set preference: "${value}" must be an integer.`); - } - - this.#prefs[name] = value; - try { - await this._writeToStorage(this.#prefs); - } catch (reason) { - // Revert all preference values, since writing to storage failed. - this.#prefs = oldPrefs; - throw reason; - } + await this._writeToStorage( + typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL") + ? { [name]: AppOptions.get(name) } + : AppOptions.getAll(OptionKind.PREFERENCE) + ); } /** @@ -193,12 +129,7 @@ class BasePreferences { throw new Error("Not implemented: get"); } await this.#initializedPromise; - const defaultValue = this.#defaults[name]; - - if (defaultValue === undefined) { - throw new Error(`Get preference: "${name}" is undefined.`); - } - return this.#prefs[name] ?? defaultValue; + return AppOptions.get(name); } get initializedPromise() { diff --git a/web/text_layer_builder.css b/web/text_layer_builder.css index 9e9f5b6fa6bed..0937130c9a974 100644 --- a/web/text_layer_builder.css +++ b/web/text_layer_builder.css @@ -96,10 +96,11 @@ } ::selection { + /* stylelint-disable declaration-block-no-duplicate-properties */ /*#if !MOZCENTRAL*/ background: rgba(0 0 255 / 0.25); /*#endif*/ - /* stylelint-disable-next-line declaration-block-no-duplicate-properties */ + /* stylelint-enable declaration-block-no-duplicate-properties */ background: color-mix(in srgb, AccentColor, transparent 75%); } diff --git a/web/toolbar.js b/web/toolbar.js index e92b546c4aad4..f37b5d4046d7e 100644 --- a/web/toolbar.js +++ b/web/toolbar.js @@ -50,8 +50,13 @@ class Toolbar { /** * @param {ToolbarOptions} options * @param {EventBus} eventBus + * @param {number} toolbarDensity - The toolbar density value. + * The possible values are: + * - 0 (default) - The regular toolbar size. + * - 1 (compact) - The small toolbar size. + * - 2 (touch) - The large toolbar size. */ - constructor(options, eventBus) { + constructor(options, eventBus, toolbarDensity = 0) { this.#opts = options; this.eventBus = eventBus; const buttons = [ @@ -136,9 +141,14 @@ class Toolbar { } }); + eventBus._on("toolbardensity", this.#updateToolbarDensity.bind(this)); + this.#updateToolbarDensity({ value: toolbarDensity }); + this.reset(); } + #updateToolbarDensity() {} + #setAnnotationEditorUIManager(uiManager, parentContainer) { const colorPicker = new ColorPicker({ uiManager }); uiManager.setMainHighlightColorPicker(colorPicker); diff --git a/web/ui_utils.js b/web/ui_utils.js index 19f2bd25744b8..08455659cf959 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -193,6 +193,11 @@ function watchScroll(viewAreaElement, callback, abortSignal = undefined) { useCapture: true, signal: abortSignal, }); + abortSignal?.addEventListener( + "abort", + () => window.cancelAnimationFrame(rAF), + { once: true } + ); return state; } @@ -263,6 +268,7 @@ function binarySearchFirstItem(items, condition, start = 0) { * @param {number} x - Positive float number. * @returns {Array} Estimated fraction: the first array item is a numerator, * the second one is a denominator. + * They are both natural numbers. */ function approximateFraction(x) { // Fast paths for int numbers or their inversions. @@ -309,9 +315,12 @@ function approximateFraction(x) { return result; } -function roundToDivide(x, div) { - const r = x % div; - return r === 0 ? x : Math.round(x - r + div); +/** + * @param {number} x - A positive number to round to a multiple of `div`. + * @param {number} div - A natural number. + */ +function floorToDivide(x, div) { + return x - (x % div); } /** @@ -731,7 +740,7 @@ class ProgressBar { } setDisableAutoFetch(delay = /* ms = */ 5000) { - if (isNaN(this.#percent)) { + if (this.#percent === 100 || isNaN(this.#percent)) { return; } if (this.#disableAutoFetchTimeout) { @@ -866,6 +875,7 @@ export { DEFAULT_SCALE_DELTA, DEFAULT_SCALE_VALUE, docStyle, + floorToDivide, getActiveOrFocusedElement, getPageSizeInches, getVisibleElements, @@ -884,7 +894,6 @@ export { ProgressBar, removeNullCharacters, RenderingStates, - roundToDivide, SCROLLBAR_PADDING, scrollIntoView, ScrollMode, diff --git a/web/viewer-geckoview.html b/web/viewer-geckoview.html index 7d0d936f74e18..dee94a04d6394 100644 --- a/web/viewer-geckoview.html +++ b/web/viewer-geckoview.html @@ -42,6 +42,12 @@ + +