mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-08-02 20:37:21 +02:00
Compare commits
No commits in common. "25c7d9eaace0438316714aff7033dd5f4c1a542e" and "bf9ae7622f723596f2ab4de19b115f544bed8e33" have entirely different histories.
25c7d9eaac
...
bf9ae7622f
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -24,13 +24,13 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: security-and-quality
|
queries: security-and-quality
|
||||||
|
|
||||||
- name: Autobuild CodeQL
|
- name: Autobuild CodeQL
|
||||||
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
|
|
||||||
- name: Perform CodeQL analysis
|
- name: Perform CodeQL analysis
|
||||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||||
|
|||||||
21
.github/workflows/integration_tests.yml
vendored
21
.github/workflows/integration_tests.yml
vendored
@ -41,7 +41,6 @@ jobs:
|
|||||||
skip: --noFirefox
|
skip: --noFirefox
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
environment: code-coverage
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@ -76,13 +75,13 @@ jobs:
|
|||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
run: Set-DisplayResolution -Width 1920 -Height 1080 -Force
|
run: Set-DisplayResolution -Width 1920 -Height 1080 -Force
|
||||||
|
|
||||||
- name: Run integration tests with code coverage (Windows)
|
- name: Run integration tests (Windows)
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
run: npx gulp integrationtest --coverage --coverage-output build/coverage/integration ${{ matrix.skip }}
|
run: npx gulp integrationtest ${{ matrix.skip }}
|
||||||
|
|
||||||
- name: Run integration tests with code coverage (Linux)
|
- name: Run integration tests (Linux)
|
||||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
run: xvfb-run -a --server-args="-screen 0, 1920x1080x24" npx gulp integrationtest --coverage --coverage-output build/coverage/integration ${{ matrix.skip }}
|
run: xvfb-run -a --server-args="-screen 0, 1920x1080x24" npx gulp integrationtest ${{ matrix.skip }}
|
||||||
|
|
||||||
- name: Save cached PDF files
|
- name: Save cached PDF files
|
||||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
@ -90,15 +89,3 @@ jobs:
|
|||||||
path: test/pdfs/*.pdf
|
path: test/pdfs/*.pdf
|
||||||
key: cached-pdf-files-${{ hashFiles('test/pdfs/*.pdf') }}
|
key: cached-pdf-files-${{ hashFiles('test/pdfs/*.pdf') }}
|
||||||
enableCrossOsArchive: true
|
enableCrossOsArchive: true
|
||||||
|
|
||||||
- name: Upload results to Codecov
|
|
||||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
fail_ci_if_error: true
|
|
||||||
files: ./build/coverage/integration/lcov.info
|
|
||||||
flags: integrationtest
|
|
||||||
name: codecov-umbrella
|
|
||||||
disable_search: true
|
|
||||||
disable_telem: true
|
|
||||||
verbose: true
|
|
||||||
|
|||||||
2
.github/workflows/notify-pdf-sync.yml
vendored
2
.github/workflows/notify-pdf-sync.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Generate app token
|
- name: Generate app token
|
||||||
if: steps.check.outputs.has_added == 'true'
|
if: steps.check.outputs.has_added == 'true'
|
||||||
id: app-token
|
id: app-token
|
||||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.CLIENT_ID }}
|
client-id: ${{ secrets.CLIENT_ID }}
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|||||||
2
.github/workflows/update_locales.yml
vendored
2
.github/workflows/update_locales.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate app token
|
- name: Generate app token
|
||||||
id: app-token
|
id: app-token
|
||||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.CLIENT_ID }}
|
client-id: ${{ secrets.CLIENT_ID }}
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
flag_management:
|
|
||||||
default_rules:
|
|
||||||
carryforward: true
|
|
||||||
0
external/iccs/CGATS001Compat-v2-micro.icc
vendored
Normal file → Executable file
0
external/iccs/CGATS001Compat-v2-micro.icc
vendored
Normal file → Executable file
95
gulpfile.mjs
95
gulpfile.mjs
@ -113,7 +113,6 @@ const BABEL_PRESET_ENV_OPTS = Object.freeze({
|
|||||||
|
|
||||||
const DEFINES = Object.freeze({
|
const DEFINES = Object.freeze({
|
||||||
SKIP_BABEL: true,
|
SKIP_BABEL: true,
|
||||||
WORKER_THREAD: false,
|
|
||||||
TESTING: undefined,
|
TESTING: undefined,
|
||||||
COVERAGE: undefined,
|
COVERAGE: undefined,
|
||||||
// The main build targets:
|
// The main build targets:
|
||||||
@ -537,12 +536,8 @@ function createSandboxBundle(defines, extraOptions = undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createWorkerBundle(defines) {
|
function createWorkerBundle(defines) {
|
||||||
const workerDefines = {
|
const workerFileConfig = createWebpackConfig(defines, {
|
||||||
...defines,
|
filename: defines.MINIFIED ? "pdf.worker.min.mjs" : "pdf.worker.mjs",
|
||||||
WORKER_THREAD: true,
|
|
||||||
};
|
|
||||||
const workerFileConfig = createWebpackConfig(workerDefines, {
|
|
||||||
filename: workerDefines.MINIFIED ? "pdf.worker.min.mjs" : "pdf.worker.mjs",
|
|
||||||
library: {
|
library: {
|
||||||
type: "module",
|
type: "module",
|
||||||
},
|
},
|
||||||
@ -1214,10 +1209,6 @@ function discardCommentsCSS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function preprocessHTML(source, defines) {
|
function preprocessHTML(source, defines) {
|
||||||
defines = {
|
|
||||||
...defines,
|
|
||||||
TESTING: defines.TESTING ?? process.env.TESTING === "true",
|
|
||||||
};
|
|
||||||
const outName = getTempFile("~preprocess", ".html");
|
const outName = getTempFile("~preprocess", ".html");
|
||||||
preprocess(source, outName, defines);
|
preprocess(source, outName, defines);
|
||||||
const out = fs.readFileSync(outName).toString();
|
const out = fs.readFileSync(outName).toString();
|
||||||
@ -2320,86 +2311,6 @@ gulp.task("lint-licenses", function (done) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task("lint-chmod", function (done) {
|
|
||||||
console.log("\n### Checking executable bit on tracked and untracked files");
|
|
||||||
|
|
||||||
// Files allowed to keep the executable bit (shebang scripts).
|
|
||||||
const EXECUTABLE_FILES = new Set(["test/chromium/test-telemetry.js"]);
|
|
||||||
|
|
||||||
// Cover untracked-but-not-ignored files too: a `gulp lint` run before
|
|
||||||
// `git add` would otherwise miss any 0755 file the developer just created.
|
|
||||||
// `-z` is NUL-separated to tolerate whitespace in paths; `--exclude-standard`
|
|
||||||
// honours .gitignore / .git/info/exclude / core.excludesFile.
|
|
||||||
let lsFiles;
|
|
||||||
try {
|
|
||||||
lsFiles = execSync("git ls-files -coz --exclude-standard", {
|
|
||||||
encoding: "utf8",
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
done(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offenders = [];
|
|
||||||
for (const file of lsFiles.split("\0")) {
|
|
||||||
if (!file || EXECUTABLE_FILES.has(file)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let stat;
|
|
||||||
try {
|
|
||||||
stat = fs.lstatSync(file);
|
|
||||||
} catch {
|
|
||||||
// Tracked file removed from the working tree, broken symlink, etc.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Skip symlinks (stored as mode 120000) and directories (submodules show
|
|
||||||
// up here as 160000 in the index, never as a regular file on disk).
|
|
||||||
if (!stat.isFile()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Match git's own heuristic in `ce_permissions`: only the owner execute
|
|
||||||
// bit matters when deciding between 100644 and 100755.
|
|
||||||
if (stat.mode & 0o100) {
|
|
||||||
offenders.push(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offenders.length === 0) {
|
|
||||||
console.log("files checked, no errors found");
|
|
||||||
done();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.argv.includes("--fix")) {
|
|
||||||
for (const file of offenders.sort()) {
|
|
||||||
console.log(` Unexpected executable bit: ${file}`);
|
|
||||||
}
|
|
||||||
done(
|
|
||||||
new Error(
|
|
||||||
"Executable-bit check failed (run `gulp lint-chmod --fix` to clear)."
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Working-tree-only fix, like every other --fix in `gulp lint`: clear the
|
|
||||||
// bit on disk and let the user stage the change. On filesystems with
|
|
||||||
// `core.filemode=false` (e.g. Windows) this is a no-op and the offending
|
|
||||||
// mode must be cleared with `git update-index --chmod=-x` instead.
|
|
||||||
for (const file of offenders.sort()) {
|
|
||||||
try {
|
|
||||||
const { mode } = fs.statSync(file);
|
|
||||||
fs.chmodSync(file, mode & ~0o111);
|
|
||||||
} catch (e) {
|
|
||||||
done(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(` cleared executable bit: ${file}`);
|
|
||||||
}
|
|
||||||
console.log(`done: ${offenders.length} file(s) updated`);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task("lint", function (done) {
|
gulp.task("lint", function (done) {
|
||||||
console.log("\n### Linting JS/CSS/JSON/SVG/HTML files");
|
console.log("\n### Linting JS/CSS/JSON/SVG/HTML files");
|
||||||
|
|
||||||
@ -2464,7 +2375,7 @@ gulp.task("lint", function (done) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
gulp.series("lint-licenses", "lint-chmod")(done);
|
gulp.task("lint-licenses")(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -661,58 +661,12 @@ pdfjs-views-manager-view-selector-button-label = Visninger
|
|||||||
pdfjs-views-manager-pages-title = Sider
|
pdfjs-views-manager-pages-title = Sider
|
||||||
pdfjs-views-manager-attachments-title = Vedhæftede filer
|
pdfjs-views-manager-attachments-title = Vedhæftede filer
|
||||||
pdfjs-views-manager-pages-option-label = Sider
|
pdfjs-views-manager-pages-option-label = Sider
|
||||||
pdfjs-views-manager-attachments-option-label = Vedhæftede filer
|
|
||||||
pdfjs-views-manager-layers-option-label = Lag
|
|
||||||
pdfjs-views-manager-add-file-button =
|
|
||||||
.title = Tilføj fil
|
|
||||||
pdfjs-views-manager-add-file-button-label = Tilføj fil
|
|
||||||
# Variables:
|
|
||||||
# $count (Number) - the number of selected pages.
|
|
||||||
pdfjs-views-manager-pages-status-action-label =
|
|
||||||
{ $count ->
|
|
||||||
[one] { $count } valgt
|
|
||||||
*[other] { $count } valgt
|
|
||||||
}
|
|
||||||
pdfjs-views-manager-pages-status-none-action-label = Vælg sider
|
|
||||||
pdfjs-views-manager-pages-status-action-button-label = Håndter
|
|
||||||
pdfjs-views-manager-pages-status-copy-button-label = Kopier
|
|
||||||
pdfjs-views-manager-pages-status-cut-button-label = Klip
|
|
||||||
pdfjs-views-manager-pages-status-delete-button-label = Slet
|
|
||||||
# Variables:
|
|
||||||
# $count (Number) - the number of selected pages to be cut.
|
|
||||||
pdfjs-views-manager-status-undo-cut-label =
|
|
||||||
{ $count ->
|
|
||||||
[one] 1 side klippet
|
|
||||||
*[other] { $count } sider klippet
|
|
||||||
}
|
|
||||||
# Variables:
|
|
||||||
# $count (Number) - the number of selected pages to be copied.
|
|
||||||
pdfjs-views-manager-pages-status-undo-copy-label =
|
|
||||||
{ $count ->
|
|
||||||
[one] 1 side kopieret
|
|
||||||
*[other] { $count } sider kopieret
|
|
||||||
}
|
|
||||||
# Variables:
|
|
||||||
# $count (Number) - the number of selected pages to be deleted.
|
|
||||||
pdfjs-views-manager-pages-status-undo-delete-label =
|
|
||||||
{ $count ->
|
|
||||||
[one] 1 side slettet
|
|
||||||
*[other] { $count } sider slettet
|
|
||||||
}
|
|
||||||
pdfjs-views-manager-status-undo-button-label = Fortryd
|
|
||||||
pdfjs-views-manager-status-done-button-label = Færdig
|
|
||||||
pdfjs-views-manager-status-close-button =
|
|
||||||
.title = Luk
|
|
||||||
pdfjs-views-manager-status-close-button-label = Luk
|
|
||||||
pdfjs-views-manager-paste-button-label = Indsæt
|
|
||||||
pdfjs-views-manager-paste-button-before =
|
pdfjs-views-manager-paste-button-before =
|
||||||
.title = Indsæt før første side
|
.title = Indsæt før første side
|
||||||
# Variables:
|
# Variables:
|
||||||
# $page (Number) - the page number after which the paste button is.
|
# $page (Number) - the page number after which the paste button is.
|
||||||
pdfjs-views-manager-paste-button-after =
|
pdfjs-views-manager-paste-button-after =
|
||||||
.title = Indsæt efter side { $page }
|
.title = Indsæt efter side { $page }
|
||||||
pdfjs-toggle-views-manager-button1 =
|
|
||||||
.title = Håndter sider
|
|
||||||
|
|
||||||
## Main menu for adding/removing signatures
|
## Main menu for adding/removing signatures
|
||||||
|
|
||||||
|
|||||||
@ -661,8 +661,6 @@ pdfjs-views-manager-view-selector-button =
|
|||||||
.title = Vistas
|
.title = Vistas
|
||||||
pdfjs-views-manager-view-selector-button-label = Vistas
|
pdfjs-views-manager-view-selector-button-label = Vistas
|
||||||
pdfjs-views-manager-pages-title = Páginas
|
pdfjs-views-manager-pages-title = Páginas
|
||||||
pdfjs-views-manager-outlines-title1 = Esquema del documento
|
|
||||||
.title = Esquema del documento (doble-clic para expandir/contraer todos los elementos)
|
|
||||||
pdfjs-views-manager-attachments-title = Adjuntos
|
pdfjs-views-manager-attachments-title = Adjuntos
|
||||||
pdfjs-views-manager-layers-title1 = Capas
|
pdfjs-views-manager-layers-title1 = Capas
|
||||||
.title = Capas (doble clic para restablecer todas las capas a su estado predeterminado)
|
.title = Capas (doble clic para restablecer todas las capas a su estado predeterminado)
|
||||||
@ -728,7 +726,6 @@ pdfjs-views-manager-paste-button-after =
|
|||||||
# Badge used to promote a new feature in the UI, keep it as short as possible.
|
# Badge used to promote a new feature in the UI, keep it as short as possible.
|
||||||
# It's spelled uppercase for English, but it can be translated as usual.
|
# It's spelled uppercase for English, but it can be translated as usual.
|
||||||
pdfjs-new-badge-content = NUEVO
|
pdfjs-new-badge-content = NUEVO
|
||||||
pdfjs-views-manager-waiting-for-file = Subiendo el archivo…
|
|
||||||
pdfjs-toggle-views-manager-button1 =
|
pdfjs-toggle-views-manager-button1 =
|
||||||
.title = Administrar páginas
|
.title = Administrar páginas
|
||||||
|
|
||||||
|
|||||||
@ -686,10 +686,6 @@ pdfjs-views-manager-pages-status-action-button-label = Upravljaj
|
|||||||
pdfjs-views-manager-pages-status-copy-button-label = Kopiraj
|
pdfjs-views-manager-pages-status-copy-button-label = Kopiraj
|
||||||
pdfjs-views-manager-pages-status-cut-button-label = Izreži
|
pdfjs-views-manager-pages-status-cut-button-label = Izreži
|
||||||
pdfjs-views-manager-pages-status-delete-button-label = Izbriši
|
pdfjs-views-manager-pages-status-delete-button-label = Izbriši
|
||||||
pdfjs-views-manager-status-warning-cut-label = Nije moguće izrezati. Osvježi stranicu i pokušaj ponovo.
|
|
||||||
pdfjs-views-manager-status-warning-copy-label = Nije moguće kopirati. Osvježi stranicu i pokušaj ponovo.
|
|
||||||
pdfjs-views-manager-status-warning-delete-label = Nije moguće izbrisati. Osvježi stranicu i pokušaj ponovo.
|
|
||||||
pdfjs-views-manager-status-warning-save-label = Nije moguće spremiti. Osvježi stranicu i pokušaj ponovo.
|
|
||||||
pdfjs-views-manager-status-undo-button-label = Poništi
|
pdfjs-views-manager-status-undo-button-label = Poništi
|
||||||
pdfjs-views-manager-status-done-button-label = Gotovo
|
pdfjs-views-manager-status-done-button-label = Gotovo
|
||||||
pdfjs-views-manager-status-close-button =
|
pdfjs-views-manager-status-close-button =
|
||||||
|
|||||||
@ -488,8 +488,8 @@ pdfjs-editor-new-alt-text-error-close-button = Lat att
|
|||||||
# Variables:
|
# Variables:
|
||||||
# $totalSize (Number) - the total size (in MB) of the AI model.
|
# $totalSize (Number) - the total size (in MB) of the AI model.
|
||||||
# $downloadedSize (Number) - the downloaded size (in MB) of the AI model.
|
# $downloadedSize (Number) - the downloaded size (in MB) of the AI model.
|
||||||
pdfjs-editor-new-alt-text-ai-model-downloading-progress = Lastar ned KI-modell med alternativ tekst ({ $downloadedSize } av { $totalSize } MB)
|
pdfjs-editor-new-alt-text-ai-model-downloading-progress = Lastar ned AI-modell med alternativ tekst ({ $downloadedSize } av { $totalSize } MB)
|
||||||
.aria-valuetext = Lastar ned KI-modell med alternativ tekst ({ $downloadedSize } av { $totalSize } MB)
|
.aria-valuetext = Lastar ned AI-modell med alternativ tekst ({ $downloadedSize } av { $totalSize } MB)
|
||||||
# This is a button that users can click to edit the alt text they have already added.
|
# This is a button that users can click to edit the alt text they have already added.
|
||||||
pdfjs-editor-new-alt-text-added-button =
|
pdfjs-editor-new-alt-text-added-button =
|
||||||
.aria-label = Alternativ tekst lagt til
|
.aria-label = Alternativ tekst lagt til
|
||||||
@ -518,7 +518,7 @@ pdfjs-editor-alt-text-settings-create-model-button-label = Opprett alternativ te
|
|||||||
pdfjs-editor-alt-text-settings-create-model-description = Foreslår skildringar for å hjelpe folk som ikkje kan sjå bildet eller når bildet ikkje blir lasta inn.
|
pdfjs-editor-alt-text-settings-create-model-description = Foreslår skildringar for å hjelpe folk som ikkje kan sjå bildet eller når bildet ikkje blir lasta inn.
|
||||||
# Variables:
|
# Variables:
|
||||||
# $totalSize (Number) - the total size (in MB) of the AI model.
|
# $totalSize (Number) - the total size (in MB) of the AI model.
|
||||||
pdfjs-editor-alt-text-settings-download-model-label = KI-modell for alternativ tekst ({ $totalSize } MB)
|
pdfjs-editor-alt-text-settings-download-model-label = AI-modell for alternativ tekst ({ $totalSize } MB)
|
||||||
pdfjs-editor-alt-text-settings-ai-model-description = Køyrer lokalt på eininga di slik at dataa dine blir verande private. Påkravd for automatisk alternativ tekst.
|
pdfjs-editor-alt-text-settings-ai-model-description = Køyrer lokalt på eininga di slik at dataa dine blir verande private. Påkravd for automatisk alternativ tekst.
|
||||||
pdfjs-editor-alt-text-settings-delete-model-button = Slett
|
pdfjs-editor-alt-text-settings-delete-model-button = Slett
|
||||||
pdfjs-editor-alt-text-settings-download-model-button = Last ned
|
pdfjs-editor-alt-text-settings-download-model-button = Last ned
|
||||||
|
|||||||
@ -728,7 +728,6 @@ pdfjs-views-manager-paste-button-after =
|
|||||||
# Badge used to promote a new feature in the UI, keep it as short as possible.
|
# Badge used to promote a new feature in the UI, keep it as short as possible.
|
||||||
# It's spelled uppercase for English, but it can be translated as usual.
|
# It's spelled uppercase for English, but it can be translated as usual.
|
||||||
pdfjs-new-badge-content = ਨਵਾਂ
|
pdfjs-new-badge-content = ਨਵਾਂ
|
||||||
pdfjs-views-manager-waiting-for-file = …ਫ਼ਾਇਲ ਨੂੰ ਅੱਪਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ
|
|
||||||
pdfjs-toggle-views-manager-button1 =
|
pdfjs-toggle-views-manager-button1 =
|
||||||
.title = ਸਫ਼ਿਆਂ ਦਾ ਇੰਤਜ਼ਾਮ
|
.title = ਸਫ਼ਿਆਂ ਦਾ ਇੰਤਜ਼ਾਮ
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
OPS,
|
OPS,
|
||||||
RenderingIntentFlag,
|
RenderingIntentFlag,
|
||||||
shadow,
|
shadow,
|
||||||
|
stringToPDFString,
|
||||||
unreachable,
|
unreachable,
|
||||||
Util,
|
Util,
|
||||||
warn,
|
warn,
|
||||||
@ -52,6 +53,8 @@ import {
|
|||||||
numberToString,
|
numberToString,
|
||||||
RESOURCES_KEYS_OPERATOR_LIST,
|
RESOURCES_KEYS_OPERATOR_LIST,
|
||||||
RESOURCES_KEYS_TEXT_CONTENT,
|
RESOURCES_KEYS_TEXT_CONTENT,
|
||||||
|
stringToAsciiOrUTF16BE,
|
||||||
|
stringToUTF16String,
|
||||||
} from "./core_utils.js";
|
} from "./core_utils.js";
|
||||||
import {
|
import {
|
||||||
createDefaultAppearance,
|
createDefaultAppearance,
|
||||||
@ -63,11 +66,6 @@ import {
|
|||||||
import { DateFormats, TimeFormats } from "../shared/scripting_utils.js";
|
import { DateFormats, TimeFormats } from "../shared/scripting_utils.js";
|
||||||
import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js";
|
import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js";
|
||||||
import { Stream, StringStream } from "./stream.js";
|
import { Stream, StringStream } from "./stream.js";
|
||||||
import {
|
|
||||||
stringToAsciiOrUTF16BE,
|
|
||||||
stringToPDFString,
|
|
||||||
stringToUTF16String,
|
|
||||||
} from "./string_utils.js";
|
|
||||||
import { BaseStream } from "./base_stream.js";
|
import { BaseStream } from "./base_stream.js";
|
||||||
import { bidi } from "./bidi.js";
|
import { bidi } from "./bidi.js";
|
||||||
import { Catalog } from "./catalog.js";
|
import { Catalog } from "./catalog.js";
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
objectSize,
|
objectSize,
|
||||||
PermissionFlag,
|
PermissionFlag,
|
||||||
shadow,
|
shadow,
|
||||||
|
stringToPDFString,
|
||||||
stringToUTF8String,
|
stringToUTF8String,
|
||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
@ -52,7 +53,6 @@ import { clearGlobalCaches } from "./cleanup_helper.js";
|
|||||||
import { ColorSpaceUtils } from "./colorspace_utils.js";
|
import { ColorSpaceUtils } from "./colorspace_utils.js";
|
||||||
import { FileSpec } from "./file_spec.js";
|
import { FileSpec } from "./file_spec.js";
|
||||||
import { MetadataParser } from "./metadata_parser.js";
|
import { MetadataParser } from "./metadata_parser.js";
|
||||||
import { stringToPDFString } from "./string_utils.js";
|
|
||||||
import { StructTreeRoot } from "./struct_tree.js";
|
import { StructTreeRoot } from "./struct_tree.js";
|
||||||
|
|
||||||
const isRef = v => v instanceof Ref;
|
const isRef = v => v instanceof Ref;
|
||||||
|
|||||||
@ -108,11 +108,6 @@ const CFFStandardStrings = [
|
|||||||
|
|
||||||
const NUM_STANDARD_CFF_STRINGS = 391;
|
const NUM_STANDARD_CFF_STRINGS = 391;
|
||||||
|
|
||||||
const DEFAULT_BLUE_SCALE = 0.039625;
|
|
||||||
const DEFAULT_BLUE_SHIFT = 7;
|
|
||||||
const DEFAULT_BLUE_FUZZ = 1;
|
|
||||||
const DEFAULT_EXPANSION_FACTOR = 0.06;
|
|
||||||
|
|
||||||
const CharstringValidationData = [
|
const CharstringValidationData = [
|
||||||
/* 0 */ null,
|
/* 0 */ null,
|
||||||
/* 1 */ { id: "hstem", min: 2, stackClearing: true, stem: true },
|
/* 1 */ { id: "hstem", min: 2, stackClearing: true, stem: true },
|
||||||
@ -267,16 +262,8 @@ class CFFParser {
|
|||||||
properties.fontMatrix = fontMatrix;
|
properties.fontMatrix = fontMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fontBBox = topDict.getByName("FontBBox");
|
const fontBBox = topDict.getByName("FontBBox");
|
||||||
if (fontBBox?.every(coord => coord === 0) && properties.bbox) {
|
if (fontBBox) {
|
||||||
fontBBox = Util.normalizeRect(
|
|
||||||
properties.bbox.map(coord =>
|
|
||||||
coord > 0x7fff && coord <= 0xffff ? coord - 0x10000 : coord
|
|
||||||
)
|
|
||||||
);
|
|
||||||
topDict.setByName("FontBBox", fontBBox);
|
|
||||||
}
|
|
||||||
if (fontBBox?.some(coord => coord !== 0)) {
|
|
||||||
// adjusting ascent/descent
|
// adjusting ascent/descent
|
||||||
properties.ascent = Math.max(fontBBox[3], fontBBox[1]);
|
properties.ascent = Math.max(fontBBox[3], fontBBox[1]);
|
||||||
properties.descent = Math.min(fontBBox[1], fontBBox[3]);
|
properties.descent = Math.min(fontBBox[1], fontBBox[3]);
|
||||||
@ -798,28 +785,10 @@ class CFFParser {
|
|||||||
);
|
);
|
||||||
parentDict.privateDict = privateDict;
|
parentDict.privateDict = privateDict;
|
||||||
|
|
||||||
const blueScale = privateDict.getByName("BlueScale");
|
if (privateDict.getByName("ExpansionFactor") === 0) {
|
||||||
const blueShift = privateDict.getByName("BlueShift");
|
|
||||||
const blueFuzz = privateDict.getByName("BlueFuzz");
|
|
||||||
const expansionFactor = privateDict.getByName("ExpansionFactor");
|
|
||||||
if (
|
|
||||||
blueScale === 0 &&
|
|
||||||
blueShift === 0 &&
|
|
||||||
blueFuzz === 0 &&
|
|
||||||
expansionFactor === 0
|
|
||||||
) {
|
|
||||||
// Ghostscript can fail to initialize Private DICT defaults before
|
|
||||||
// writing them, which leaves omitted blue zone values as explicit
|
|
||||||
// zeroes. This has been seen in FDArray entries.
|
|
||||||
privateDict.setByName("BlueScale", DEFAULT_BLUE_SCALE);
|
|
||||||
privateDict.setByName("BlueShift", DEFAULT_BLUE_SHIFT);
|
|
||||||
privateDict.setByName("BlueFuzz", DEFAULT_BLUE_FUZZ);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expansionFactor === 0) {
|
|
||||||
// Firefox doesn't render correctly such a font on Windows (see issue
|
// Firefox doesn't render correctly such a font on Windows (see issue
|
||||||
// 15289), hence we just reset it to its default value.
|
// 15289), hence we just reset it to its default value.
|
||||||
privateDict.setByName("ExpansionFactor", DEFAULT_EXPANSION_FACTOR);
|
privateDict.setByName("ExpansionFactor", 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the Subrs index also since it's relative to the private dict.
|
// Parse the Subrs index also since it's relative to the private dict.
|
||||||
@ -1278,16 +1247,16 @@ const CFFPrivateDictLayout = [
|
|||||||
[7, "OtherBlues", "delta", null],
|
[7, "OtherBlues", "delta", null],
|
||||||
[8, "FamilyBlues", "delta", null],
|
[8, "FamilyBlues", "delta", null],
|
||||||
[9, "FamilyOtherBlues", "delta", null],
|
[9, "FamilyOtherBlues", "delta", null],
|
||||||
[[12, 9], "BlueScale", "num", DEFAULT_BLUE_SCALE],
|
[[12, 9], "BlueScale", "num", 0.039625],
|
||||||
[[12, 10], "BlueShift", "num", DEFAULT_BLUE_SHIFT],
|
[[12, 10], "BlueShift", "num", 7],
|
||||||
[[12, 11], "BlueFuzz", "num", DEFAULT_BLUE_FUZZ],
|
[[12, 11], "BlueFuzz", "num", 1],
|
||||||
[10, "StdHW", "num", null],
|
[10, "StdHW", "num", null],
|
||||||
[11, "StdVW", "num", null],
|
[11, "StdVW", "num", null],
|
||||||
[[12, 12], "StemSnapH", "delta", null],
|
[[12, 12], "StemSnapH", "delta", null],
|
||||||
[[12, 13], "StemSnapV", "delta", null],
|
[[12, 13], "StemSnapV", "delta", null],
|
||||||
[[12, 14], "ForceBold", "num", 0],
|
[[12, 14], "ForceBold", "num", 0],
|
||||||
[[12, 17], "LanguageGroup", "num", 0],
|
[[12, 17], "LanguageGroup", "num", 0],
|
||||||
[[12, 18], "ExpansionFactor", "num", DEFAULT_EXPANSION_FACTOR],
|
[[12, 18], "ExpansionFactor", "num", 0.06],
|
||||||
[[12, 19], "initialRandomSeed", "num", 0],
|
[[12, 19], "initialRandomSeed", "num", 0],
|
||||||
[20, "defaultWidthX", "num", 0],
|
[20, "defaultWidthX", "num", 0],
|
||||||
[21, "nominalWidthX", "num", 0],
|
[21, "nominalWidthX", "num", 0],
|
||||||
|
|||||||
@ -19,12 +19,12 @@ import {
|
|||||||
BaseException,
|
BaseException,
|
||||||
makeArr,
|
makeArr,
|
||||||
objectSize,
|
objectSize,
|
||||||
|
stringToPDFString,
|
||||||
Util,
|
Util,
|
||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js";
|
import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js";
|
||||||
import { BaseStream } from "./base_stream.js";
|
import { BaseStream } from "./base_stream.js";
|
||||||
import { stringToPDFString } from "./string_utils.js";
|
|
||||||
|
|
||||||
const PDF_VERSION_REGEXP = /^[1-9]\.\d$/;
|
const PDF_VERSION_REGEXP = /^[1-9]\.\d$/;
|
||||||
const MAX_INT_32 = 2 ** 31 - 1;
|
const MAX_INT_32 = 2 ** 31 - 1;
|
||||||
@ -684,6 +684,45 @@ function getNewAnnotationsMap(annotationStorage) {
|
|||||||
return newAnnotationsByPage.size > 0 ? newAnnotationsByPage : null;
|
return newAnnotationsByPage.size > 0 ? newAnnotationsByPage : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the string is null or undefined then it is returned as is.
|
||||||
|
function stringToAsciiOrUTF16BE(str) {
|
||||||
|
if (str === null || str === undefined) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return isAscii(str) ? str : stringToUTF16String(str, /* bigEndian = */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAscii(str) {
|
||||||
|
if (typeof str !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !str || /^[\x00-\x7F]*$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringToUTF16HexString(str) {
|
||||||
|
const buf = [];
|
||||||
|
for (let i = 0, ii = str.length; i < ii; i++) {
|
||||||
|
const char = str.charCodeAt(i);
|
||||||
|
buf.push(Util.hexNums[(char >> 8) & 0xff], Util.hexNums[char & 0xff]);
|
||||||
|
}
|
||||||
|
return buf.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringToUTF16String(str, bigEndian = false) {
|
||||||
|
const buf = [];
|
||||||
|
if (bigEndian) {
|
||||||
|
buf.push("\xFE\xFF");
|
||||||
|
}
|
||||||
|
for (let i = 0, ii = str.length; i < ii; i++) {
|
||||||
|
const char = str.charCodeAt(i);
|
||||||
|
buf.push(
|
||||||
|
String.fromCharCode((char >> 8) & 0xff),
|
||||||
|
String.fromCharCode(char & 0xff)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return buf.join("");
|
||||||
|
}
|
||||||
|
|
||||||
function getModificationDate(date = new Date()) {
|
function getModificationDate(date = new Date()) {
|
||||||
if (!(date instanceof Date)) {
|
if (!(date instanceof Date)) {
|
||||||
date = new Date(date);
|
date = new Date(date);
|
||||||
@ -743,6 +782,7 @@ export {
|
|||||||
getRotationMatrix,
|
getRotationMatrix,
|
||||||
getSizeInBytes,
|
getSizeInBytes,
|
||||||
IDENTITY_MATRIX,
|
IDENTITY_MATRIX,
|
||||||
|
isAscii,
|
||||||
isBooleanArray,
|
isBooleanArray,
|
||||||
isNumberArray,
|
isNumberArray,
|
||||||
isWhiteSpace,
|
isWhiteSpace,
|
||||||
@ -758,6 +798,9 @@ export {
|
|||||||
recoverJsURL,
|
recoverJsURL,
|
||||||
RESOURCES_KEYS_OPERATOR_LIST,
|
RESOURCES_KEYS_OPERATOR_LIST,
|
||||||
RESOURCES_KEYS_TEXT_CONTENT,
|
RESOURCES_KEYS_TEXT_CONTENT,
|
||||||
|
stringToAsciiOrUTF16BE,
|
||||||
|
stringToUTF16HexString,
|
||||||
|
stringToUTF16String,
|
||||||
toRomanNumerals,
|
toRomanNumerals,
|
||||||
validateCSSFont,
|
validateCSSFont,
|
||||||
validateFontName,
|
validateFontName,
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
escapePDFName,
|
escapePDFName,
|
||||||
getRotationMatrix,
|
getRotationMatrix,
|
||||||
numberToString,
|
numberToString,
|
||||||
|
stringToUTF16HexString,
|
||||||
} from "./core_utils.js";
|
} from "./core_utils.js";
|
||||||
import { Dict, Name } from "./primitives.js";
|
import { Dict, Name } from "./primitives.js";
|
||||||
import {
|
import {
|
||||||
@ -32,7 +33,6 @@ import { EvaluatorPreprocessor } from "./evaluator.js";
|
|||||||
import { LocalColorSpaceCache } from "./image_utils.js";
|
import { LocalColorSpaceCache } from "./image_utils.js";
|
||||||
import { PDFFunctionFactory } from "./function.js";
|
import { PDFFunctionFactory } from "./function.js";
|
||||||
import { StringStream } from "./stream.js";
|
import { StringStream } from "./stream.js";
|
||||||
import { stringToUTF16HexString } from "./string_utils.js";
|
|
||||||
|
|
||||||
class DefaultAppearanceEvaluator extends EvaluatorPreprocessor {
|
class DefaultAppearanceEvaluator extends EvaluatorPreprocessor {
|
||||||
constructor(str) {
|
constructor(str) {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
RenderingIntentFlag,
|
RenderingIntentFlag,
|
||||||
shadow,
|
shadow,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
|
stringToPDFString,
|
||||||
stringToUTF8String,
|
stringToUTF8String,
|
||||||
unreachable,
|
unreachable,
|
||||||
Util,
|
Util,
|
||||||
@ -75,7 +76,6 @@ import { OperatorList } from "./operator_list.js";
|
|||||||
import { PartialEvaluator } from "./evaluator.js";
|
import { PartialEvaluator } from "./evaluator.js";
|
||||||
import { PDFImage } from "./image.js";
|
import { PDFImage } from "./image.js";
|
||||||
import { StreamsSequenceStream } from "./decode_stream.js";
|
import { StreamsSequenceStream } from "./decode_stream.js";
|
||||||
import { stringToPDFString } from "./string_utils.js";
|
|
||||||
import { StructTreePage } from "./struct_tree.js";
|
import { StructTreePage } from "./struct_tree.js";
|
||||||
import { XFAFactory } from "./xfa/factory.js";
|
import { XFAFactory } from "./xfa/factory.js";
|
||||||
import { XRef } from "./xref.js";
|
import { XRef } from "./xref.js";
|
||||||
|
|||||||
@ -25,15 +25,15 @@ import {
|
|||||||
getInheritableProperty,
|
getInheritableProperty,
|
||||||
getModificationDate,
|
getModificationDate,
|
||||||
getNewAnnotationsMap,
|
getNewAnnotationsMap,
|
||||||
|
stringToAsciiOrUTF16BE,
|
||||||
} from "../core_utils.js";
|
} from "../core_utils.js";
|
||||||
import { Dict, isName, Name, Ref, RefSet, RefSetCache } from "../primitives.js";
|
import { Dict, isName, Name, Ref, RefSet, RefSetCache } from "../primitives.js";
|
||||||
import { incrementalUpdate, writeValue } from "../writer.js";
|
import { incrementalUpdate, writeValue } from "../writer.js";
|
||||||
import { NameTree, NumberTree } from "../name_number_tree.js";
|
import { NameTree, NumberTree } from "../name_number_tree.js";
|
||||||
import { stringToAsciiOrUTF16BE, stringToPDFString } from "../string_utils.js";
|
import { stringToBytes, stringToPDFString } from "../../shared/util.js";
|
||||||
import { AnnotationFactory } from "../annotation.js";
|
import { AnnotationFactory } from "../annotation.js";
|
||||||
import { BaseStream } from "../base_stream.js";
|
import { BaseStream } from "../base_stream.js";
|
||||||
import { StringStream } from "../stream.js";
|
import { StringStream } from "../stream.js";
|
||||||
import { stringToBytes } from "../../shared/util.js";
|
|
||||||
|
|
||||||
const MAX_LEAVES_PER_PAGES_NODE = 16;
|
const MAX_LEAVES_PER_PAGES_NODE = 16;
|
||||||
const MAX_IN_NAME_TREE_NODE = 64;
|
const MAX_IN_NAME_TREE_NODE = 64;
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
normalizeUnicode,
|
normalizeUnicode,
|
||||||
OPS,
|
OPS,
|
||||||
shadow,
|
shadow,
|
||||||
|
stringToPDFString,
|
||||||
TextRenderingMode,
|
TextRenderingMode,
|
||||||
Util,
|
Util,
|
||||||
warn,
|
warn,
|
||||||
@ -89,7 +90,6 @@ import { getUnicodeForGlyph } from "./unicode.js";
|
|||||||
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
|
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
|
||||||
import { PDFImage } from "./image.js";
|
import { PDFImage } from "./image.js";
|
||||||
import { Stream } from "./stream.js";
|
import { Stream } from "./stream.js";
|
||||||
import { stringToPDFString } from "./string_utils.js";
|
|
||||||
|
|
||||||
const DefaultPartialEvaluatorOptions = Object.freeze({
|
const DefaultPartialEvaluatorOptions = Object.freeze({
|
||||||
maxImageSize: -1,
|
maxImageSize: -1,
|
||||||
|
|||||||
@ -13,10 +13,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { stripPath, warn } from "../shared/util.js";
|
import { stringToPDFString, stripPath, warn } from "../shared/util.js";
|
||||||
import { BaseStream } from "./base_stream.js";
|
import { BaseStream } from "./base_stream.js";
|
||||||
import { Dict } from "./primitives.js";
|
import { Dict } from "./primitives.js";
|
||||||
import { stringToPDFString } from "./string_utils.js";
|
|
||||||
|
|
||||||
function pickPlatformItem(dict) {
|
function pickPlatformItem(dict) {
|
||||||
if (dict instanceof Dict) {
|
if (dict instanceof Dict) {
|
||||||
|
|||||||
@ -163,9 +163,6 @@ function lookupCmap(ranges, unicode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function compileGlyf(code, cmds, font, visitedGlyphs = new Set()) {
|
function compileGlyf(code, cmds, font, visitedGlyphs = new Set()) {
|
||||||
if (!code?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (visitedGlyphs.has(code)) {
|
if (visitedGlyphs.has(code)) {
|
||||||
warn("compileGlyf: skipping recursive composite glyph reference.");
|
warn("compileGlyf: skipping recursive composite glyph reference.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -55,13 +55,13 @@ import {
|
|||||||
getSupplementalGlyphMapForArialBlack,
|
getSupplementalGlyphMapForArialBlack,
|
||||||
getSupplementalGlyphMapForCalibri,
|
getSupplementalGlyphMapForCalibri,
|
||||||
} from "./standard_fonts.js";
|
} from "./standard_fonts.js";
|
||||||
import { GlyfTable, pruneCompositeGlyphCycles } from "./glyf.js";
|
|
||||||
import { IdentityToUnicodeMap, ToUnicodeMap } from "./to_unicode_map.js";
|
import { IdentityToUnicodeMap, ToUnicodeMap } from "./to_unicode_map.js";
|
||||||
import { CFFFont } from "./cff_font.js";
|
import { CFFFont } from "./cff_font.js";
|
||||||
import { compileFontInfo } from "./obj_bin_transform_core.js";
|
import { compileFontInfo } from "./obj_bin_transform_core.js";
|
||||||
import { DataBuilder } from "./data_builder.js";
|
import { DataBuilder } from "./data_builder.js";
|
||||||
import { FontRendererFactory } from "./font_renderer.js";
|
import { FontRendererFactory } from "./font_renderer.js";
|
||||||
import { getFontBasicMetrics } from "./metrics.js";
|
import { getFontBasicMetrics } from "./metrics.js";
|
||||||
|
import { GlyfTable } from "./glyf.js";
|
||||||
import { OpenTypeFileBuilder } from "./opentype_file_builder.js";
|
import { OpenTypeFileBuilder } from "./opentype_file_builder.js";
|
||||||
import { Stream } from "./stream.js";
|
import { Stream } from "./stream.js";
|
||||||
import { Type1Font } from "./type1_font.js";
|
import { Type1Font } from "./type1_font.js";
|
||||||
@ -720,11 +720,6 @@ function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
|
|||||||
function validateOS2Table(os2, file) {
|
function validateOS2Table(os2, file) {
|
||||||
file.pos = (file.start || 0) + os2.offset;
|
file.pos = (file.start || 0) + os2.offset;
|
||||||
const version = file.getUint16();
|
const version = file.getUint16();
|
||||||
// https://learn.microsoft.com/en-us/typography/opentype/spec/os2
|
|
||||||
const minLength = [78, 86, 96, 96, 96, 100][version];
|
|
||||||
if (minLength === undefined || os2.length < minLength) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// TODO verify all OS/2 tables fields, but currently we validate only those
|
// TODO verify all OS/2 tables fields, but currently we validate only those
|
||||||
// that give us issues
|
// that give us issues
|
||||||
file.skip(60); // skipping type, misc sizes, panose, unicode ranges
|
file.skip(60); // skipping type, misc sizes, panose, unicode ranges
|
||||||
@ -2200,25 +2195,18 @@ class Font {
|
|||||||
last.endOffset = oldGlyfDataLength;
|
last.endOffset = oldGlyfDataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
const droppedGlyphs = pruneCompositeGlyphCycles(
|
|
||||||
oldGlyfData,
|
|
||||||
locaEntries,
|
|
||||||
numGlyphs
|
|
||||||
);
|
|
||||||
const missingGlyphs = Object.create(null);
|
const missingGlyphs = Object.create(null);
|
||||||
let writeOffset = 0;
|
let writeOffset = 0;
|
||||||
itemEncode(locaData, 0, writeOffset);
|
itemEncode(locaData, 0, writeOffset);
|
||||||
for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
|
for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
|
||||||
const glyphProfile = droppedGlyphs.has(i)
|
const glyphProfile = sanitizeGlyph(
|
||||||
? { length: 0, sizeOfInstructions: 0 }
|
oldGlyfData,
|
||||||
: sanitizeGlyph(
|
locaEntries[i].offset,
|
||||||
oldGlyfData,
|
locaEntries[i].endOffset,
|
||||||
locaEntries[i].offset,
|
newGlyfData,
|
||||||
locaEntries[i].endOffset,
|
writeOffset,
|
||||||
newGlyfData,
|
hintsValid
|
||||||
writeOffset,
|
);
|
||||||
hintsValid
|
|
||||||
);
|
|
||||||
const newLength = glyphProfile.length;
|
const newLength = glyphProfile.length;
|
||||||
if (newLength === 0) {
|
if (newLength === 0) {
|
||||||
missingGlyphs[i] = true;
|
missingGlyphs[i] = true;
|
||||||
@ -2849,19 +2837,6 @@ class Font {
|
|||||||
maxFunctionDefs = font.getUint16();
|
maxFunctionDefs = font.getUint16();
|
||||||
font.pos += 4;
|
font.pos += 4;
|
||||||
maxSizeOfInstructions = font.getUint16();
|
maxSizeOfInstructions = font.getUint16();
|
||||||
} else if (isTrueType && version === 0x00005000) {
|
|
||||||
const newMaxp = new Uint8Array(32);
|
|
||||||
writeUint32(newMaxp, 0, 0x00010000);
|
|
||||||
newMaxp[4] = (numGlyphs >> 8) & 0xff;
|
|
||||||
newMaxp[5] = numGlyphs & 0xff;
|
|
||||||
newMaxp.fill(0xff, 6, 14);
|
|
||||||
newMaxp[15] = 2;
|
|
||||||
newMaxp[28] = 0xff;
|
|
||||||
newMaxp[29] = 0xff;
|
|
||||||
newMaxp[31] = 0x10;
|
|
||||||
tables.maxp.data = newMaxp;
|
|
||||||
tables.maxp.length = 32;
|
|
||||||
version = 0x00010000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tables.maxp.data[4] = numGlyphsOut >> 8;
|
tables.maxp.data[4] = numGlyphsOut >> 8;
|
||||||
|
|||||||
122
src/core/glyf.js
122
src/core/glyf.js
@ -34,8 +34,6 @@ const WE_HAVE_INSTRUCTIONS = 1 << 8;
|
|||||||
// const SCALED_COMPONENT_OFFSET = 1 << 11;
|
// const SCALED_COMPONENT_OFFSET = 1 << 11;
|
||||||
// const UNSCALED_COMPONENT_OFFSET = 1 << 12;
|
// const UNSCALED_COMPONENT_OFFSET = 1 << 12;
|
||||||
|
|
||||||
const GLYPH_HEADER_SIZE = 10;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GlyfTable object represents a glyf table containing glyph information:
|
* GlyfTable object represents a glyf table containing glyph information:
|
||||||
* - glyph header (xMin, yMin, xMax, yMax);
|
* - glyph header (xMin, yMin, xMax, yMax);
|
||||||
@ -220,7 +218,7 @@ class GlyphHeader {
|
|||||||
|
|
||||||
static parse(pos, glyf) {
|
static parse(pos, glyf) {
|
||||||
return [
|
return [
|
||||||
GLYPH_HEADER_SIZE,
|
10,
|
||||||
new GlyphHeader({
|
new GlyphHeader({
|
||||||
numberOfContours: glyf.getInt16(pos),
|
numberOfContours: glyf.getInt16(pos),
|
||||||
xMin: glyf.getInt16(pos + 2),
|
xMin: glyf.getInt16(pos + 2),
|
||||||
@ -232,7 +230,7 @@ class GlyphHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSize() {
|
getSize() {
|
||||||
return GLYPH_HEADER_SIZE;
|
return 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
write(pos, buf) {
|
write(pos, buf) {
|
||||||
@ -242,7 +240,7 @@ class GlyphHeader {
|
|||||||
buf.setInt16(pos + 6, this.xMax);
|
buf.setInt16(pos + 6, this.xMax);
|
||||||
buf.setInt16(pos + 8, this.yMax);
|
buf.setInt16(pos + 8, this.yMax);
|
||||||
|
|
||||||
return GLYPH_HEADER_SIZE;
|
return 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
scale(x, factor) {
|
scale(x, factor) {
|
||||||
@ -698,116 +696,4 @@ class CompositeGlyph {
|
|||||||
scale(x, factor) {}
|
scale(x, factor) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pruneCompositeGlyphCycles(glyfTable, locaEntries, numGlyphs) {
|
export { GlyfTable };
|
||||||
const glyf = new DataView(
|
|
||||||
glyfTable.buffer,
|
|
||||||
glyfTable.byteOffset,
|
|
||||||
glyfTable.byteLength
|
|
||||||
);
|
|
||||||
const components = new Array(numGlyphs);
|
|
||||||
for (let i = 0; i < numGlyphs; i++) {
|
|
||||||
const offset = locaEntries[i].offset;
|
|
||||||
const endOffset = Math.min(locaEntries[i].endOffset, glyf.byteLength);
|
|
||||||
if (endOffset - offset <= GLYPH_HEADER_SIZE || glyf.getInt16(offset) >= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const comps = [];
|
|
||||||
let p = offset + GLYPH_HEADER_SIZE;
|
|
||||||
while (p + 4 <= endOffset) {
|
|
||||||
const flags = glyf.getUint16(p);
|
|
||||||
const gid = glyf.getUint16(p + 2);
|
|
||||||
let size = 4 + (flags & ARG_1_AND_2_ARE_WORDS ? 4 : 2);
|
|
||||||
if (flags & WE_HAVE_A_SCALE) {
|
|
||||||
size += 2;
|
|
||||||
} else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
|
|
||||||
size += 4;
|
|
||||||
} else if (flags & WE_HAVE_A_TWO_BY_TWO) {
|
|
||||||
size += 8;
|
|
||||||
}
|
|
||||||
comps.push({ gid, offset: p, size, flags });
|
|
||||||
p += size;
|
|
||||||
if (!(flags & MORE_COMPONENTS)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (comps.length) {
|
|
||||||
components[i] = comps;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const WHITE = 0,
|
|
||||||
GRAY = 1,
|
|
||||||
BLACK = 2;
|
|
||||||
const state = new Uint8Array(numGlyphs);
|
|
||||||
const backEdges = new Map();
|
|
||||||
for (let start = 0; start < numGlyphs; start++) {
|
|
||||||
if (state[start] !== WHITE || !components[start]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const stack = [{ node: start, idx: 0 }];
|
|
||||||
state[start] = GRAY;
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const top = stack.at(-1);
|
|
||||||
const comps = components[top.node];
|
|
||||||
if (!comps || top.idx >= comps.length) {
|
|
||||||
state[top.node] = BLACK;
|
|
||||||
stack.pop();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const compIdx = top.idx++;
|
|
||||||
const next = comps[compIdx].gid;
|
|
||||||
if (next >= numGlyphs || state[next] === BLACK) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (state[next] === WHITE) {
|
|
||||||
state[next] = GRAY;
|
|
||||||
stack.push({ node: next, idx: 0 });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let removeSet = backEdges.get(top.node);
|
|
||||||
if (!removeSet) {
|
|
||||||
removeSet = new Set();
|
|
||||||
backEdges.set(top.node, removeSet);
|
|
||||||
}
|
|
||||||
removeSet.add(compIdx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const droppedGlyphs = new Set();
|
|
||||||
for (const [gIdx, removeSet] of backEdges) {
|
|
||||||
const comps = components[gIdx];
|
|
||||||
const remaining = [];
|
|
||||||
for (let ci = 0; ci < comps.length; ci++) {
|
|
||||||
if (!removeSet.has(ci)) {
|
|
||||||
remaining.push(comps[ci]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (remaining.length === 0) {
|
|
||||||
droppedGlyphs.add(gIdx);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const start = locaEntries[gIdx].offset;
|
|
||||||
const endOffset = Math.min(locaEntries[gIdx].endOffset, glyf.byteLength);
|
|
||||||
let writePos = start + GLYPH_HEADER_SIZE;
|
|
||||||
for (let ci = 0; ci < remaining.length; ci++) {
|
|
||||||
const c = remaining[ci];
|
|
||||||
const isLast = ci === remaining.length - 1;
|
|
||||||
let newFlags = c.flags & ~WE_HAVE_INSTRUCTIONS;
|
|
||||||
newFlags = isLast
|
|
||||||
? newFlags & ~MORE_COMPONENTS
|
|
||||||
: newFlags | MORE_COMPONENTS;
|
|
||||||
if (writePos !== c.offset) {
|
|
||||||
glyfTable.copyWithin(writePos, c.offset, c.offset + c.size);
|
|
||||||
}
|
|
||||||
glyf.setUint16(writePos, newFlags);
|
|
||||||
writePos += c.size;
|
|
||||||
}
|
|
||||||
if (writePos < endOffset) {
|
|
||||||
glyfTable.fill(0, writePos, endOffset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return droppedGlyphs;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { GlyfTable, pruneCompositeGlyphCycles };
|
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
/* Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { stringToBytes, Util, warn } from "../shared/util.js";
|
|
||||||
|
|
||||||
function isAscii(str) {
|
|
||||||
return typeof str === "string" && (!str || /^[\x00-\x7F]*$/.test(str));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the string is null or undefined then it is returned as is.
|
|
||||||
function stringToAsciiOrUTF16BE(str) {
|
|
||||||
if (str === null || str === undefined) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
return isAscii(str) ? str : stringToUTF16String(str, /* bigEndian = */ true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringToUTF16HexString(str) {
|
|
||||||
const buf = [];
|
|
||||||
for (let i = 0, ii = str.length; i < ii; i++) {
|
|
||||||
const char = str.charCodeAt(i);
|
|
||||||
buf.push(Util.hexNums[(char >> 8) & 0xff], Util.hexNums[char & 0xff]);
|
|
||||||
}
|
|
||||||
return buf.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringToUTF16String(str, bigEndian = false) {
|
|
||||||
const buf = [];
|
|
||||||
if (bigEndian) {
|
|
||||||
buf.push("\xFE\xFF");
|
|
||||||
}
|
|
||||||
for (let i = 0, ii = str.length; i < ii; i++) {
|
|
||||||
const char = str.charCodeAt(i);
|
|
||||||
buf.push(
|
|
||||||
String.fromCharCode((char >> 8) & 0xff),
|
|
||||||
String.fromCharCode(char & 0xff)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return buf.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
const PDFStringTranslateTable = [
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2d8,
|
|
||||||
0x2c7, 0x2c6, 0x2d9, 0x2dd, 0x2db, 0x2da, 0x2dc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, 0x2013, 0x192,
|
|
||||||
0x2044, 0x2039, 0x203a, 0x2212, 0x2030, 0x201e, 0x201c, 0x201d, 0x2018,
|
|
||||||
0x2019, 0x201a, 0x2122, 0xfb01, 0xfb02, 0x141, 0x152, 0x160, 0x178, 0x17d,
|
|
||||||
0x131, 0x142, 0x153, 0x161, 0x17e, 0, 0x20ac,
|
|
||||||
];
|
|
||||||
|
|
||||||
function stringToPDFString(str, keepEscapeSequence = false) {
|
|
||||||
// See section 7.9.2.2 Text String Type.
|
|
||||||
// The string can contain some language codes bracketed with 0x1b,
|
|
||||||
// so we must remove them.
|
|
||||||
if (str[0] >= "\xEF") {
|
|
||||||
let encoding;
|
|
||||||
if (str[0] === "\xFE" && str[1] === "\xFF") {
|
|
||||||
encoding = "utf-16be";
|
|
||||||
if (str.length % 2 === 1) {
|
|
||||||
str = str.slice(0, -1);
|
|
||||||
}
|
|
||||||
} else if (str[0] === "\xFF" && str[1] === "\xFE") {
|
|
||||||
encoding = "utf-16le";
|
|
||||||
if (str.length % 2 === 1) {
|
|
||||||
str = str.slice(0, -1);
|
|
||||||
}
|
|
||||||
} else if (str[0] === "\xEF" && str[1] === "\xBB" && str[2] === "\xBF") {
|
|
||||||
encoding = "utf-8";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoding) {
|
|
||||||
try {
|
|
||||||
const decoder = new TextDecoder(encoding, { fatal: true });
|
|
||||||
const buffer = stringToBytes(str);
|
|
||||||
const decoded = decoder.decode(buffer);
|
|
||||||
if (keepEscapeSequence || !decoded.includes("\x1b")) {
|
|
||||||
return decoded;
|
|
||||||
}
|
|
||||||
return decoded.replaceAll(/\x1b[^\x1b]*(?:\x1b|$)/g, "");
|
|
||||||
} catch (ex) {
|
|
||||||
warn(`stringToPDFString: "${ex}".`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ISO Latin 1
|
|
||||||
const strBuf = [];
|
|
||||||
for (let i = 0, ii = str.length; i < ii; i++) {
|
|
||||||
const charCode = str.charCodeAt(i);
|
|
||||||
if (!keepEscapeSequence && charCode === 0x1b) {
|
|
||||||
// eslint-disable-next-line no-empty
|
|
||||||
while (++i < ii && str.charCodeAt(i) !== 0x1b) {}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const code = PDFStringTranslateTable[charCode];
|
|
||||||
strBuf.push(code ? String.fromCharCode(code) : str.charAt(i));
|
|
||||||
}
|
|
||||||
return strBuf.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
isAscii,
|
|
||||||
stringToAsciiOrUTF16BE,
|
|
||||||
stringToPDFString,
|
|
||||||
stringToUTF16HexString,
|
|
||||||
stringToUTF16String,
|
|
||||||
};
|
|
||||||
@ -16,13 +16,13 @@
|
|||||||
import {
|
import {
|
||||||
AnnotationPrefix,
|
AnnotationPrefix,
|
||||||
makeArr,
|
makeArr,
|
||||||
|
stringToPDFString,
|
||||||
stringToUTF8String,
|
stringToUTF8String,
|
||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js";
|
import { Dict, isName, Name, Ref, RefSetCache } from "./primitives.js";
|
||||||
import { stringToAsciiOrUTF16BE, stringToPDFString } from "./string_utils.js";
|
import { lookupNormalRect, stringToAsciiOrUTF16BE } from "./core_utils.js";
|
||||||
import { BaseStream } from "./base_stream.js";
|
import { BaseStream } from "./base_stream.js";
|
||||||
import { lookupNormalRect } from "./core_utils.js";
|
|
||||||
import { NumberTree } from "./name_number_tree.js";
|
import { NumberTree } from "./name_number_tree.js";
|
||||||
|
|
||||||
const MAX_DEPTH = 40;
|
const MAX_DEPTH = 40;
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
isNodeJS,
|
isNodeJS,
|
||||||
PasswordException,
|
PasswordException,
|
||||||
setVerbosityLevel,
|
setVerbosityLevel,
|
||||||
|
stringToPDFString,
|
||||||
VerbosityLevel,
|
VerbosityLevel,
|
||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
@ -37,7 +38,6 @@ import { clearGlobalCaches } from "./cleanup_helper.js";
|
|||||||
import { incrementalUpdate } from "./writer.js";
|
import { incrementalUpdate } from "./writer.js";
|
||||||
import { PDFEditor } from "./editor/pdf_editor.js";
|
import { PDFEditor } from "./editor/pdf_editor.js";
|
||||||
import { PDFWorkerStream } from "./worker_stream.js";
|
import { PDFWorkerStream } from "./worker_stream.js";
|
||||||
import { stringToPDFString } from "./string_utils.js";
|
|
||||||
import { StructTreeRoot } from "./struct_tree.js";
|
import { StructTreeRoot } from "./struct_tree.js";
|
||||||
|
|
||||||
class WorkerTask {
|
class WorkerTask {
|
||||||
@ -1036,9 +1036,6 @@ class WorkerMessageHandler {
|
|||||||
.getPage(data.pageIndex)
|
.getPage(data.pageIndex)
|
||||||
.then(page => page.annotations.map(a => a.toString()));
|
.then(page => page.annotations.map(a => a.toString()));
|
||||||
});
|
});
|
||||||
handler.on("GetWorkerCoverage", function () {
|
|
||||||
return globalThis.__coverage__ ?? {};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return workerHandlerName;
|
return workerHandlerName;
|
||||||
|
|||||||
@ -102,12 +102,13 @@ import {
|
|||||||
getStringOption,
|
getStringOption,
|
||||||
HTMLResult,
|
HTMLResult,
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
import { SVG_NS, Util, warn } from "../../shared/util.js";
|
import { Util, warn } from "../../shared/util.js";
|
||||||
import { getMetrics } from "./fonts.js";
|
import { getMetrics } from "./fonts.js";
|
||||||
import { recoverJsURL } from "../core_utils.js";
|
import { recoverJsURL } from "../core_utils.js";
|
||||||
import { searchNode } from "./som.js";
|
import { searchNode } from "./som.js";
|
||||||
|
|
||||||
const TEMPLATE_NS_ID = NamespaceIds.template.id;
|
const TEMPLATE_NS_ID = NamespaceIds.template.id;
|
||||||
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
// In case of lr-tb (and rl-tb) layouts, we try:
|
// In case of lr-tb (and rl-tb) layouts, we try:
|
||||||
// - to put the container at the end of a line
|
// - to put the container at the end of a line
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** @typedef {import("./api").PDFPageProxy} PDFPageProxy */
|
/** @typedef {import("./api").PDFPageProxy} PDFPageProxy */
|
||||||
/** @typedef {import("./page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("./display_utils").PageViewport} PageViewport */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
/** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
@ -38,7 +38,6 @@ import {
|
|||||||
LINE_FACTOR,
|
LINE_FACTOR,
|
||||||
makeArr,
|
makeArr,
|
||||||
shadow,
|
shadow,
|
||||||
SVG_NS,
|
|
||||||
unreachable,
|
unreachable,
|
||||||
Util,
|
Util,
|
||||||
warn,
|
warn,
|
||||||
@ -667,7 +666,8 @@ class AnnotationElement {
|
|||||||
style.borderWidth = 0;
|
style.borderWidth = 0;
|
||||||
svgBuffer = [
|
svgBuffer = [
|
||||||
"url('data:image/svg+xml;utf8,",
|
"url('data:image/svg+xml;utf8,",
|
||||||
`<svg xmlns="${SVG_NS}" preserveAspectRatio="none" viewBox="0 0 1 1">`,
|
`<svg xmlns="http://www.w3.org/2000/svg"`,
|
||||||
|
` preserveAspectRatio="none" viewBox="0 0 1 1">`,
|
||||||
`<g fill="transparent" stroke="${borderColor}" stroke-width="${borderWidth}">`,
|
`<g fill="transparent" stroke="${borderColor}" stroke-width="${borderWidth}">`,
|
||||||
];
|
];
|
||||||
this.container.classList.add("hasBorder");
|
this.container.classList.add("hasBorder");
|
||||||
|
|||||||
@ -57,6 +57,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
isDataScheme,
|
isDataScheme,
|
||||||
isValidFetchUrl,
|
isValidFetchUrl,
|
||||||
|
PageViewport,
|
||||||
RenderingCancelledException,
|
RenderingCancelledException,
|
||||||
StatTimer,
|
StatTimer,
|
||||||
} from "./display_utils.js";
|
} from "./display_utils.js";
|
||||||
@ -77,7 +78,6 @@ import { MathClamp } from "../shared/math_clamp.js";
|
|||||||
import { Metadata } from "./metadata.js";
|
import { Metadata } from "./metadata.js";
|
||||||
import { OptionalContentConfig } from "./optional_content_config.js";
|
import { OptionalContentConfig } from "./optional_content_config.js";
|
||||||
import { PagesMapper } from "./pages_mapper.js";
|
import { PagesMapper } from "./pages_mapper.js";
|
||||||
import { PageViewport } from "./page_viewport.js";
|
|
||||||
import { PDFDataTransportStream } from "./transport_stream.js";
|
import { PDFDataTransportStream } from "./transport_stream.js";
|
||||||
import { PDFObjects } from "./pdf_objects.js";
|
import { PDFObjects } from "./pdf_objects.js";
|
||||||
import { TextLayer } from "./text_layer.js";
|
import { TextLayer } from "./text_layer.js";
|
||||||
|
|||||||
@ -1850,13 +1850,17 @@ class CanvasGraphics {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the full scratch canvas, not just the dirty box. Pixels left
|
// Whatever was drawn has been moved to the suspended canvas, now clear it
|
||||||
// outside dirtyBox can leak into a later compose() whose destination-in
|
// out of the current canvas. Only the dirty box region needs clearing;
|
||||||
// pass doesn't overwrite them, producing stale output -- this is what
|
// everything outside it is already transparent.
|
||||||
// breaks `firefox-issue17779-partial` (issue #21276).
|
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
this.ctx.clearRect(
|
||||||
|
dirtyBox[0],
|
||||||
|
dirtyBox[1],
|
||||||
|
dirtyBox[2] - dirtyBox[0],
|
||||||
|
dirtyBox[3] - dirtyBox[1]
|
||||||
|
);
|
||||||
this.ctx.restore();
|
this.ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { BBOX_INIT, FeatureTest, Util } from "../shared/util.js";
|
import { BBOX_INIT, FeatureTest, Util } from "../shared/util.js";
|
||||||
import { getCurrentTransform } from "./display_utils.js";
|
|
||||||
import { MathClamp } from "../shared/math_clamp.js";
|
import { MathClamp } from "../shared/math_clamp.js";
|
||||||
|
|
||||||
const FORCED_DEPENDENCY_LABEL = "__forcedDependency";
|
const FORCED_DEPENDENCY_LABEL = "__forcedDependency";
|
||||||
@ -28,56 +27,6 @@ function expandBBox(array, index, minX, minY, maxX, maxY) {
|
|||||||
array[index * 4 + 3] = Math.max(array[index * 4 + 3], maxY);
|
array[index * 4 + 3] = Math.max(array[index * 4 + 3], maxY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply a scaling matrix to some min/max values.
|
|
||||||
// If a scaling factor is negative then min and max must be swapped.
|
|
||||||
function scaleMinMax(transform, minMax) {
|
|
||||||
let temp;
|
|
||||||
if (transform[0]) {
|
|
||||||
if (transform[0] < 0) {
|
|
||||||
temp = minMax[0];
|
|
||||||
minMax[0] = minMax[2];
|
|
||||||
minMax[2] = temp;
|
|
||||||
}
|
|
||||||
minMax[0] *= transform[0];
|
|
||||||
minMax[2] *= transform[0];
|
|
||||||
|
|
||||||
if (transform[3] < 0) {
|
|
||||||
temp = minMax[1];
|
|
||||||
minMax[1] = minMax[3];
|
|
||||||
minMax[3] = temp;
|
|
||||||
}
|
|
||||||
minMax[1] *= transform[3];
|
|
||||||
minMax[3] *= transform[3];
|
|
||||||
} else {
|
|
||||||
temp = minMax[0];
|
|
||||||
minMax[0] = minMax[1];
|
|
||||||
minMax[1] = temp;
|
|
||||||
temp = minMax[2];
|
|
||||||
minMax[2] = minMax[3];
|
|
||||||
minMax[3] = temp;
|
|
||||||
|
|
||||||
if (transform[1] < 0) {
|
|
||||||
temp = minMax[1];
|
|
||||||
minMax[1] = minMax[3];
|
|
||||||
minMax[3] = temp;
|
|
||||||
}
|
|
||||||
minMax[1] *= transform[1];
|
|
||||||
minMax[3] *= transform[1];
|
|
||||||
|
|
||||||
if (transform[2] < 0) {
|
|
||||||
temp = minMax[0];
|
|
||||||
minMax[0] = minMax[2];
|
|
||||||
minMax[2] = temp;
|
|
||||||
}
|
|
||||||
minMax[0] *= transform[2];
|
|
||||||
minMax[2] *= transform[2];
|
|
||||||
}
|
|
||||||
minMax[0] += transform[4];
|
|
||||||
minMax[1] += transform[5];
|
|
||||||
minMax[2] += transform[4];
|
|
||||||
minMax[3] += transform[5];
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is computed rathter than hard-coded to keep into
|
// This is computed rathter than hard-coded to keep into
|
||||||
// account the platform's endianess.
|
// account the platform's endianess.
|
||||||
const EMPTY_BBOX = new Uint32Array(new Uint8Array([255, 255, 0, 0]).buffer)[0];
|
const EMPTY_BBOX = new Uint32Array(new Uint8Array([255, 255, 0, 0]).buffer)[0];
|
||||||
@ -663,7 +612,7 @@ class CanvasDependencyTracker {
|
|||||||
computedBBox = [0, 0, 0, 0];
|
computedBBox = [0, 0, 0, 0];
|
||||||
Util.axialAlignedBoundingBox(fontBBox, font.fontMatrix, computedBBox);
|
Util.axialAlignedBoundingBox(fontBBox, font.fontMatrix, computedBBox);
|
||||||
if (scale !== 1 || x !== 0 || y !== 0) {
|
if (scale !== 1 || x !== 0 || y !== 0) {
|
||||||
scaleMinMax([scale, 0, 0, -scale, x, y], computedBBox);
|
Util.scaleMinMax([scale, 0, 0, -scale, x, y], computedBBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBBoxTrustworthy) {
|
if (isBBoxTrustworthy) {
|
||||||
@ -1172,7 +1121,7 @@ class CanvasImagesTracker {
|
|||||||
this.#coords = newCoords;
|
this.#coords = newCoords;
|
||||||
}
|
}
|
||||||
|
|
||||||
const transform = getCurrentTransform(ctx);
|
const transform = Util.domMatrixToTransform(ctx.getTransform());
|
||||||
|
|
||||||
// We want top left, bottom left, top right.
|
// We want top left, bottom left, top right.
|
||||||
// (0, 0) is the bottom left corner.
|
// (0, 0) is the bottom left corner.
|
||||||
|
|||||||
@ -22,9 +22,10 @@ import {
|
|||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
import { MathClamp } from "../shared/math_clamp.js";
|
import { MathClamp } from "../shared/math_clamp.js";
|
||||||
import { PageViewport } from "./page_viewport.js";
|
|
||||||
import { XfaLayer } from "./xfa_layer.js";
|
import { XfaLayer } from "./xfa_layer.js";
|
||||||
|
|
||||||
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
class PixelsPerInch {
|
class PixelsPerInch {
|
||||||
static CSS = 96.0;
|
static CSS = 96.0;
|
||||||
|
|
||||||
@ -83,6 +84,220 @@ async function fetchData(url, type = "text") {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PageViewportParameters
|
||||||
|
* @property {Array<number>} viewBox - The xMin, yMin, xMax and
|
||||||
|
* yMax coordinates.
|
||||||
|
* @property {number} userUnit - The size of units.
|
||||||
|
* @property {number} scale - The scale of the viewport.
|
||||||
|
* @property {number} rotation - The rotation, in degrees, of the viewport.
|
||||||
|
* @property {number} [offsetX] - The horizontal, i.e. x-axis, offset. The
|
||||||
|
* default value is `0`.
|
||||||
|
* @property {number} [offsetY] - The vertical, i.e. y-axis, offset. The
|
||||||
|
* default value is `0`.
|
||||||
|
* @property {boolean} [dontFlip] - If true, the y-axis will not be flipped.
|
||||||
|
* The default value is `false`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PageViewportCloneParameters
|
||||||
|
* @property {number} [scale] - The scale, overriding the one in the cloned
|
||||||
|
* viewport. The default value is `this.scale`.
|
||||||
|
* @property {number} [rotation] - The rotation, in degrees, overriding the one
|
||||||
|
* in the cloned viewport. The default value is `this.rotation`.
|
||||||
|
* @property {number} [offsetX] - The horizontal, i.e. x-axis, offset.
|
||||||
|
* The default value is `this.offsetX`.
|
||||||
|
* @property {number} [offsetY] - The vertical, i.e. y-axis, offset.
|
||||||
|
* The default value is `this.offsetY`.
|
||||||
|
* @property {boolean} [dontFlip] - If true, the x-axis will not be flipped.
|
||||||
|
* The default value is `false`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PDF page viewport created based on scale, rotation and offset.
|
||||||
|
*/
|
||||||
|
class PageViewport {
|
||||||
|
/**
|
||||||
|
* @param {PageViewportParameters}
|
||||||
|
*/
|
||||||
|
constructor({
|
||||||
|
viewBox,
|
||||||
|
userUnit,
|
||||||
|
scale,
|
||||||
|
rotation,
|
||||||
|
offsetX = 0,
|
||||||
|
offsetY = 0,
|
||||||
|
dontFlip = false,
|
||||||
|
}) {
|
||||||
|
this.viewBox = viewBox;
|
||||||
|
this.userUnit = userUnit;
|
||||||
|
this.scale = scale;
|
||||||
|
this.rotation = rotation;
|
||||||
|
this.offsetX = offsetX;
|
||||||
|
this.offsetY = offsetY;
|
||||||
|
|
||||||
|
scale *= userUnit; // Take the userUnit into account.
|
||||||
|
|
||||||
|
// creating transform to convert pdf coordinate system to the normal
|
||||||
|
// canvas like coordinates taking in account scale and rotation
|
||||||
|
const centerX = (viewBox[2] + viewBox[0]) / 2;
|
||||||
|
const centerY = (viewBox[3] + viewBox[1]) / 2;
|
||||||
|
let rotateA, rotateB, rotateC, rotateD;
|
||||||
|
// Normalize the rotation, by clamping it to the [0, 360) range.
|
||||||
|
rotation %= 360;
|
||||||
|
if (rotation < 0) {
|
||||||
|
rotation += 360;
|
||||||
|
}
|
||||||
|
switch (rotation) {
|
||||||
|
case 180:
|
||||||
|
rotateA = -1;
|
||||||
|
rotateB = 0;
|
||||||
|
rotateC = 0;
|
||||||
|
rotateD = 1;
|
||||||
|
break;
|
||||||
|
case 90:
|
||||||
|
rotateA = 0;
|
||||||
|
rotateB = 1;
|
||||||
|
rotateC = 1;
|
||||||
|
rotateD = 0;
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
rotateA = 0;
|
||||||
|
rotateB = -1;
|
||||||
|
rotateC = -1;
|
||||||
|
rotateD = 0;
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
rotateA = 1;
|
||||||
|
rotateB = 0;
|
||||||
|
rotateC = 0;
|
||||||
|
rotateD = -1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
"PageViewport: Invalid rotation, must be a multiple of 90 degrees."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dontFlip) {
|
||||||
|
rotateC = -rotateC;
|
||||||
|
rotateD = -rotateD;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offsetCanvasX, offsetCanvasY;
|
||||||
|
let width, height;
|
||||||
|
if (rotateA === 0) {
|
||||||
|
offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX;
|
||||||
|
offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY;
|
||||||
|
width = (viewBox[3] - viewBox[1]) * scale;
|
||||||
|
height = (viewBox[2] - viewBox[0]) * scale;
|
||||||
|
} else {
|
||||||
|
offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX;
|
||||||
|
offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY;
|
||||||
|
width = (viewBox[2] - viewBox[0]) * scale;
|
||||||
|
height = (viewBox[3] - viewBox[1]) * scale;
|
||||||
|
}
|
||||||
|
// creating transform for the following operations:
|
||||||
|
// translate(-centerX, -centerY), rotate and flip vertically,
|
||||||
|
// scale, and translate(offsetCanvasX, offsetCanvasY)
|
||||||
|
this.transform = [
|
||||||
|
rotateA * scale,
|
||||||
|
rotateB * scale,
|
||||||
|
rotateC * scale,
|
||||||
|
rotateD * scale,
|
||||||
|
offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY,
|
||||||
|
offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY,
|
||||||
|
];
|
||||||
|
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The original, un-scaled, viewport dimensions.
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
get rawDims() {
|
||||||
|
const dims = this.viewBox;
|
||||||
|
|
||||||
|
return shadow(this, "rawDims", {
|
||||||
|
pageWidth: dims[2] - dims[0],
|
||||||
|
pageHeight: dims[3] - dims[1],
|
||||||
|
pageX: dims[0],
|
||||||
|
pageY: dims[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones viewport, with optional additional properties.
|
||||||
|
* @param {PageViewportCloneParameters} [params]
|
||||||
|
* @returns {PageViewport} Cloned viewport.
|
||||||
|
*/
|
||||||
|
clone({
|
||||||
|
scale = this.scale,
|
||||||
|
rotation = this.rotation,
|
||||||
|
offsetX = this.offsetX,
|
||||||
|
offsetY = this.offsetY,
|
||||||
|
dontFlip = false,
|
||||||
|
} = {}) {
|
||||||
|
return new PageViewport({
|
||||||
|
viewBox: this.viewBox.slice(),
|
||||||
|
userUnit: this.userUnit,
|
||||||
|
scale,
|
||||||
|
rotation,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
dontFlip,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts PDF point to the viewport coordinates. For examples, useful for
|
||||||
|
* converting PDF location into canvas pixel coordinates.
|
||||||
|
* @param {number} x - The x-coordinate.
|
||||||
|
* @param {number} y - The y-coordinate.
|
||||||
|
* @returns {Array} Array containing `x`- and `y`-coordinates of the
|
||||||
|
* point in the viewport coordinate space.
|
||||||
|
* @see {@link convertToPdfPoint}
|
||||||
|
* @see {@link convertToViewportRectangle}
|
||||||
|
*/
|
||||||
|
convertToViewportPoint(x, y) {
|
||||||
|
const p = [x, y];
|
||||||
|
Util.applyTransform(p, this.transform);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts PDF rectangle to the viewport coordinates.
|
||||||
|
* @param {Array} rect - The xMin, yMin, xMax and yMax coordinates.
|
||||||
|
* @returns {Array} Array containing corresponding coordinates of the
|
||||||
|
* rectangle in the viewport coordinate space.
|
||||||
|
* @see {@link convertToViewportPoint}
|
||||||
|
*/
|
||||||
|
convertToViewportRectangle(rect) {
|
||||||
|
const topLeft = [rect[0], rect[1]];
|
||||||
|
Util.applyTransform(topLeft, this.transform);
|
||||||
|
const bottomRight = [rect[2], rect[3]];
|
||||||
|
Util.applyTransform(bottomRight, this.transform);
|
||||||
|
return [topLeft[0], topLeft[1], bottomRight[0], bottomRight[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts viewport coordinates to the PDF location. For examples, useful
|
||||||
|
* for converting canvas pixel location into PDF one.
|
||||||
|
* @param {number} x - The x-coordinate.
|
||||||
|
* @param {number} y - The y-coordinate.
|
||||||
|
* @returns {Array} Array containing `x`- and `y`-coordinates of the
|
||||||
|
* point in the PDF coordinate space.
|
||||||
|
* @see {@link convertToViewportPoint}
|
||||||
|
*/
|
||||||
|
convertToPdfPoint(x, y) {
|
||||||
|
const p = [x, y];
|
||||||
|
Util.applyInverseTransform(p, this.transform);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RenderingCancelledException extends BaseException {
|
class RenderingCancelledException extends BaseException {
|
||||||
constructor(msg, extraDelay = 0) {
|
constructor(msg, extraDelay = 0) {
|
||||||
super(msg, "RenderingCancelledException");
|
super(msg, "RenderingCancelledException");
|
||||||
@ -346,6 +561,21 @@ class PDFDateString {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: This is (mostly) intended to support printing of XFA forms.
|
||||||
|
*/
|
||||||
|
function getXfaPageViewport(xfaPage, { scale = 1, rotation = 0 }) {
|
||||||
|
const { width, height } = xfaPage.attributes.style;
|
||||||
|
const viewBox = [0, 0, parseInt(width, 10), parseInt(height, 10)];
|
||||||
|
|
||||||
|
return new PageViewport({
|
||||||
|
viewBox,
|
||||||
|
userUnit: 1,
|
||||||
|
scale,
|
||||||
|
rotation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getRGBA(color) {
|
function getRGBA(color) {
|
||||||
if (color.startsWith("#")) {
|
if (color.startsWith("#")) {
|
||||||
// #RRGGBB or #RRGGBBAA
|
// #RRGGBB or #RRGGBBAA
|
||||||
@ -828,12 +1058,14 @@ export {
|
|||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
getRGB,
|
getRGB,
|
||||||
getRGBA,
|
getRGBA,
|
||||||
|
getXfaPageViewport,
|
||||||
isDataScheme,
|
isDataScheme,
|
||||||
isPdfFile,
|
isPdfFile,
|
||||||
isValidFetchUrl,
|
isValidFetchUrl,
|
||||||
makePathFromDrawOPS,
|
makePathFromDrawOPS,
|
||||||
noContextMenu,
|
noContextMenu,
|
||||||
OutputScale,
|
OutputScale,
|
||||||
|
PageViewport,
|
||||||
PDFDateString,
|
PDFDateString,
|
||||||
PixelsPerInch,
|
PixelsPerInch,
|
||||||
RenderingCancelledException,
|
RenderingCancelledException,
|
||||||
@ -842,4 +1074,5 @@ export {
|
|||||||
StatTimer,
|
StatTimer,
|
||||||
stopEvent,
|
stopEvent,
|
||||||
SupportedImageMimeTypes,
|
SupportedImageMimeTypes,
|
||||||
|
SVG_NS,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -16,174 +16,6 @@
|
|||||||
import { DOMSVGFactory } from "./svg_factory.js";
|
import { DOMSVGFactory } from "./svg_factory.js";
|
||||||
import { shadow } from "../shared/util.js";
|
import { shadow } from "../shared/util.js";
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef DrawLayerOptions
|
|
||||||
* Configuration for {@linkcode DrawLayer}.
|
|
||||||
* @property {Object | null} [filterFactory]
|
|
||||||
* Filter factory used to style selections (optional).
|
|
||||||
* @property {Object | null} [pageColors]
|
|
||||||
* Page foreground/background colors for HCM (optional).
|
|
||||||
* @property {number} pageIndex
|
|
||||||
* Zero-based page index.
|
|
||||||
* @property {Element | null} [textLayer]
|
|
||||||
* Text layer element (optional).
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef EdgeBoundaryResult
|
|
||||||
* Result of {@linkcode normalizeEdgeBoundary}.
|
|
||||||
* @property {Node} container
|
|
||||||
* Normalized container.
|
|
||||||
* @property {number} offset
|
|
||||||
* Normalized offset.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef SelectionRotatorResult
|
|
||||||
* Result of {@linkcode SelectionRotator}.
|
|
||||||
* @property {number} x
|
|
||||||
* Rotated X coordinate.
|
|
||||||
* @property {number} y
|
|
||||||
* Rotated Y coordinate.
|
|
||||||
* @property {number} width
|
|
||||||
* Rotated width.
|
|
||||||
* @property {number} height
|
|
||||||
* Rotated height.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @callback SelectionRotator
|
|
||||||
* Rotate the coordinates of a rectangle according to the position of the
|
|
||||||
* text layer in the viewport.
|
|
||||||
* @param {number} x
|
|
||||||
* X coordinate.
|
|
||||||
* @param {number} y
|
|
||||||
* Y coordinate.
|
|
||||||
* @param {number} width
|
|
||||||
* Width.
|
|
||||||
* @param {number} height
|
|
||||||
* Height.
|
|
||||||
* @returns {SelectionRotatorResult}
|
|
||||||
* Rotated coordinates.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef TextLayerSelectionData
|
|
||||||
* Data related to the selection for a text layer.
|
|
||||||
* @property {DrawLayer} drawLayer
|
|
||||||
* Draw layer associated with the text layer.
|
|
||||||
* @property {SVGPathElement | null} [path]
|
|
||||||
* Node (SVG path element) used to draw the selection.
|
|
||||||
* @property {HTMLDivElement | null} [selectionDiv]
|
|
||||||
* Node (div element) used to display the selection.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare the document position of two text layers.
|
|
||||||
*
|
|
||||||
* @param {Element} a
|
|
||||||
* Text layer.
|
|
||||||
* @param {Element} b
|
|
||||||
* Other text layer.
|
|
||||||
* @returns {-1 | 0 | 1}
|
|
||||||
* `-1` if the `a` is before `b`, `1` if after, or `0`.
|
|
||||||
*/
|
|
||||||
function compareTextLayers(a, b) {
|
|
||||||
if (a === b) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
||||||
? -1
|
|
||||||
: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the closest text layer upwards.
|
|
||||||
*
|
|
||||||
* @param {Node | null} node
|
|
||||||
* Node.
|
|
||||||
* @returns {Element | null}
|
|
||||||
* Closest ancestral text layer or `null`.
|
|
||||||
*/
|
|
||||||
function getTextLayer(node) {
|
|
||||||
if (!node) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
return node.closest(".textLayer");
|
|
||||||
}
|
|
||||||
return node.parentElement?.closest(".textLayer") || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare the position of two points in the document order.
|
|
||||||
*
|
|
||||||
* @param {Node} nodeA
|
|
||||||
* Node.
|
|
||||||
* @param {number} offsetA
|
|
||||||
* Offset.
|
|
||||||
* @param {Node} nodeB
|
|
||||||
* Other node.
|
|
||||||
* @param {number} offsetB
|
|
||||||
* Other offset.
|
|
||||||
* @returns {boolean | null}
|
|
||||||
* Whether the first point is before the second one, or `null` if they are
|
|
||||||
* not comparable.
|
|
||||||
*/
|
|
||||||
function isPointBefore(nodeA, offsetA, nodeB, offsetB) {
|
|
||||||
if (nodeA === nodeB) {
|
|
||||||
return offsetA <= offsetB;
|
|
||||||
}
|
|
||||||
const relation = nodeA.compareDocumentPosition(nodeB);
|
|
||||||
if (relation & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (relation & Node.DOCUMENT_POSITION_PRECEDING) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize the position of a boundary point when it's at the end of a text
|
|
||||||
* layer.
|
|
||||||
* In that case, we want to move it to the last valid position within
|
|
||||||
* the text layer, which can be either the end of the last text node or the end
|
|
||||||
* of the last text node before the endOfContent element if it exists.
|
|
||||||
*
|
|
||||||
* @param {Node} container
|
|
||||||
* Container.
|
|
||||||
* @param {number} offset
|
|
||||||
* Offset.
|
|
||||||
* @param {Element} textLayer
|
|
||||||
* Text layer.
|
|
||||||
* @returns {EdgeBoundaryResult | null}
|
|
||||||
* Normalized position or `null` if the position is not valid.
|
|
||||||
*/
|
|
||||||
function normalizeEdgeBoundary(container, offset, textLayer) {
|
|
||||||
if (
|
|
||||||
container.nodeType !== Node.ELEMENT_NODE ||
|
|
||||||
!container.classList.contains("textLayer") ||
|
|
||||||
offset !== container.childNodes.length
|
|
||||||
) {
|
|
||||||
return { container, offset };
|
|
||||||
}
|
|
||||||
let lastNode = container.lastChild;
|
|
||||||
if (
|
|
||||||
lastNode?.nodeType === Node.ELEMENT_NODE &&
|
|
||||||
lastNode.classList.contains("endOfContent")
|
|
||||||
) {
|
|
||||||
lastNode = lastNode.previousSibling;
|
|
||||||
}
|
|
||||||
if (!lastNode || !textLayer.contains(lastNode)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (lastNode.nodeType === Node.TEXT_NODE) {
|
|
||||||
return { container: lastNode, offset: lastNode.textContent.length };
|
|
||||||
}
|
|
||||||
return { container: lastNode, offset: lastNode.childNodes.length };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage the SVGs drawn on top of the page canvas.
|
* Manage the SVGs drawn on top of the page canvas.
|
||||||
* It's important to have them directly on top of the canvas because we want to
|
* It's important to have them directly on top of the canvas because we want to
|
||||||
@ -194,131 +26,13 @@ class DrawLayer {
|
|||||||
|
|
||||||
#mapping = new Map();
|
#mapping = new Map();
|
||||||
|
|
||||||
/** @type {Element | null} */
|
|
||||||
#textLayer = null;
|
|
||||||
|
|
||||||
/** @type {Object | null} */
|
|
||||||
#filterFactory = null;
|
|
||||||
|
|
||||||
/** @type {Object | null} */
|
|
||||||
#pageColors = null;
|
|
||||||
|
|
||||||
/** @type {MutationObserver | null} */
|
|
||||||
#textLayerObserver = null;
|
|
||||||
|
|
||||||
#toUpdate = new Map();
|
#toUpdate = new Map();
|
||||||
|
|
||||||
static #id = 0;
|
static #id = 0;
|
||||||
|
|
||||||
static #selectionId = 0;
|
|
||||||
|
|
||||||
/** @type {AbortController | null} */
|
|
||||||
static #selectionChangeAC = null;
|
|
||||||
|
|
||||||
/** @type {Set<HTMLDivElement>} */
|
|
||||||
static #selections = new Set();
|
|
||||||
|
|
||||||
/** @type {boolean} */
|
|
||||||
static #isSelecting = false;
|
|
||||||
|
|
||||||
/** @type {Set<Element>} */
|
|
||||||
static #textLayerSet = new Set();
|
|
||||||
|
|
||||||
/** @type {WeakMap<Element, TextLayerSelectionData>} */
|
|
||||||
static #textLayers = new WeakMap();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {DrawLayerOptions} options
|
|
||||||
* Configuration.
|
|
||||||
* @returns
|
|
||||||
* Instance.
|
|
||||||
*/
|
|
||||||
constructor({
|
|
||||||
filterFactory = null,
|
|
||||||
pageColors = null,
|
|
||||||
pageIndex,
|
|
||||||
textLayer = null,
|
|
||||||
}) {
|
|
||||||
this.pageIndex = pageIndex;
|
|
||||||
this.#filterFactory = filterFactory;
|
|
||||||
this.#pageColors = pageColors;
|
|
||||||
if (textLayer) {
|
|
||||||
const previousData = DrawLayer.#textLayers.get(textLayer);
|
|
||||||
if (previousData?.selectionDiv) {
|
|
||||||
previousData.selectionDiv.remove();
|
|
||||||
DrawLayer.#selections.delete(previousData.selectionDiv);
|
|
||||||
}
|
|
||||||
DrawLayer.#textLayers.set(textLayer, { drawLayer: this });
|
|
||||||
DrawLayer.#textLayerSet.add(textLayer);
|
|
||||||
this.#textLayer = textLayer;
|
|
||||||
this.#textLayerObserver = new MutationObserver(records => {
|
|
||||||
if (
|
|
||||||
!this.#parent ||
|
|
||||||
!this.#textLayer?.isConnected ||
|
|
||||||
!DrawLayer.#hasSelection()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const { addedNodes } of records) {
|
|
||||||
for (const node of addedNodes) {
|
|
||||||
if (
|
|
||||||
node.nodeType === Node.ELEMENT_NODE &&
|
|
||||||
node.classList.contains("endOfContent")
|
|
||||||
) {
|
|
||||||
DrawLayer.#selectionChange();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.#textLayerObserver.observe(textLayer, { childList: true });
|
|
||||||
|
|
||||||
if (DrawLayer.#selectionChangeAC === null) {
|
|
||||||
DrawLayer.#selectionChangeAC = new AbortController();
|
|
||||||
const { signal } = DrawLayer.#selectionChangeAC;
|
|
||||||
document.addEventListener(
|
|
||||||
"selectionchange",
|
|
||||||
DrawLayer.#selectionChange.bind(DrawLayer),
|
|
||||||
{ signal }
|
|
||||||
);
|
|
||||||
// Track pointer selection state to preserve selections during
|
|
||||||
// cross-boundary drags.
|
|
||||||
document.addEventListener(
|
|
||||||
"pointerdown",
|
|
||||||
() => {
|
|
||||||
DrawLayer.#isSelecting = true;
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
);
|
|
||||||
document.addEventListener(
|
|
||||||
"pointerup",
|
|
||||||
() => {
|
|
||||||
DrawLayer.#isSelecting = false;
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
);
|
|
||||||
// If the pointer is released outside the window, we may not get a
|
|
||||||
// corresponding `pointerup` event.
|
|
||||||
window.addEventListener(
|
|
||||||
"blur",
|
|
||||||
() => {
|
|
||||||
DrawLayer.#isSelecting = false;
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setParent(parent) {
|
setParent(parent) {
|
||||||
if (!this.#parent) {
|
if (!this.#parent) {
|
||||||
this.#parent = parent;
|
this.#parent = parent;
|
||||||
// A new text layer just became live (e.g. its page was scrolled into
|
|
||||||
// view). If a selection already exists, redraw overlays so that the
|
|
||||||
// selection extends into this newly-rendered text layer.
|
|
||||||
if (this.#textLayer?.isConnected && DrawLayer.#hasSelection()) {
|
|
||||||
DrawLayer.#selectionChange();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,321 +47,6 @@ class DrawLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up the selection for a text layer.
|
|
||||||
*
|
|
||||||
* @param {Element} textLayer
|
|
||||||
* Text layer.
|
|
||||||
* @returns {undefined}
|
|
||||||
* Nothing.
|
|
||||||
*/
|
|
||||||
static #cleanupTextLayerSelection(textLayer) {
|
|
||||||
const textLayerData = this.#textLayers.get(textLayer);
|
|
||||||
if (!textLayerData?.selectionDiv) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
textLayerData.selectionDiv.remove();
|
|
||||||
this.#selections.delete(textLayerData.selectionDiv);
|
|
||||||
textLayerData.selectionDiv = null;
|
|
||||||
textLayerData.path = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean}
|
|
||||||
* Whether there is a non-collapsed document selection.
|
|
||||||
*/
|
|
||||||
static #hasSelection() {
|
|
||||||
const selection = document.getSelection();
|
|
||||||
return !!selection && !selection.isCollapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Array<Element>}
|
|
||||||
* Connected text layers sorted in document order.
|
|
||||||
*/
|
|
||||||
static #getOrderedTextLayers() {
|
|
||||||
return [...this.#textLayerSet]
|
|
||||||
.filter(textLayer => textLayer.isConnected)
|
|
||||||
.sort(compareTextLayers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle `selectionchange` to update the selection display for text layers.
|
|
||||||
* We want to display the selection in a separate layer on top of the text
|
|
||||||
* layer because the text layer has `mix-blend-mode: multiply` and we want
|
|
||||||
* the selection to have a different blend mode.
|
|
||||||
*
|
|
||||||
* @returns {undefined}
|
|
||||||
* Nothing.
|
|
||||||
*/
|
|
||||||
static #selectionChange() {
|
|
||||||
const selection = document.getSelection();
|
|
||||||
if (!selection || selection.isCollapsed) {
|
|
||||||
for (const root of this.#selections) {
|
|
||||||
root.remove();
|
|
||||||
}
|
|
||||||
this.#selections.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/** @type {WeakMap<Node, SelectionRotator>} */
|
|
||||||
const rotators = new WeakMap();
|
|
||||||
const orderedTextLayers = this.#getOrderedTextLayers();
|
|
||||||
/** @type {Array<[Range, Element]>} */
|
|
||||||
const ranges = [];
|
|
||||||
for (let i = 0, ii = selection.rangeCount; i < ii; i++) {
|
|
||||||
const range = selection.getRangeAt(i);
|
|
||||||
if (range.collapsed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let { startContainer, startOffset, endContainer, endOffset } = range;
|
|
||||||
let startTextLayer = getTextLayer(startContainer);
|
|
||||||
let endTextLayer = getTextLayer(endContainer);
|
|
||||||
const startMissing = startTextLayer === null;
|
|
||||||
const endMissing = endTextLayer === null;
|
|
||||||
|
|
||||||
// XOR case: exactly one boundary is outside tracked text layers.
|
|
||||||
// In Firefox/Safari this can happen transiently while dragging outside
|
|
||||||
// the page. Preserve the current overlay and exit early.
|
|
||||||
if (this.#isSelecting && startMissing !== endMissing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selection.rangeCount === 1) {
|
|
||||||
const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
|
|
||||||
const anchorLayer = getTextLayer(anchorNode);
|
|
||||||
const focusLayer = getTextLayer(focusNode);
|
|
||||||
const anchorBeforeFocus = isPointBefore(
|
|
||||||
anchorNode,
|
|
||||||
anchorOffset,
|
|
||||||
focusNode,
|
|
||||||
focusOffset
|
|
||||||
);
|
|
||||||
if (anchorLayer && focusLayer && anchorBeforeFocus !== null) {
|
|
||||||
if (anchorBeforeFocus) {
|
|
||||||
startContainer = anchorNode;
|
|
||||||
startOffset = anchorOffset;
|
|
||||||
startTextLayer = anchorLayer;
|
|
||||||
endContainer = focusNode;
|
|
||||||
endOffset = focusOffset;
|
|
||||||
endTextLayer = focusLayer;
|
|
||||||
} else {
|
|
||||||
startContainer = focusNode;
|
|
||||||
startOffset = focusOffset;
|
|
||||||
startTextLayer = focusLayer;
|
|
||||||
endContainer = anchorNode;
|
|
||||||
endOffset = anchorOffset;
|
|
||||||
endTextLayer = anchorLayer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeTextLayers = orderedTextLayers.filter(textLayer =>
|
|
||||||
range.intersectsNode(textLayer)
|
|
||||||
);
|
|
||||||
if (activeTextLayers.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a boundary is outside any text layer, use the selected live text
|
|
||||||
// layers as the range edges. This handles Select All, whose DOM range can
|
|
||||||
// span ancestors of the text layers.
|
|
||||||
let boundarySubstituted = false;
|
|
||||||
if (!startTextLayer) {
|
|
||||||
startTextLayer = activeTextLayers[0];
|
|
||||||
startContainer = startTextLayer;
|
|
||||||
startOffset = 0;
|
|
||||||
boundarySubstituted = true;
|
|
||||||
}
|
|
||||||
if (!endTextLayer) {
|
|
||||||
endTextLayer = activeTextLayers.at(-1);
|
|
||||||
endContainer = endTextLayer;
|
|
||||||
endOffset = endTextLayer.childNodes.length;
|
|
||||||
boundarySubstituted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endContainer.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
if (endContainer.classList.contains("endOfContent")) {
|
|
||||||
const previousNode = endContainer.previousSibling;
|
|
||||||
if (!previousNode) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
endContainer = previousNode;
|
|
||||||
endOffset =
|
|
||||||
previousNode.nodeType === Node.TEXT_NODE
|
|
||||||
? previousNode.textContent.length
|
|
||||||
: previousNode.childNodes.length;
|
|
||||||
} else if (
|
|
||||||
endContainer.classList.contains("textLayer") &&
|
|
||||||
endContainer.childNodes.length === endOffset
|
|
||||||
) {
|
|
||||||
const normalizedEnd = normalizeEdgeBoundary(
|
|
||||||
endContainer,
|
|
||||||
endOffset,
|
|
||||||
endTextLayer
|
|
||||||
);
|
|
||||||
if (!normalizedEnd) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
endContainer = normalizedEnd.container;
|
|
||||||
endOffset = normalizedEnd.offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startContainer.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
const normalizedStart = normalizeEdgeBoundary(
|
|
||||||
startContainer,
|
|
||||||
startOffset,
|
|
||||||
startTextLayer
|
|
||||||
);
|
|
||||||
if (!normalizedStart) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
startContainer = normalizedStart.container;
|
|
||||||
startOffset = normalizedStart.offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
startTextLayer === endTextLayer &&
|
|
||||||
!boundarySubstituted &&
|
|
||||||
activeTextLayers.includes(startTextLayer)
|
|
||||||
) {
|
|
||||||
ranges.push([range, startTextLayer]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const textLayer of activeTextLayers) {
|
|
||||||
const firstNode = textLayer.firstChild;
|
|
||||||
if (!firstNode) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const subRange = document.createRange();
|
|
||||||
if (textLayer === startTextLayer) {
|
|
||||||
subRange.setStart(startContainer, startOffset);
|
|
||||||
} else {
|
|
||||||
subRange.setStartBefore(firstNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textLayer === endTextLayer) {
|
|
||||||
subRange.setEnd(endContainer, endOffset);
|
|
||||||
} else {
|
|
||||||
const lastNode = textLayer.lastChild;
|
|
||||||
if (!lastNode) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
lastNode.nodeType === Node.ELEMENT_NODE &&
|
|
||||||
lastNode.classList.contains("endOfContent")
|
|
||||||
) {
|
|
||||||
const lastTextNode = lastNode.previousSibling;
|
|
||||||
if (!lastTextNode) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
subRange.setEndAfter(lastTextNode);
|
|
||||||
} else {
|
|
||||||
subRange.setEndAfter(lastNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subRange.collapsed) {
|
|
||||||
ranges.push([subRange, textLayer]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {Set<Element>} */
|
|
||||||
const selectedTextLayers = new Set(ranges.map(range => range[1]));
|
|
||||||
|
|
||||||
for (const textLayer of this.#textLayerSet) {
|
|
||||||
if (!selectedTextLayers.has(textLayer)) {
|
|
||||||
this.#cleanupTextLayerSelection(textLayer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [range, textLayer] of ranges) {
|
|
||||||
const textLayerData = DrawLayer.#textLayers.get(textLayer);
|
|
||||||
if (!textLayerData) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rotator = rotators.get(textLayer);
|
|
||||||
if (!rotator) {
|
|
||||||
const clientRect = textLayer.getBoundingClientRect();
|
|
||||||
|
|
||||||
rotator = (x, y, w, h) => ({
|
|
||||||
x: (x - clientRect.x) / clientRect.width,
|
|
||||||
y: (y - clientRect.y) / clientRect.height,
|
|
||||||
width: w / clientRect.width,
|
|
||||||
height: h / clientRect.height,
|
|
||||||
});
|
|
||||||
rotators.set(textLayer, rotator);
|
|
||||||
}
|
|
||||||
/** @type {Array<string>} */
|
|
||||||
const boxes = [];
|
|
||||||
for (let { x, y, width, height } of range.getClientRects()) {
|
|
||||||
if (width === 0 || height === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
({ x, y, width, height } = rotator(x, y, width, height));
|
|
||||||
if (width === 1 && height === 1) {
|
|
||||||
// The entire page is selected.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
boxes.push(`M${x} ${y} h${width} v${height} h-${width} Z`);
|
|
||||||
}
|
|
||||||
if (boxes.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const drawLayer = textLayerData.drawLayer;
|
|
||||||
let div = textLayerData.selectionDiv;
|
|
||||||
let path = textLayerData.path;
|
|
||||||
if (!div) {
|
|
||||||
const clipPathId = `clip_selection_${DrawLayer.#selectionId++}`;
|
|
||||||
|
|
||||||
div = document.createElement("div");
|
|
||||||
div.className = "selection";
|
|
||||||
div.style.clipPath = `url(#${clipPathId})`;
|
|
||||||
|
|
||||||
const selectionStyle = drawLayer.#filterFactory?.createSelectionStyle(
|
|
||||||
drawLayer.#pageColors
|
|
||||||
);
|
|
||||||
if (selectionStyle) {
|
|
||||||
for (const [name, value] of Object.entries(selectionStyle)) {
|
|
||||||
div.style.setProperty(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const svg = DrawLayer._svgFactory.create(
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
/* skipDimensions = */ true
|
|
||||||
);
|
|
||||||
svg.setAttribute("aria-hidden", "true");
|
|
||||||
svg.setAttribute("width", "100%");
|
|
||||||
svg.setAttribute("height", "100%");
|
|
||||||
|
|
||||||
const clipPath = DrawLayer._svgFactory.createElement("clipPath");
|
|
||||||
clipPath.setAttribute("id", clipPathId);
|
|
||||||
clipPath.setAttribute("clipPathUnits", "objectBoundingBox");
|
|
||||||
|
|
||||||
path = DrawLayer._svgFactory.createElement("path");
|
|
||||||
|
|
||||||
clipPath.append(path);
|
|
||||||
svg.append(clipPath);
|
|
||||||
div.append(svg);
|
|
||||||
|
|
||||||
textLayerData.path = path;
|
|
||||||
textLayerData.selectionDiv = div;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!div.parentNode && drawLayer.#parent) {
|
|
||||||
drawLayer.#parent.append(div);
|
|
||||||
this.#selections.add(div);
|
|
||||||
}
|
|
||||||
|
|
||||||
path.setAttribute("d", boxes.join(" "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get _svgFactory() {
|
static get _svgFactory() {
|
||||||
return shadow(this, "_svgFactory", new DOMSVGFactory());
|
return shadow(this, "_svgFactory", new DOMSVGFactory());
|
||||||
}
|
}
|
||||||
@ -663,7 +62,7 @@ class DrawLayer {
|
|||||||
#createSVG() {
|
#createSVG() {
|
||||||
const svg = DrawLayer._svgFactory.create(1, 1, /* skipDimensions = */ true);
|
const svg = DrawLayer._svgFactory.create(1, 1, /* skipDimensions = */ true);
|
||||||
this.#parent.append(svg);
|
this.#parent.append(svg);
|
||||||
svg.setAttribute("aria-hidden", "true");
|
svg.setAttribute("aria-hidden", true);
|
||||||
|
|
||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
@ -840,22 +239,6 @@ class DrawLayer {
|
|||||||
}
|
}
|
||||||
this.#mapping.clear();
|
this.#mapping.clear();
|
||||||
this.#toUpdate.clear();
|
this.#toUpdate.clear();
|
||||||
this.#textLayerObserver?.disconnect();
|
|
||||||
this.#textLayerObserver = null;
|
|
||||||
if (this.#textLayer) {
|
|
||||||
const data = DrawLayer.#textLayers.get(this.#textLayer);
|
|
||||||
if (data?.drawLayer === this) {
|
|
||||||
DrawLayer.#cleanupTextLayerSelection(this.#textLayer);
|
|
||||||
DrawLayer.#textLayers.delete(this.#textLayer);
|
|
||||||
DrawLayer.#textLayerSet.delete(this.#textLayer);
|
|
||||||
if (DrawLayer.#textLayerSet.size === 0) {
|
|
||||||
DrawLayer.#selectionChangeAC?.abort();
|
|
||||||
DrawLayer.#selectionChangeAC = null;
|
|
||||||
DrawLayer.#isSelecting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.#textLayer = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
/** @typedef {import("./tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||||
/** @typedef {import("../page_viewport.js").PageViewport} PageViewport */
|
/** @typedef {import("../display_utils.js").PageViewport} PageViewport */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
/** @typedef {import("../../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
|||||||
@ -85,6 +85,19 @@ class Outline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static _normalizePagePoint(x, y, rotation) {
|
||||||
|
switch (rotation) {
|
||||||
|
case 90:
|
||||||
|
return [1 - y, x];
|
||||||
|
case 180:
|
||||||
|
return [1 - x, 1 - y];
|
||||||
|
case 270:
|
||||||
|
return [y, 1 - x];
|
||||||
|
default:
|
||||||
|
return [x, y];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static createBezierPoints(x1, y1, x2, y2, x3, y3) {
|
static createBezierPoints(x1, y1, x2, y2, x3, y3) {
|
||||||
return [
|
return [
|
||||||
(x1 + 5 * x2) / 6,
|
(x1 + 5 * x2) / 6,
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import {
|
|||||||
FeatureTest,
|
FeatureTest,
|
||||||
getUuid,
|
getUuid,
|
||||||
shadow,
|
shadow,
|
||||||
SVG_NS,
|
|
||||||
Util,
|
Util,
|
||||||
warn,
|
warn,
|
||||||
} from "../../shared/util.js";
|
} from "../../shared/util.js";
|
||||||
@ -171,7 +170,7 @@ class ImageManager {
|
|||||||
// The "workaround" is to append "svgView(preserveAspectRatio(none))" to the
|
// The "workaround" is to append "svgView(preserveAspectRatio(none))" to the
|
||||||
// url, but according to comment #15, it seems that it leads to unexpected
|
// url, but according to comment #15, it seems that it leads to unexpected
|
||||||
// behavior in Safari.
|
// behavior in Safari.
|
||||||
const svg = `data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 1 1" width="1" height="1" xmlns="${SVG_NS}"><rect width="1" height="1" style="fill:red;"/></svg>`;
|
const svg = `data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 1 1" width="1" height="1" xmlns="http://www.w3.org/2000/svg"><rect width="1" height="1" style="fill:red;"/></svg>`;
|
||||||
const canvas = new OffscreenCanvas(1, 3);
|
const canvas = new OffscreenCanvas(1, 3);
|
||||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
|
|||||||
@ -13,15 +13,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { getRGB, isDataScheme, SVG_NS } from "./display_utils.js";
|
||||||
FeatureTest,
|
import { unreachable, updateUrlHash, Util, warn } from "../shared/util.js";
|
||||||
SVG_NS,
|
|
||||||
unreachable,
|
|
||||||
updateUrlHash,
|
|
||||||
Util,
|
|
||||||
warn,
|
|
||||||
} from "../shared/util.js";
|
|
||||||
import { getRGB, getRGBA, isDataScheme } from "./display_utils.js";
|
|
||||||
|
|
||||||
class BaseFilterFactory {
|
class BaseFilterFactory {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -57,36 +50,6 @@ class BaseFilterFactory {
|
|||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a filter for the selection of text, given colors.
|
|
||||||
*
|
|
||||||
* @param {string} fgColor
|
|
||||||
* @param {string} bgColor
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
addSelectionHCMFilter(fgColor, bgColor) {
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a filter for the selection of text.
|
|
||||||
*
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
addSelectionFilter() {
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} [pageColors]
|
|
||||||
* @param {string} [pageColors.background]
|
|
||||||
* @param {string} [pageColors.foreground]
|
|
||||||
* @returns {Record<string, string> | null}
|
|
||||||
*/
|
|
||||||
createSelectionStyle(pageColors = null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(keepHCM = false) {}
|
destroy(keepHCM = false) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,66 +269,6 @@ class DOMFilterFactory extends BaseFilterFactory {
|
|||||||
return info.url;
|
return info.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a filter for the selection of text, given colors.
|
|
||||||
*
|
|
||||||
* @param {string} fgColor
|
|
||||||
* @param {string} bgColor
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
addSelectionHCMFilter(fgColor, bgColor) {
|
|
||||||
return this.addHighlightHCMFilter(
|
|
||||||
"selection",
|
|
||||||
fgColor,
|
|
||||||
bgColor,
|
|
||||||
// Background becomes foreground so these are flipped.
|
|
||||||
"HighlightText",
|
|
||||||
"Highlight"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a filter for the selection of text.
|
|
||||||
*
|
|
||||||
* @param {string} fgColor
|
|
||||||
* @param {string} bgColor
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
addSelectionFilter() {
|
|
||||||
return this.addHighlightHCMFilter(
|
|
||||||
"selection_default",
|
|
||||||
"black",
|
|
||||||
"white",
|
|
||||||
"HighlightText",
|
|
||||||
"Highlight"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} [pageColors]
|
|
||||||
* @param {string} [pageColors.background]
|
|
||||||
* @param {string} [pageColors.foreground]
|
|
||||||
* @returns {Record<string, string> | null}
|
|
||||||
*/
|
|
||||||
createSelectionStyle(pageColors = null) {
|
|
||||||
const filter = pageColors
|
|
||||||
? this.addSelectionHCMFilter(pageColors.foreground, pageColors.background)
|
|
||||||
: this.addSelectionFilter();
|
|
||||||
|
|
||||||
// Safari does not supported SVG filters in `backdrop-filter`:
|
|
||||||
// <https://bugs.webkit.org/show_bug.cgi?id=245510>.
|
|
||||||
// Chrome *and* Safari do not use the user’s preferred text selection color.
|
|
||||||
// So this is Firefox-specific for now.
|
|
||||||
if (filter === "none" || !FeatureTest.platform.isFirefox) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"backdrop-filter": filter,
|
|
||||||
"background-color": "transparent",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
addAlphaFilter(map) {
|
addAlphaFilter(map) {
|
||||||
// When a page is zoomed the page is re-drawn but the maps are likely
|
// When a page is zoomed the page is re-drawn but the maps are likely
|
||||||
// the same.
|
// the same.
|
||||||
@ -494,7 +397,7 @@ class DOMFilterFactory extends BaseFilterFactory {
|
|||||||
0.2126 * bgRGB[0] + 0.7152 * bgRGB[1] + 0.0722 * bgRGB[2]
|
0.2126 * bgRGB[0] + 0.7152 * bgRGB[1] + 0.0722 * bgRGB[2]
|
||||||
);
|
);
|
||||||
let [newFgRGB, newBgRGB] = [newFgColor, newBgColor].map(
|
let [newFgRGB, newBgRGB] = [newFgColor, newBgColor].map(
|
||||||
this.#getOpaqueTextColor.bind(this)
|
this.#getRGB.bind(this)
|
||||||
);
|
);
|
||||||
if (bgGray < fgGray) {
|
if (bgGray < fgGray) {
|
||||||
[fgGray, bgGray, newFgRGB, newBgRGB] = [
|
[fgGray, bgGray, newFgRGB, newBgRGB] = [
|
||||||
@ -636,62 +539,6 @@ class DOMFilterFactory extends BaseFilterFactory {
|
|||||||
this.#defs.style.color = color;
|
this.#defs.style.color = color;
|
||||||
return getRGB(getComputedStyle(this.#defs).getPropertyValue("color"));
|
return getRGB(getComputedStyle(this.#defs).getPropertyValue("color"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the RGBA channels of a color.
|
|
||||||
*
|
|
||||||
* @param {string} color
|
|
||||||
* Color in any valid CSS format (such as `x` in `color: x`).
|
|
||||||
* @returns {[number, number, number, number]}
|
|
||||||
* RGBA values of the color;
|
|
||||||
* the RGB channels are in the range `[0, 255]`;
|
|
||||||
* the alpha channel is in the range `[0, 1]`.
|
|
||||||
*/
|
|
||||||
#getRGBA(color) {
|
|
||||||
this.#defs.style.color = color;
|
|
||||||
return getRGBA(getComputedStyle(this.#defs).getPropertyValue("color"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the opaque text color by, if it has an alpha layer, blending it with
|
|
||||||
* the `Canvas` background.
|
|
||||||
*
|
|
||||||
* @param {string} color
|
|
||||||
* Color in any valid CSS format (such as `x` in `color: x`).
|
|
||||||
* @returns {[number, number, number]}
|
|
||||||
* RGB values of the opaque color.
|
|
||||||
*/
|
|
||||||
#getOpaqueTextColor(color) {
|
|
||||||
const [r, g, b, alpha] = this.#getRGBA(color);
|
|
||||||
|
|
||||||
if (alpha === 1) {
|
|
||||||
return [r, g, b];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [canvasR, canvasG, canvasB] = this.#getRGB("Canvas");
|
|
||||||
|
|
||||||
return [
|
|
||||||
blend(r, canvasR, alpha),
|
|
||||||
blend(g, canvasG, alpha),
|
|
||||||
blend(b, canvasB, alpha),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blend a foreground color with a background color using the alpha value.
|
|
||||||
*
|
|
||||||
* @param {number} fg
|
|
||||||
* Foreground color channel value in the range `[0, 255]`.
|
|
||||||
* @param {number} bg
|
|
||||||
* Background color channel value in the range `[0, 255]`.
|
|
||||||
* @param {number} alpha
|
|
||||||
* Alpha value in the range `[0, 1]`.
|
|
||||||
* @returns {number}
|
|
||||||
* Blended color channel value in the range `[0, 255]`.
|
|
||||||
*/
|
|
||||||
function blend(fg, bg, alpha) {
|
|
||||||
return Math.round(alpha * fg + (1 - alpha) * bg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { BaseFilterFactory, DOMFilterFactory };
|
export { BaseFilterFactory, DOMFilterFactory };
|
||||||
|
|||||||
@ -1,232 +0,0 @@
|
|||||||
/* Copyright 2015 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { shadow, Util } from "../shared/util.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} PageViewportParameters
|
|
||||||
* @property {Array<number>} viewBox - The xMin, yMin, xMax and
|
|
||||||
* yMax coordinates.
|
|
||||||
* @property {number} userUnit - The size of units.
|
|
||||||
* @property {number} scale - The scale of the viewport.
|
|
||||||
* @property {number} rotation - The rotation, in degrees, of the viewport.
|
|
||||||
* @property {number} [offsetX] - The horizontal, i.e. x-axis, offset. The
|
|
||||||
* default value is `0`.
|
|
||||||
* @property {number} [offsetY] - The vertical, i.e. y-axis, offset. The
|
|
||||||
* default value is `0`.
|
|
||||||
* @property {boolean} [dontFlip] - If true, the y-axis will not be flipped.
|
|
||||||
* The default value is `false`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} PageViewportCloneParameters
|
|
||||||
* @property {number} [scale] - The scale, overriding the one in the cloned
|
|
||||||
* viewport. The default value is `this.scale`.
|
|
||||||
* @property {number} [rotation] - The rotation, in degrees, overriding the one
|
|
||||||
* in the cloned viewport. The default value is `this.rotation`.
|
|
||||||
* @property {number} [offsetX] - The horizontal, i.e. x-axis, offset.
|
|
||||||
* The default value is `this.offsetX`.
|
|
||||||
* @property {number} [offsetY] - The vertical, i.e. y-axis, offset.
|
|
||||||
* The default value is `this.offsetY`.
|
|
||||||
* @property {boolean} [dontFlip] - If true, the x-axis will not be flipped.
|
|
||||||
* The default value is `false`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PDF page viewport created based on scale, rotation and offset.
|
|
||||||
*/
|
|
||||||
class PageViewport {
|
|
||||||
/**
|
|
||||||
* @param {PageViewportParameters}
|
|
||||||
*/
|
|
||||||
constructor({
|
|
||||||
viewBox,
|
|
||||||
userUnit,
|
|
||||||
scale,
|
|
||||||
rotation,
|
|
||||||
offsetX = 0,
|
|
||||||
offsetY = 0,
|
|
||||||
dontFlip = false,
|
|
||||||
}) {
|
|
||||||
this.viewBox = viewBox;
|
|
||||||
this.userUnit = userUnit;
|
|
||||||
this.scale = scale;
|
|
||||||
this.rotation = rotation;
|
|
||||||
this.offsetX = offsetX;
|
|
||||||
this.offsetY = offsetY;
|
|
||||||
|
|
||||||
scale *= userUnit; // Take the userUnit into account.
|
|
||||||
|
|
||||||
// creating transform to convert pdf coordinate system to the normal
|
|
||||||
// canvas like coordinates taking in account scale and rotation
|
|
||||||
const centerX = (viewBox[2] + viewBox[0]) / 2;
|
|
||||||
const centerY = (viewBox[3] + viewBox[1]) / 2;
|
|
||||||
let rotateA, rotateB, rotateC, rotateD;
|
|
||||||
// Normalize the rotation, by clamping it to the [0, 360) range.
|
|
||||||
rotation %= 360;
|
|
||||||
if (rotation < 0) {
|
|
||||||
rotation += 360;
|
|
||||||
}
|
|
||||||
switch (rotation) {
|
|
||||||
case 180:
|
|
||||||
rotateA = -1;
|
|
||||||
rotateB = 0;
|
|
||||||
rotateC = 0;
|
|
||||||
rotateD = 1;
|
|
||||||
break;
|
|
||||||
case 90:
|
|
||||||
rotateA = 0;
|
|
||||||
rotateB = 1;
|
|
||||||
rotateC = 1;
|
|
||||||
rotateD = 0;
|
|
||||||
break;
|
|
||||||
case 270:
|
|
||||||
rotateA = 0;
|
|
||||||
rotateB = -1;
|
|
||||||
rotateC = -1;
|
|
||||||
rotateD = 0;
|
|
||||||
break;
|
|
||||||
case 0:
|
|
||||||
rotateA = 1;
|
|
||||||
rotateB = 0;
|
|
||||||
rotateC = 0;
|
|
||||||
rotateD = -1;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
"PageViewport: Invalid rotation, must be a multiple of 90 degrees."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dontFlip) {
|
|
||||||
rotateC = -rotateC;
|
|
||||||
rotateD = -rotateD;
|
|
||||||
}
|
|
||||||
|
|
||||||
let offsetCanvasX, offsetCanvasY;
|
|
||||||
let width, height;
|
|
||||||
if (rotateA === 0) {
|
|
||||||
offsetCanvasX = Math.abs(centerY - viewBox[1]) * scale + offsetX;
|
|
||||||
offsetCanvasY = Math.abs(centerX - viewBox[0]) * scale + offsetY;
|
|
||||||
width = (viewBox[3] - viewBox[1]) * scale;
|
|
||||||
height = (viewBox[2] - viewBox[0]) * scale;
|
|
||||||
} else {
|
|
||||||
offsetCanvasX = Math.abs(centerX - viewBox[0]) * scale + offsetX;
|
|
||||||
offsetCanvasY = Math.abs(centerY - viewBox[1]) * scale + offsetY;
|
|
||||||
width = (viewBox[2] - viewBox[0]) * scale;
|
|
||||||
height = (viewBox[3] - viewBox[1]) * scale;
|
|
||||||
}
|
|
||||||
// creating transform for the following operations:
|
|
||||||
// translate(-centerX, -centerY), rotate and flip vertically,
|
|
||||||
// scale, and translate(offsetCanvasX, offsetCanvasY)
|
|
||||||
this.transform = [
|
|
||||||
rotateA * scale,
|
|
||||||
rotateB * scale,
|
|
||||||
rotateC * scale,
|
|
||||||
rotateD * scale,
|
|
||||||
offsetCanvasX - rotateA * scale * centerX - rotateC * scale * centerY,
|
|
||||||
offsetCanvasY - rotateB * scale * centerX - rotateD * scale * centerY,
|
|
||||||
];
|
|
||||||
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The original, un-scaled, viewport dimensions.
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
get rawDims() {
|
|
||||||
const dims = this.viewBox;
|
|
||||||
|
|
||||||
return shadow(this, "rawDims", {
|
|
||||||
pageWidth: dims[2] - dims[0],
|
|
||||||
pageHeight: dims[3] - dims[1],
|
|
||||||
pageX: dims[0],
|
|
||||||
pageY: dims[1],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clones viewport, with optional additional properties.
|
|
||||||
* @param {PageViewportCloneParameters} [params]
|
|
||||||
* @returns {PageViewport} Cloned viewport.
|
|
||||||
*/
|
|
||||||
clone({
|
|
||||||
scale = this.scale,
|
|
||||||
rotation = this.rotation,
|
|
||||||
offsetX = this.offsetX,
|
|
||||||
offsetY = this.offsetY,
|
|
||||||
dontFlip = false,
|
|
||||||
} = {}) {
|
|
||||||
return new PageViewport({
|
|
||||||
viewBox: this.viewBox.slice(),
|
|
||||||
userUnit: this.userUnit,
|
|
||||||
scale,
|
|
||||||
rotation,
|
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
dontFlip,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts PDF point to the viewport coordinates. For examples, useful for
|
|
||||||
* converting PDF location into canvas pixel coordinates.
|
|
||||||
* @param {number} x - The x-coordinate.
|
|
||||||
* @param {number} y - The y-coordinate.
|
|
||||||
* @returns {Array} Array containing `x`- and `y`-coordinates of the
|
|
||||||
* point in the viewport coordinate space.
|
|
||||||
* @see {@link convertToPdfPoint}
|
|
||||||
* @see {@link convertToViewportRectangle}
|
|
||||||
*/
|
|
||||||
convertToViewportPoint(x, y) {
|
|
||||||
const p = [x, y];
|
|
||||||
Util.applyTransform(p, this.transform);
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts PDF rectangle to the viewport coordinates.
|
|
||||||
* @param {Array} rect - The xMin, yMin, xMax and yMax coordinates.
|
|
||||||
* @returns {Array} Array containing corresponding coordinates of the
|
|
||||||
* rectangle in the viewport coordinate space.
|
|
||||||
* @see {@link convertToViewportPoint}
|
|
||||||
*/
|
|
||||||
convertToViewportRectangle(rect) {
|
|
||||||
const topLeft = [rect[0], rect[1]];
|
|
||||||
Util.applyTransform(topLeft, this.transform);
|
|
||||||
const bottomRight = [rect[2], rect[3]];
|
|
||||||
Util.applyTransform(bottomRight, this.transform);
|
|
||||||
return [topLeft[0], topLeft[1], bottomRight[0], bottomRight[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts viewport coordinates to the PDF location. For examples, useful
|
|
||||||
* for converting canvas pixel location into PDF one.
|
|
||||||
* @param {number} x - The x-coordinate.
|
|
||||||
* @param {number} y - The y-coordinate.
|
|
||||||
* @returns {Array} Array containing `x`- and `y`-coordinates of the
|
|
||||||
* point in the PDF coordinate space.
|
|
||||||
* @see {@link convertToViewportPoint}
|
|
||||||
*/
|
|
||||||
convertToPdfPoint(x, y) {
|
|
||||||
const p = [x, y];
|
|
||||||
Util.applyInverseTransform(p, this.transform);
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { PageViewport };
|
|
||||||
@ -13,7 +13,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SVG_NS, unreachable } from "../shared/util.js";
|
import { SVG_NS } from "./display_utils.js";
|
||||||
|
import { unreachable } from "../shared/util.js";
|
||||||
|
|
||||||
class BaseSVGFactory {
|
class BaseSVGFactory {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @typedef {import("./page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("./display_utils").PageViewport} PageViewport */
|
||||||
/** @typedef {import("./api").TextContent} TextContent */
|
/** @typedef {import("./api").TextContent} TextContent */
|
||||||
/** @typedef {import("./text_layer_images").TextLayerImages} TextLayerImages */
|
/** @typedef {import("./text_layer_images").TextLayerImages} TextLayerImages */
|
||||||
|
|
||||||
@ -472,8 +472,7 @@ class TextLayer {
|
|||||||
// their replacements when they aren't embedded) and then we can use an
|
// their replacements when they aren't embedded) and then we can use an
|
||||||
// OffscreenCanvas.
|
// OffscreenCanvas.
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.style.cssText =
|
canvas.className = "hiddenCanvasElement";
|
||||||
"position:absolute;top:0;left:0;width:0;height:0;display:none";
|
|
||||||
canvas.lang = lang;
|
canvas.lang = lang;
|
||||||
document.body.append(canvas);
|
document.body.append(canvas);
|
||||||
ctx = canvas.getContext("2d", {
|
ctx = canvas.getContext("2d", {
|
||||||
|
|||||||
@ -15,10 +15,10 @@
|
|||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./annotation_storage").AnnotationStorage} AnnotationStorage */
|
/** @typedef {import("./annotation_storage").AnnotationStorage} AnnotationStorage */
|
||||||
|
/** @typedef {import("./display_utils").PageViewport} PageViewport */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../../web/pdf_link_service.js").PDFLinkService} PDFLinkService */
|
/** @typedef {import("../../web/pdf_link_service.js").PDFLinkService} PDFLinkService */
|
||||||
|
|
||||||
import { PageViewport } from "./page_viewport.js";
|
|
||||||
import { XfaText } from "./xfa_text.js";
|
import { XfaText } from "./xfa_text.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -293,20 +293,6 @@ class XfaLayer {
|
|||||||
parameters.div.style.transform = transform;
|
parameters.div.style.transform = transform;
|
||||||
parameters.div.hidden = false;
|
parameters.div.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* NOTE: This is (mostly) intended to support printing of XFA forms.
|
|
||||||
*/
|
|
||||||
static getPageViewport(xfaPage, { scale = 1, rotation = 0 }) {
|
|
||||||
const { width, height } = xfaPage.attributes.style;
|
|
||||||
|
|
||||||
return new PageViewport({
|
|
||||||
viewBox: [0, 0, parseInt(width, 10), parseInt(height, 10)],
|
|
||||||
userUnit: 1,
|
|
||||||
scale,
|
|
||||||
rotation,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { XfaLayer };
|
export { XfaLayer };
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
/** @typedef {import("./display/api").PDFDocumentProxy} PDFDocumentProxy */
|
/** @typedef {import("./display/api").PDFDocumentProxy} PDFDocumentProxy */
|
||||||
/** @typedef {import("./display/api").PDFPageProxy} PDFPageProxy */
|
/** @typedef {import("./display/api").PDFPageProxy} PDFPageProxy */
|
||||||
/** @typedef {import("./display/api").RenderTask} RenderTask */
|
/** @typedef {import("./display/api").RenderTask} RenderTask */
|
||||||
/** @typedef {import("./display/page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("./display/display_utils").PageViewport} PageViewport */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AbortException,
|
AbortException,
|
||||||
@ -55,6 +55,7 @@ import {
|
|||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
getRGB,
|
getRGB,
|
||||||
getRGBA,
|
getRGBA,
|
||||||
|
getXfaPageViewport,
|
||||||
isDataScheme,
|
isDataScheme,
|
||||||
isPdfFile,
|
isPdfFile,
|
||||||
noContextMenu,
|
noContextMenu,
|
||||||
@ -121,6 +122,7 @@ globalThis.pdfjsLib = {
|
|||||||
getRGB,
|
getRGB,
|
||||||
getRGBA,
|
getRGBA,
|
||||||
getUuid,
|
getUuid,
|
||||||
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
ImageKind,
|
ImageKind,
|
||||||
InvalidPDFException,
|
InvalidPDFException,
|
||||||
@ -184,6 +186,7 @@ export {
|
|||||||
getRGB,
|
getRGB,
|
||||||
getRGBA,
|
getRGBA,
|
||||||
getUuid,
|
getUuid,
|
||||||
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
ImageKind,
|
ImageKind,
|
||||||
InvalidPDFException,
|
InvalidPDFException,
|
||||||
|
|||||||
@ -36,8 +36,6 @@ const LINE_FACTOR = 1.35;
|
|||||||
const LINE_DESCENT_FACTOR = 0.35;
|
const LINE_DESCENT_FACTOR = 0.35;
|
||||||
const BASELINE_FACTOR = LINE_DESCENT_FACTOR / LINE_FACTOR;
|
const BASELINE_FACTOR = LINE_DESCENT_FACTOR / LINE_FACTOR;
|
||||||
|
|
||||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refer to the `WorkerTransport.getRenderingIntent`-method in the API, to see
|
* Refer to the `WorkerTransport.getRenderingIntent`-method in the API, to see
|
||||||
* how these flags are being used:
|
* how these flags are being used:
|
||||||
@ -663,10 +661,7 @@ class FeatureTest {
|
|||||||
let ctx;
|
let ctx;
|
||||||
if (this.isOffscreenCanvasSupported) {
|
if (this.isOffscreenCanvasSupported) {
|
||||||
ctx = new OffscreenCanvas(1, 1).getContext("2d");
|
ctx = new OffscreenCanvas(1, 1).getContext("2d");
|
||||||
} else if (
|
} else if (typeof document !== "undefined") {
|
||||||
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("WORKER_THREAD")) &&
|
|
||||||
typeof document !== "undefined"
|
|
||||||
) {
|
|
||||||
ctx = document.createElement("canvas").getContext("2d");
|
ctx = document.createElement("canvas").getContext("2d");
|
||||||
}
|
}
|
||||||
// Spec-compliant Canvas2D defaults `ctx.filter` to "none". On
|
// Spec-compliant Canvas2D defaults `ctx.filter` to "none". On
|
||||||
@ -678,22 +673,21 @@ class FeatureTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get isAlphaColorInputSupported() {
|
static get isAlphaColorInputSupported() {
|
||||||
if (
|
|
||||||
(typeof PDFJSDev !== "undefined" && PDFJSDev.test("WORKER_THREAD")) ||
|
|
||||||
typeof document === "undefined"
|
|
||||||
) {
|
|
||||||
return shadow(this, "isAlphaColorInputSupported", false);
|
|
||||||
}
|
|
||||||
const input = document.createElement("input");
|
|
||||||
input.type = "color";
|
|
||||||
input.setAttribute("alpha", "");
|
|
||||||
input.value = "#ff000080";
|
|
||||||
// If alpha is supported the color picker retains the alpha channel, so
|
|
||||||
// the value won't be a plain opaque color (7-char #rrggbb).
|
|
||||||
return shadow(
|
return shadow(
|
||||||
this,
|
this,
|
||||||
"isAlphaColorInputSupported",
|
"isAlphaColorInputSupported",
|
||||||
input.value !== "#ff0000"
|
(() => {
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "color";
|
||||||
|
input.setAttribute("alpha", "");
|
||||||
|
input.value = "#ff000080";
|
||||||
|
// If alpha is supported the color picker retains the alpha channel, so
|
||||||
|
// the value won't be a plain opaque color (7-char #rrggbb).
|
||||||
|
return input.value !== "#ff0000";
|
||||||
|
})()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -711,6 +705,61 @@ class Util {
|
|||||||
return `#${this.hexNums[r]}${this.hexNums[g]}${this.hexNums[b]}`;
|
return `#${this.hexNums[r]}${this.hexNums[g]}${this.hexNums[b]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static domMatrixToTransform(dm) {
|
||||||
|
return [dm.a, dm.b, dm.c, dm.d, dm.e, dm.f];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply a scaling matrix to some min/max values.
|
||||||
|
// If a scaling factor is negative then min and max must be
|
||||||
|
// swapped.
|
||||||
|
static scaleMinMax(transform, minMax) {
|
||||||
|
let temp;
|
||||||
|
if (transform[0]) {
|
||||||
|
if (transform[0] < 0) {
|
||||||
|
temp = minMax[0];
|
||||||
|
minMax[0] = minMax[2];
|
||||||
|
minMax[2] = temp;
|
||||||
|
}
|
||||||
|
minMax[0] *= transform[0];
|
||||||
|
minMax[2] *= transform[0];
|
||||||
|
|
||||||
|
if (transform[3] < 0) {
|
||||||
|
temp = minMax[1];
|
||||||
|
minMax[1] = minMax[3];
|
||||||
|
minMax[3] = temp;
|
||||||
|
}
|
||||||
|
minMax[1] *= transform[3];
|
||||||
|
minMax[3] *= transform[3];
|
||||||
|
} else {
|
||||||
|
temp = minMax[0];
|
||||||
|
minMax[0] = minMax[1];
|
||||||
|
minMax[1] = temp;
|
||||||
|
temp = minMax[2];
|
||||||
|
minMax[2] = minMax[3];
|
||||||
|
minMax[3] = temp;
|
||||||
|
|
||||||
|
if (transform[1] < 0) {
|
||||||
|
temp = minMax[1];
|
||||||
|
minMax[1] = minMax[3];
|
||||||
|
minMax[3] = temp;
|
||||||
|
}
|
||||||
|
minMax[1] *= transform[1];
|
||||||
|
minMax[3] *= transform[1];
|
||||||
|
|
||||||
|
if (transform[2] < 0) {
|
||||||
|
temp = minMax[0];
|
||||||
|
minMax[0] = minMax[2];
|
||||||
|
minMax[2] = temp;
|
||||||
|
}
|
||||||
|
minMax[0] *= transform[2];
|
||||||
|
minMax[2] *= transform[2];
|
||||||
|
}
|
||||||
|
minMax[0] += transform[4];
|
||||||
|
minMax[1] += transform[5];
|
||||||
|
minMax[2] += transform[4];
|
||||||
|
minMax[3] += transform[5];
|
||||||
|
}
|
||||||
|
|
||||||
// Concatenates two transformation matrices together and returns the result.
|
// Concatenates two transformation matrices together and returns the result.
|
||||||
static transform(m1, m2) {
|
static transform(m1, m2) {
|
||||||
return [
|
return [
|
||||||
@ -1008,6 +1057,67 @@ class Util {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PDFStringTranslateTable = [
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x2d8,
|
||||||
|
0x2c7, 0x2c6, 0x2d9, 0x2dd, 0x2db, 0x2da, 0x2dc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0, 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, 0x2013, 0x192,
|
||||||
|
0x2044, 0x2039, 0x203a, 0x2212, 0x2030, 0x201e, 0x201c, 0x201d, 0x2018,
|
||||||
|
0x2019, 0x201a, 0x2122, 0xfb01, 0xfb02, 0x141, 0x152, 0x160, 0x178, 0x17d,
|
||||||
|
0x131, 0x142, 0x153, 0x161, 0x17e, 0, 0x20ac,
|
||||||
|
];
|
||||||
|
|
||||||
|
function stringToPDFString(str, keepEscapeSequence = false) {
|
||||||
|
// See section 7.9.2.2 Text String Type.
|
||||||
|
// The string can contain some language codes bracketed with 0x1b,
|
||||||
|
// so we must remove them.
|
||||||
|
if (str[0] >= "\xEF") {
|
||||||
|
let encoding;
|
||||||
|
if (str[0] === "\xFE" && str[1] === "\xFF") {
|
||||||
|
encoding = "utf-16be";
|
||||||
|
if (str.length % 2 === 1) {
|
||||||
|
str = str.slice(0, -1);
|
||||||
|
}
|
||||||
|
} else if (str[0] === "\xFF" && str[1] === "\xFE") {
|
||||||
|
encoding = "utf-16le";
|
||||||
|
if (str.length % 2 === 1) {
|
||||||
|
str = str.slice(0, -1);
|
||||||
|
}
|
||||||
|
} else if (str[0] === "\xEF" && str[1] === "\xBB" && str[2] === "\xBF") {
|
||||||
|
encoding = "utf-8";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding) {
|
||||||
|
try {
|
||||||
|
const decoder = new TextDecoder(encoding, { fatal: true });
|
||||||
|
const buffer = stringToBytes(str);
|
||||||
|
const decoded = decoder.decode(buffer);
|
||||||
|
if (keepEscapeSequence || !decoded.includes("\x1b")) {
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
return decoded.replaceAll(/\x1b[^\x1b]*(?:\x1b|$)/g, "");
|
||||||
|
} catch (ex) {
|
||||||
|
warn(`stringToPDFString: "${ex}".`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ISO Latin 1
|
||||||
|
const strBuf = [];
|
||||||
|
for (let i = 0, ii = str.length; i < ii; i++) {
|
||||||
|
const charCode = str.charCodeAt(i);
|
||||||
|
if (!keepEscapeSequence && charCode === 0x1b) {
|
||||||
|
// eslint-disable-next-line no-empty
|
||||||
|
while (++i < ii && str.charCodeAt(i) !== 0x1b) {}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const code = PDFStringTranslateTable[charCode];
|
||||||
|
strBuf.push(code ? String.fromCharCode(code) : str.charAt(i));
|
||||||
|
}
|
||||||
|
return strBuf.join("");
|
||||||
|
}
|
||||||
|
|
||||||
function stringToUTF8String(str) {
|
function stringToUTF8String(str) {
|
||||||
return decodeURIComponent(escape(str));
|
return decodeURIComponent(escape(str));
|
||||||
}
|
}
|
||||||
@ -1186,9 +1296,9 @@ export {
|
|||||||
setVerbosityLevel,
|
setVerbosityLevel,
|
||||||
shadow,
|
shadow,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
|
stringToPDFString,
|
||||||
stringToUTF8String,
|
stringToUTF8String,
|
||||||
stripPath,
|
stripPath,
|
||||||
SVG_NS,
|
|
||||||
TextRenderingMode,
|
TextRenderingMode,
|
||||||
UnknownErrorException,
|
UnknownErrorException,
|
||||||
unreachable,
|
unreachable,
|
||||||
|
|||||||
@ -15,15 +15,15 @@
|
|||||||
|
|
||||||
// Istanbul coverage objects use s (statements), b (branches), and f (functions)
|
// Istanbul coverage objects use s (statements), b (branches), and f (functions)
|
||||||
// as shorthand keys for the hit-count maps.
|
// as shorthand keys for the hit-count maps.
|
||||||
function mergeCoverageIntoGlobal(coverage) {
|
function mergeWorkerCoverageIntoWindow(coverage) {
|
||||||
if (!coverage || Object.keys(coverage).length === 0) {
|
if (!coverage || Object.keys(coverage).length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
globalThis.__coverage__ ??= {};
|
window.__coverage__ ??= {};
|
||||||
for (const [key, fileCoverage] of Object.entries(coverage)) {
|
for (const [key, fileCoverage] of Object.entries(coverage)) {
|
||||||
const existing = globalThis.__coverage__[key];
|
const existing = window.__coverage__[key];
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
globalThis.__coverage__[key] = fileCoverage;
|
window.__coverage__[key] = fileCoverage;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const id of Object.keys(fileCoverage.s)) {
|
for (const id of Object.keys(fileCoverage.s)) {
|
||||||
@ -49,10 +49,10 @@ async function fetchAndMergeWorkerCoverage(pdfWorker) {
|
|||||||
"GetWorkerCoverage",
|
"GetWorkerCoverage",
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
mergeCoverageIntoGlobal(coverage);
|
mergeWorkerCoverageIntoWindow(coverage);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Failed to collect worker coverage: ${e}`);
|
console.warn(`Failed to collect worker coverage: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { fetchAndMergeWorkerCoverage, mergeCoverageIntoGlobal };
|
export { fetchAndMergeWorkerCoverage, mergeWorkerCoverageIntoWindow };
|
||||||
|
|||||||
@ -1,141 +0,0 @@
|
|||||||
/* Copyright 2026 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ttx, verifyTtxOutput } from "./fontutils.js";
|
|
||||||
import { Font } from "../../src/core/fonts.js";
|
|
||||||
import { Stream } from "../../src/core/stream.js";
|
|
||||||
import { ToUnicodeMap } from "../../src/core/to_unicode_map.js";
|
|
||||||
|
|
||||||
// Minimal TrueType font: 4 glyphs (.notdef, space, A, B), OS/2 v1 / 86 bytes,
|
|
||||||
// no hinting tables.
|
|
||||||
const baseFont = Uint8Array.fromBase64(
|
|
||||||
"AAEAAAAKAIAAAwAgT1MvMkTeRDYAAAEoAAAAVmNtYXAAdQBcAAABjAAAADxnbHlmmNLJuAAAAdQAAABKaGVhZC3Q8mwAAACsAAAANmhoZWEFFgH2AAAA5AAAACRobXR4AlgAAAAAAYAAAAAKbG9jYQAyACYAAAHIAAAACm1heHAABgAGAAABCAAAACBuYW1lAJlcyAAAAiAAAAA8cG9zdAAuACQAAAJcAAAAKgABAAAAAQAAfM/c718PPPUAAQPoAAAAAOYyVzYAAAAA5jJXNgAAAAACWAMgAAAAAwACAAAAAAAAAAEAAAMg/zgAAAJYAAAAZAH0AAEAAAAAAAAAAAAAAAAAAAABAAEAAAAEAAQAAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAQJYAZAABQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAPz8/PwAAACAAQgMg/zgAAAMgAMgAAAAAAAAAAAAAAlgAAAAAAAAAAAAAAAAAAgAAAAMAAAAUAAMAAQAAABQABAAoAAAABgAEAAEAAgAgAEL//wAAACAAQf///+H/wQABAAAAAAAAAAAADQANABkAJQAAAAEAZAAAAlgDIAADAAAzIREhZAH0/gwDIAAAAQAAAAAB9AK8AAMAADEhESEB9P4MArwAAQAAAAAB9AK8AAMAADEhESEB9P4MArwAAAAAAAQANgABAAAAAAABAAEAAAABAAAAAAACAAEAAQADAAEECQABAAIAAgADAAEECQACAAIABFRSAFQAUgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAADACQAJQAA"
|
|
||||||
);
|
|
||||||
|
|
||||||
function clone(buf) {
|
|
||||||
return new Uint8Array(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readUint16(buf, pos) {
|
|
||||||
return (buf[pos] << 8) | buf[pos + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function readUint32(buf, pos) {
|
|
||||||
return (
|
|
||||||
buf[pos] * 0x1000000 +
|
|
||||||
((buf[pos + 1] << 16) | (buf[pos + 2] << 8) | buf[pos + 3])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTables(buf) {
|
|
||||||
const tables = Object.create(null);
|
|
||||||
const numTables = readUint16(buf, 4);
|
|
||||||
for (let i = 0; i < numTables; i++) {
|
|
||||||
const off = 12 + i * 16;
|
|
||||||
const tag = String.fromCharCode(
|
|
||||||
buf[off],
|
|
||||||
buf[off + 1],
|
|
||||||
buf[off + 2],
|
|
||||||
buf[off + 3]
|
|
||||||
);
|
|
||||||
tables[tag] = {
|
|
||||||
offset: readUint32(buf, off + 8),
|
|
||||||
length: readUint32(buf, off + 12),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return tables;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeProperties(toUnicode) {
|
|
||||||
return {
|
|
||||||
loadedName: "font",
|
|
||||||
type: "TrueType",
|
|
||||||
differences: [],
|
|
||||||
defaultEncoding: [],
|
|
||||||
toUnicode,
|
|
||||||
xHeight: 0,
|
|
||||||
capHeight: 0,
|
|
||||||
italicAngle: 0,
|
|
||||||
firstChar: 0,
|
|
||||||
lastChar: 255,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("font_glyf", function () {
|
|
||||||
describe("Cyclic composite glyph 0", function () {
|
|
||||||
it("removes a self-referencing composite glyph 0 (issue 21298)", async function () {
|
|
||||||
const buggy = clone(baseFont);
|
|
||||||
const tables = getTables(buggy);
|
|
||||||
const headOff = tables.head.offset;
|
|
||||||
const indexToLocFormat = readUint16(buggy, headOff + 50);
|
|
||||||
const locaOff = tables.loca.offset;
|
|
||||||
const glyf0 =
|
|
||||||
indexToLocFormat === 0
|
|
||||||
? readUint16(buggy, locaOff) * 2
|
|
||||||
: readUint32(buggy, locaOff);
|
|
||||||
const glyf0End =
|
|
||||||
indexToLocFormat === 0
|
|
||||||
? readUint16(buggy, locaOff + 2) * 2
|
|
||||||
: readUint32(buggy, locaOff + 4);
|
|
||||||
const pos = tables.glyf.offset + glyf0;
|
|
||||||
buggy.fill(0, pos, tables.glyf.offset + glyf0End);
|
|
||||||
buggy[pos] = 0xff;
|
|
||||||
buggy[pos + 1] = 0xff;
|
|
||||||
buggy[pos + 11] = 0x02;
|
|
||||||
|
|
||||||
const font = new Font(
|
|
||||||
"font",
|
|
||||||
new Stream(buggy),
|
|
||||||
makeProperties(new ToUnicodeMap([])),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const output = await ttx(font.data);
|
|
||||||
verifyTtxOutput(output);
|
|
||||||
const notdef =
|
|
||||||
/<TTGlyph[^>]*name="\.notdef"[^>]*\/>|<TTGlyph[^>]*name="\.notdef"[^>]*>([\s\S]*?)<\/TTGlyph>/.exec(
|
|
||||||
output
|
|
||||||
);
|
|
||||||
expect(notdef).not.toBeNull();
|
|
||||||
expect(notdef[1] || "").not.toMatch(
|
|
||||||
/<component\b[^>]*glyphName="\.notdef"/
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("OS/2 table length validation", function () {
|
|
||||||
it("rewrites the OS/2 table when its length doesn't match the declared version", async function () {
|
|
||||||
const buggy = clone(baseFont);
|
|
||||||
const tables = getTables(buggy);
|
|
||||||
const os2 = tables["OS/2"].offset;
|
|
||||||
buggy[os2 + 62] = 0x00;
|
|
||||||
buggy[os2 + 63] = 0x40;
|
|
||||||
buggy[os2 + 1] = 0x03;
|
|
||||||
|
|
||||||
const font = new Font(
|
|
||||||
"font",
|
|
||||||
new Stream(buggy),
|
|
||||||
makeProperties(new ToUnicodeMap([])),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const output = await ttx(font.data);
|
|
||||||
verifyTtxOutput(output);
|
|
||||||
expect(
|
|
||||||
/<OS_2>\s*(<!--[\s\S]*?-->\s*)?<version value="3"\/>/.test(output)
|
|
||||||
).toEqual(true);
|
|
||||||
expect(/<sCapHeight\b/.test(output)).toEqual(true);
|
|
||||||
expect(/<usMaxContext\b/.test(output)).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -46,7 +46,6 @@ async function initializePDFJS(callback) {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
[
|
[
|
||||||
"pdfjs-test/font/font_core_spec.js",
|
"pdfjs-test/font/font_core_spec.js",
|
||||||
"pdfjs-test/font/font_glyf_spec.js",
|
|
||||||
"pdfjs-test/font/font_os2_spec.js",
|
"pdfjs-test/font/font_os2_spec.js",
|
||||||
"pdfjs-test/font/font_post_spec.js",
|
"pdfjs-test/font/font_post_spec.js",
|
||||||
"pdfjs-test/font/font_fpgm_spec.js",
|
"pdfjs-test/font/font_fpgm_spec.js",
|
||||||
|
|||||||
0
test/images/samplesignature.png
Normal file → Executable file
0
test/images/samplesignature.png
Normal file → Executable file
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@ -14,7 +14,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs";
|
import { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs";
|
||||||
import fs from "fs/promises";
|
|
||||||
|
|
||||||
const FIELDS = [
|
const FIELDS = [
|
||||||
"fileName",
|
"fileName",
|
||||||
@ -33,50 +32,21 @@ const FIELDS = [
|
|||||||
"linearized",
|
"linearized",
|
||||||
];
|
];
|
||||||
|
|
||||||
async function openDocumentProperties(page) {
|
|
||||||
await page.click("#secondaryToolbarToggleButton");
|
|
||||||
await page.waitForSelector("#secondaryToolbar", { hidden: false });
|
|
||||||
|
|
||||||
await page.click("#documentProperties");
|
|
||||||
await page.waitForSelector("#documentPropertiesDialog", {
|
|
||||||
hidden: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function closeDocumentProperties(page) {
|
|
||||||
await page.click("#documentPropertiesClose");
|
|
||||||
await page.waitForSelector("#documentPropertiesDialog", {
|
|
||||||
hidden: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkFieldProperties(page, expectedProps) {
|
|
||||||
await page.waitForFunction(
|
|
||||||
`document.getElementById("fileSizeField").textContent !== "-"`
|
|
||||||
);
|
|
||||||
const promises = [];
|
|
||||||
|
|
||||||
for (const name of FIELDS) {
|
|
||||||
promises.push(
|
|
||||||
page.evaluate(
|
|
||||||
n => [n, document.getElementById(`${n}Field`).textContent],
|
|
||||||
name
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const props = Object.fromEntries(await Promise.all(promises));
|
|
||||||
expect(props).toEqual(expectedProps);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFieldDataLastUpdated(page) {
|
|
||||||
return page.evaluate(
|
|
||||||
() =>
|
|
||||||
document.getElementById("documentPropertiesDialog").dataset
|
|
||||||
.fieldDataLastUpdated
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("PDFDocumentProperties", () => {
|
describe("PDFDocumentProperties", () => {
|
||||||
|
async function getFieldProperties(page) {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (const name of FIELDS) {
|
||||||
|
promises.push(
|
||||||
|
page.evaluate(
|
||||||
|
n => [n, document.getElementById(`${n}Field`).textContent],
|
||||||
|
name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Object.fromEntries(await Promise.all(promises));
|
||||||
|
}
|
||||||
|
|
||||||
describe("Document with both /Info and /Metadata", () => {
|
describe("Document with both /Info and /Metadata", () => {
|
||||||
let pages;
|
let pages;
|
||||||
|
|
||||||
@ -88,12 +58,23 @@ describe("PDFDocumentProperties", () => {
|
|||||||
await closePages(pages);
|
await closePages(pages);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("check that the document properties dialog has the correct information", async () => {
|
it("must check that the document properties dialog has the correct information", async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
await openDocumentProperties(page);
|
await page.click("#secondaryToolbarToggleButton");
|
||||||
|
await page.waitForSelector("#secondaryToolbar", { hidden: false });
|
||||||
|
|
||||||
await checkFieldProperties(page, {
|
await page.click("#documentProperties");
|
||||||
|
await page.waitForSelector("#documentPropertiesDialog", {
|
||||||
|
hidden: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForFunction(
|
||||||
|
`document.getElementById("fileSizeField").textContent !== "-"`
|
||||||
|
);
|
||||||
|
const props = await getFieldProperties(page);
|
||||||
|
|
||||||
|
expect(props).toEqual({
|
||||||
fileName: "basicapi.pdf",
|
fileName: "basicapi.pdf",
|
||||||
fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
|
fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
|
||||||
title: "Basic API Test",
|
title: "Basic API Test",
|
||||||
@ -109,319 +90,6 @@ describe("PDFDocumentProperties", () => {
|
|||||||
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
|
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
|
||||||
linearized: "No",
|
linearized: "No",
|
||||||
});
|
});
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Document with approximately A4-sized page", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"arial_unicode_en_cidfont.pdf",
|
|
||||||
".textLayer .endOfContent"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check that the document properties dialog has the correct information", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await openDocumentProperties(page);
|
|
||||||
|
|
||||||
await checkFieldProperties(page, {
|
|
||||||
fileName: "arial_unicode_en_cidfont.pdf",
|
|
||||||
fileSize: `${FSI}15.4${PDI} KB (${FSI}15,779${PDI} bytes)`,
|
|
||||||
title: "-",
|
|
||||||
author: "Adil Allawi",
|
|
||||||
subject: "-",
|
|
||||||
keywords: "-",
|
|
||||||
creationDate: "7/10/11, 7:17:28 PM",
|
|
||||||
modificationDate: "-",
|
|
||||||
creator: "Writer",
|
|
||||||
producer: "NeoOffice 3.2 Beta",
|
|
||||||
version: "1.4",
|
|
||||||
pageCount: "1",
|
|
||||||
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
|
|
||||||
linearized: "No",
|
|
||||||
});
|
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Document without contentLength", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait("empty.pdf", ".textLayer .endOfContent");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check that the document properties dialog has the correct information", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
// Open a binary PDF document, such that `contentLength` is undefined.
|
|
||||||
const base64 = await fs.readFile("./pdfs/clippath.pdf", {
|
|
||||||
encoding: "base64",
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.evaluate(async b64 => {
|
|
||||||
await window.PDFViewerApplication.open({
|
|
||||||
data: Uint8Array.fromBase64(b64),
|
|
||||||
});
|
|
||||||
}, base64);
|
|
||||||
|
|
||||||
await openDocumentProperties(page);
|
|
||||||
|
|
||||||
await checkFieldProperties(page, {
|
|
||||||
fileName: "document.pdf",
|
|
||||||
fileSize: `${FSI}0.448${PDI} KB (${FSI}459${PDI} bytes)`,
|
|
||||||
title: "-",
|
|
||||||
author: "-",
|
|
||||||
subject: "-",
|
|
||||||
keywords: "-",
|
|
||||||
creationDate: "-",
|
|
||||||
modificationDate: "-",
|
|
||||||
creator: "-",
|
|
||||||
producer: "-",
|
|
||||||
version: "1.1",
|
|
||||||
pageCount: "1",
|
|
||||||
pageSize: `${FSI}2.78${PDI} × ${FSI}1.39${PDI} ${FSI}in${PDI} (${FSI}landscape${PDI})`,
|
|
||||||
linearized: "No",
|
|
||||||
});
|
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Document with multiple pages, and changed viewer page/rotation", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check that the document properties dialog has the correct information", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await openDocumentProperties(page);
|
|
||||||
|
|
||||||
await checkFieldProperties(page, {
|
|
||||||
fileName: "basicapi.pdf",
|
|
||||||
fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
|
|
||||||
title: "Basic API Test",
|
|
||||||
author: "Brendan Dahl",
|
|
||||||
subject: "-",
|
|
||||||
keywords: "TCPDF",
|
|
||||||
creationDate: "4/10/12, 7:30:26 AM",
|
|
||||||
modificationDate: "4/10/12, 7:30:26 AM",
|
|
||||||
creator: "TCPDF",
|
|
||||||
producer: "TCPDF 5.9.133 (http://www.tcpdf.org)",
|
|
||||||
version: "1.7",
|
|
||||||
pageCount: "3",
|
|
||||||
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
|
|
||||||
linearized: "No",
|
|
||||||
});
|
|
||||||
const fieldDataLastUpdated = await getFieldDataLastUpdated(page);
|
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
|
|
||||||
// Ensure that immediately re-opening the dialog doesn't cause
|
|
||||||
// the field-data to be fetched and parsed again.
|
|
||||||
await openDocumentProperties(page);
|
|
||||||
|
|
||||||
expect(await getFieldDataLastUpdated(page)).toEqual(
|
|
||||||
fieldDataLastUpdated
|
|
||||||
);
|
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
|
|
||||||
// Goto the second page, and rotate the document.
|
|
||||||
await page.click("#next");
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => window.PDFViewerApplication.page === 2
|
|
||||||
);
|
|
||||||
await page.keyboard.press("r");
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => window.PDFViewerApplication.pdfViewer.pagesRotation === 90
|
|
||||||
);
|
|
||||||
|
|
||||||
await openDocumentProperties(page);
|
|
||||||
|
|
||||||
await checkFieldProperties(page, {
|
|
||||||
fileName: "basicapi.pdf",
|
|
||||||
fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
|
|
||||||
title: "Basic API Test",
|
|
||||||
author: "Brendan Dahl",
|
|
||||||
subject: "-",
|
|
||||||
keywords: "TCPDF",
|
|
||||||
creationDate: "4/10/12, 7:30:26 AM",
|
|
||||||
modificationDate: "4/10/12, 7:30:26 AM",
|
|
||||||
creator: "TCPDF",
|
|
||||||
producer: "TCPDF 5.9.133 (http://www.tcpdf.org)",
|
|
||||||
version: "1.7",
|
|
||||||
pageCount: "3",
|
|
||||||
pageSize: `${FSI}11.69${PDI} × ${FSI}8.27${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}landscape${PDI})`,
|
|
||||||
linearized: "No",
|
|
||||||
});
|
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Document with different page sizes", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait("sizes.pdf", ".textLayer .endOfContent");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check that the document properties dialog has the correct information", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await openDocumentProperties(page);
|
|
||||||
|
|
||||||
await checkFieldProperties(page, {
|
|
||||||
fileName: "sizes.pdf",
|
|
||||||
fileSize: `${FSI}13.4${PDI} KB (${FSI}13,739${PDI} bytes)`,
|
|
||||||
title: "-",
|
|
||||||
author: "Yury ",
|
|
||||||
subject: "-",
|
|
||||||
keywords: "-",
|
|
||||||
creationDate: "6/26/11, 1:26:03 PM",
|
|
||||||
modificationDate: "-",
|
|
||||||
creator: "Writer",
|
|
||||||
producer: "OpenOffice.org 3.3",
|
|
||||||
version: "1.4",
|
|
||||||
pageCount: "3",
|
|
||||||
pageSize: `${FSI}8.5${PDI} × ${FSI}11${PDI} ${FSI}in${PDI} (${FSI}Letter${PDI}, ${FSI}portrait${PDI})`,
|
|
||||||
linearized: "No",
|
|
||||||
});
|
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
|
|
||||||
// Goto the second page.
|
|
||||||
await page.click("#next");
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => window.PDFViewerApplication.page === 2
|
|
||||||
);
|
|
||||||
|
|
||||||
await openDocumentProperties(page);
|
|
||||||
|
|
||||||
await checkFieldProperties(page, {
|
|
||||||
fileName: "sizes.pdf",
|
|
||||||
fileSize: `${FSI}13.4${PDI} KB (${FSI}13,739${PDI} bytes)`,
|
|
||||||
title: "-",
|
|
||||||
author: "Yury ",
|
|
||||||
subject: "-",
|
|
||||||
keywords: "-",
|
|
||||||
creationDate: "6/26/11, 1:26:03 PM",
|
|
||||||
modificationDate: "-",
|
|
||||||
creator: "Writer",
|
|
||||||
producer: "OpenOffice.org 3.3",
|
|
||||||
version: "1.4",
|
|
||||||
pageCount: "3",
|
|
||||||
pageSize: `${FSI}9.01${PDI} × ${FSI}4.49${PDI} ${FSI}in${PDI} (${FSI}landscape${PDI})`,
|
|
||||||
linearized: "No",
|
|
||||||
});
|
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Document with corrupt page", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"Pages-tree-refs.pdf",
|
|
||||||
".textLayer .endOfContent",
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
{ page: 2 }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check that the document properties dialog has the correct information", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await openDocumentProperties(page);
|
|
||||||
|
|
||||||
await checkFieldProperties(page, {
|
|
||||||
fileName: "Pages-tree-refs.pdf",
|
|
||||||
fileSize: `${FSI}1.07${PDI} KB (${FSI}1,098${PDI} bytes)`,
|
|
||||||
title: "-",
|
|
||||||
author: "-",
|
|
||||||
subject: "-",
|
|
||||||
keywords: "-",
|
|
||||||
creationDate: "-",
|
|
||||||
modificationDate: "-",
|
|
||||||
creator: "-",
|
|
||||||
producer: "-",
|
|
||||||
version: "1.7",
|
|
||||||
pageCount: "2",
|
|
||||||
pageSize: "-",
|
|
||||||
linearized: "No",
|
|
||||||
});
|
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
|
|
||||||
// Goto the first page (which is *not* corrupt).
|
|
||||||
await page.click("#previous");
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => window.PDFViewerApplication.page === 1
|
|
||||||
);
|
|
||||||
|
|
||||||
await openDocumentProperties(page);
|
|
||||||
|
|
||||||
await checkFieldProperties(page, {
|
|
||||||
fileName: "Pages-tree-refs.pdf",
|
|
||||||
fileSize: `${FSI}1.07${PDI} KB (${FSI}1,098${PDI} bytes)`,
|
|
||||||
title: "-",
|
|
||||||
author: "-",
|
|
||||||
subject: "-",
|
|
||||||
keywords: "-",
|
|
||||||
creationDate: "-",
|
|
||||||
modificationDate: "-",
|
|
||||||
creator: "-",
|
|
||||||
producer: "-",
|
|
||||||
version: "1.7",
|
|
||||||
pageCount: "2",
|
|
||||||
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
|
|
||||||
linearized: "No",
|
|
||||||
});
|
|
||||||
|
|
||||||
await closeDocumentProperties(page);
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1726,6 +1726,10 @@ describe("Highlight Editor", () => {
|
|||||||
it("must check that an existing highlight is ignored on hovering", async () => {
|
it("must check that an existing highlight is ignored on hovering", async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
|
if (navigator.platform.includes("Win")) {
|
||||||
|
pending("Fails consistently on Windows (issue #20136).");
|
||||||
|
}
|
||||||
|
|
||||||
await switchToHighlight(page);
|
await switchToHighlight(page);
|
||||||
|
|
||||||
const rect = await getSpanRectFromText(
|
const rect = await getSpanRectFromText(
|
||||||
|
|||||||
@ -38,13 +38,11 @@ async function runTests(results) {
|
|||||||
"freetext_editor_spec.mjs",
|
"freetext_editor_spec.mjs",
|
||||||
"highlight_editor_spec.mjs",
|
"highlight_editor_spec.mjs",
|
||||||
"ink_editor_spec.mjs",
|
"ink_editor_spec.mjs",
|
||||||
"presentation_mode_spec.mjs",
|
|
||||||
"reorganize_pages_spec.mjs",
|
"reorganize_pages_spec.mjs",
|
||||||
"scripting_spec.mjs",
|
"scripting_spec.mjs",
|
||||||
"signature_editor_spec.mjs",
|
"signature_editor_spec.mjs",
|
||||||
"simple_viewer_spec.mjs",
|
"simple_viewer_spec.mjs",
|
||||||
"stamp_editor_spec.mjs",
|
"stamp_editor_spec.mjs",
|
||||||
"text_extractor_spec.mjs",
|
|
||||||
"text_field_spec.mjs",
|
"text_field_spec.mjs",
|
||||||
"text_layer_spec.mjs",
|
"text_layer_spec.mjs",
|
||||||
"text_layer_images_spec.mjs",
|
"text_layer_images_spec.mjs",
|
||||||
|
|||||||
@ -1,174 +0,0 @@
|
|||||||
/* Copyright 2026 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
awaitPromise,
|
|
||||||
closePages,
|
|
||||||
createPromise,
|
|
||||||
loadAndWait,
|
|
||||||
waitForTimeout,
|
|
||||||
} from "./test_utils.mjs";
|
|
||||||
|
|
||||||
async function enterPresentationMode(page) {
|
|
||||||
await page.click("#secondaryToolbarToggleButton");
|
|
||||||
await page.waitForSelector("#secondaryToolbar", { hidden: false });
|
|
||||||
|
|
||||||
const handlePresentationModeChanged = await createPromise(page, resolve => {
|
|
||||||
window.PDFViewerApplication.eventBus.on(
|
|
||||||
"presentationmodechanged",
|
|
||||||
resolve,
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
await page.click("#presentationMode");
|
|
||||||
await awaitPromise(handlePresentationModeChanged);
|
|
||||||
|
|
||||||
// Check that presentation mode is active and that the toolbar is
|
|
||||||
// invisible; the latter differentiates between proper presentation
|
|
||||||
// mode and pressing F11 to only hide the browser's UI elements.
|
|
||||||
await page.waitForFunction(`document.fullscreenElement !== null`);
|
|
||||||
await page.waitForSelector("#viewerContainer.pdfPresentationMode", {
|
|
||||||
visible: true,
|
|
||||||
});
|
|
||||||
await page.waitForSelector("#toolbarContainer", { visible: false });
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
|
|
||||||
);
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentScaleValue === "page-fit"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exitPresentationMode(page, browserName) {
|
|
||||||
// Note that in Chrome pressing Escape does not work to exit full screen mode
|
|
||||||
// in the Puppeteer scope, so there we exit full screen mode programmatically
|
|
||||||
// instead, which is equivalent to what happens if Escape is pressed.
|
|
||||||
const handlePresentationModeChanged = await createPromise(page, resolve => {
|
|
||||||
window.PDFViewerApplication.eventBus.on(
|
|
||||||
"presentationmodechanged",
|
|
||||||
resolve,
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
await (browserName === "chrome"
|
|
||||||
? page.evaluate(() => document.exitFullscreen())
|
|
||||||
: page.keyboard.press("Escape"));
|
|
||||||
await awaitPromise(handlePresentationModeChanged);
|
|
||||||
|
|
||||||
// Check that presentation mode is not active anymore and the toolbar
|
|
||||||
// is visible again.
|
|
||||||
await page.waitForFunction(`document.fullscreenElement === null`);
|
|
||||||
await page.waitForSelector("#viewerContainer:not(.pdfPresentationMode)", {
|
|
||||||
visible: true,
|
|
||||||
});
|
|
||||||
await page.waitForSelector("#toolbarContainer", { visible: true });
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
|
|
||||||
);
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentScaleValue !== "page-fit"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("PDFPresentationMode", () => {
|
|
||||||
describe("Changing pages", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"basicapi.pdf",
|
|
||||||
".textLayer .endOfContent",
|
|
||||||
100
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("must check that changing pages using arrow keys works in presentation mode", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await enterPresentationMode(page);
|
|
||||||
|
|
||||||
// Go to the next page.
|
|
||||||
await page.keyboard.press("ArrowDown");
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 2`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Go to the previous page.
|
|
||||||
await page.keyboard.press("ArrowUp");
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
|
|
||||||
);
|
|
||||||
|
|
||||||
await exitPresentationMode(page, browserName);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("must check that changing pages using mouse wheel works in presentation mode", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await enterPresentationMode(page);
|
|
||||||
|
|
||||||
// Go to the next page.
|
|
||||||
await page.mouse.wheel({ deltaY: 100 });
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 2`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait until the viewer accepts a new mouse scroll event; see
|
|
||||||
// `MOUSE_SCROLL_COOLDOWN_TIME` in `web/pdf_presentation_mode.js`.
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
await waitForTimeout(50);
|
|
||||||
|
|
||||||
// Go to the previous page.
|
|
||||||
await page.mouse.wheel({ deltaY: -100 });
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
|
|
||||||
);
|
|
||||||
|
|
||||||
await exitPresentationMode(page, browserName);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("must check that changing pages using mouse click works in presentation mode", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await enterPresentationMode(page);
|
|
||||||
|
|
||||||
// Go to the next page.
|
|
||||||
await page.click(".page[data-page-number='1']");
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 2`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Go to the previous page.
|
|
||||||
await page.keyboard.down("Shift");
|
|
||||||
await page.click(".page[data-page-number='2']");
|
|
||||||
await page.keyboard.up("Shift");
|
|
||||||
await page.waitForFunction(
|
|
||||||
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
|
|
||||||
);
|
|
||||||
|
|
||||||
await exitPresentationMode(page, browserName);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -40,27 +40,10 @@ import {
|
|||||||
waitForTextToBe,
|
waitForTextToBe,
|
||||||
waitForTooltipToBe,
|
waitForTooltipToBe,
|
||||||
} from "./test_utils.mjs";
|
} from "./test_utils.mjs";
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const __dirname = import.meta.dirname;
|
const __dirname = import.meta.dirname;
|
||||||
|
|
||||||
async function createPDFDataTransfer(page, filename) {
|
|
||||||
const pdfPath = path.join(__dirname, "../pdfs", filename);
|
|
||||||
const pdfData = fs.readFileSync(pdfPath).toString("base64");
|
|
||||||
return page.evaluateHandle(
|
|
||||||
(data, name) => {
|
|
||||||
const transfer = new DataTransfer();
|
|
||||||
const view = Uint8Array.fromBase64(data);
|
|
||||||
const file = new File([view], name, { type: "application/pdf" });
|
|
||||||
transfer.items.add(file);
|
|
||||||
return transfer;
|
|
||||||
},
|
|
||||||
pdfData,
|
|
||||||
filename
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForThumbnailVisible(page, pageNums) {
|
async function waitForThumbnailVisible(page, pageNums) {
|
||||||
await showViewsManager(page);
|
await showViewsManager(page);
|
||||||
|
|
||||||
@ -3083,7 +3066,7 @@ describe("Reorganize Pages View", () => {
|
|||||||
await waitAndClick(page, getThumbnailSelector(2));
|
await waitAndClick(page, getThumbnailSelector(2));
|
||||||
|
|
||||||
const handleMerged = await createPromise(page, resolve => {
|
const handleMerged = await createPromise(page, resolve => {
|
||||||
window.PDFViewerApplication.eventBus.on(
|
window.PDFViewerApplication.eventBus._on(
|
||||||
"thumbnailsloaded",
|
"thumbnailsloaded",
|
||||||
resolve,
|
resolve,
|
||||||
{ once: true }
|
{ once: true }
|
||||||
@ -3142,7 +3125,7 @@ describe("Reorganize Pages View", () => {
|
|||||||
await waitAndClick(page, getThumbnailSelector(1));
|
await waitAndClick(page, getThumbnailSelector(1));
|
||||||
|
|
||||||
const handleMerged = await createPromise(page, resolve => {
|
const handleMerged = await createPromise(page, resolve => {
|
||||||
window.PDFViewerApplication.eventBus.on(
|
window.PDFViewerApplication.eventBus._on(
|
||||||
"thumbnailsloaded",
|
"thumbnailsloaded",
|
||||||
resolve,
|
resolve,
|
||||||
{ once: true }
|
{ once: true }
|
||||||
@ -3178,7 +3161,7 @@ describe("Reorganize Pages View", () => {
|
|||||||
await waitForThumbnailVisible(page, 1);
|
await waitForThumbnailVisible(page, 1);
|
||||||
|
|
||||||
const handleMerged = await createPromise(page, resolve => {
|
const handleMerged = await createPromise(page, resolve => {
|
||||||
window.PDFViewerApplication.eventBus.on(
|
window.PDFViewerApplication.eventBus._on(
|
||||||
"thumbnailsloaded",
|
"thumbnailsloaded",
|
||||||
resolve,
|
resolve,
|
||||||
{ once: true }
|
{ once: true }
|
||||||
@ -3214,7 +3197,7 @@ describe("Reorganize Pages View", () => {
|
|||||||
await waitForTextToBe(page, labelSelector, `${FSI}1${PDI} selected`);
|
await waitForTextToBe(page, labelSelector, `${FSI}1${PDI} selected`);
|
||||||
|
|
||||||
const handleMerged = await createPromise(page, resolve => {
|
const handleMerged = await createPromise(page, resolve => {
|
||||||
window.PDFViewerApplication.eventBus.on(
|
window.PDFViewerApplication.eventBus._on(
|
||||||
"thumbnailsloaded",
|
"thumbnailsloaded",
|
||||||
resolve,
|
resolve,
|
||||||
{ once: true }
|
{ once: true }
|
||||||
@ -3244,136 +3227,4 @@ describe("Reorganize Pages View", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Drag-and-drop PDF merge", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"three_pages_with_number.pdf",
|
|
||||||
'.page[data-page-number = "1"] .endOfContent',
|
|
||||||
"1",
|
|
||||||
null,
|
|
||||||
{ enableSplitMerge: true, enableMerge: true }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the marker and merge before the first thumbnail", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await waitForThumbnailVisible(page, [1, 2, 3]);
|
|
||||||
|
|
||||||
const dataTransfer = await createPDFDataTransfer(
|
|
||||||
page,
|
|
||||||
"three_pages_with_number.pdf"
|
|
||||||
);
|
|
||||||
const markerInfo = await page.evaluate(
|
|
||||||
(transfer, selector) => {
|
|
||||||
const container = document.getElementById("thumbnailsView");
|
|
||||||
const target = document.querySelector(selector);
|
|
||||||
const { left, top, width, height } =
|
|
||||||
target.getBoundingClientRect();
|
|
||||||
const clientX = left + width / 4;
|
|
||||||
const clientY = top + height / 4;
|
|
||||||
const dispatchDragEvent = type => {
|
|
||||||
target.dispatchEvent(
|
|
||||||
new DragEvent(type, {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
clientX,
|
|
||||||
clientY,
|
|
||||||
dataTransfer: transfer,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatchDragEvent("dragenter");
|
|
||||||
dispatchDragEvent("dragover");
|
|
||||||
|
|
||||||
const marker = container.querySelector(":scope > .dragMarker");
|
|
||||||
const { width: markerWidth = 0, height: markerHeight = 0 } =
|
|
||||||
marker?.getBoundingClientRect() ?? {};
|
|
||||||
const translate = marker?.style.translate ?? "";
|
|
||||||
const filesLength = transfer.files.length;
|
|
||||||
|
|
||||||
dispatchDragEvent("dragleave");
|
|
||||||
const survivedDragLeave = !!container.querySelector(
|
|
||||||
":scope > .dragMarker"
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
markerHeight,
|
|
||||||
markerWidth,
|
|
||||||
filesLength,
|
|
||||||
survivedDragLeave,
|
|
||||||
translate,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
dataTransfer,
|
|
||||||
getThumbnailSelector(1)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(markerInfo.markerWidth + markerInfo.markerHeight)
|
|
||||||
.withContext(`In ${browserName}, marker dimensions`)
|
|
||||||
.toBeGreaterThan(0);
|
|
||||||
expect(markerInfo.filesLength)
|
|
||||||
.withContext(`In ${browserName}, dropped files`)
|
|
||||||
.toBe(1);
|
|
||||||
expect(markerInfo.translate.includes("NaN"))
|
|
||||||
.withContext(`In ${browserName}, marker position`)
|
|
||||||
.toBeFalse();
|
|
||||||
expect(markerInfo.survivedDragLeave)
|
|
||||||
.withContext(`In ${browserName}, marker after child dragleave`)
|
|
||||||
.toBeTrue();
|
|
||||||
|
|
||||||
const handleMerged = await createPromise(page, resolve => {
|
|
||||||
const listener = ({ pagesCount }) => {
|
|
||||||
if (pagesCount !== 6) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.PDFViewerApplication.eventBus.off("pagesloaded", listener);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
window.PDFViewerApplication.eventBus.on("pagesloaded", listener);
|
|
||||||
});
|
|
||||||
await page.evaluate(
|
|
||||||
(transfer, selector) => {
|
|
||||||
const target = document.querySelector(selector);
|
|
||||||
const { left, top, width, height } =
|
|
||||||
target.getBoundingClientRect();
|
|
||||||
target.dispatchEvent(
|
|
||||||
new DragEvent("drop", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
clientX: left + width / 4,
|
|
||||||
clientY: top + height / 4,
|
|
||||||
dataTransfer: transfer,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
dataTransfer,
|
|
||||||
getThumbnailSelector(1)
|
|
||||||
);
|
|
||||||
await awaitPromise(handleMerged);
|
|
||||||
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => parseInt(document.getElementById("pageNumber").max, 10) === 6
|
|
||||||
);
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => window.PDFViewerApplication.page === 1
|
|
||||||
);
|
|
||||||
await waitForHavingContents(page, [1, 2, 3, 1, 2, 3]);
|
|
||||||
await waitForTextToBe(
|
|
||||||
page,
|
|
||||||
"#viewsManagerStatusActionLabel",
|
|
||||||
`${FSI}3${PDI} selected`
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mergeCoverageIntoGlobal } from "../coverage_utils.js";
|
|
||||||
import os from "os";
|
import os from "os";
|
||||||
|
|
||||||
const isMac = os.platform() === "darwin";
|
const isMac = os.platform() === "darwin";
|
||||||
@ -150,42 +149,11 @@ function closePages(pages) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function closeSinglePage(page) {
|
async function closeSinglePage(page) {
|
||||||
const coverage = await page.evaluate(async () => {
|
// Avoid to keep something from a previous test.
|
||||||
// Collect coverage data from the worker before the document is closed.
|
await page.evaluate(async () => {
|
||||||
let workerCoverage = null;
|
|
||||||
const handler =
|
|
||||||
window.PDFViewerApplication.pdfDocument?._transport?.messageHandler;
|
|
||||||
if (handler) {
|
|
||||||
try {
|
|
||||||
workerCoverage = await handler.sendWithPromise(
|
|
||||||
"GetWorkerCoverage",
|
|
||||||
null
|
|
||||||
);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the viewer gracefully, and clear local storage to avoid state
|
|
||||||
// leaking from one test to another.
|
|
||||||
await window.PDFViewerApplication.testingClose();
|
await window.PDFViewerApplication.testingClose();
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
|
||||||
// Serialize the coverage data to a JSON string because that is a lot
|
|
||||||
// faster/cheaper to transfer from the browser to Node.js over the WebDriver
|
|
||||||
// BiDi protocol, otherwise Puppeteer's (significantly slower) serialization
|
|
||||||
// logic kicks in (see https://github.com/puppeteer/puppeteer/issues/2427).
|
|
||||||
return {
|
|
||||||
page: window.__coverage__ ? JSON.stringify(window.__coverage__) : null,
|
|
||||||
worker: workerCoverage ? JSON.stringify(workerCoverage) : null,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (coverage.page) {
|
|
||||||
mergeCoverageIntoGlobal(JSON.parse(coverage.page));
|
|
||||||
}
|
|
||||||
if (coverage.worker) {
|
|
||||||
mergeCoverageIntoGlobal(JSON.parse(coverage.worker));
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.close({ runBeforeUnload: false });
|
await page.close({ runBeforeUnload: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,8 +236,13 @@ function getSelector(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getRect(page, selector) {
|
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 });
|
await page.waitForSelector(selector, { visible: true });
|
||||||
return (await page.$(selector)).boundingBox();
|
return page.$eval(selector, el => {
|
||||||
|
const { x, y, width, height } = el.getBoundingClientRect();
|
||||||
|
return { x, y, width, height };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQuerySelector(id) {
|
function getQuerySelector(id) {
|
||||||
|
|||||||
@ -1,119 +0,0 @@
|
|||||||
/* Copyright 2026 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { closePages, loadAndWait } from "./test_utils.mjs";
|
|
||||||
|
|
||||||
async function dispatchRequestTextContent(page, id) {
|
|
||||||
return page.evaluate(requestId => {
|
|
||||||
const event = new CustomEvent("requestTextContent", {
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
detail: { requestId },
|
|
||||||
});
|
|
||||||
window.dispatchEvent(event);
|
|
||||||
}, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getReportTextData(page) {
|
|
||||||
await page.waitForFunction(() => window._reportTextData !== undefined);
|
|
||||||
return page.evaluate(() => {
|
|
||||||
const data = window._reportTextData;
|
|
||||||
delete window._reportTextData;
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("PdfTextExtractor", () => {
|
|
||||||
describe("Simple multi-page document", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check that all text is extracted", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await dispatchRequestTextContent(page, 1);
|
|
||||||
|
|
||||||
const { text, requestId } = await getReportTextData(page);
|
|
||||||
|
|
||||||
expect(text).toEqual(
|
|
||||||
[
|
|
||||||
"Table Of Content",
|
|
||||||
"Chapter 1 .......................................................... 2",
|
|
||||||
"Paragraph 1.1 ...................................................... 3",
|
|
||||||
"page 1 / 3",
|
|
||||||
"Chapter 1",
|
|
||||||
"page 2 / 3",
|
|
||||||
"Paragraph 1.1",
|
|
||||||
"Powered by TCPDF (www.tcpdf.org)",
|
|
||||||
"page 3 / 3",
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
expect(requestId).toEqual(1);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Multi-page document, with disableAutoFetch=true set", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"tracemonkey.pdf",
|
|
||||||
".textLayer .endOfContent",
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
disableAutoFetch: true,
|
|
||||||
disableStream: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("check that all text is extracted", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await dispatchRequestTextContent(page, 2);
|
|
||||||
|
|
||||||
const { text, requestId } = await getReportTextData(page);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
text.startsWith(
|
|
||||||
"Trace-based Just-in-Time Type Specialization for Dynamic\nLanguages"
|
|
||||||
)
|
|
||||||
).toBeTrue();
|
|
||||||
expect(
|
|
||||||
text.endsWith(
|
|
||||||
"Conference on Virtual Execution Environments, pages 83–93. ACM\nPress, 2007."
|
|
||||||
)
|
|
||||||
).toBeTrue();
|
|
||||||
expect(text.length).toEqual(82804);
|
|
||||||
expect(requestId).toEqual(2);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -13,41 +13,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @import { Page } from "puppeteer"
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
closePages,
|
closePages,
|
||||||
closeSinglePage,
|
closeSinglePage,
|
||||||
getSpanRectFromText,
|
getSpanRectFromText,
|
||||||
kbSelectAll,
|
|
||||||
loadAndWait,
|
loadAndWait,
|
||||||
waitForEvent,
|
waitForEvent,
|
||||||
} from "./test_utils.mjs";
|
} from "./test_utils.mjs";
|
||||||
import { MathClamp } from "../../src/shared/math_clamp.js";
|
|
||||||
import { startBrowser } from "../test.mjs";
|
import { startBrowser } from "../test.mjs";
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef Point
|
|
||||||
* @property {number} x
|
|
||||||
* @property {number} y
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef Rect
|
|
||||||
* @property {number} x
|
|
||||||
* @property {number} y
|
|
||||||
* @property {number} width
|
|
||||||
* @property {number} height
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef SpanInfo
|
|
||||||
* @property {Rect} rect
|
|
||||||
* @property {string} text
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe("Text layer", () => {
|
describe("Text layer", () => {
|
||||||
describe("Text selection", () => {
|
describe("Text selection", () => {
|
||||||
// page.mouse.move(x, y, { steps: ... }) doesn't work in Firefox, because
|
// page.mouse.move(x, y, { steps: ... }) doesn't work in Firefox, because
|
||||||
@ -85,144 +59,6 @@ describe("Text layer", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pick a point outside the page while remaining inside the viewer.
|
|
||||||
*
|
|
||||||
* @param {Rect} page
|
|
||||||
* Page rectangle.
|
|
||||||
* @param {Rect} viewer
|
|
||||||
* Viewer rectangle.
|
|
||||||
* @param {number} preferredY
|
|
||||||
* Preferred Y coordinate for the pointer target, to avoid unnecessarily
|
|
||||||
* moving the pointer too far.
|
|
||||||
* @returns {Point}
|
|
||||||
* Point outside the page bounds but inside the viewer.
|
|
||||||
*/
|
|
||||||
function getOutsidePagePosition(page, viewer, preferredY) {
|
|
||||||
// The pointer target must remain inside the visible viewer area;
|
|
||||||
// otherwise Firefox can fail with an out-of-bounds move.
|
|
||||||
const minX = Math.ceil(viewer.x) + 5;
|
|
||||||
const maxX = Math.floor(viewer.x + viewer.width) - 5;
|
|
||||||
const minY = Math.ceil(viewer.y) + 5;
|
|
||||||
const maxY = Math.floor(viewer.y + viewer.height) - 5;
|
|
||||||
const y = MathClamp(minY, Math.round(preferredY), maxY);
|
|
||||||
|
|
||||||
const candidates = [
|
|
||||||
{ x: Math.round(page.x + page.width + 20), y },
|
|
||||||
// Prefer below over left: going left retraces through existing text
|
|
||||||
// and shrinks the selection before exiting the page boundary.
|
|
||||||
{
|
|
||||||
x: Math.round(page.x + page.width / 2),
|
|
||||||
y: Math.round(page.y + page.height + 20),
|
|
||||||
},
|
|
||||||
{ x: Math.round(page.x - 20), y },
|
|
||||||
{
|
|
||||||
x: Math.round(page.x + page.width / 2),
|
|
||||||
y: Math.round(page.y - 20),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (
|
|
||||||
candidate.x >= minX &&
|
|
||||||
candidate.x <= maxX &&
|
|
||||||
candidate.y >= minY &&
|
|
||||||
candidate.y <= maxY
|
|
||||||
) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: still return a safe in-view point if preferred directions
|
|
||||||
// are clipped by the viewport at this scroll position.
|
|
||||||
return { x: maxX, y };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current selection.
|
|
||||||
*
|
|
||||||
* @param {Page} page
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
async function getSelectionText(page) {
|
|
||||||
return page.evaluate(
|
|
||||||
() => window.getSelection()?.toString().replaceAll("\r\n", "\n") || ""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the draw layer contains a non-empty selection.
|
|
||||||
*
|
|
||||||
* @param {Page} page
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
async function hasDrawnSelection(page) {
|
|
||||||
return page.evaluate(() => {
|
|
||||||
// If there is no selection, the `div.selection` is removed.
|
|
||||||
for (const path of document.querySelectorAll(
|
|
||||||
".canvasWrapper .selection svg path"
|
|
||||||
)) {
|
|
||||||
if (path.getAttribute("d")?.trim()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the first non-empty text span on a page.
|
|
||||||
*
|
|
||||||
* @param {Page} page
|
|
||||||
* @param {number} pageNumber
|
|
||||||
* @returns {Promise<SpanInfo | null>}
|
|
||||||
*/
|
|
||||||
async function getFirstSpanInfo(page, pageNumber) {
|
|
||||||
await page.waitForSelector(
|
|
||||||
`.page[data-page-number="${pageNumber}"] > .textLayer .endOfContent`
|
|
||||||
);
|
|
||||||
return page.evaluate(number => {
|
|
||||||
for (const el of document.querySelectorAll(
|
|
||||||
`.page[data-page-number="${number}"] > .textLayer span:not(:has(> span))`
|
|
||||||
)) {
|
|
||||||
const text = el.textContent?.trim();
|
|
||||||
if (!text) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const { x, y, width, height } = el.getBoundingClientRect();
|
|
||||||
return { rect: { x, y, width, height }, text };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, pageNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the last non-empty text span on a page.
|
|
||||||
*
|
|
||||||
* @param {Page} page
|
|
||||||
* @param {number} pageNumber
|
|
||||||
* @returns {Promise<SpanInfo | null>}
|
|
||||||
*/
|
|
||||||
async function getLastSpanInfo(page, pageNumber) {
|
|
||||||
await page.waitForSelector(
|
|
||||||
`.page[data-page-number="${pageNumber}"] > .textLayer .endOfContent`
|
|
||||||
);
|
|
||||||
return page.evaluate(number => {
|
|
||||||
let last = null;
|
|
||||||
for (const el of document.querySelectorAll(
|
|
||||||
`.page[data-page-number="${number}"] > .textLayer span:not(:has(> span))`
|
|
||||||
)) {
|
|
||||||
const text = el.textContent?.trim();
|
|
||||||
if (!text) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const { x, y, width, height } = el.getBoundingClientRect();
|
|
||||||
last = { rect: { x, y, width, height }, text };
|
|
||||||
}
|
|
||||||
return last;
|
|
||||||
}, pageNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jasmine.addAsyncMatchers({
|
jasmine.addAsyncMatchers({
|
||||||
// Check that a page has a selection containing the given text, with
|
// Check that a page has a selection containing the given text, with
|
||||||
@ -231,7 +67,11 @@ describe("Text layer", () => {
|
|||||||
return {
|
return {
|
||||||
async compare(page, expected) {
|
async compare(page, expected) {
|
||||||
const TOLERANCE = 10;
|
const TOLERANCE = 10;
|
||||||
const actual = await getSelectionText(page);
|
|
||||||
|
const actual = await page.evaluate(() =>
|
||||||
|
// We need to normalize EOL for Windows
|
||||||
|
window.getSelection().toString().replaceAll("\r\n", "\n")
|
||||||
|
);
|
||||||
|
|
||||||
let start, end;
|
let start, end;
|
||||||
if (expected instanceof RegExp) {
|
if (expected instanceof RegExp) {
|
||||||
@ -268,275 +108,6 @@ describe("Text layer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("using mouse", () => {
|
describe("using mouse", () => {
|
||||||
describe("selection is preserved when dragging outside page bounds", () => {
|
|
||||||
/** @type {Array<[string, Page]>} */
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"tracemonkey.pdf",
|
|
||||||
`.page[data-page-number = "1"] .endOfContent`,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
(_page, browserName) => ({
|
|
||||||
imagesRightClickMinSize: browserName === "firefox" ? 16 : -1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps selection when dragging to another page and then outside", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
const scrollTarget = await getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
1,
|
|
||||||
"Unlike method-based dynamic compilers, our dynamic com-"
|
|
||||||
);
|
|
||||||
await page.evaluate(top => {
|
|
||||||
document.getElementById("viewerContainer").scrollTop = top;
|
|
||||||
}, scrollTarget.y - 50);
|
|
||||||
|
|
||||||
const [
|
|
||||||
positionStartPage1,
|
|
||||||
positionStartPage2,
|
|
||||||
positionEndPage2,
|
|
||||||
page2Rect,
|
|
||||||
viewerRect,
|
|
||||||
] = await Promise.all([
|
|
||||||
getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
1,
|
|
||||||
"Each compiled trace covers one path through the program with"
|
|
||||||
).then(middlePosition),
|
|
||||||
getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
2,
|
|
||||||
"Hence, recording and compiling a trace"
|
|
||||||
).then(middlePosition),
|
|
||||||
getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
2,
|
|
||||||
"cache. Alternatively, the VM could simply stop tracing, and give up"
|
|
||||||
).then(belowEndPosition),
|
|
||||||
page.$eval('.page[data-page-number="2"]', div => {
|
|
||||||
const { x, y, width, height } = div.getBoundingClientRect();
|
|
||||||
return { x, y, width, height };
|
|
||||||
}),
|
|
||||||
page.$eval("#viewerContainer", div => {
|
|
||||||
const { x, y, width, height } = div.getBoundingClientRect();
|
|
||||||
return { x, y, width, height };
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const outsidePage2 = getOutsidePagePosition(
|
|
||||||
page2Rect,
|
|
||||||
viewerRect,
|
|
||||||
positionEndPage2.y
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.mouse.move(positionStartPage1.x, positionStartPage1.y);
|
|
||||||
await page.mouse.down();
|
|
||||||
// First cross into page 2 while still in-bounds so we can verify
|
|
||||||
// the multi-page selection is established before exiting page 2.
|
|
||||||
await moveInSteps(
|
|
||||||
page,
|
|
||||||
positionStartPage1,
|
|
||||||
positionStartPage2,
|
|
||||||
20
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectionBeforeOutside = await getSelectionText(page);
|
|
||||||
expect(selectionBeforeOutside)
|
|
||||||
.withContext(`In ${browserName}, before leaving page 2`)
|
|
||||||
.toMatch(/path through.*Hence, recording/s);
|
|
||||||
|
|
||||||
await moveInSteps(page, positionStartPage2, positionEndPage2, 20);
|
|
||||||
const selectionInsidePage2 = await getSelectionText(page);
|
|
||||||
expect(selectionInsidePage2)
|
|
||||||
.withContext(`In ${browserName}, while still on page 2`)
|
|
||||||
.toMatch(/path through.*Hence, recording and .* tracing/s);
|
|
||||||
|
|
||||||
await moveInSteps(page, positionEndPage2, outsidePage2, 20);
|
|
||||||
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
expect(await hasDrawnSelection(page))
|
|
||||||
.withContext(
|
|
||||||
`In ${browserName}, selection drawn while outside page`
|
|
||||||
)
|
|
||||||
.toBeTrue();
|
|
||||||
|
|
||||||
const selectedText = await getSelectionText(page);
|
|
||||||
expect(selectedText.length)
|
|
||||||
.withContext(
|
|
||||||
`In ${browserName}, selection not lost after mouseup`
|
|
||||||
)
|
|
||||||
.toBeGreaterThan(10);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps selection when dragging outside the current page", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
const [positionStart, page1Rect, viewerRect] = await Promise.all([
|
|
||||||
getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
1,
|
|
||||||
"(frequently executed) bytecode sequences, records"
|
|
||||||
).then(middlePosition),
|
|
||||||
page.$eval('.page[data-page-number="1"]', div => {
|
|
||||||
const { x, y, width, height } = div.getBoundingClientRect();
|
|
||||||
return { x, y, width, height };
|
|
||||||
}),
|
|
||||||
page.$eval("#viewerContainer", div => {
|
|
||||||
const { x, y, width, height } = div.getBoundingClientRect();
|
|
||||||
return { x, y, width, height };
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const outsidePage1 = getOutsidePagePosition(
|
|
||||||
page1Rect,
|
|
||||||
viewerRect,
|
|
||||||
positionStart.y
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.mouse.move(positionStart.x, positionStart.y);
|
|
||||||
await page.mouse.down();
|
|
||||||
await moveInSteps(page, positionStart, outsidePage1, 20);
|
|
||||||
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
expect(await hasDrawnSelection(page))
|
|
||||||
.withContext(
|
|
||||||
`In ${browserName}, selection drawn while outside page`
|
|
||||||
)
|
|
||||||
.toBeTrue();
|
|
||||||
|
|
||||||
const selectedText = await getSelectionText(page);
|
|
||||||
expect(selectedText.length)
|
|
||||||
.withContext(`In ${browserName}`)
|
|
||||||
.toBeGreaterThan(5);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("selection with tagged PDFs", () => {
|
|
||||||
/** @type {Array<[string, Page]>} */
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"structure_simple.pdf",
|
|
||||||
`.page[data-page-number = "1"] .endOfContent`,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
(_page, browserName) => ({
|
|
||||||
imagesRightClickMinSize: browserName === "firefox" ? 16 : -1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps selection when dragging outside the page", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
const [firstSpanInfo, pageRect, viewerRect] = await Promise.all([
|
|
||||||
getFirstSpanInfo(page, 1),
|
|
||||||
page.$eval('.page[data-page-number="1"]', div => {
|
|
||||||
const { x, y, width, height } = div.getBoundingClientRect();
|
|
||||||
return { x, y, width, height };
|
|
||||||
}),
|
|
||||||
page.$eval("#viewerContainer", div => {
|
|
||||||
const { x, y, width, height } = div.getBoundingClientRect();
|
|
||||||
return { x, y, width, height };
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(firstSpanInfo)
|
|
||||||
.withContext(`In ${browserName}`)
|
|
||||||
.not.toBeNull();
|
|
||||||
|
|
||||||
const positionStart = middlePosition(firstSpanInfo.rect);
|
|
||||||
const outsidePage = getOutsidePagePosition(
|
|
||||||
pageRect,
|
|
||||||
viewerRect,
|
|
||||||
positionStart.y
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.mouse.move(positionStart.x, positionStart.y);
|
|
||||||
await page.mouse.down();
|
|
||||||
await moveInSteps(page, positionStart, outsidePage, 20);
|
|
||||||
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
expect(await hasDrawnSelection(page))
|
|
||||||
.withContext(
|
|
||||||
`In ${browserName}, selection drawn while outside page`
|
|
||||||
)
|
|
||||||
.toBeTrue();
|
|
||||||
|
|
||||||
const selectedText = await getSelectionText(page);
|
|
||||||
expect(selectedText.length)
|
|
||||||
.withContext(`In ${browserName}`)
|
|
||||||
.toBeGreaterThan(0);
|
|
||||||
expect(selectedText)
|
|
||||||
.withContext(`In ${browserName}`)
|
|
||||||
.toContain(firstSpanInfo.text.slice(0, 1));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't jump when hovering on an empty area", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
const [firstSpanInfo, lastSpanInfo] = await Promise.all([
|
|
||||||
getFirstSpanInfo(page, 1),
|
|
||||||
getLastSpanInfo(page, 1),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(firstSpanInfo)
|
|
||||||
.withContext(`In ${browserName}, first span`)
|
|
||||||
.not.toBeNull();
|
|
||||||
expect(lastSpanInfo)
|
|
||||||
.withContext(`In ${browserName}, last span`)
|
|
||||||
.not.toBeNull();
|
|
||||||
|
|
||||||
const positionStart = middlePosition(firstSpanInfo.rect);
|
|
||||||
const positionEnd = belowEndPosition(lastSpanInfo.rect);
|
|
||||||
|
|
||||||
await page.mouse.move(positionStart.x, positionStart.y);
|
|
||||||
await page.mouse.down();
|
|
||||||
// Drag from the first to the last text run to pass through the
|
|
||||||
// tagged content and end in the empty area below the text.
|
|
||||||
await moveInSteps(page, positionStart, positionEnd, 20);
|
|
||||||
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
expect(await hasDrawnSelection(page))
|
|
||||||
.withContext(`In ${browserName}, selection drawn in tagged PDF`)
|
|
||||||
.toBeTrue();
|
|
||||||
|
|
||||||
await expectAsync(page)
|
|
||||||
.withContext(`In ${browserName}`)
|
|
||||||
// Selection starts mid-word in Heading 1, so assert the stable
|
|
||||||
// trailing content rather than exact full-line boundaries.
|
|
||||||
.toHaveRoughlySelected(
|
|
||||||
/ing 1\s+This paragraph 1\.\s+Heading 2\s+This paragraph 2/s
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("doesn't jump when hovering on an empty area", () => {
|
describe("doesn't jump when hovering on an empty area", () => {
|
||||||
let pages;
|
let pages;
|
||||||
|
|
||||||
@ -702,7 +273,7 @@ describe("Text layer", () => {
|
|||||||
.withContext(`In ${browserName}`)
|
.withContext(`In ${browserName}`)
|
||||||
.toHaveRoughlySelected(
|
.toHaveRoughlySelected(
|
||||||
"rs as the railway projects under\n" +
|
"rs as the railway projects under\n" +
|
||||||
"development enter the construction phase (estimated at"
|
"development enter the construction phase (estimated at "
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -948,7 +519,9 @@ describe("Text layer", () => {
|
|||||||
);
|
);
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
|
|
||||||
const selection = await getSelectionText(page);
|
const selection = await page.evaluate(() =>
|
||||||
|
window.getSelection().toString()
|
||||||
|
);
|
||||||
expect(selection).withContext(`In ${browserName}`).toEqual("AB");
|
expect(selection).withContext(`In ${browserName}`).toEqual("AB");
|
||||||
|
|
||||||
// The selectionchange handler in TextLayerBuilder walks up
|
// The selectionchange handler in TextLayerBuilder walks up
|
||||||
@ -968,65 +541,6 @@ describe("Text layer", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with `enableSelectionRendering` disabled", () => {
|
|
||||||
/** @type {Array<[string, Page]>} */
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"tracemonkey.pdf",
|
|
||||||
`.page[data-page-number = "1"] .endOfContent`,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
(_page, browserName) => ({
|
|
||||||
enableSelectionRendering: false,
|
|
||||||
imagesRightClickMinSize: browserName === "firefox" ? 16 : -1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not render a selection overlay in the draw layer", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
const [positionStart, positionEnd] = await Promise.all([
|
|
||||||
getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
1,
|
|
||||||
"(frequently executed) bytecode sequences, records"
|
|
||||||
).then(middlePosition),
|
|
||||||
getSpanRectFromText(
|
|
||||||
page,
|
|
||||||
1,
|
|
||||||
"them, and compiles them to fast native code. We call such a se-"
|
|
||||||
).then(belowEndPosition),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await page.mouse.move(positionStart.x, positionStart.y);
|
|
||||||
await page.mouse.down();
|
|
||||||
await moveInSteps(page, positionStart, positionEnd, 20);
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
// Text should still be selectable.
|
|
||||||
const selectedText = await getSelectionText(page);
|
|
||||||
expect(selectedText.length)
|
|
||||||
.withContext(`In ${browserName}, text is still selectable`)
|
|
||||||
.toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// But no selection overlay should appear in the draw layer.
|
|
||||||
expect(await hasDrawnSelection(page))
|
|
||||||
.withContext(
|
|
||||||
`In ${browserName}, no selection drawn when disabled`
|
|
||||||
)
|
|
||||||
.toBeFalse();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("using selection carets", () => {
|
describe("using selection carets", () => {
|
||||||
@ -1119,142 +633,6 @@ describe("Text layer", () => {
|
|||||||
.toHaveRoughlySelected(/frequently .* We call such a s/s);
|
.toHaveRoughlySelected(/frequently .* We call such a s/s);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with select-all (Ctrl+A)", () => {
|
|
||||||
/** @type {Array<[string, Page]>} */
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the set of page numbers that have a non-empty selection
|
|
||||||
* overlay path in their draw layer.
|
|
||||||
*
|
|
||||||
* @param {Page} page
|
|
||||||
* @returns {Promise<Array<number>>}
|
|
||||||
*/
|
|
||||||
async function pagesWithDrawnSelection(page) {
|
|
||||||
return page.evaluate(() => {
|
|
||||||
const numbers = new Set();
|
|
||||||
for (const path of document.querySelectorAll(
|
|
||||||
".page .canvasWrapper .selection svg path"
|
|
||||||
)) {
|
|
||||||
if (path.getAttribute("d")?.trim()) {
|
|
||||||
const n = path.closest(".page")?.dataset.pageNumber;
|
|
||||||
if (n) {
|
|
||||||
numbers.add(Number(n));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [...numbers].sort((a, b) => a - b);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"tracemonkey.pdf",
|
|
||||||
`.page[data-page-number = "1"] .endOfContent`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("draws a selection overlay on currently-rendered pages", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
// Wait for at least two text layers to be rendered so the
|
|
||||||
// overlay can be expected on multiple pages. The number of
|
|
||||||
// pages rendered up-front at the default zoom can vary
|
|
||||||
// depending on the viewport size on CI.
|
|
||||||
await page.waitForFunction(
|
|
||||||
() =>
|
|
||||||
document.querySelectorAll(".textLayer .endOfContent").length >=
|
|
||||||
2
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitForEvent({
|
|
||||||
page,
|
|
||||||
eventName: "selectionchange",
|
|
||||||
action: () => kbSelectAll(page),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await hasDrawnSelection(page))
|
|
||||||
.withContext(`In ${browserName}`)
|
|
||||||
.toBeTrue();
|
|
||||||
|
|
||||||
// Several text layers are rendered at the default zoom and
|
|
||||||
// each one should now carry a selection overlay.
|
|
||||||
const drawn = await pagesWithDrawnSelection(page);
|
|
||||||
expect(drawn.length)
|
|
||||||
.withContext(
|
|
||||||
`In ${browserName}, pages with selection overlay: ` +
|
|
||||||
`${drawn.join(",")}`
|
|
||||||
)
|
|
||||||
.toBeGreaterThan(1);
|
|
||||||
expect(drawn[0])
|
|
||||||
.withContext(`In ${browserName}, first selected page`)
|
|
||||||
.toBe(1);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("extends the overlay onto pages rendered after scroll", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await waitForEvent({
|
|
||||||
page,
|
|
||||||
eventName: "selectionchange",
|
|
||||||
action: () => kbSelectAll(page),
|
|
||||||
});
|
|
||||||
|
|
||||||
const initial = await pagesWithDrawnSelection(page);
|
|
||||||
expect(initial.length)
|
|
||||||
.withContext(`In ${browserName}, initial pages with overlay`)
|
|
||||||
.toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Pick the first page that hasn't been rendered with a
|
|
||||||
// selection overlay yet, scroll to it, and verify the overlay
|
|
||||||
// gets drawn on it once its draw layer is parented.
|
|
||||||
const lastInitial = initial.at(-1);
|
|
||||||
const targetPage = lastInitial + 1;
|
|
||||||
|
|
||||||
await page.evaluate(n => {
|
|
||||||
const pageDiv = document.querySelector(
|
|
||||||
`.page[data-page-number="${n}"]`
|
|
||||||
);
|
|
||||||
pageDiv.scrollIntoView({ block: "center" });
|
|
||||||
}, targetPage);
|
|
||||||
|
|
||||||
await page.waitForSelector(
|
|
||||||
`.page[data-page-number="${targetPage}"] .textLayer .endOfContent`,
|
|
||||||
{ timeout: 0 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// After the new page is rendered, its draw layer becomes
|
|
||||||
// "live" (`setParent` is called) and the selection overlay
|
|
||||||
// must be extended onto it without requiring a new
|
|
||||||
// `selectionchange` event.
|
|
||||||
await page.waitForFunction(
|
|
||||||
n => {
|
|
||||||
const path = document.querySelector(
|
|
||||||
`.page[data-page-number="${n}"] .canvasWrapper .selection svg path`
|
|
||||||
);
|
|
||||||
return !!path?.getAttribute("d")?.trim();
|
|
||||||
},
|
|
||||||
{ timeout: 0 },
|
|
||||||
targetPage
|
|
||||||
);
|
|
||||||
|
|
||||||
const afterScroll = await pagesWithDrawnSelection(page);
|
|
||||||
expect(afterScroll)
|
|
||||||
.withContext(
|
|
||||||
`In ${browserName}, target page ${targetPage} has overlay`
|
|
||||||
)
|
|
||||||
.toContain(targetPage);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when the browser enforces a minimum font size", () => {
|
describe("when the browser enforces a minimum font size", () => {
|
||||||
|
|||||||
@ -26,11 +26,8 @@ import {
|
|||||||
waitForPageChanging,
|
waitForPageChanging,
|
||||||
waitForPageRendered,
|
waitForPageRendered,
|
||||||
} from "./test_utils.mjs";
|
} from "./test_utils.mjs";
|
||||||
import path from "path";
|
|
||||||
import { PNG } from "pngjs";
|
import { PNG } from "pngjs";
|
||||||
|
|
||||||
const __dirname = import.meta.dirname;
|
|
||||||
|
|
||||||
describe("PDF viewer", () => {
|
describe("PDF viewer", () => {
|
||||||
describe("Zoom origin", () => {
|
describe("Zoom origin", () => {
|
||||||
let pages;
|
let pages;
|
||||||
@ -1406,10 +1403,6 @@ describe("PDF viewer", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps the content under the pinch centre fixed on the screen", async () => {
|
it("keeps the content under the pinch centre fixed on the screen", async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
@ -1617,10 +1610,6 @@ describe("PDF viewer", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Check that the top right corner of the annotation is centered vertically", async () => {
|
it("Check that the top right corner of the annotation is centered vertically", async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pages.map(async ([browserName, page]) => {
|
pages.map(async ([browserName, page]) => {
|
||||||
@ -1903,108 +1892,5 @@ describe("PDF viewer", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("@page size stylesheet under CSP", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait(
|
|
||||||
"basicapi.pdf",
|
|
||||||
".textLayer .endOfContent",
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
earlySetup: () => {
|
|
||||||
// Capture state while window.print() runs — the print service's
|
|
||||||
// destroy() removes the @page stylesheet right after, on the
|
|
||||||
// afterprint event.
|
|
||||||
window._pageRuleApplied = null;
|
|
||||||
window.print = () => {
|
|
||||||
window._pageRuleApplied = [
|
|
||||||
...document.querySelectorAll("style"),
|
|
||||||
].some(
|
|
||||||
s =>
|
|
||||||
s.sheet?.cssRules.length > 0 &&
|
|
||||||
[...s.sheet.cssRules].some(r => r.cssText.includes("@page"))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
appSetup: app => {
|
|
||||||
app._testPrintResolver = Promise.withResolvers();
|
|
||||||
},
|
|
||||||
eventBusSetup: eventBus => {
|
|
||||||
eventBus.on(
|
|
||||||
"afterprint",
|
|
||||||
() => {
|
|
||||||
window.PDFViewerApplication._testPrintResolver.resolve();
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The print service injects an inline
|
|
||||||
// <style>@page { size: WxH pt }</style> to match the PDF's page
|
|
||||||
// dimensions. If the CSP `style-src-elem` directive blocks inline
|
|
||||||
// <style> elements, the element is created but its content is never
|
|
||||||
// parsed — `sheet.cssRules` stays empty and the @page rule has no
|
|
||||||
// effect. See web/viewer.html.
|
|
||||||
it("must apply the injected @page rule (no CSP block)", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
await waitAndClick(page, "#printButton");
|
|
||||||
await awaitPromise(
|
|
||||||
await page.evaluateHandle(() => [
|
|
||||||
window.PDFViewerApplication._testPrintResolver.promise,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasPageRule = await page.evaluate(
|
|
||||||
() => window._pageRuleApplied
|
|
||||||
);
|
|
||||||
expect(hasPageRule)
|
|
||||||
.withContext(
|
|
||||||
`In ${browserName}: injected @page stylesheet was parsed`
|
|
||||||
)
|
|
||||||
.toBeTrue();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Open a new PDF via the file input", () => {
|
|
||||||
let pages;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
pages = await loadAndWait("tracemonkey.pdf", ".textLayer .endOfContent");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await closePages(pages);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The "Open" input wraps the chosen file in a blob URL,
|
|
||||||
// which the worker then fetches. `connect-src` in the production CSP must
|
|
||||||
// therefore allow `blob:` — see web/viewer.html.
|
|
||||||
it("must load a PDF picked through the file input (blob URL)", async () => {
|
|
||||||
await Promise.all(
|
|
||||||
pages.map(async ([browserName, page]) => {
|
|
||||||
const fileInput = await page.$("#fileInput");
|
|
||||||
await fileInput.uploadFile(
|
|
||||||
path.join(__dirname, "../pdfs/basicapi.pdf")
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.waitForFunction(
|
|
||||||
() => window.PDFViewerApplication.pdfDocument?.numPages === 3
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -890,7 +890,6 @@ async function startIntegrationTest() {
|
|||||||
sessions[0].numRuns = results.runs;
|
sessions[0].numRuns = results.runs;
|
||||||
sessions[0].numErrors = results.failures;
|
sessions[0].numErrors = results.failures;
|
||||||
sessions[0].failures = results.failureList;
|
sessions[0].failures = results.failureList;
|
||||||
sessions[0].coverage = globalThis.__coverage__;
|
|
||||||
await Promise.all(sessions.map(session => closeSession(session.name)));
|
await Promise.all(sessions.map(session => closeSession(session.name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -992,20 +991,12 @@ async function startBrowser({
|
|||||||
// calls can run before events triggered by the previous protocol calls had
|
// calls can run before events triggered by the previous protocol calls had
|
||||||
// a chance to be processed (essentially causing events to get lost). This
|
// a chance to be processed (essentially causing events to get lost). This
|
||||||
// value gives Chrome a more similar execution speed as Firefox.
|
// value gives Chrome a more similar execution speed as Firefox.
|
||||||
options.slowMo = 3;
|
options.slowMo = 5;
|
||||||
|
|
||||||
options.args = [
|
// avoid crash
|
||||||
// Avoid crashing because no sandbox is shipped by default and we only run
|
options.args = ["--no-sandbox", "--disable-setuid-sandbox"];
|
||||||
// our own trusted content in the scope of the tests (for more information
|
// silent printing in a pdf
|
||||||
// see https://pptr.dev/troubleshooting#setting-up-chrome-linux-sandbox).
|
options.args.push("--kiosk-printing");
|
||||||
"--no-sandbox",
|
|
||||||
"--disable-setuid-sandbox",
|
|
||||||
// Print PDFs silently (without print preview or user interaction).
|
|
||||||
"--kiosk-printing",
|
|
||||||
// Disable hardware acceleration (fixes rendering issues, see #15168 and
|
|
||||||
// #21272, and environments like GitHub Actions don't expose GPUs anyway).
|
|
||||||
"--disable-gpu",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (browserName === "firefox") {
|
if (browserName === "firefox") {
|
||||||
@ -1024,13 +1015,12 @@ async function startBrowser({
|
|||||||
// Save file in output
|
// Save file in output
|
||||||
"browser.download.folderList": 2,
|
"browser.download.folderList": 2,
|
||||||
"browser.download.dir": tempDir,
|
"browser.download.dir": tempDir,
|
||||||
// Print PDFs silently (without print preview or user interaction).
|
// Print silently in a pdf
|
||||||
"print.always_print_silent": true,
|
"print.always_print_silent": true,
|
||||||
print_printer: "PDF",
|
print_printer: "PDF",
|
||||||
"print.printer_PDF.print_to_file": true,
|
"print.printer_PDF.print_to_file": true,
|
||||||
"print.printer_PDF.print_to_filename": printFile,
|
"print.printer_PDF.print_to_filename": printFile,
|
||||||
// Disable hardware acceleration (fixes rendering issues, see #15168 and
|
// Disable gpu acceleration
|
||||||
// #21272, and environments like GitHub Actions don't expose GPUs anyway).
|
|
||||||
"gfx.canvas.accelerated": false,
|
"gfx.canvas.accelerated": false,
|
||||||
// It's helpful to see where the caret is.
|
// It's helpful to see where the caret is.
|
||||||
"accessibility.browsewithcaret": true,
|
"accessibility.browsewithcaret": true,
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import {
|
|||||||
} from "./test_utils.js";
|
} from "./test_utils.js";
|
||||||
import {
|
import {
|
||||||
fetchData as fetchDataDOM,
|
fetchData as fetchDataDOM,
|
||||||
|
PageViewport,
|
||||||
RenderingCancelledException,
|
RenderingCancelledException,
|
||||||
StatTimer,
|
StatTimer,
|
||||||
} from "../../src/display/display_utils.js";
|
} from "../../src/display/display_utils.js";
|
||||||
@ -55,7 +56,6 @@ import { AutoPrintRegExp } from "../../web/ui_utils.js";
|
|||||||
import { GlobalImageCache } from "../../src/core/image_utils.js";
|
import { GlobalImageCache } from "../../src/core/image_utils.js";
|
||||||
import { GlobalWorkerOptions } from "../../src/display/worker_options.js";
|
import { GlobalWorkerOptions } from "../../src/display/worker_options.js";
|
||||||
import { Metadata } from "../../src/display/metadata.js";
|
import { Metadata } from "../../src/display/metadata.js";
|
||||||
import { PageViewport } from "../../src/display/page_viewport.js";
|
|
||||||
|
|
||||||
const WORKER_SRC = "../../build/generic/build/pdf.worker.mjs";
|
const WORKER_SRC = "../../build/generic/build/pdf.worker.mjs";
|
||||||
|
|
||||||
@ -4590,27 +4590,6 @@ have written that much by now. So, here’s to squashing bugs.`);
|
|||||||
await loadingTask.destroy();
|
await loadingTask.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets operatorList, from PDF with /BrotliDecode", async function () {
|
|
||||||
const loadingTask = getDocument(
|
|
||||||
buildGetDocumentParams("Brotli-Prototype-FileA.pdf")
|
|
||||||
);
|
|
||||||
expect(loadingTask).toBeInstanceOf(PDFDocumentLoadingTask);
|
|
||||||
|
|
||||||
const pdfDoc = await loadingTask.promise;
|
|
||||||
expect(pdfDoc.numPages).toEqual(25);
|
|
||||||
|
|
||||||
const pdfPage = await pdfDoc.getPage(1);
|
|
||||||
expect(pdfPage).toBeInstanceOf(PDFPageProxy);
|
|
||||||
|
|
||||||
const opList = await pdfPage.getOperatorList();
|
|
||||||
expect(opList.fnArray.length).toBeGreaterThan(9800);
|
|
||||||
expect(opList.argsArray.length).toBeGreaterThan(9800);
|
|
||||||
expect(opList.lastChunk).toBeTrue();
|
|
||||||
expect(opList.separateAnnots).toBeNull();
|
|
||||||
|
|
||||||
await loadingTask.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gets page stats after parsing page, without `pdfBug` set", async function () {
|
it("gets page stats after parsing page, without `pdfBug` set", async function () {
|
||||||
await page.getOperatorList();
|
await page.getOperatorList();
|
||||||
expect(page.stats).toEqual(null);
|
expect(page.stats).toEqual(null);
|
||||||
|
|||||||
@ -18,9 +18,7 @@ import {
|
|||||||
CFFCompiler,
|
CFFCompiler,
|
||||||
CFFFDSelect,
|
CFFFDSelect,
|
||||||
CFFParser,
|
CFFParser,
|
||||||
CFFPrivateDict,
|
|
||||||
CFFStrings,
|
CFFStrings,
|
||||||
CFFTopDict,
|
|
||||||
} from "../../src/core/cff_parser.js";
|
} from "../../src/core/cff_parser.js";
|
||||||
import { SEAC_ANALYSIS_ENABLED } from "../../src/core/fonts_utils.js";
|
import { SEAC_ANALYSIS_ENABLED } from "../../src/core/fonts_utils.js";
|
||||||
import { Stream } from "../../src/core/stream.js";
|
import { Stream } from "../../src/core/stream.js";
|
||||||
@ -114,77 +112,6 @@ describe("CFFParser", function () {
|
|||||||
expect(topDict.getByName("Private")).toEqual([45, 102]);
|
expect(topDict.getByName("Private")).toEqual([45, 102]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ignores an empty FontBBox when adjusting ascent/descent", function () {
|
|
||||||
cff.topDict.setByName("FontBBox", [0, 0, 0, 0]);
|
|
||||||
const fontDataWithEmptyBBox = new CFFCompiler(cff).compile();
|
|
||||||
|
|
||||||
const properties = {
|
|
||||||
ascent: 800,
|
|
||||||
descent: -200,
|
|
||||||
};
|
|
||||||
new CFFParser(
|
|
||||||
new Stream(fontDataWithEmptyBBox),
|
|
||||||
properties,
|
|
||||||
SEAC_ANALYSIS_ENABLED
|
|
||||||
).parse();
|
|
||||||
|
|
||||||
expect(properties.ascent).toEqual(800);
|
|
||||||
expect(properties.descent).toEqual(-200);
|
|
||||||
expect(properties.ascentScaled).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("repairs an empty FontBBox from font descriptor data", function () {
|
|
||||||
cff.topDict.setByName("FontBBox", [0, 0, 0, 0]);
|
|
||||||
const fontDataWithEmptyBBox = new CFFCompiler(cff).compile();
|
|
||||||
|
|
||||||
const properties = {
|
|
||||||
bbox: [2974, -300, 64236, 900],
|
|
||||||
};
|
|
||||||
const reparsedCff = new CFFParser(
|
|
||||||
new Stream(fontDataWithEmptyBBox),
|
|
||||||
properties,
|
|
||||||
SEAC_ANALYSIS_ENABLED
|
|
||||||
).parse();
|
|
||||||
|
|
||||||
expect(reparsedCff.topDict.getByName("FontBBox")).toEqual([
|
|
||||||
-1300, -300, 2974, 900,
|
|
||||||
]);
|
|
||||||
expect(properties.ascent).toEqual(900);
|
|
||||||
expect(properties.descent).toEqual(-300);
|
|
||||||
expect(properties.ascentScaled).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("repairs likely Ghostscript-zeroed FDArray private defaults", function () {
|
|
||||||
cff.isCIDFont = true;
|
|
||||||
cff.topDict.setByName("ROS", [0, 0, 0]);
|
|
||||||
cff.topDict.setByName("FDSelect", 0);
|
|
||||||
cff.topDict.setByName("FDArray", 0);
|
|
||||||
|
|
||||||
const fdDict = new CFFTopDict(cff.strings);
|
|
||||||
fdDict.setByName("Private", [0, 0]);
|
|
||||||
fdDict.privateDict = new CFFPrivateDict(cff.strings);
|
|
||||||
fdDict.privateDict.setByName("BlueScale", 0);
|
|
||||||
fdDict.privateDict.setByName("BlueShift", 0);
|
|
||||||
fdDict.privateDict.setByName("BlueFuzz", 0);
|
|
||||||
fdDict.privateDict.setByName("ExpansionFactor", 0);
|
|
||||||
|
|
||||||
cff.fdArray = [fdDict];
|
|
||||||
cff.fdSelect = new CFFFDSelect(0, Array(cff.charStrings.count).fill(0));
|
|
||||||
const fontDataWithBrokenFDPrivate = new CFFCompiler(cff).compile();
|
|
||||||
|
|
||||||
const reparsedCff = new CFFParser(
|
|
||||||
new Stream(fontDataWithBrokenFDPrivate),
|
|
||||||
{},
|
|
||||||
SEAC_ANALYSIS_ENABLED
|
|
||||||
).parse();
|
|
||||||
const privateDict = reparsedCff.fdArray[0].privateDict;
|
|
||||||
|
|
||||||
expect(privateDict.getByName("BlueScale")).toEqual(0.039625);
|
|
||||||
expect(privateDict.getByName("BlueShift")).toEqual(7);
|
|
||||||
expect(privateDict.getByName("BlueFuzz")).toEqual(1);
|
|
||||||
expect(privateDict.getByName("ExpansionFactor")).toEqual(0.06);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("refuses to add topDict key with invalid value (bug 1068432)", function () {
|
it("refuses to add topDict key with invalid value (bug 1068432)", function () {
|
||||||
const topDict = cff.topDict;
|
const topDict = cff.topDict;
|
||||||
const defaultValue = topDict.getByName("UnderlinePosition");
|
const defaultValue = topDict.getByName("UnderlinePosition");
|
||||||
|
|||||||
@ -48,9 +48,7 @@
|
|||||||
"pdf_viewer_spec.js",
|
"pdf_viewer_spec.js",
|
||||||
"postscript_spec.js",
|
"postscript_spec.js",
|
||||||
"primitives_spec.js",
|
"primitives_spec.js",
|
||||||
"scripting_utils_spec.js",
|
|
||||||
"stream_spec.js",
|
"stream_spec.js",
|
||||||
"string_utils_spec.js",
|
|
||||||
"struct_tree_spec.js",
|
"struct_tree_spec.js",
|
||||||
"svg_factory_spec.js",
|
"svg_factory_spec.js",
|
||||||
"text_layer_spec.js",
|
"text_layer_spec.js",
|
||||||
|
|||||||
@ -22,10 +22,13 @@ import {
|
|||||||
getInheritableProperty,
|
getInheritableProperty,
|
||||||
getModificationDate,
|
getModificationDate,
|
||||||
getSizeInBytes,
|
getSizeInBytes,
|
||||||
|
isAscii,
|
||||||
isWhiteSpace,
|
isWhiteSpace,
|
||||||
numberToString,
|
numberToString,
|
||||||
parseXFAPath,
|
parseXFAPath,
|
||||||
recoverJsURL,
|
recoverJsURL,
|
||||||
|
stringToUTF16HexString,
|
||||||
|
stringToUTF16String,
|
||||||
toRomanNumerals,
|
toRomanNumerals,
|
||||||
validateCSSFont,
|
validateCSSFont,
|
||||||
} from "../../src/core/core_utils.js";
|
} from "../../src/core/core_utils.js";
|
||||||
@ -413,6 +416,56 @@ describe("core_utils", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isAscii", function () {
|
||||||
|
it("handles ascii/non-ascii strings", function () {
|
||||||
|
expect(isAscii("hello world")).toEqual(true);
|
||||||
|
expect(isAscii("こんにちは世界の")).toEqual(false);
|
||||||
|
expect(isAscii("hello world in Japanese is こんにちは世界の")).toEqual(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(isAscii("")).toEqual(true);
|
||||||
|
expect(isAscii(123)).toEqual(false);
|
||||||
|
expect(isAscii(null)).toEqual(false);
|
||||||
|
expect(isAscii(undefined)).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("stringToUTF16HexString", function () {
|
||||||
|
it("should encode a string in UTF16 hexadecimal format", function () {
|
||||||
|
expect(stringToUTF16HexString("hello world")).toEqual(
|
||||||
|
"00680065006c006c006f00200077006f0072006c0064"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stringToUTF16HexString("こんにちは世界の")).toEqual(
|
||||||
|
"30533093306b3061306f4e16754c306e"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("stringToUTF16String", function () {
|
||||||
|
it("should encode a string in UTF16", function () {
|
||||||
|
expect(stringToUTF16String("hello world")).toEqual(
|
||||||
|
"\0h\0e\0l\0l\0o\0 \0w\0o\0r\0l\0d"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stringToUTF16String("こんにちは世界の")).toEqual(
|
||||||
|
"\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should encode a string in UTF16BE with a BOM", function () {
|
||||||
|
expect(
|
||||||
|
stringToUTF16String("hello world", /* bigEndian = */ true)
|
||||||
|
).toEqual("\xfe\xff\0h\0e\0l\0l\0o\0 \0w\0o\0r\0l\0d");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
stringToUTF16String("こんにちは世界の", /* bigEndian = */ true)
|
||||||
|
).toEqual(
|
||||||
|
"\xfe\xff\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("deepCompare", function () {
|
describe("deepCompare", function () {
|
||||||
it("should return true for the same reference", function () {
|
it("should return true for the same reference", function () {
|
||||||
const dict = new Dict();
|
const dict = new Dict();
|
||||||
|
|||||||
@ -151,9 +151,6 @@ describe("display_utils", function () {
|
|||||||
expect(getPdfFilenameFromUrl("/pdfs/%AA.pdf")).toEqual("%AA.pdf");
|
expect(getPdfFilenameFromUrl("/pdfs/%AA.pdf")).toEqual("%AA.pdf");
|
||||||
|
|
||||||
expect(getPdfFilenameFromUrl("/pdfs/%2F.pdf")).toEqual("%2F.pdf");
|
expect(getPdfFilenameFromUrl("/pdfs/%2F.pdf")).toEqual("%2F.pdf");
|
||||||
|
|
||||||
// A corrupt relative URL.
|
|
||||||
expect(getPdfFilenameFromUrl("//%%file.pdf")).toEqual("document.pdf");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets PDF filename from (some) standard protocols", function () {
|
it("gets PDF filename from (some) standard protocols", function () {
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
import { GlobalWorkerOptions } from "pdfjs/display/worker_options.js";
|
import { GlobalWorkerOptions } from "pdfjs/display/worker_options.js";
|
||||||
import { isNodeJS } from "../../src/shared/util.js";
|
import { isNodeJS } from "../../src/shared/util.js";
|
||||||
import { mergeCoverageIntoGlobal } from "../coverage_utils.js";
|
import { mergeWorkerCoverageIntoWindow } from "../coverage_utils.js";
|
||||||
import { MessageHandler } from "pdfjs/shared/message_handler.js";
|
import { MessageHandler } from "pdfjs/shared/message_handler.js";
|
||||||
import { PDFWorker } from "pdfjs/display/api.js";
|
import { PDFWorker } from "pdfjs/display/api.js";
|
||||||
import { TestReporter } from "../reporter.js";
|
import { TestReporter } from "../reporter.js";
|
||||||
@ -95,9 +95,7 @@ async function initializePDFJS(callback) {
|
|||||||
"pdfjs-test/unit/postscript_spec.js",
|
"pdfjs-test/unit/postscript_spec.js",
|
||||||
"pdfjs-test/unit/primitives_spec.js",
|
"pdfjs-test/unit/primitives_spec.js",
|
||||||
"pdfjs-test/unit/scripting_spec.js",
|
"pdfjs-test/unit/scripting_spec.js",
|
||||||
"pdfjs-test/unit/scripting_utils_spec.js",
|
|
||||||
"pdfjs-test/unit/stream_spec.js",
|
"pdfjs-test/unit/stream_spec.js",
|
||||||
"pdfjs-test/unit/string_utils_spec.js",
|
|
||||||
"pdfjs-test/unit/struct_tree_spec.js",
|
"pdfjs-test/unit/struct_tree_spec.js",
|
||||||
"pdfjs-test/unit/svg_factory_spec.js",
|
"pdfjs-test/unit/svg_factory_spec.js",
|
||||||
"pdfjs-test/unit/text_layer_spec.js",
|
"pdfjs-test/unit/text_layer_spec.js",
|
||||||
@ -158,7 +156,7 @@ function installWorkerCoverageHook() {
|
|||||||
const handler = new MessageHandler("main", "worker", webWorker);
|
const handler = new MessageHandler("main", "worker", webWorker);
|
||||||
const promise = handler
|
const promise = handler
|
||||||
.sendWithPromise("GetWorkerCoverage", null)
|
.sendWithPromise("GetWorkerCoverage", null)
|
||||||
.then(mergeCoverageIntoGlobal)
|
.then(mergeWorkerCoverageIntoWindow)
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.warn(`Failed to collect worker coverage: ${e}`);
|
console.warn(`Failed to collect worker coverage: ${e}`);
|
||||||
})
|
})
|
||||||
|
|||||||
@ -46,6 +46,7 @@ import {
|
|||||||
getPdfFilenameFromUrl,
|
getPdfFilenameFromUrl,
|
||||||
getRGB,
|
getRGB,
|
||||||
getRGBA,
|
getRGBA,
|
||||||
|
getXfaPageViewport,
|
||||||
isDataScheme,
|
isDataScheme,
|
||||||
isPdfFile,
|
isPdfFile,
|
||||||
noContextMenu,
|
noContextMenu,
|
||||||
@ -105,6 +106,7 @@ const expectedAPI = Object.freeze({
|
|||||||
getRGB,
|
getRGB,
|
||||||
getRGBA,
|
getRGBA,
|
||||||
getUuid,
|
getUuid,
|
||||||
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
ImageKind,
|
ImageKind,
|
||||||
InvalidPDFException,
|
InvalidPDFException,
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
/* Copyright 2026 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ColorConverters } from "../../src/shared/scripting_utils.js";
|
|
||||||
|
|
||||||
describe("scripting_utils", function () {
|
|
||||||
describe("ColorConverters", function () {
|
|
||||||
it("should check G conversion", function () {
|
|
||||||
const color = [0.5];
|
|
||||||
|
|
||||||
expect(ColorConverters.G_CMYK(color)).toEqual(["CMYK", 0, 0, 0, 0.5]);
|
|
||||||
|
|
||||||
expect(ColorConverters.G_RGB(color)).toEqual(["RGB", 0.5, 0.5, 0.5]);
|
|
||||||
|
|
||||||
expect(ColorConverters.G_rgb(color)).toEqual([127.5, 127.5, 127.5]);
|
|
||||||
|
|
||||||
expect(ColorConverters.G_HTML(color)).toEqual("#7f7f7f");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should check RGB conversion", function () {
|
|
||||||
const color = [0.4, 0.5, 0.6];
|
|
||||||
|
|
||||||
expect(ColorConverters.RGB_CMYK(color)).toEqual([
|
|
||||||
"CMYK",
|
|
||||||
0.6,
|
|
||||||
0.5,
|
|
||||||
0.4,
|
|
||||||
0.4,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(ColorConverters.RGB_G(color)).toEqual(["G", 0.481]);
|
|
||||||
|
|
||||||
expect(ColorConverters.RGB_rgb(color)).toEqual([102, 127.5, 153]);
|
|
||||||
|
|
||||||
expect(ColorConverters.RGB_HTML(color)).toEqual("#667f99");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should check CMYK conversion", function () {
|
|
||||||
const color = [0.4, 0.5, 0.6, 0];
|
|
||||||
|
|
||||||
expect(ColorConverters.CMYK_RGB(color)).toEqual(["RGB", 0.6, 0.4, 0.5]);
|
|
||||||
|
|
||||||
expect(ColorConverters.CMYK_G(color)).toEqual(["G", 0.471]);
|
|
||||||
|
|
||||||
expect(ColorConverters.CMYK_rgb(color)).toEqual([153, 102, 127.5]);
|
|
||||||
|
|
||||||
expect(ColorConverters.CMYK_HTML(color)).toEqual("#99667f");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should check T conversion", function () {
|
|
||||||
const color = [0.4, 0.5, 0.6];
|
|
||||||
|
|
||||||
expect(ColorConverters.T_rgb(color)).toEqual([null]);
|
|
||||||
|
|
||||||
expect(ColorConverters.T_HTML(color)).toEqual("#00000000");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
/* Copyright 2019 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
isAscii,
|
|
||||||
stringToPDFString,
|
|
||||||
stringToUTF16HexString,
|
|
||||||
stringToUTF16String,
|
|
||||||
} from "../../src/core/string_utils.js";
|
|
||||||
|
|
||||||
describe("string_utils", function () {
|
|
||||||
describe("isAscii", function () {
|
|
||||||
it("handles ascii/non-ascii strings", function () {
|
|
||||||
expect(isAscii("hello world")).toEqual(true);
|
|
||||||
expect(isAscii("こんにちは世界の")).toEqual(false);
|
|
||||||
expect(isAscii("hello world in Japanese is こんにちは世界の")).toEqual(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
expect(isAscii("")).toEqual(true);
|
|
||||||
expect(isAscii(123)).toEqual(false);
|
|
||||||
expect(isAscii(null)).toEqual(false);
|
|
||||||
expect(isAscii(undefined)).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("stringToUTF16HexString", function () {
|
|
||||||
it("should encode a string in UTF16 hexadecimal format", function () {
|
|
||||||
expect(stringToUTF16HexString("hello world")).toEqual(
|
|
||||||
"00680065006c006c006f00200077006f0072006c0064"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(stringToUTF16HexString("こんにちは世界の")).toEqual(
|
|
||||||
"30533093306b3061306f4e16754c306e"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("stringToUTF16String", function () {
|
|
||||||
it("should encode a string in UTF16", function () {
|
|
||||||
expect(stringToUTF16String("hello world")).toEqual(
|
|
||||||
"\0h\0e\0l\0l\0o\0 \0w\0o\0r\0l\0d"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(stringToUTF16String("こんにちは世界の")).toEqual(
|
|
||||||
"\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should encode a string in UTF16BE with a BOM", function () {
|
|
||||||
expect(
|
|
||||||
stringToUTF16String("hello world", /* bigEndian = */ true)
|
|
||||||
).toEqual("\xfe\xff\0h\0e\0l\0l\0o\0 \0w\0o\0r\0l\0d");
|
|
||||||
|
|
||||||
expect(
|
|
||||||
stringToUTF16String("こんにちは世界の", /* bigEndian = */ true)
|
|
||||||
).toEqual(
|
|
||||||
"\xfe\xff\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("stringToPDFString", function () {
|
|
||||||
it("handles ISO Latin 1 strings", function () {
|
|
||||||
const str = "\x8Dstring\x8E";
|
|
||||||
expect(stringToPDFString(str)).toEqual("\u201Cstring\u201D");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles UTF-16 big-endian strings", function () {
|
|
||||||
const str = "\xFE\xFF\x00\x73\x00\x74\x00\x72\x00\x69\x00\x6E\x00\x67";
|
|
||||||
expect(stringToPDFString(str)).toEqual("string");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles incomplete UTF-16 big-endian strings", function () {
|
|
||||||
const str = "\xFE\xFF\x00\x73\x00\x74\x00\x72\x00\x69\x00\x6E\x00";
|
|
||||||
expect(stringToPDFString(str)).toEqual("strin");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles UTF-16 little-endian strings", function () {
|
|
||||||
const str = "\xFF\xFE\x73\x00\x74\x00\x72\x00\x69\x00\x6E\x00\x67\x00";
|
|
||||||
expect(stringToPDFString(str)).toEqual("string");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles incomplete UTF-16 little-endian strings", function () {
|
|
||||||
const str = "\xFF\xFE\x73\x00\x74\x00\x72\x00\x69\x00\x6E\x00\x67";
|
|
||||||
expect(stringToPDFString(str)).toEqual("strin");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles UTF-8 strings", function () {
|
|
||||||
const simpleStr = "\xEF\xBB\xBF\x73\x74\x72\x69\x6E\x67";
|
|
||||||
expect(stringToPDFString(simpleStr)).toEqual("string");
|
|
||||||
|
|
||||||
const complexStr =
|
|
||||||
"\xEF\xBB\xBF\xE8\xA1\xA8\xE3\x83\x9D\xE3\x81\x82\x41\xE9\xB7\x97" +
|
|
||||||
"\xC5\x92\xC3\xA9\xEF\xBC\xA2\xE9\x80\x8D\xC3\x9C\xC3\x9F\xC2\xAA" +
|
|
||||||
"\xC4\x85\xC3\xB1\xE4\xB8\x82\xE3\x90\x80\xF0\xA0\x80\x80";
|
|
||||||
expect(stringToPDFString(complexStr)).toEqual(
|
|
||||||
"表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles empty strings", function () {
|
|
||||||
// ISO Latin 1
|
|
||||||
const str1 = "";
|
|
||||||
expect(stringToPDFString(str1)).toEqual("");
|
|
||||||
|
|
||||||
// UTF-16BE
|
|
||||||
const str2 = "\xFE\xFF";
|
|
||||||
expect(stringToPDFString(str2)).toEqual("");
|
|
||||||
|
|
||||||
// UTF-16LE
|
|
||||||
const str3 = "\xFF\xFE";
|
|
||||||
expect(stringToPDFString(str3)).toEqual("");
|
|
||||||
|
|
||||||
// UTF-8
|
|
||||||
const str4 = "\xEF\xBB\xBF";
|
|
||||||
expect(stringToPDFString(str4)).toEqual("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles strings with language code", function () {
|
|
||||||
// ISO Latin 1
|
|
||||||
const str1 = "hello \x1benUS\x1bworld";
|
|
||||||
expect(stringToPDFString(str1)).toEqual("hello world");
|
|
||||||
|
|
||||||
// UTF-16BE
|
|
||||||
const str2 =
|
|
||||||
"\xFE\xFF\x00h\x00e\x00l\x00l\x00o\x00 \x00\x1b\x00e\x00n\x00U\x00S\x00\x1b\x00w\x00o\x00r\x00l\x00d";
|
|
||||||
expect(stringToPDFString(str2)).toEqual("hello world");
|
|
||||||
|
|
||||||
// UTF-16LE
|
|
||||||
const str3 =
|
|
||||||
"\xFF\xFEh\x00e\x00l\x00l\x00o\x00 \x00\x1b\x00e\x00n\x00U\x00S\x00\x1b\x00w\x00o\x00r\x00l\x00d\x00";
|
|
||||||
expect(stringToPDFString(str3)).toEqual("hello world");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -19,6 +19,7 @@ import {
|
|||||||
createValidAbsoluteUrl,
|
createValidAbsoluteUrl,
|
||||||
getUuid,
|
getUuid,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
|
stringToPDFString,
|
||||||
} from "../../src/shared/util.js";
|
} from "../../src/shared/util.js";
|
||||||
|
|
||||||
describe("util", function () {
|
describe("util", function () {
|
||||||
@ -82,6 +83,80 @@ describe("util", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("stringToPDFString", function () {
|
||||||
|
it("handles ISO Latin 1 strings", function () {
|
||||||
|
const str = "\x8Dstring\x8E";
|
||||||
|
expect(stringToPDFString(str)).toEqual("\u201Cstring\u201D");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles UTF-16 big-endian strings", function () {
|
||||||
|
const str = "\xFE\xFF\x00\x73\x00\x74\x00\x72\x00\x69\x00\x6E\x00\x67";
|
||||||
|
expect(stringToPDFString(str)).toEqual("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles incomplete UTF-16 big-endian strings", function () {
|
||||||
|
const str = "\xFE\xFF\x00\x73\x00\x74\x00\x72\x00\x69\x00\x6E\x00";
|
||||||
|
expect(stringToPDFString(str)).toEqual("strin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles UTF-16 little-endian strings", function () {
|
||||||
|
const str = "\xFF\xFE\x73\x00\x74\x00\x72\x00\x69\x00\x6E\x00\x67\x00";
|
||||||
|
expect(stringToPDFString(str)).toEqual("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles incomplete UTF-16 little-endian strings", function () {
|
||||||
|
const str = "\xFF\xFE\x73\x00\x74\x00\x72\x00\x69\x00\x6E\x00\x67";
|
||||||
|
expect(stringToPDFString(str)).toEqual("strin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles UTF-8 strings", function () {
|
||||||
|
const simpleStr = "\xEF\xBB\xBF\x73\x74\x72\x69\x6E\x67";
|
||||||
|
expect(stringToPDFString(simpleStr)).toEqual("string");
|
||||||
|
|
||||||
|
const complexStr =
|
||||||
|
"\xEF\xBB\xBF\xE8\xA1\xA8\xE3\x83\x9D\xE3\x81\x82\x41\xE9\xB7\x97" +
|
||||||
|
"\xC5\x92\xC3\xA9\xEF\xBC\xA2\xE9\x80\x8D\xC3\x9C\xC3\x9F\xC2\xAA" +
|
||||||
|
"\xC4\x85\xC3\xB1\xE4\xB8\x82\xE3\x90\x80\xF0\xA0\x80\x80";
|
||||||
|
expect(stringToPDFString(complexStr)).toEqual(
|
||||||
|
"表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles empty strings", function () {
|
||||||
|
// ISO Latin 1
|
||||||
|
const str1 = "";
|
||||||
|
expect(stringToPDFString(str1)).toEqual("");
|
||||||
|
|
||||||
|
// UTF-16BE
|
||||||
|
const str2 = "\xFE\xFF";
|
||||||
|
expect(stringToPDFString(str2)).toEqual("");
|
||||||
|
|
||||||
|
// UTF-16LE
|
||||||
|
const str3 = "\xFF\xFE";
|
||||||
|
expect(stringToPDFString(str3)).toEqual("");
|
||||||
|
|
||||||
|
// UTF-8
|
||||||
|
const str4 = "\xEF\xBB\xBF";
|
||||||
|
expect(stringToPDFString(str4)).toEqual("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles strings with language code", function () {
|
||||||
|
// ISO Latin 1
|
||||||
|
const str1 = "hello \x1benUS\x1bworld";
|
||||||
|
expect(stringToPDFString(str1)).toEqual("hello world");
|
||||||
|
|
||||||
|
// UTF-16BE
|
||||||
|
const str2 =
|
||||||
|
"\xFE\xFF\x00h\x00e\x00l\x00l\x00o\x00 \x00\x1b\x00e\x00n\x00U\x00S\x00\x1b\x00w\x00o\x00r\x00l\x00d";
|
||||||
|
expect(stringToPDFString(str2)).toEqual("hello world");
|
||||||
|
|
||||||
|
// UTF-16LE
|
||||||
|
const str3 =
|
||||||
|
"\xFF\xFEh\x00e\x00l\x00l\x00o\x00 \x00\x1b\x00e\x00n\x00U\x00S\x00\x1b\x00w\x00o\x00r\x00l\x00d\x00";
|
||||||
|
expect(stringToPDFString(str3)).toEqual("hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("createValidAbsoluteUrl", function () {
|
describe("createValidAbsoluteUrl", function () {
|
||||||
it("handles invalid URLs", function () {
|
it("handles invalid URLs", function () {
|
||||||
expect(createValidAbsoluteUrl(undefined, undefined)).toEqual(null);
|
expect(createValidAbsoluteUrl(undefined, undefined)).toEqual(null);
|
||||||
|
|||||||
@ -285,7 +285,6 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
padding: var(--editor-toolbar-padding);
|
padding: var(--editor-toolbar-padding);
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
|
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
|||||||
@ -376,7 +376,6 @@ const PDFViewerApplication = {
|
|||||||
enableNewBadge: x => x === "true",
|
enableNewBadge: x => x === "true",
|
||||||
enablePermissions: x => x === "true",
|
enablePermissions: x => x === "true",
|
||||||
enableMerge: x => x === "true",
|
enableMerge: x => x === "true",
|
||||||
enableSelectionRendering: x => x === "true",
|
|
||||||
enableSplitMerge: x => x === "true",
|
enableSplitMerge: x => x === "true",
|
||||||
enableUpdatedAddImage: x => x === "true",
|
enableUpdatedAddImage: x => x === "true",
|
||||||
highlightEditorColors: x => x,
|
highlightEditorColors: x => x,
|
||||||
@ -579,7 +578,6 @@ const PDFViewerApplication = {
|
|||||||
enableOptimizedPartialRendering: AppOptions.get(
|
enableOptimizedPartialRendering: AppOptions.get(
|
||||||
"enableOptimizedPartialRendering"
|
"enableOptimizedPartialRendering"
|
||||||
),
|
),
|
||||||
enableSelectionRendering: AppOptions.get("enableSelectionRendering"),
|
|
||||||
imagesRightClickMinSize: AppOptions.get("imagesRightClickMinSize"),
|
imagesRightClickMinSize: AppOptions.get("imagesRightClickMinSize"),
|
||||||
pageColors,
|
pageColors,
|
||||||
mlManager,
|
mlManager,
|
||||||
|
|||||||
@ -320,11 +320,6 @@ const defaultOptions = {
|
|||||||
: "./images/",
|
: "./images/",
|
||||||
kind: OptionKind.VIEWER,
|
kind: OptionKind.VIEWER,
|
||||||
},
|
},
|
||||||
enableSelectionRendering: {
|
|
||||||
/** @type {boolean} */
|
|
||||||
value: true,
|
|
||||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
|
||||||
},
|
|
||||||
imagesRightClickMinSize: {
|
imagesRightClickMinSize: {
|
||||||
/** @type {number} */
|
/** @type {number} */
|
||||||
value:
|
value:
|
||||||
|
|||||||
@ -36,8 +36,6 @@ class BasePDFPageView extends RenderableView {
|
|||||||
|
|
||||||
enableOptimizedPartialRendering = false;
|
enableOptimizedPartialRendering = false;
|
||||||
|
|
||||||
enableSelectionRendering = true;
|
|
||||||
|
|
||||||
imagesRightClickMinSize = -1;
|
imagesRightClickMinSize = -1;
|
||||||
|
|
||||||
eventBus = null;
|
eventBus = null;
|
||||||
@ -60,7 +58,6 @@ class BasePDFPageView extends RenderableView {
|
|||||||
this.renderingQueue = options.renderingQueue;
|
this.renderingQueue = options.renderingQueue;
|
||||||
this.enableOptimizedPartialRendering =
|
this.enableOptimizedPartialRendering =
|
||||||
options.enableOptimizedPartialRendering ?? false;
|
options.enableOptimizedPartialRendering ?? false;
|
||||||
this.enableSelectionRendering = options.enableSelectionRendering !== false;
|
|
||||||
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
|
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
|
||||||
this.minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
this.minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,16 +14,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.canvasWrapper {
|
.canvasWrapper {
|
||||||
.selection {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
background: rgb(0 90 255 / 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
transform: none;
|
transform: none;
|
||||||
|
|
||||||
|
|||||||
@ -15,19 +15,6 @@
|
|||||||
|
|
||||||
import { DrawLayer } from "pdfjs-lib";
|
import { DrawLayer } from "pdfjs-lib";
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef DrawLayerBuilderOptions
|
|
||||||
* Configuration for {@linkcode DrawLayerBuilder}.
|
|
||||||
* @property {number} pageIndex
|
|
||||||
* Zero-based page index.
|
|
||||||
* @property {Element | null} [textLayer]
|
|
||||||
* Text layer element (optional).
|
|
||||||
* @property {Object | null} [filterFactory]
|
|
||||||
* Filter factory used to style selections (optional).
|
|
||||||
* @property {Object | null} [pageColors]
|
|
||||||
* Page foreground/background colors for HCM (optional).
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} DrawLayerBuilderRenderOptions
|
* @typedef {Object} DrawLayerBuilderRenderOptions
|
||||||
* @property {string} [intent] - The default value is "display".
|
* @property {string} [intent] - The default value is "display".
|
||||||
@ -36,19 +23,6 @@ import { DrawLayer } from "pdfjs-lib";
|
|||||||
class DrawLayerBuilder {
|
class DrawLayerBuilder {
|
||||||
#drawLayer = null;
|
#drawLayer = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {DrawLayerBuilderOptions} options
|
|
||||||
* Configuration.
|
|
||||||
* @returns
|
|
||||||
* Instance.
|
|
||||||
*/
|
|
||||||
constructor(options) {
|
|
||||||
this.pageIndex = options.pageIndex;
|
|
||||||
this.textLayer = options.textLayer || null;
|
|
||||||
this.filterFactory = options.filterFactory || null;
|
|
||||||
this.pageColors = options.pageColors || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DrawLayerBuilderRenderOptions} options
|
* @param {DrawLayerBuilderRenderOptions} options
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
@ -57,12 +31,7 @@ class DrawLayerBuilder {
|
|||||||
if (intent !== "display" || this.#drawLayer || this._cancelled) {
|
if (intent !== "display" || this.#drawLayer || this._cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#drawLayer = new DrawLayer({
|
this.#drawLayer = new DrawLayer();
|
||||||
pageIndex: this.pageIndex,
|
|
||||||
textLayer: this.textLayer,
|
|
||||||
filterFactory: this.filterFactory,
|
|
||||||
pageColors: this.pageColors,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
|
|||||||
@ -39,19 +39,6 @@ class Preferences extends BasePreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ExternalServices extends BaseExternalServices {
|
class ExternalServices extends BaseExternalServices {
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
|
||||||
// For testing purposes.
|
|
||||||
Object.defineProperty(this, "reportText", {
|
|
||||||
value: data => {
|
|
||||||
window._reportTextData = data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createL10n() {
|
async createL10n() {
|
||||||
return new GenericL10n(AppOptions.get("localeProperties")?.lang);
|
return new GenericL10n(AppOptions.get("localeProperties")?.lang);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,9 +111,6 @@ class PDFDocumentProperties {
|
|||||||
this.#updateUI();
|
this.#updateUI();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
|
||||||
this._fieldDataLastUpdated = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the document properties.
|
// Get the document properties.
|
||||||
const [
|
const [
|
||||||
@ -121,13 +118,7 @@ class PDFDocumentProperties {
|
|||||||
pdfPage,
|
pdfPage,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.pdfDocument.getMetadata(),
|
this.pdfDocument.getMetadata(),
|
||||||
this.pdfDocument.getPage(currentPageNumber).catch(reason => {
|
this.pdfDocument.getPage(currentPageNumber),
|
||||||
console.error(
|
|
||||||
`PDFDocumentProperties - unable to get page ${currentPageNumber}.`,
|
|
||||||
reason
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@ -144,7 +135,7 @@ class PDFDocumentProperties {
|
|||||||
this._titleLookup(),
|
this._titleLookup(),
|
||||||
this.#parseDate(metadata?.get("xmp:createdate"), info.CreationDate),
|
this.#parseDate(metadata?.get("xmp:createdate"), info.CreationDate),
|
||||||
this.#parseDate(metadata?.get("xmp:modifydate"), info.ModDate),
|
this.#parseDate(metadata?.get("xmp:modifydate"), info.ModDate),
|
||||||
this.#parsePageSize(pdfPage, pagesRotation),
|
this.#parsePageSize(getPageSizeInches(pdfPage), pagesRotation),
|
||||||
this.#parseLinearization(info.IsLinearized),
|
this.#parseLinearization(info.IsLinearized),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -229,9 +220,6 @@ class PDFDocumentProperties {
|
|||||||
// since it will be updated the next time `this.open` is called.
|
// since it will be updated the next time `this.open` is called.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
|
||||||
this.dialog.dataset.fieldDataLastUpdated = this._fieldDataLastUpdated;
|
|
||||||
}
|
|
||||||
for (const id in this.fields) {
|
for (const id in this.fields) {
|
||||||
const content = this.#fieldData?.[id];
|
const content = this.#fieldData?.[id];
|
||||||
this.fields[id].textContent = content || content === 0 ? content : "-";
|
this.fields[id].textContent = content || content === 0 ? content : "-";
|
||||||
@ -251,11 +239,10 @@ class PDFDocumentProperties {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #parsePageSize(pdfPage, pagesRotation) {
|
async #parsePageSize(pageSizeInches, pagesRotation) {
|
||||||
if (!pdfPage) {
|
if (!pageSizeInches) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let pageSizeInches = getPageSizeInches(pdfPage);
|
|
||||||
// Take the viewer rotation into account as well; compare with Adobe Reader.
|
// Take the viewer rotation into account as well; compare with Adobe Reader.
|
||||||
if (pagesRotation % 180 !== 0) {
|
if (pagesRotation % 180 !== 0) {
|
||||||
pageSizeInches = {
|
pageSizeInches = {
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
||||||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||||
@ -93,9 +93,6 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
|||||||
* @property {number} [imagesRightClickMinSize] - All images whose width and
|
* @property {number} [imagesRightClickMinSize] - All images whose width and
|
||||||
* height are at least this value (in pixels) will be lazily inserted in the
|
* height are at least this value (in pixels) will be lazily inserted in the
|
||||||
* dom to allow right-clicking and saving them. Use `-1` to disable this.
|
* dom to allow right-clicking and saving them. Use `-1` to disable this.
|
||||||
* @property {boolean} [enableSelectionRendering] - When enabled, renders text
|
|
||||||
* selections in the draw layer.
|
|
||||||
* The default value is `true`.
|
|
||||||
* @property {boolean} [enableOptimizedPartialRendering] - When enabled, PDF
|
* @property {boolean} [enableOptimizedPartialRendering] - When enabled, PDF
|
||||||
* rendering will keep track of which areas of the page each PDF operation
|
* rendering will keep track of which areas of the page each PDF operation
|
||||||
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
* affects. Then, when rendering a partial page (if `enableDetailCanvas` is
|
||||||
@ -1194,20 +1191,14 @@ class PDFPageView extends BasePDFPageView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.drawLayer ||= new DrawLayerBuilder({
|
|
||||||
pageIndex: this.id,
|
|
||||||
textLayer: this.enableSelectionRendering ? this.textLayer?.div : null,
|
|
||||||
filterFactory: this.pdfPage?.filterFactory,
|
|
||||||
pageColors: this.pageColors,
|
|
||||||
});
|
|
||||||
await this.#renderDrawLayer();
|
|
||||||
this.drawLayer.setParent(canvasWrapper);
|
|
||||||
|
|
||||||
const { annotationEditorUIManager } = this.#layerProperties;
|
const { annotationEditorUIManager } = this.#layerProperties;
|
||||||
|
|
||||||
if (!annotationEditorUIManager) {
|
if (!annotationEditorUIManager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.drawLayer ||= new DrawLayerBuilder();
|
||||||
|
await this.#renderDrawLayer();
|
||||||
|
this.drawLayer.setParent(canvasWrapper);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.annotationLayer ||
|
this.annotationLayer ||
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
|
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
|
||||||
|
|||||||
@ -94,10 +94,6 @@ class PDFThumbnailViewer {
|
|||||||
|
|
||||||
#dragAC = null;
|
#dragAC = null;
|
||||||
|
|
||||||
#abortSignal = undefined;
|
|
||||||
|
|
||||||
#externalDragActive = false;
|
|
||||||
|
|
||||||
#draggedContainer = null;
|
#draggedContainer = null;
|
||||||
|
|
||||||
#thumbnailsPositions = null;
|
#thumbnailsPositions = null;
|
||||||
@ -205,7 +201,6 @@ class PDFThumbnailViewer {
|
|||||||
this.maxCanvasPixels = maxCanvasPixels;
|
this.maxCanvasPixels = maxCanvasPixels;
|
||||||
this.maxCanvasDim = maxCanvasDim;
|
this.maxCanvasDim = maxCanvasDim;
|
||||||
this.pageColors = pageColors || null;
|
this.pageColors = pageColors || null;
|
||||||
this.#abortSignal = abortSignal;
|
|
||||||
this.#enableMerge = enableMerge || false;
|
this.#enableMerge = enableMerge || false;
|
||||||
this.#enableSplitMerge = enableSplitMerge || false;
|
this.#enableSplitMerge = enableSplitMerge || false;
|
||||||
this.#statusLabel = statusBar?.viewsManagerStatusActionLabel || null;
|
this.#statusLabel = statusBar?.viewsManagerStatusActionLabel || null;
|
||||||
@ -320,11 +315,62 @@ class PDFThumbnailViewer {
|
|||||||
|
|
||||||
if (this.#enableMerge && addFileComponent) {
|
if (this.#enableMerge && addFileComponent) {
|
||||||
const { picker, button } = addFileComponent;
|
const { picker, button } = addFileComponent;
|
||||||
picker.addEventListener("change", () => {
|
picker.addEventListener("change", async () => {
|
||||||
const file = picker.files?.[0];
|
const file = picker.files?.[0];
|
||||||
if (file) {
|
if (!file) {
|
||||||
this.#mergeFile(file, this._currentPageNumber - 1);
|
return;
|
||||||
}
|
}
|
||||||
|
if (file.type !== "application/pdf") {
|
||||||
|
const magic = await file.slice(0, 5).text();
|
||||||
|
if (magic !== "%PDF-") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#toggleBar("waiting", "pdfjs-views-manager-waiting-for-file");
|
||||||
|
const currentPageIndex = this._currentPageNumber - 1;
|
||||||
|
const buffer = await file.bytes();
|
||||||
|
const pagesCount = this.#pagesMapper.pagesNumber;
|
||||||
|
const data = this.hasStructuralChanges()
|
||||||
|
? this.getStructuralChanges()
|
||||||
|
: [{ document: null }];
|
||||||
|
data.push({
|
||||||
|
document: buffer,
|
||||||
|
insertAfter: currentPageIndex ?? -1,
|
||||||
|
});
|
||||||
|
this.eventBus._on(
|
||||||
|
"pagesloaded",
|
||||||
|
() => {
|
||||||
|
// Clear any pre-merge selection: thumbnails are rebuilt fresh
|
||||||
|
// (all unchecked), so the old set would cause a label/visual
|
||||||
|
// mismatch.
|
||||||
|
this.#selectedPages = null;
|
||||||
|
this.#updateMenuEntries();
|
||||||
|
this.#toggleBar("status");
|
||||||
|
const newPagesCount = this.#pagesMapper.pagesNumber;
|
||||||
|
const insertedPagesCount = newPagesCount - pagesCount;
|
||||||
|
for (
|
||||||
|
let i = currentPageIndex + 1,
|
||||||
|
ii = currentPageIndex + 1 + insertedPagesCount;
|
||||||
|
i < ii;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
this._thumbnails[i].checkbox.checked = true;
|
||||||
|
this.#selectPage(i + 1, true);
|
||||||
|
}
|
||||||
|
if (insertedPagesCount) {
|
||||||
|
this.#updateCurrentPage(
|
||||||
|
currentPageIndex + 2,
|
||||||
|
/* force = */ true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
this.#reportTelemetry({ action: "merge" });
|
||||||
|
this.eventBus.dispatch("saveandload", {
|
||||||
|
source: this,
|
||||||
|
data,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
picker.click();
|
picker.click();
|
||||||
@ -351,55 +397,6 @@ class PDFThumbnailViewer {
|
|||||||
this.renderingQueue.renderHighestPriority();
|
this.renderingQueue.renderHighestPriority();
|
||||||
}
|
}
|
||||||
|
|
||||||
async #mergeFile(file, insertAfter) {
|
|
||||||
if (file.type !== "application/pdf") {
|
|
||||||
const magic = await file.slice(0, 5).text();
|
|
||||||
if (magic !== "%PDF-") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.#toggleBar("waiting", "pdfjs-views-manager-waiting-for-file");
|
|
||||||
const buffer = await file.bytes();
|
|
||||||
const pagesCount = this.#pagesMapper.pagesNumber;
|
|
||||||
const data = this.hasStructuralChanges()
|
|
||||||
? this.getStructuralChanges()
|
|
||||||
: [{ document: null }];
|
|
||||||
data.push({
|
|
||||||
document: buffer,
|
|
||||||
insertAfter,
|
|
||||||
});
|
|
||||||
this.eventBus._on(
|
|
||||||
"pagesloaded",
|
|
||||||
() => {
|
|
||||||
// Clear any pre-merge selection: thumbnails are rebuilt fresh
|
|
||||||
// (all unchecked), so the old set would cause a label/visual
|
|
||||||
// mismatch.
|
|
||||||
this.#selectedPages = null;
|
|
||||||
this.#updateMenuEntries();
|
|
||||||
this.#toggleBar("status");
|
|
||||||
const newPagesCount = this.#pagesMapper.pagesNumber;
|
|
||||||
const insertedPagesCount = newPagesCount - pagesCount;
|
|
||||||
for (
|
|
||||||
let i = insertAfter + 1, ii = insertAfter + 1 + insertedPagesCount;
|
|
||||||
i < ii;
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
this._thumbnails[i].checkbox.checked = true;
|
|
||||||
this.#selectPage(i + 1, true);
|
|
||||||
}
|
|
||||||
if (insertedPagesCount) {
|
|
||||||
this.#updateCurrentPage(insertAfter + 2, /* force = */ true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
this.#reportTelemetry({ action: "merge" });
|
|
||||||
this.eventBus.dispatch("saveandload", {
|
|
||||||
source: this,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getThumbnail(index) {
|
getThumbnail(index) {
|
||||||
return this._thumbnails[index];
|
return this._thumbnails[index];
|
||||||
}
|
}
|
||||||
@ -1188,10 +1185,6 @@ class PDFThumbnailViewer {
|
|||||||
this.#draggedImageX + this.#draggedImageWidth / 2,
|
this.#draggedImageX + this.#draggedImageWidth / 2,
|
||||||
this.#draggedImageY + this.#draggedImageHeight / 2
|
this.#draggedImageY + this.#draggedImageHeight / 2
|
||||||
);
|
);
|
||||||
this.#positionDragMarker(positionData);
|
|
||||||
}
|
|
||||||
|
|
||||||
#positionDragMarker(positionData) {
|
|
||||||
if (!positionData) {
|
if (!positionData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1209,7 +1202,7 @@ class PDFThumbnailViewer {
|
|||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
if (xPos.length === 1) {
|
if (xPos.length === 1) {
|
||||||
y = bbox[1] - SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT;
|
y = bbox[1] - SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT;
|
||||||
x = bbox[0];
|
x = bbox[4];
|
||||||
width = bbox[2];
|
width = bbox[2];
|
||||||
} else {
|
} else {
|
||||||
y = bbox[1];
|
y = bbox[1];
|
||||||
@ -1279,22 +1272,16 @@ class PDFThumbnailViewer {
|
|||||||
lastRightX ??= cx + w;
|
lastRightX ??= cx + w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let space;
|
const space =
|
||||||
if (positionsX.length > 1) {
|
positionsX.length > 1
|
||||||
space = (positionsX[1] - firstRightX) / 2;
|
? (positionsX[1] - firstRightX) / 2
|
||||||
} else if (positionsY.length > 1) {
|
: (positionsY[1] - firstBottomY) / 2;
|
||||||
space = (positionsY[1] - firstBottomY) / 2;
|
|
||||||
} else {
|
|
||||||
space = SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT;
|
|
||||||
}
|
|
||||||
this.#thumbnailsPositions = {
|
this.#thumbnailsPositions = {
|
||||||
x: positionsX,
|
x: positionsX,
|
||||||
y: positionsY,
|
y: positionsY,
|
||||||
lastX: positionsLastX,
|
lastX: positionsLastX,
|
||||||
space,
|
space,
|
||||||
lastSpace: positionsLastX.length
|
lastSpace: (positionsLastX.at(-1) - lastRightX) / 2,
|
||||||
? (positionsLastX.at(-1) - lastRightX) / 2
|
|
||||||
: space,
|
|
||||||
bbox,
|
bbox,
|
||||||
};
|
};
|
||||||
this.#isOneColumnView = positionsX.length === 1;
|
this.#isOneColumnView = positionsX.length === 1;
|
||||||
@ -1393,7 +1380,6 @@ class PDFThumbnailViewer {
|
|||||||
this.#goToPage(e);
|
this.#goToPage(e);
|
||||||
});
|
});
|
||||||
this.#addDragListeners();
|
this.#addDragListeners();
|
||||||
this.#addExternalFileDropListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#selectPage(pageNumber, checked) {
|
#selectPage(pageNumber, checked) {
|
||||||
@ -1564,140 +1550,6 @@ class PDFThumbnailViewer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#addExternalFileDropListeners() {
|
|
||||||
if (!this.#enableMerge) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const container = this.container;
|
|
||||||
const signal = this.#abortSignal;
|
|
||||||
|
|
||||||
const hasPdfItem = dataTransfer => {
|
|
||||||
if (!dataTransfer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// The file's bytes aren't readable during dragover, so the MIME type is
|
|
||||||
// the only available signal. Matches the existing global drop handler
|
|
||||||
// in app.js. Files with no MIME (e.g. some macOS sources) are rejected
|
|
||||||
// here to keep the "copy" cursor honest; if needed, drop-time magic-byte
|
|
||||||
// validation in #mergeFile would still catch a permissive variant.
|
|
||||||
for (const item of dataTransfer.items) {
|
|
||||||
if (item.kind === "file" && item.type === "application/pdf") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
const pointerInContainer = ({ clientX, clientY }) => {
|
|
||||||
const { left, right, top, bottom } = container.getBoundingClientRect();
|
|
||||||
return (
|
|
||||||
clientX >= left && clientX < right && clientY >= top && clientY < bottom
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
container.addEventListener(
|
|
||||||
"dragenter",
|
|
||||||
e => {
|
|
||||||
if (
|
|
||||||
this.#externalDragActive ||
|
|
||||||
// A page-move drag is already in progress.
|
|
||||||
!isNaN(this.#lastDraggedOverIndex) ||
|
|
||||||
!this._thumbnails.length ||
|
|
||||||
!hasPdfItem(e.dataTransfer)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
e.dataTransfer.dropEffect = "copy";
|
|
||||||
this.#externalDragActive = true;
|
|
||||||
this.container.classList.add("isDraggingFile");
|
|
||||||
// Recompute positions in case the layout changed since last time.
|
|
||||||
this.#thumbnailsPositions = null;
|
|
||||||
this.#computeThumbnailsPosition();
|
|
||||||
// Marker hasn't been positioned yet — first dragover will do it.
|
|
||||||
this.#lastDraggedOverIndex = NaN;
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
);
|
|
||||||
|
|
||||||
container.addEventListener(
|
|
||||||
"dragover",
|
|
||||||
e => {
|
|
||||||
if (!this.#externalDragActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
e.dataTransfer.dropEffect = "copy";
|
|
||||||
if (!this.#thumbnailsPositions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rect = container.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
const positionData = this.#findClosestThumbnail(x, y);
|
|
||||||
this.#positionDragMarker(positionData);
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
);
|
|
||||||
|
|
||||||
container.addEventListener(
|
|
||||||
"dragleave",
|
|
||||||
e => {
|
|
||||||
if (!this.#externalDragActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// dragleave fires when crossing into a child element too; only treat
|
|
||||||
// it as a true leave when the cursor has actually left the container.
|
|
||||||
if (
|
|
||||||
(e.relatedTarget && container.contains(e.relatedTarget)) ||
|
|
||||||
pointerInContainer(e)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.#endExternalFileDrag();
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
);
|
|
||||||
|
|
||||||
container.addEventListener(
|
|
||||||
"drop",
|
|
||||||
e => {
|
|
||||||
if (!this.#externalDragActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const file = e.dataTransfer.files?.[0];
|
|
||||||
// If no dragover ever ran (e.g. instant drop), compute the index from
|
|
||||||
// the drop event itself so we don't fall through to a stale fallback.
|
|
||||||
if (isNaN(this.#lastDraggedOverIndex) && this.#thumbnailsPositions) {
|
|
||||||
const rect = container.getBoundingClientRect();
|
|
||||||
this.#findClosestThumbnail(
|
|
||||||
e.clientX - rect.left,
|
|
||||||
e.clientY - rect.top
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const insertAfter = isNaN(this.#lastDraggedOverIndex)
|
|
||||||
? -1
|
|
||||||
: this.#lastDraggedOverIndex;
|
|
||||||
this.#endExternalFileDrag();
|
|
||||||
if (file) {
|
|
||||||
this.#mergeFile(file, insertAfter);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ signal }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endExternalFileDrag() {
|
|
||||||
this.#externalDragActive = false;
|
|
||||||
this.container.classList.remove("isDraggingFile");
|
|
||||||
this.#dragMarker?.remove();
|
|
||||||
this.#dragMarker = null;
|
|
||||||
this.#lastDraggedOverIndex = NaN;
|
|
||||||
}
|
|
||||||
|
|
||||||
#goToPage(e) {
|
#goToPage(e) {
|
||||||
const container = e.target.closest(".thumbnailImageContainer");
|
const container = e.target.closest(".thumbnailImageContainer");
|
||||||
if (container) {
|
if (container) {
|
||||||
|
|||||||
@ -78,6 +78,16 @@
|
|||||||
transform: rotate(270deg) translateX(-100%);
|
transform: rotate(270deg) translateX(-100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#hiddenCopyElement,
|
||||||
|
.hiddenCanvasElement {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.pdfViewer {
|
.pdfViewer {
|
||||||
/* Define this variable here and not in :root to avoid to reflow all the UI
|
/* Define this variable here and not in :root to avoid to reflow all the UI
|
||||||
when scaling (see #15929). */
|
when scaling (see #15929). */
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
|
/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
|
||||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
||||||
/** @typedef {import("./event_utils").EventBus} EventBus */
|
/** @typedef {import("./event_utils").EventBus} EventBus */
|
||||||
@ -132,9 +132,6 @@ function isValidAnnotationEditorMode(mode) {
|
|||||||
* `maxCanvasDim`, it will draw a second canvas on top of the CSS-zoomed one,
|
* `maxCanvasDim`, it will draw a second canvas on top of the CSS-zoomed one,
|
||||||
* that only renders the part of the page that is close to the viewport.
|
* that only renders the part of the page that is close to the viewport.
|
||||||
* The default value is `true`.
|
* The default value is `true`.
|
||||||
* @property {boolean} [enableSelectionRendering] - Enables rendering of text
|
|
||||||
* selections in the draw layer.
|
|
||||||
* The default value is `true`.
|
|
||||||
* @property {number} [imagesRightClickMinSize] - All images whose width and
|
* @property {number} [imagesRightClickMinSize] - All images whose width and
|
||||||
* height are at least this value (in pixels) will be lazily inserted in the
|
* height are at least this value (in pixels) will be lazily inserted in the
|
||||||
* dom to allow right-clicking and saving them. Use `-1` to disable this.
|
* dom to allow right-clicking and saving them. Use `-1` to disable this.
|
||||||
@ -365,7 +362,6 @@ class PDFViewer {
|
|||||||
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||||
this.enableOptimizedPartialRendering =
|
this.enableOptimizedPartialRendering =
|
||||||
options.enableOptimizedPartialRendering ?? false;
|
options.enableOptimizedPartialRendering ?? false;
|
||||||
this.enableSelectionRendering = options.enableSelectionRendering !== false;
|
|
||||||
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
|
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
|
||||||
this.l10n = options.l10n;
|
this.l10n = options.l10n;
|
||||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||||
@ -461,25 +457,6 @@ class PDFViewer {
|
|||||||
return this._pages.every(pageView => pageView?.pdfPage);
|
return this._pages.every(pageView => pageView?.pdfPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear text selections within the viewer.
|
|
||||||
*/
|
|
||||||
clearSelection() {
|
|
||||||
const selection = document.getSelection();
|
|
||||||
if (!selection || selection.isCollapsed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0, ii = selection.rangeCount; i < ii; i++) {
|
|
||||||
if (selection.getRangeAt(i).intersectsNode(this.viewer)) {
|
|
||||||
// `empty()` is non-standard; `removeAllRanges()` is the standard API.
|
|
||||||
selection.removeAllRanges?.();
|
|
||||||
selection.empty?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
@ -511,9 +488,6 @@ class PDFViewer {
|
|||||||
if (!this.pdfDocument) {
|
if (!this.pdfDocument) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this._currentPageNumber !== val) {
|
|
||||||
this.clearSelection();
|
|
||||||
}
|
|
||||||
// The intent can be to just reset a scroll position and/or scale.
|
// The intent can be to just reset a scroll position and/or scale.
|
||||||
if (!this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true)) {
|
if (!this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true)) {
|
||||||
console.error(`currentPageNumber: "${val}" is not a valid page.`);
|
console.error(`currentPageNumber: "${val}" is not a valid page.`);
|
||||||
@ -573,9 +547,6 @@ class PDFViewer {
|
|||||||
page = i + 1;
|
page = i + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this._currentPageNumber !== page) {
|
|
||||||
this.clearSelection();
|
|
||||||
}
|
|
||||||
// The intent can be to just reset a scroll position and/or scale.
|
// The intent can be to just reset a scroll position and/or scale.
|
||||||
if (!this._setCurrentPageNumber(page, /* resetCurrentPageView = */ true)) {
|
if (!this._setCurrentPageNumber(page, /* resetCurrentPageView = */ true)) {
|
||||||
console.error(`currentPageLabel: "${val}" is not a valid page.`);
|
console.error(`currentPageLabel: "${val}" is not a valid page.`);
|
||||||
@ -646,7 +617,6 @@ class PDFViewer {
|
|||||||
if (this._pagesRotation === rotation) {
|
if (this._pagesRotation === rotation) {
|
||||||
return; // The rotation didn't change.
|
return; // The rotation didn't change.
|
||||||
}
|
}
|
||||||
this.clearSelection();
|
|
||||||
this._pagesRotation = rotation;
|
this._pagesRotation = rotation;
|
||||||
|
|
||||||
const pageNumber = this._currentPageNumber;
|
const pageNumber = this._currentPageNumber;
|
||||||
@ -997,8 +967,6 @@ class PDFViewer {
|
|||||||
const element = (this.#hiddenCopyElement =
|
const element = (this.#hiddenCopyElement =
|
||||||
document.createElement("div"));
|
document.createElement("div"));
|
||||||
element.id = "hiddenCopyElement";
|
element.id = "hiddenCopyElement";
|
||||||
element.style.cssText =
|
|
||||||
"position:absolute;top:0;left:0;width:0;height:0;display:none";
|
|
||||||
viewer.before(element);
|
viewer.before(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1096,7 +1064,6 @@ class PDFViewer {
|
|||||||
enableDetailCanvas: this.enableDetailCanvas,
|
enableDetailCanvas: this.enableDetailCanvas,
|
||||||
enableOptimizedPartialRendering:
|
enableOptimizedPartialRendering:
|
||||||
this.enableOptimizedPartialRendering,
|
this.enableOptimizedPartialRendering,
|
||||||
enableSelectionRendering: this.enableSelectionRendering,
|
|
||||||
imagesRightClickMinSize: this.imagesRightClickMinSize,
|
imagesRightClickMinSize: this.imagesRightClickMinSize,
|
||||||
pageColors,
|
pageColors,
|
||||||
l10n: this.l10n,
|
l10n: this.l10n,
|
||||||
@ -1519,7 +1486,6 @@ class PDFViewer {
|
|||||||
newValue,
|
newValue,
|
||||||
{ noScroll = false, preset = false, drawingDelay = -1, origin = null }
|
{ noScroll = false, preset = false, drawingDelay = -1, origin = null }
|
||||||
) {
|
) {
|
||||||
this.clearSelection();
|
|
||||||
this._currentScaleValue = newValue.toString();
|
this._currentScaleValue = newValue.toString();
|
||||||
|
|
||||||
if (this.#isSameScale(newScale)) {
|
if (this.#isSameScale(newScale)) {
|
||||||
@ -2242,7 +2208,6 @@ class PDFViewer {
|
|||||||
}
|
}
|
||||||
this._previousScrollMode = this._scrollMode;
|
this._previousScrollMode = this._scrollMode;
|
||||||
|
|
||||||
this.clearSelection();
|
|
||||||
this._scrollMode = mode;
|
this._scrollMode = mode;
|
||||||
this.eventBus.dispatch("scrollmodechanged", { source: this, mode });
|
this.eventBus.dispatch("scrollmodechanged", { source: this, mode });
|
||||||
|
|
||||||
@ -2308,7 +2273,6 @@ class PDFViewer {
|
|||||||
if (!isValidSpreadMode(mode)) {
|
if (!isValidSpreadMode(mode)) {
|
||||||
throw new Error(`Invalid spread mode: ${mode}`);
|
throw new Error(`Invalid spread mode: ${mode}`);
|
||||||
}
|
}
|
||||||
this.clearSelection();
|
|
||||||
this._spreadMode = mode;
|
this._spreadMode = mode;
|
||||||
this.eventBus.dispatch("spreadmodechanged", { source: this, mode });
|
this.eventBus.dispatch("spreadmodechanged", { source: this, mode });
|
||||||
|
|
||||||
|
|||||||
@ -38,6 +38,7 @@ const {
|
|||||||
getRGB,
|
getRGB,
|
||||||
getRGBA,
|
getRGBA,
|
||||||
getUuid,
|
getUuid,
|
||||||
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
ImageKind,
|
ImageKind,
|
||||||
InvalidPDFException,
|
InvalidPDFException,
|
||||||
@ -101,6 +102,7 @@ export {
|
|||||||
getRGB,
|
getRGB,
|
||||||
getRGBA,
|
getRGBA,
|
||||||
getUuid,
|
getUuid,
|
||||||
|
getXfaPageViewport,
|
||||||
GlobalWorkerOptions,
|
GlobalWorkerOptions,
|
||||||
ImageKind,
|
ImageKind,
|
||||||
InvalidPDFException,
|
InvalidPDFException,
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PixelsPerInch, XfaLayer } from "pdfjs-lib";
|
import { getXfaPageViewport, PixelsPerInch } from "pdfjs-lib";
|
||||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||||
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ function getXfaHtmlForPrinting(printContainer, pdfDocument) {
|
|||||||
linkService,
|
linkService,
|
||||||
xfaHtml: xfaPage,
|
xfaHtml: xfaPage,
|
||||||
});
|
});
|
||||||
const viewport = XfaLayer.getPageViewport(xfaPage, { scale });
|
const viewport = getXfaPageViewport(xfaPage, { scale });
|
||||||
|
|
||||||
builder.render({ viewport, intent: "print" });
|
builder.render({ viewport, intent: "print" });
|
||||||
page.append(builder.div);
|
page.append(builder.div);
|
||||||
|
|||||||
@ -38,7 +38,6 @@
|
|||||||
white-space: pre;
|
white-space: pre;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
transform-origin: 0% 0%;
|
transform-origin: 0% 0%;
|
||||||
user-select: text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* We multiply the font size by --min-font-size, and then scale the text
|
/* We multiply the font size by --min-font-size, and then scale the text
|
||||||
@ -116,7 +115,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: transparent;
|
/* stylelint-disable declaration-block-no-duplicate-properties */
|
||||||
|
/*#if !MOZCENTRAL*/
|
||||||
|
background: rgba(0 0 255 / 0.25);
|
||||||
|
/*#endif*/
|
||||||
|
/* stylelint-enable declaration-block-no-duplicate-properties */
|
||||||
|
background: color-mix(in srgb, AccentColor, transparent 75%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
|
/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/text_layer_images.js").TextLayerImages} TextLayerImages */
|
/** @typedef {import("../src/display/text_layer_images.js").TextLayerImages} TextLayerImages */
|
||||||
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
|
||||||
|
|||||||
@ -26,20 +26,6 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
<title>PDF.js viewer</title>
|
<title>PDF.js viewer</title>
|
||||||
|
|
||||||
<!--
|
|
||||||
The print service injects an inline <style>@page { size: … }</style>
|
|
||||||
at print time (web/pdf_print_service.js, web/firefox_print_service.js)
|
|
||||||
to match the PDF's page dimensions. Since the size varies per PDF the
|
|
||||||
content can't be pre-hashed, so style-src-elem allows 'unsafe-inline'.
|
|
||||||
Inline style="…" attributes stay blocked via style-src (no fallback).
|
|
||||||
-->
|
|
||||||
<!--#if MOZCENTRAL-->
|
|
||||||
<!--<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="default-src 'none'; script-src resource: 'wasm-unsafe-eval'; worker-src resource:; style-src resource:; style-src-elem resource: 'unsafe-inline'; img-src resource: blob: data:; font-src resource:; connect-src resource:; base-uri 'none'; form-action 'none';"
|
|
||||||
/>-->
|
|
||||||
<!--#endif-->
|
|
||||||
|
|
||||||
<!--#if MOZCENTRAL-->
|
<!--#if MOZCENTRAL-->
|
||||||
<!--#include viewer-snippet-firefox-extension.html-->
|
<!--#include viewer-snippet-firefox-extension.html-->
|
||||||
<!--#endif-->
|
<!--#endif-->
|
||||||
|
|||||||
@ -4,7 +4,19 @@
|
|||||||
users with recognizing which checkbox they have to click when they
|
users with recognizing which checkbox they have to click when they
|
||||||
visit chrome://extensions.
|
visit chrome://extensions.
|
||||||
-->
|
-->
|
||||||
<p id="chrome-pdfjs-logo-bg">
|
<p
|
||||||
|
id="chrome-pdfjs-logo-bg"
|
||||||
|
style="
|
||||||
|
display: block;
|
||||||
|
padding-left: 60px;
|
||||||
|
min-height: 48px;
|
||||||
|
background-size: 48px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.8em;
|
||||||
|
word-break: break-all;
|
||||||
|
"
|
||||||
|
>
|
||||||
Click on "<span id="chrome-file-access-label">Allow access to file URLs</span>" at
|
Click on "<span id="chrome-file-access-label">Allow access to file URLs</span>" at
|
||||||
<a id="chrome-link-to-extensions-page">chrome://extensions</a>
|
<a id="chrome-link-to-extensions-page">chrome://extensions</a>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@ -713,25 +713,6 @@ dialog :link {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*#if !MOZCENTRAL*/
|
|
||||||
#printServiceDialog {
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
/*#endif*/
|
|
||||||
|
|
||||||
/*#if CHROME*/
|
|
||||||
#chrome-pdfjs-logo-bg {
|
|
||||||
display: block;
|
|
||||||
padding-left: 60px;
|
|
||||||
min-height: 48px;
|
|
||||||
background-size: 48px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.8em;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
/*#endif*/
|
|
||||||
|
|
||||||
.grab-to-pan-grab {
|
.grab-to-pan-grab {
|
||||||
cursor: grab !important;
|
cursor: grab !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,35 +29,6 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||||||
<!--#endif-->
|
<!--#endif-->
|
||||||
<title>PDF.js viewer</title>
|
<title>PDF.js viewer</title>
|
||||||
|
|
||||||
<!--
|
|
||||||
The print service injects an inline <style>@page { size: … }</style>
|
|
||||||
at print time (web/pdf_print_service.js, web/firefox_print_service.js)
|
|
||||||
to match the PDF's page dimensions. Since the size varies per PDF the
|
|
||||||
content can't be pre-hashed, so style-src-elem allows 'unsafe-inline'.
|
|
||||||
Inline style="…" attributes stay blocked via style-src (no fallback).
|
|
||||||
-->
|
|
||||||
<!--#if MOZCENTRAL-->
|
|
||||||
<!--<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="default-src 'none'; script-src resource: 'wasm-unsafe-eval'; worker-src resource:; style-src resource:; style-src-elem resource: 'unsafe-inline'; img-src resource: blob: data:; font-src resource:; connect-src resource:; base-uri 'none'; form-action 'none';"
|
|
||||||
/>-->
|
|
||||||
<!--#elif TESTING-->
|
|
||||||
<!--<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval'; worker-src 'self' blob:; style-src 'self'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self' data:; connect-src * blob: data:; base-uri 'self'; form-action 'none';"
|
|
||||||
/>-->
|
|
||||||
<!--#elif CHROME-->
|
|
||||||
<!--<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self' data:; connect-src * blob: data:; base-uri 'self'; form-action 'none';"
|
|
||||||
/>-->
|
|
||||||
<!--#else-->
|
|
||||||
<!--<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; style-src 'self'; style-src-elem 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self' data:; connect-src * blob: data:; base-uri 'none'; form-action 'none';"
|
|
||||||
/>-->
|
|
||||||
<!--#endif-->
|
|
||||||
|
|
||||||
<!--#if MOZCENTRAL-->
|
<!--#if MOZCENTRAL-->
|
||||||
<!--#include viewer-snippet-firefox-extension.html-->
|
<!--#include viewer-snippet-firefox-extension.html-->
|
||||||
<!--#elif CHROME-->
|
<!--#elif CHROME-->
|
||||||
@ -1266,7 +1237,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
|||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<!--#if !MOZCENTRAL-->
|
<!--#if !MOZCENTRAL-->
|
||||||
<dialog id="printServiceDialog">
|
<dialog id="printServiceDialog" style="min-width: 200px">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span data-l10n-id="pdfjs-print-progress-message"></span>
|
<span data-l10n-id="pdfjs-print-progress-message"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -632,15 +632,14 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.isDragging > .dragMarker,
|
> .dragMarker {
|
||||||
&.isDraggingFile > .dragMarker {
|
position: absolute;
|
||||||
position: absolute;
|
top: 0;
|
||||||
top: 0;
|
left: 0;
|
||||||
left: 0;
|
border: 2px solid var(--indicator-color);
|
||||||
border: 2px solid var(--indicator-color);
|
contain: strict;
|
||||||
contain: strict;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.pasteMode {
|
&.pasteMode {
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
|
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
/** @typedef {import("../src/display/page_viewport").PageViewport} PageViewport */
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
||||||
/** @typedef {import("./pdf_link_service.js").PDFLinkService} PDFLinkService */
|
/** @typedef {import("./pdf_link_service.js").PDFLinkService} PDFLinkService */
|
||||||
|
|
||||||
import { XfaLayer } from "pdfjs-lib";
|
import { XfaLayer } from "pdfjs-lib";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user