Compare commits

..

No commits in common. "25c7d9eaace0438316714aff7033dd5f4c1a542e" and "bf9ae7622f723596f2ab4de19b115f544bed8e33" have entirely different histories.

93 changed files with 817 additions and 4118 deletions

View File

@ -24,13 +24,13 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: ${{ matrix.language }}
queries: security-and-quality
- 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
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3

View File

@ -41,7 +41,6 @@ jobs:
skip: --noFirefox
runs-on: ${{ matrix.os }}
environment: code-coverage
steps:
- name: Checkout repository
@ -76,13 +75,13 @@ jobs:
if: ${{ matrix.os == 'windows-latest' }}
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' }}
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' }}
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
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@ -90,15 +89,3 @@ jobs:
path: test/pdfs/*.pdf
key: cached-pdf-files-${{ hashFiles('test/pdfs/*.pdf') }}
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

View File

@ -46,7 +46,7 @@ jobs:
- name: Generate app token
if: steps.check.outputs.has_added == 'true'
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
client-id: ${{ secrets.CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

View File

@ -17,7 +17,7 @@ jobs:
steps:
- name: Generate 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:
client-id: ${{ secrets.CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

View File

@ -1,3 +0,0 @@
flag_management:
default_rules:
carryforward: true

0
external/iccs/CGATS001Compat-v2-micro.icc vendored Normal file → Executable file
View File

View File

@ -113,7 +113,6 @@ const BABEL_PRESET_ENV_OPTS = Object.freeze({
const DEFINES = Object.freeze({
SKIP_BABEL: true,
WORKER_THREAD: false,
TESTING: undefined,
COVERAGE: undefined,
// The main build targets:
@ -537,12 +536,8 @@ function createSandboxBundle(defines, extraOptions = undefined) {
}
function createWorkerBundle(defines) {
const workerDefines = {
...defines,
WORKER_THREAD: true,
};
const workerFileConfig = createWebpackConfig(workerDefines, {
filename: workerDefines.MINIFIED ? "pdf.worker.min.mjs" : "pdf.worker.mjs",
const workerFileConfig = createWebpackConfig(defines, {
filename: defines.MINIFIED ? "pdf.worker.min.mjs" : "pdf.worker.mjs",
library: {
type: "module",
},
@ -1214,10 +1209,6 @@ function discardCommentsCSS() {
}
function preprocessHTML(source, defines) {
defines = {
...defines,
TESTING: defines.TESTING ?? process.env.TESTING === "true",
};
const outName = getTempFile("~preprocess", ".html");
preprocess(source, outName, defines);
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) {
console.log("\n### Linting JS/CSS/JSON/SVG/HTML files");
@ -2464,7 +2375,7 @@ gulp.task("lint", function (done) {
return;
}
gulp.series("lint-licenses", "lint-chmod")(done);
gulp.task("lint-licenses")(done);
});
});

View File

@ -661,58 +661,12 @@ pdfjs-views-manager-view-selector-button-label = Visninger
pdfjs-views-manager-pages-title = Sider
pdfjs-views-manager-attachments-title = Vedhæftede filer
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 =
.title = Indsæt før første side
# Variables:
# $page (Number) - the page number after which the paste button is.
pdfjs-views-manager-paste-button-after =
.title = Indsæt efter side { $page }
pdfjs-toggle-views-manager-button1 =
.title = Håndter sider
## Main menu for adding/removing signatures

View File

@ -661,8 +661,6 @@ pdfjs-views-manager-view-selector-button =
.title = Vistas
pdfjs-views-manager-view-selector-button-label = Vistas
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-layers-title1 = Capas
.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.
# It's spelled uppercase for English, but it can be translated as usual.
pdfjs-new-badge-content = NUEVO
pdfjs-views-manager-waiting-for-file = Subiendo el archivo…
pdfjs-toggle-views-manager-button1 =
.title = Administrar páginas

View File

@ -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-cut-button-label = Izrež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-done-button-label = Gotovo
pdfjs-views-manager-status-close-button =

View File

@ -488,8 +488,8 @@ pdfjs-editor-new-alt-text-error-close-button = Lat att
# Variables:
# $totalSize (Number) - the total 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)
.aria-valuetext = 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 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.
pdfjs-editor-new-alt-text-added-button =
.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.
# Variables:
# $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-delete-model-button = Slett
pdfjs-editor-alt-text-settings-download-model-button = Last ned

View File

@ -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.
# It's spelled uppercase for English, but it can be translated as usual.
pdfjs-new-badge-content = ਨਵਾਂ
pdfjs-views-manager-waiting-for-file = …ਫ਼ਾਇਲ ਨੂੰ ਅੱਪਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ
pdfjs-toggle-views-manager-button1 =
.title = ਸਫ਼ਿਆਂ ਦਾ ਇੰਤਜ਼ਾਮ

View File

@ -33,6 +33,7 @@ import {
OPS,
RenderingIntentFlag,
shadow,
stringToPDFString,
unreachable,
Util,
warn,
@ -52,6 +53,8 @@ import {
numberToString,
RESOURCES_KEYS_OPERATOR_LIST,
RESOURCES_KEYS_TEXT_CONTENT,
stringToAsciiOrUTF16BE,
stringToUTF16String,
} from "./core_utils.js";
import {
createDefaultAppearance,
@ -63,11 +66,6 @@ import {
import { DateFormats, TimeFormats } from "../shared/scripting_utils.js";
import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js";
import { Stream, StringStream } from "./stream.js";
import {
stringToAsciiOrUTF16BE,
stringToPDFString,
stringToUTF16String,
} from "./string_utils.js";
import { BaseStream } from "./base_stream.js";
import { bidi } from "./bidi.js";
import { Catalog } from "./catalog.js";

View File

@ -22,6 +22,7 @@ import {
objectSize,
PermissionFlag,
shadow,
stringToPDFString,
stringToUTF8String,
warn,
} from "../shared/util.js";
@ -52,7 +53,6 @@ import { clearGlobalCaches } from "./cleanup_helper.js";
import { ColorSpaceUtils } from "./colorspace_utils.js";
import { FileSpec } from "./file_spec.js";
import { MetadataParser } from "./metadata_parser.js";
import { stringToPDFString } from "./string_utils.js";
import { StructTreeRoot } from "./struct_tree.js";
const isRef = v => v instanceof Ref;

View File

@ -108,11 +108,6 @@ const CFFStandardStrings = [
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 = [
/* 0 */ null,
/* 1 */ { id: "hstem", min: 2, stackClearing: true, stem: true },
@ -267,16 +262,8 @@ class CFFParser {
properties.fontMatrix = fontMatrix;
}
let fontBBox = topDict.getByName("FontBBox");
if (fontBBox?.every(coord => coord === 0) && properties.bbox) {
fontBBox = Util.normalizeRect(
properties.bbox.map(coord =>
coord > 0x7fff && coord <= 0xffff ? coord - 0x10000 : coord
)
);
topDict.setByName("FontBBox", fontBBox);
}
if (fontBBox?.some(coord => coord !== 0)) {
const fontBBox = topDict.getByName("FontBBox");
if (fontBBox) {
// adjusting ascent/descent
properties.ascent = Math.max(fontBBox[3], fontBBox[1]);
properties.descent = Math.min(fontBBox[1], fontBBox[3]);
@ -798,28 +785,10 @@ class CFFParser {
);
parentDict.privateDict = privateDict;
const blueScale = privateDict.getByName("BlueScale");
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) {
if (privateDict.getByName("ExpansionFactor") === 0) {
// Firefox doesn't render correctly such a font on Windows (see issue
// 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.
@ -1278,16 +1247,16 @@ const CFFPrivateDictLayout = [
[7, "OtherBlues", "delta", null],
[8, "FamilyBlues", "delta", null],
[9, "FamilyOtherBlues", "delta", null],
[[12, 9], "BlueScale", "num", DEFAULT_BLUE_SCALE],
[[12, 10], "BlueShift", "num", DEFAULT_BLUE_SHIFT],
[[12, 11], "BlueFuzz", "num", DEFAULT_BLUE_FUZZ],
[[12, 9], "BlueScale", "num", 0.039625],
[[12, 10], "BlueShift", "num", 7],
[[12, 11], "BlueFuzz", "num", 1],
[10, "StdHW", "num", null],
[11, "StdVW", "num", null],
[[12, 12], "StemSnapH", "delta", null],
[[12, 13], "StemSnapV", "delta", null],
[[12, 14], "ForceBold", "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],
[20, "defaultWidthX", "num", 0],
[21, "nominalWidthX", "num", 0],

View File

@ -19,12 +19,12 @@ import {
BaseException,
makeArr,
objectSize,
stringToPDFString,
Util,
warn,
} from "../shared/util.js";
import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js";
import { BaseStream } from "./base_stream.js";
import { stringToPDFString } from "./string_utils.js";
const PDF_VERSION_REGEXP = /^[1-9]\.\d$/;
const MAX_INT_32 = 2 ** 31 - 1;
@ -684,6 +684,45 @@ function getNewAnnotationsMap(annotationStorage) {
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()) {
if (!(date instanceof Date)) {
date = new Date(date);
@ -743,6 +782,7 @@ export {
getRotationMatrix,
getSizeInBytes,
IDENTITY_MATRIX,
isAscii,
isBooleanArray,
isNumberArray,
isWhiteSpace,
@ -758,6 +798,9 @@ export {
recoverJsURL,
RESOURCES_KEYS_OPERATOR_LIST,
RESOURCES_KEYS_TEXT_CONTENT,
stringToAsciiOrUTF16BE,
stringToUTF16HexString,
stringToUTF16String,
toRomanNumerals,
validateCSSFont,
validateFontName,

View File

@ -18,6 +18,7 @@ import {
escapePDFName,
getRotationMatrix,
numberToString,
stringToUTF16HexString,
} from "./core_utils.js";
import { Dict, Name } from "./primitives.js";
import {
@ -32,7 +33,6 @@ import { EvaluatorPreprocessor } from "./evaluator.js";
import { LocalColorSpaceCache } from "./image_utils.js";
import { PDFFunctionFactory } from "./function.js";
import { StringStream } from "./stream.js";
import { stringToUTF16HexString } from "./string_utils.js";
class DefaultAppearanceEvaluator extends EvaluatorPreprocessor {
constructor(str) {

View File

@ -26,6 +26,7 @@ import {
RenderingIntentFlag,
shadow,
stringToBytes,
stringToPDFString,
stringToUTF8String,
unreachable,
Util,
@ -75,7 +76,6 @@ import { OperatorList } from "./operator_list.js";
import { PartialEvaluator } from "./evaluator.js";
import { PDFImage } from "./image.js";
import { StreamsSequenceStream } from "./decode_stream.js";
import { stringToPDFString } from "./string_utils.js";
import { StructTreePage } from "./struct_tree.js";
import { XFAFactory } from "./xfa/factory.js";
import { XRef } from "./xref.js";

View File

@ -25,15 +25,15 @@ import {
getInheritableProperty,
getModificationDate,
getNewAnnotationsMap,
stringToAsciiOrUTF16BE,
} from "../core_utils.js";
import { Dict, isName, Name, Ref, RefSet, RefSetCache } from "../primitives.js";
import { incrementalUpdate, writeValue } from "../writer.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 { BaseStream } from "../base_stream.js";
import { StringStream } from "../stream.js";
import { stringToBytes } from "../../shared/util.js";
const MAX_LEAVES_PER_PAGES_NODE = 16;
const MAX_IN_NAME_TREE_NODE = 64;

View File

@ -26,6 +26,7 @@ import {
normalizeUnicode,
OPS,
shadow,
stringToPDFString,
TextRenderingMode,
Util,
warn,
@ -89,7 +90,6 @@ import { getUnicodeForGlyph } from "./unicode.js";
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
import { PDFImage } from "./image.js";
import { Stream } from "./stream.js";
import { stringToPDFString } from "./string_utils.js";
const DefaultPartialEvaluatorOptions = Object.freeze({
maxImageSize: -1,

View File

@ -13,10 +13,9 @@
* 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 { Dict } from "./primitives.js";
import { stringToPDFString } from "./string_utils.js";
function pickPlatformItem(dict) {
if (dict instanceof Dict) {

View File

@ -163,9 +163,6 @@ function lookupCmap(ranges, unicode) {
}
function compileGlyf(code, cmds, font, visitedGlyphs = new Set()) {
if (!code?.length) {
return;
}
if (visitedGlyphs.has(code)) {
warn("compileGlyf: skipping recursive composite glyph reference.");
return;

View File

@ -55,13 +55,13 @@ import {
getSupplementalGlyphMapForArialBlack,
getSupplementalGlyphMapForCalibri,
} from "./standard_fonts.js";
import { GlyfTable, pruneCompositeGlyphCycles } from "./glyf.js";
import { IdentityToUnicodeMap, ToUnicodeMap } from "./to_unicode_map.js";
import { CFFFont } from "./cff_font.js";
import { compileFontInfo } from "./obj_bin_transform_core.js";
import { DataBuilder } from "./data_builder.js";
import { FontRendererFactory } from "./font_renderer.js";
import { getFontBasicMetrics } from "./metrics.js";
import { GlyfTable } from "./glyf.js";
import { OpenTypeFileBuilder } from "./opentype_file_builder.js";
import { Stream } from "./stream.js";
import { Type1Font } from "./type1_font.js";
@ -720,11 +720,6 @@ function createCmapTable(glyphs, toUnicodeExtraMap, numGlyphs) {
function validateOS2Table(os2, file) {
file.pos = (file.start || 0) + os2.offset;
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
// that give us issues
file.skip(60); // skipping type, misc sizes, panose, unicode ranges
@ -2200,25 +2195,18 @@ class Font {
last.endOffset = oldGlyfDataLength;
}
const droppedGlyphs = pruneCompositeGlyphCycles(
oldGlyfData,
locaEntries,
numGlyphs
);
const missingGlyphs = Object.create(null);
let writeOffset = 0;
itemEncode(locaData, 0, writeOffset);
for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
const glyphProfile = droppedGlyphs.has(i)
? { length: 0, sizeOfInstructions: 0 }
: sanitizeGlyph(
oldGlyfData,
locaEntries[i].offset,
locaEntries[i].endOffset,
newGlyfData,
writeOffset,
hintsValid
);
const glyphProfile = sanitizeGlyph(
oldGlyfData,
locaEntries[i].offset,
locaEntries[i].endOffset,
newGlyfData,
writeOffset,
hintsValid
);
const newLength = glyphProfile.length;
if (newLength === 0) {
missingGlyphs[i] = true;
@ -2849,19 +2837,6 @@ class Font {
maxFunctionDefs = font.getUint16();
font.pos += 4;
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;

View File

@ -34,8 +34,6 @@ const WE_HAVE_INSTRUCTIONS = 1 << 8;
// const SCALED_COMPONENT_OFFSET = 1 << 11;
// const UNSCALED_COMPONENT_OFFSET = 1 << 12;
const GLYPH_HEADER_SIZE = 10;
/**
* GlyfTable object represents a glyf table containing glyph information:
* - glyph header (xMin, yMin, xMax, yMax);
@ -220,7 +218,7 @@ class GlyphHeader {
static parse(pos, glyf) {
return [
GLYPH_HEADER_SIZE,
10,
new GlyphHeader({
numberOfContours: glyf.getInt16(pos),
xMin: glyf.getInt16(pos + 2),
@ -232,7 +230,7 @@ class GlyphHeader {
}
getSize() {
return GLYPH_HEADER_SIZE;
return 10;
}
write(pos, buf) {
@ -242,7 +240,7 @@ class GlyphHeader {
buf.setInt16(pos + 6, this.xMax);
buf.setInt16(pos + 8, this.yMax);
return GLYPH_HEADER_SIZE;
return 10;
}
scale(x, factor) {
@ -698,116 +696,4 @@ class CompositeGlyph {
scale(x, factor) {}
}
function pruneCompositeGlyphCycles(glyfTable, locaEntries, numGlyphs) {
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 };
export { GlyfTable };

View File

@ -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,
};

View File

@ -16,13 +16,13 @@
import {
AnnotationPrefix,
makeArr,
stringToPDFString,
stringToUTF8String,
warn,
} from "../shared/util.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 { lookupNormalRect } from "./core_utils.js";
import { NumberTree } from "./name_number_tree.js";
const MAX_DEPTH = 40;

View File

@ -21,6 +21,7 @@ import {
isNodeJS,
PasswordException,
setVerbosityLevel,
stringToPDFString,
VerbosityLevel,
warn,
} from "../shared/util.js";
@ -37,7 +38,6 @@ import { clearGlobalCaches } from "./cleanup_helper.js";
import { incrementalUpdate } from "./writer.js";
import { PDFEditor } from "./editor/pdf_editor.js";
import { PDFWorkerStream } from "./worker_stream.js";
import { stringToPDFString } from "./string_utils.js";
import { StructTreeRoot } from "./struct_tree.js";
class WorkerTask {
@ -1036,9 +1036,6 @@ class WorkerMessageHandler {
.getPage(data.pageIndex)
.then(page => page.annotations.map(a => a.toString()));
});
handler.on("GetWorkerCoverage", function () {
return globalThis.__coverage__ ?? {};
});
}
return workerHandlerName;

View File

@ -102,12 +102,13 @@ import {
getStringOption,
HTMLResult,
} 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 { recoverJsURL } from "../core_utils.js";
import { searchNode } from "./som.js";
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:
// - to put the container at the end of a line

View File

@ -14,7 +14,7 @@
*/
/** @typedef {import("./api").PDFPageProxy} PDFPageProxy */
/** @typedef {import("./page_viewport").PageViewport} PageViewport */
/** @typedef {import("./display_utils").PageViewport} PageViewport */
// eslint-disable-next-line max-len
/** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
// eslint-disable-next-line max-len
@ -38,7 +38,6 @@ import {
LINE_FACTOR,
makeArr,
shadow,
SVG_NS,
unreachable,
Util,
warn,
@ -667,7 +666,8 @@ class AnnotationElement {
style.borderWidth = 0;
svgBuffer = [
"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}">`,
];
this.container.classList.add("hasBorder");

View File

@ -57,6 +57,7 @@ import {
import {
isDataScheme,
isValidFetchUrl,
PageViewport,
RenderingCancelledException,
StatTimer,
} from "./display_utils.js";
@ -77,7 +78,6 @@ import { MathClamp } from "../shared/math_clamp.js";
import { Metadata } from "./metadata.js";
import { OptionalContentConfig } from "./optional_content_config.js";
import { PagesMapper } from "./pages_mapper.js";
import { PageViewport } from "./page_viewport.js";
import { PDFDataTransportStream } from "./transport_stream.js";
import { PDFObjects } from "./pdf_objects.js";
import { TextLayer } from "./text_layer.js";

View File

@ -1850,13 +1850,17 @@ class CanvasGraphics {
return;
}
// Clear the full scratch canvas, not just the dirty box. Pixels left
// outside dirtyBox can leak into a later compose() whose destination-in
// pass doesn't overwrite them, producing stale output -- this is what
// breaks `firefox-issue17779-partial` (issue #21276).
// Whatever was drawn has been moved to the suspended canvas, now clear it
// out of the current canvas. Only the dirty box region needs clearing;
// everything outside it is already transparent.
this.ctx.save();
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();
}

View File

@ -14,7 +14,6 @@
*/
import { BBOX_INIT, FeatureTest, Util } from "../shared/util.js";
import { getCurrentTransform } from "./display_utils.js";
import { MathClamp } from "../shared/math_clamp.js";
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);
}
// 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
// account the platform's endianess.
const EMPTY_BBOX = new Uint32Array(new Uint8Array([255, 255, 0, 0]).buffer)[0];
@ -663,7 +612,7 @@ class CanvasDependencyTracker {
computedBBox = [0, 0, 0, 0];
Util.axialAlignedBoundingBox(fontBBox, font.fontMatrix, computedBBox);
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) {
@ -1172,7 +1121,7 @@ class CanvasImagesTracker {
this.#coords = newCoords;
}
const transform = getCurrentTransform(ctx);
const transform = Util.domMatrixToTransform(ctx.getTransform());
// We want top left, bottom left, top right.
// (0, 0) is the bottom left corner.

View File

@ -22,9 +22,10 @@ import {
warn,
} from "../shared/util.js";
import { MathClamp } from "../shared/math_clamp.js";
import { PageViewport } from "./page_viewport.js";
import { XfaLayer } from "./xfa_layer.js";
const SVG_NS = "http://www.w3.org/2000/svg";
class PixelsPerInch {
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 {
constructor(msg, extraDelay = 0) {
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) {
if (color.startsWith("#")) {
// #RRGGBB or #RRGGBBAA
@ -828,12 +1058,14 @@ export {
getPdfFilenameFromUrl,
getRGB,
getRGBA,
getXfaPageViewport,
isDataScheme,
isPdfFile,
isValidFetchUrl,
makePathFromDrawOPS,
noContextMenu,
OutputScale,
PageViewport,
PDFDateString,
PixelsPerInch,
RenderingCancelledException,
@ -842,4 +1074,5 @@ export {
StatTimer,
stopEvent,
SupportedImageMimeTypes,
SVG_NS,
};

View File

@ -16,174 +16,6 @@
import { DOMSVGFactory } from "./svg_factory.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.
* 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();
/** @type {Element | null} */
#textLayer = null;
/** @type {Object | null} */
#filterFactory = null;
/** @type {Object | null} */
#pageColors = null;
/** @type {MutationObserver | null} */
#textLayerObserver = null;
#toUpdate = new Map();
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) {
if (!this.#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;
}
@ -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() {
return shadow(this, "_svgFactory", new DOMSVGFactory());
}
@ -663,7 +62,7 @@ class DrawLayer {
#createSVG() {
const svg = DrawLayer._svgFactory.create(1, 1, /* skipDimensions = */ true);
this.#parent.append(svg);
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("aria-hidden", true);
return svg;
}
@ -840,22 +239,6 @@ class DrawLayer {
}
this.#mapping.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;
}
}
}

View File

@ -15,7 +15,7 @@
// eslint-disable-next-line max-len
/** @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
/** @typedef {import("../../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
// eslint-disable-next-line max-len

View File

@ -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) {
return [
(x1 + 5 * x2) / 6,

View File

@ -24,7 +24,6 @@ import {
FeatureTest,
getUuid,
shadow,
SVG_NS,
Util,
warn,
} from "../../shared/util.js";
@ -171,7 +170,7 @@ class ImageManager {
// The "workaround" is to append "svgView(preserveAspectRatio(none))" to the
// url, but according to comment #15, it seems that it leads to unexpected
// 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 ctx = canvas.getContext("2d", { willReadFrequently: true });
const image = new Image();

View File

@ -13,15 +13,8 @@
* limitations under the License.
*/
import {
FeatureTest,
SVG_NS,
unreachable,
updateUrlHash,
Util,
warn,
} from "../shared/util.js";
import { getRGB, getRGBA, isDataScheme } from "./display_utils.js";
import { getRGB, isDataScheme, SVG_NS } from "./display_utils.js";
import { unreachable, updateUrlHash, Util, warn } from "../shared/util.js";
class BaseFilterFactory {
constructor() {
@ -57,36 +50,6 @@ class BaseFilterFactory {
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) {}
}
@ -306,66 +269,6 @@ class DOMFilterFactory extends BaseFilterFactory {
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 users 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) {
// When a page is zoomed the page is re-drawn but the maps are likely
// the same.
@ -494,7 +397,7 @@ class DOMFilterFactory extends BaseFilterFactory {
0.2126 * bgRGB[0] + 0.7152 * bgRGB[1] + 0.0722 * bgRGB[2]
);
let [newFgRGB, newBgRGB] = [newFgColor, newBgColor].map(
this.#getOpaqueTextColor.bind(this)
this.#getRGB.bind(this)
);
if (bgGray < fgGray) {
[fgGray, bgGray, newFgRGB, newBgRGB] = [
@ -636,62 +539,6 @@ class DOMFilterFactory extends BaseFilterFactory {
this.#defs.style.color = 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 };

View File

@ -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 };

View File

@ -13,7 +13,8 @@
* 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 {
constructor() {

View File

@ -13,7 +13,7 @@
* limitations under the License.
*/
/** @typedef {import("./page_viewport").PageViewport} PageViewport */
/** @typedef {import("./display_utils").PageViewport} PageViewport */
/** @typedef {import("./api").TextContent} TextContent */
/** @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
// OffscreenCanvas.
const canvas = document.createElement("canvas");
canvas.style.cssText =
"position:absolute;top:0;left:0;width:0;height:0;display:none";
canvas.className = "hiddenCanvasElement";
canvas.lang = lang;
document.body.append(canvas);
ctx = canvas.getContext("2d", {

View File

@ -15,10 +15,10 @@
// eslint-disable-next-line max-len
/** @typedef {import("./annotation_storage").AnnotationStorage} AnnotationStorage */
/** @typedef {import("./display_utils").PageViewport} PageViewport */
// eslint-disable-next-line max-len
/** @typedef {import("../../web/pdf_link_service.js").PDFLinkService} PDFLinkService */
import { PageViewport } from "./page_viewport.js";
import { XfaText } from "./xfa_text.js";
/**
@ -293,20 +293,6 @@ class XfaLayer {
parameters.div.style.transform = transform;
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 };

View File

@ -20,7 +20,7 @@
/** @typedef {import("./display/api").PDFDocumentProxy} PDFDocumentProxy */
/** @typedef {import("./display/api").PDFPageProxy} PDFPageProxy */
/** @typedef {import("./display/api").RenderTask} RenderTask */
/** @typedef {import("./display/page_viewport").PageViewport} PageViewport */
/** @typedef {import("./display/display_utils").PageViewport} PageViewport */
import {
AbortException,
@ -55,6 +55,7 @@ import {
getPdfFilenameFromUrl,
getRGB,
getRGBA,
getXfaPageViewport,
isDataScheme,
isPdfFile,
noContextMenu,
@ -121,6 +122,7 @@ globalThis.pdfjsLib = {
getRGB,
getRGBA,
getUuid,
getXfaPageViewport,
GlobalWorkerOptions,
ImageKind,
InvalidPDFException,
@ -184,6 +186,7 @@ export {
getRGB,
getRGBA,
getUuid,
getXfaPageViewport,
GlobalWorkerOptions,
ImageKind,
InvalidPDFException,

View File

@ -36,8 +36,6 @@ const LINE_FACTOR = 1.35;
const LINE_DESCENT_FACTOR = 0.35;
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
* how these flags are being used:
@ -663,10 +661,7 @@ class FeatureTest {
let ctx;
if (this.isOffscreenCanvasSupported) {
ctx = new OffscreenCanvas(1, 1).getContext("2d");
} else if (
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("WORKER_THREAD")) &&
typeof document !== "undefined"
) {
} else if (typeof document !== "undefined") {
ctx = document.createElement("canvas").getContext("2d");
}
// Spec-compliant Canvas2D defaults `ctx.filter` to "none". On
@ -678,22 +673,21 @@ class FeatureTest {
}
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(
this,
"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]}`;
}
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.
static transform(m1, m2) {
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) {
return decodeURIComponent(escape(str));
}
@ -1186,9 +1296,9 @@ export {
setVerbosityLevel,
shadow,
stringToBytes,
stringToPDFString,
stringToUTF8String,
stripPath,
SVG_NS,
TextRenderingMode,
UnknownErrorException,
unreachable,

View File

@ -15,15 +15,15 @@
// Istanbul coverage objects use s (statements), b (branches), and f (functions)
// as shorthand keys for the hit-count maps.
function mergeCoverageIntoGlobal(coverage) {
function mergeWorkerCoverageIntoWindow(coverage) {
if (!coverage || Object.keys(coverage).length === 0) {
return;
}
globalThis.__coverage__ ??= {};
window.__coverage__ ??= {};
for (const [key, fileCoverage] of Object.entries(coverage)) {
const existing = globalThis.__coverage__[key];
const existing = window.__coverage__[key];
if (!existing) {
globalThis.__coverage__[key] = fileCoverage;
window.__coverage__[key] = fileCoverage;
continue;
}
for (const id of Object.keys(fileCoverage.s)) {
@ -49,10 +49,10 @@ async function fetchAndMergeWorkerCoverage(pdfWorker) {
"GetWorkerCoverage",
null
);
mergeCoverageIntoGlobal(coverage);
mergeWorkerCoverageIntoWindow(coverage);
} catch (e) {
console.warn(`Failed to collect worker coverage: ${e}`);
}
}
export { fetchAndMergeWorkerCoverage, mergeCoverageIntoGlobal };
export { fetchAndMergeWorkerCoverage, mergeWorkerCoverageIntoWindow };

View File

@ -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);
});
});
});

View File

@ -46,7 +46,6 @@ async function initializePDFJS(callback) {
await Promise.all(
[
"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_post_spec.js",
"pdfjs-test/font/font_fpgm_spec.js",

0
test/images/samplesignature.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -14,7 +14,6 @@
*/
import { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs";
import fs from "fs/promises";
const FIELDS = [
"fileName",
@ -33,50 +32,21 @@ const FIELDS = [
"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", () => {
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", () => {
let pages;
@ -88,12 +58,23 @@ describe("PDFDocumentProperties", () => {
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(
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",
fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
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})`,
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);
})
);
});

View File

@ -1726,6 +1726,10 @@ describe("Highlight Editor", () => {
it("must check that an existing highlight is ignored on hovering", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
if (navigator.platform.includes("Win")) {
pending("Fails consistently on Windows (issue #20136).");
}
await switchToHighlight(page);
const rect = await getSpanRectFromText(

View File

@ -38,13 +38,11 @@ async function runTests(results) {
"freetext_editor_spec.mjs",
"highlight_editor_spec.mjs",
"ink_editor_spec.mjs",
"presentation_mode_spec.mjs",
"reorganize_pages_spec.mjs",
"scripting_spec.mjs",
"signature_editor_spec.mjs",
"simple_viewer_spec.mjs",
"stamp_editor_spec.mjs",
"text_extractor_spec.mjs",
"text_field_spec.mjs",
"text_layer_spec.mjs",
"text_layer_images_spec.mjs",

View File

@ -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);
})
);
});
});
});

View File

@ -40,27 +40,10 @@ import {
waitForTextToBe,
waitForTooltipToBe,
} from "./test_utils.mjs";
import fs from "fs";
import path from "path";
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) {
await showViewsManager(page);
@ -3083,7 +3066,7 @@ describe("Reorganize Pages View", () => {
await waitAndClick(page, getThumbnailSelector(2));
const handleMerged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
window.PDFViewerApplication.eventBus._on(
"thumbnailsloaded",
resolve,
{ once: true }
@ -3142,7 +3125,7 @@ describe("Reorganize Pages View", () => {
await waitAndClick(page, getThumbnailSelector(1));
const handleMerged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
window.PDFViewerApplication.eventBus._on(
"thumbnailsloaded",
resolve,
{ once: true }
@ -3178,7 +3161,7 @@ describe("Reorganize Pages View", () => {
await waitForThumbnailVisible(page, 1);
const handleMerged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
window.PDFViewerApplication.eventBus._on(
"thumbnailsloaded",
resolve,
{ once: true }
@ -3214,7 +3197,7 @@ describe("Reorganize Pages View", () => {
await waitForTextToBe(page, labelSelector, `${FSI}1${PDI} selected`);
const handleMerged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
window.PDFViewerApplication.eventBus._on(
"thumbnailsloaded",
resolve,
{ 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`
);
})
);
});
});
});

View File

@ -13,7 +13,6 @@
* limitations under the License.
*/
import { mergeCoverageIntoGlobal } from "../coverage_utils.js";
import os from "os";
const isMac = os.platform() === "darwin";
@ -150,42 +149,11 @@ function closePages(pages) {
}
async function closeSinglePage(page) {
const coverage = await page.evaluate(async () => {
// Collect coverage data from the worker before the document is closed.
let workerCoverage = null;
const handler =
window.PDFViewerApplication.pdfDocument?._transport?.messageHandler;
if (handler) {
try {
workerCoverage = await handler.sendWithPromise(
"GetWorkerCoverage",
null
);
} catch {}
}
// Close the viewer gracefully, and clear local storage to avoid state
// leaking from one test to another.
// Avoid to keep something from a previous test.
await page.evaluate(async () => {
await window.PDFViewerApplication.testingClose();
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 });
}
@ -268,8 +236,13 @@ function getSelector(id) {
}
async function getRect(page, selector) {
// In Chrome something is wrong when serializing a `DomRect`,
// so we extract the values and return them ourselves.
await page.waitForSelector(selector, { visible: true });
return (await page.$(selector)).boundingBox();
return page.$eval(selector, el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
}
function getQuerySelector(id) {

View File

@ -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 8393. ACM\nPress, 2007."
)
).toBeTrue();
expect(text.length).toEqual(82804);
expect(requestId).toEqual(2);
})
);
});
});
});

View File

@ -13,41 +13,15 @@
* limitations under the License.
*/
/**
* @import { Page } from "puppeteer"
*/
import {
closePages,
closeSinglePage,
getSpanRectFromText,
kbSelectAll,
loadAndWait,
waitForEvent,
} from "./test_utils.mjs";
import { MathClamp } from "../../src/shared/math_clamp.js";
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 selection", () => {
// 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(() => {
jasmine.addAsyncMatchers({
// Check that a page has a selection containing the given text, with
@ -231,7 +67,11 @@ describe("Text layer", () => {
return {
async compare(page, expected) {
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;
if (expected instanceof RegExp) {
@ -268,275 +108,6 @@ describe("Text layer", () => {
});
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", () => {
let pages;
@ -702,7 +273,7 @@ describe("Text layer", () => {
.withContext(`In ${browserName}`)
.toHaveRoughlySelected(
"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();
const selection = await getSelectionText(page);
const selection = await page.evaluate(() =>
window.getSelection().toString()
);
expect(selection).withContext(`In ${browserName}`).toEqual("AB");
// 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", () => {
@ -1119,142 +633,6 @@ describe("Text layer", () => {
.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", () => {

View File

@ -26,11 +26,8 @@ import {
waitForPageChanging,
waitForPageRendered,
} from "./test_utils.mjs";
import path from "path";
import { PNG } from "pngjs";
const __dirname = import.meta.dirname;
describe("PDF viewer", () => {
describe("Zoom origin", () => {
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 () => {
await Promise.all(
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 () => {
await Promise.all(
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
);
})
);
});
});
});

View File

@ -890,7 +890,6 @@ async function startIntegrationTest() {
sessions[0].numRuns = results.runs;
sessions[0].numErrors = results.failures;
sessions[0].failures = results.failureList;
sessions[0].coverage = globalThis.__coverage__;
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
// a chance to be processed (essentially causing events to get lost). This
// value gives Chrome a more similar execution speed as Firefox.
options.slowMo = 3;
options.slowMo = 5;
options.args = [
// Avoid crashing because no sandbox is shipped by default and we only run
// our own trusted content in the scope of the tests (for more information
// see https://pptr.dev/troubleshooting#setting-up-chrome-linux-sandbox).
"--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",
];
// avoid crash
options.args = ["--no-sandbox", "--disable-setuid-sandbox"];
// silent printing in a pdf
options.args.push("--kiosk-printing");
}
if (browserName === "firefox") {
@ -1024,13 +1015,12 @@ async function startBrowser({
// Save file in output
"browser.download.folderList": 2,
"browser.download.dir": tempDir,
// Print PDFs silently (without print preview or user interaction).
// Print silently in a pdf
"print.always_print_silent": true,
print_printer: "PDF",
"print.printer_PDF.print_to_file": true,
"print.printer_PDF.print_to_filename": printFile,
// Disable hardware acceleration (fixes rendering issues, see #15168 and
// #21272, and environments like GitHub Actions don't expose GPUs anyway).
// Disable gpu acceleration
"gfx.canvas.accelerated": false,
// It's helpful to see where the caret is.
"accessibility.browsewithcaret": true,

View File

@ -39,6 +39,7 @@ import {
} from "./test_utils.js";
import {
fetchData as fetchDataDOM,
PageViewport,
RenderingCancelledException,
StatTimer,
} 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 { GlobalWorkerOptions } from "../../src/display/worker_options.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";
@ -4590,27 +4590,6 @@ have written that much by now. So, heres to squashing bugs.`);
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 () {
await page.getOperatorList();
expect(page.stats).toEqual(null);

View File

@ -18,9 +18,7 @@ import {
CFFCompiler,
CFFFDSelect,
CFFParser,
CFFPrivateDict,
CFFStrings,
CFFTopDict,
} from "../../src/core/cff_parser.js";
import { SEAC_ANALYSIS_ENABLED } from "../../src/core/fonts_utils.js";
import { Stream } from "../../src/core/stream.js";
@ -114,77 +112,6 @@ describe("CFFParser", function () {
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 () {
const topDict = cff.topDict;
const defaultValue = topDict.getByName("UnderlinePosition");

View File

@ -48,9 +48,7 @@
"pdf_viewer_spec.js",
"postscript_spec.js",
"primitives_spec.js",
"scripting_utils_spec.js",
"stream_spec.js",
"string_utils_spec.js",
"struct_tree_spec.js",
"svg_factory_spec.js",
"text_layer_spec.js",

View File

@ -22,10 +22,13 @@ import {
getInheritableProperty,
getModificationDate,
getSizeInBytes,
isAscii,
isWhiteSpace,
numberToString,
parseXFAPath,
recoverJsURL,
stringToUTF16HexString,
stringToUTF16String,
toRomanNumerals,
validateCSSFont,
} 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 () {
it("should return true for the same reference", function () {
const dict = new Dict();

View File

@ -151,9 +151,6 @@ describe("display_utils", function () {
expect(getPdfFilenameFromUrl("/pdfs/%AA.pdf")).toEqual("%AA.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 () {

View File

@ -42,7 +42,7 @@
import { GlobalWorkerOptions } from "pdfjs/display/worker_options.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 { PDFWorker } from "pdfjs/display/api.js";
import { TestReporter } from "../reporter.js";
@ -95,9 +95,7 @@ async function initializePDFJS(callback) {
"pdfjs-test/unit/postscript_spec.js",
"pdfjs-test/unit/primitives_spec.js",
"pdfjs-test/unit/scripting_spec.js",
"pdfjs-test/unit/scripting_utils_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/svg_factory_spec.js",
"pdfjs-test/unit/text_layer_spec.js",
@ -158,7 +156,7 @@ function installWorkerCoverageHook() {
const handler = new MessageHandler("main", "worker", webWorker);
const promise = handler
.sendWithPromise("GetWorkerCoverage", null)
.then(mergeCoverageIntoGlobal)
.then(mergeWorkerCoverageIntoWindow)
.catch(e => {
console.warn(`Failed to collect worker coverage: ${e}`);
})

View File

@ -46,6 +46,7 @@ import {
getPdfFilenameFromUrl,
getRGB,
getRGBA,
getXfaPageViewport,
isDataScheme,
isPdfFile,
noContextMenu,
@ -105,6 +106,7 @@ const expectedAPI = Object.freeze({
getRGB,
getRGBA,
getUuid,
getXfaPageViewport,
GlobalWorkerOptions,
ImageKind,
InvalidPDFException,

View File

@ -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");
});
});
});

View File

@ -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鷗Œé逍Üߪąñ丂㐀𠀀"
);
});
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");
});
});
});

View File

@ -19,6 +19,7 @@ import {
createValidAbsoluteUrl,
getUuid,
stringToBytes,
stringToPDFString,
} from "../../src/shared/util.js";
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鷗Œé逍Üߪąñ丂㐀𠀀"
);
});
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 () {
it("handles invalid URLs", function () {
expect(createValidAbsoluteUrl(undefined, undefined)).toEqual(null);

View File

@ -285,7 +285,6 @@
pointer-events: auto;
box-sizing: content-box;
padding: var(--editor-toolbar-padding);
user-select: none;
position: absolute;
inset-inline-end: 0;

View File

@ -15,7 +15,7 @@
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
// 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
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
// eslint-disable-next-line max-len

View File

@ -15,7 +15,7 @@
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
// 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
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
// eslint-disable-next-line max-len

View File

@ -376,7 +376,6 @@ const PDFViewerApplication = {
enableNewBadge: x => x === "true",
enablePermissions: x => x === "true",
enableMerge: x => x === "true",
enableSelectionRendering: x => x === "true",
enableSplitMerge: x => x === "true",
enableUpdatedAddImage: x => x === "true",
highlightEditorColors: x => x,
@ -579,7 +578,6 @@ const PDFViewerApplication = {
enableOptimizedPartialRendering: AppOptions.get(
"enableOptimizedPartialRendering"
),
enableSelectionRendering: AppOptions.get("enableSelectionRendering"),
imagesRightClickMinSize: AppOptions.get("imagesRightClickMinSize"),
pageColors,
mlManager,

View File

@ -320,11 +320,6 @@ const defaultOptions = {
: "./images/",
kind: OptionKind.VIEWER,
},
enableSelectionRendering: {
/** @type {boolean} */
value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
imagesRightClickMinSize: {
/** @type {number} */
value:

View File

@ -36,8 +36,6 @@ class BasePDFPageView extends RenderableView {
enableOptimizedPartialRendering = false;
enableSelectionRendering = true;
imagesRightClickMinSize = -1;
eventBus = null;
@ -60,7 +58,6 @@ class BasePDFPageView extends RenderableView {
this.renderingQueue = options.renderingQueue;
this.enableOptimizedPartialRendering =
options.enableOptimizedPartialRendering ?? false;
this.enableSelectionRendering = options.enableSelectionRendering !== false;
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
this.minDurationToUpdateCanvas = options.minDurationToUpdateCanvas ?? 500;
}

View File

@ -14,16 +14,6 @@
*/
.canvasWrapper {
.selection {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: rgb(0 90 255 / 0.22);
}
svg {
transform: none;

View File

@ -15,19 +15,6 @@
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
* @property {string} [intent] - The default value is "display".
@ -36,19 +23,6 @@ import { DrawLayer } from "pdfjs-lib";
class DrawLayerBuilder {
#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
* @returns {Promise<void>}
@ -57,12 +31,7 @@ class DrawLayerBuilder {
if (intent !== "display" || this.#drawLayer || this._cancelled) {
return;
}
this.#drawLayer = new DrawLayer({
pageIndex: this.pageIndex,
textLayer: this.textLayer,
filterFactory: this.filterFactory,
pageColors: this.pageColors,
});
this.#drawLayer = new DrawLayer();
}
cancel() {

View File

@ -39,19 +39,6 @@ class Preferences extends BasePreferences {
}
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() {
return new GenericL10n(AppOptions.get("localeProperties")?.lang);
}

View File

@ -111,9 +111,6 @@ class PDFDocumentProperties {
this.#updateUI();
return;
}
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
this._fieldDataLastUpdated = Date.now();
}
// Get the document properties.
const [
@ -121,13 +118,7 @@ class PDFDocumentProperties {
pdfPage,
] = await Promise.all([
this.pdfDocument.getMetadata(),
this.pdfDocument.getPage(currentPageNumber).catch(reason => {
console.error(
`PDFDocumentProperties - unable to get page ${currentPageNumber}.`,
reason
);
return null;
}),
this.pdfDocument.getPage(currentPageNumber),
]);
const [
@ -144,7 +135,7 @@ class PDFDocumentProperties {
this._titleLookup(),
this.#parseDate(metadata?.get("xmp:createdate"), info.CreationDate),
this.#parseDate(metadata?.get("xmp:modifydate"), info.ModDate),
this.#parsePageSize(pdfPage, pagesRotation),
this.#parsePageSize(getPageSizeInches(pdfPage), pagesRotation),
this.#parseLinearization(info.IsLinearized),
]);
@ -229,9 +220,6 @@ class PDFDocumentProperties {
// since it will be updated the next time `this.open` is called.
return;
}
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
this.dialog.dataset.fieldDataLastUpdated = this._fieldDataLastUpdated;
}
for (const id in this.fields) {
const content = this.#fieldData?.[id];
this.fields[id].textContent = content || content === 0 ? content : "-";
@ -251,11 +239,10 @@ class PDFDocumentProperties {
: undefined;
}
async #parsePageSize(pdfPage, pagesRotation) {
if (!pdfPage) {
async #parsePageSize(pageSizeInches, pagesRotation) {
if (!pageSizeInches) {
return undefined;
}
let pageSizeInches = getPageSizeInches(pdfPage);
// Take the viewer rotation into account as well; compare with Adobe Reader.
if (pagesRotation % 180 !== 0) {
pageSizeInches = {

View File

@ -14,7 +14,7 @@
*/
// 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
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
/** @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
* 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.
* @property {boolean} [enableSelectionRendering] - When enabled, renders text
* selections in the draw layer.
* The default value is `true`.
* @property {boolean} [enableOptimizedPartialRendering] - When enabled, PDF
* rendering will keep track of which areas of the page each PDF operation
* 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;
if (!annotationEditorUIManager) {
return;
}
this.drawLayer ||= new DrawLayerBuilder();
await this.#renderDrawLayer();
this.drawLayer.setParent(canvasWrapper);
if (
this.annotationLayer ||

View File

@ -16,7 +16,7 @@
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
// 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 */
// eslint-disable-next-line max-len
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */

View File

@ -94,10 +94,6 @@ class PDFThumbnailViewer {
#dragAC = null;
#abortSignal = undefined;
#externalDragActive = false;
#draggedContainer = null;
#thumbnailsPositions = null;
@ -205,7 +201,6 @@ class PDFThumbnailViewer {
this.maxCanvasPixels = maxCanvasPixels;
this.maxCanvasDim = maxCanvasDim;
this.pageColors = pageColors || null;
this.#abortSignal = abortSignal;
this.#enableMerge = enableMerge || false;
this.#enableSplitMerge = enableSplitMerge || false;
this.#statusLabel = statusBar?.viewsManagerStatusActionLabel || null;
@ -320,11 +315,62 @@ class PDFThumbnailViewer {
if (this.#enableMerge && addFileComponent) {
const { picker, button } = addFileComponent;
picker.addEventListener("change", () => {
picker.addEventListener("change", async () => {
const file = picker.files?.[0];
if (file) {
this.#mergeFile(file, this._currentPageNumber - 1);
if (!file) {
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", () => {
picker.click();
@ -351,55 +397,6 @@ class PDFThumbnailViewer {
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) {
return this._thumbnails[index];
}
@ -1188,10 +1185,6 @@ class PDFThumbnailViewer {
this.#draggedImageX + this.#draggedImageWidth / 2,
this.#draggedImageY + this.#draggedImageHeight / 2
);
this.#positionDragMarker(positionData);
}
#positionDragMarker(positionData) {
if (!positionData) {
return;
}
@ -1209,7 +1202,7 @@ class PDFThumbnailViewer {
if (index < 0) {
if (xPos.length === 1) {
y = bbox[1] - SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT;
x = bbox[0];
x = bbox[4];
width = bbox[2];
} else {
y = bbox[1];
@ -1279,22 +1272,16 @@ class PDFThumbnailViewer {
lastRightX ??= cx + w;
}
}
let space;
if (positionsX.length > 1) {
space = (positionsX[1] - firstRightX) / 2;
} else if (positionsY.length > 1) {
space = (positionsY[1] - firstBottomY) / 2;
} else {
space = SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT;
}
const space =
positionsX.length > 1
? (positionsX[1] - firstRightX) / 2
: (positionsY[1] - firstBottomY) / 2;
this.#thumbnailsPositions = {
x: positionsX,
y: positionsY,
lastX: positionsLastX,
space,
lastSpace: positionsLastX.length
? (positionsLastX.at(-1) - lastRightX) / 2
: space,
lastSpace: (positionsLastX.at(-1) - lastRightX) / 2,
bbox,
};
this.#isOneColumnView = positionsX.length === 1;
@ -1393,7 +1380,6 @@ class PDFThumbnailViewer {
this.#goToPage(e);
});
this.#addDragListeners();
this.#addExternalFileDropListeners();
}
#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) {
const container = e.target.closest(".thumbnailImageContainer");
if (container) {

View File

@ -78,6 +78,16 @@
transform: rotate(270deg) translateX(-100%);
}
#hiddenCopyElement,
.hiddenCanvasElement {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
display: none;
}
.pdfViewer {
/* Define this variable here and not in :root to avoid to reflow all the UI
when scaling (see #15929). */

View File

@ -16,7 +16,7 @@
/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
// 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
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
/** @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,
* that only renders the part of the page that is close to the viewport.
* 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
* 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.
@ -365,7 +362,6 @@ class PDFViewer {
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
this.enableOptimizedPartialRendering =
options.enableOptimizedPartialRendering ?? false;
this.enableSelectionRendering = options.enableSelectionRendering !== false;
this.imagesRightClickMinSize = options.imagesRightClickMinSize ?? -1;
this.l10n = options.l10n;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
@ -461,25 +457,6 @@ class PDFViewer {
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}
*/
@ -511,9 +488,6 @@ class PDFViewer {
if (!this.pdfDocument) {
return;
}
if (this._currentPageNumber !== val) {
this.clearSelection();
}
// The intent can be to just reset a scroll position and/or scale.
if (!this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true)) {
console.error(`currentPageNumber: "${val}" is not a valid page.`);
@ -573,9 +547,6 @@ class PDFViewer {
page = i + 1;
}
}
if (this._currentPageNumber !== page) {
this.clearSelection();
}
// The intent can be to just reset a scroll position and/or scale.
if (!this._setCurrentPageNumber(page, /* resetCurrentPageView = */ true)) {
console.error(`currentPageLabel: "${val}" is not a valid page.`);
@ -646,7 +617,6 @@ class PDFViewer {
if (this._pagesRotation === rotation) {
return; // The rotation didn't change.
}
this.clearSelection();
this._pagesRotation = rotation;
const pageNumber = this._currentPageNumber;
@ -997,8 +967,6 @@ class PDFViewer {
const element = (this.#hiddenCopyElement =
document.createElement("div"));
element.id = "hiddenCopyElement";
element.style.cssText =
"position:absolute;top:0;left:0;width:0;height:0;display:none";
viewer.before(element);
}
@ -1096,7 +1064,6 @@ class PDFViewer {
enableDetailCanvas: this.enableDetailCanvas,
enableOptimizedPartialRendering:
this.enableOptimizedPartialRendering,
enableSelectionRendering: this.enableSelectionRendering,
imagesRightClickMinSize: this.imagesRightClickMinSize,
pageColors,
l10n: this.l10n,
@ -1519,7 +1486,6 @@ class PDFViewer {
newValue,
{ noScroll = false, preset = false, drawingDelay = -1, origin = null }
) {
this.clearSelection();
this._currentScaleValue = newValue.toString();
if (this.#isSameScale(newScale)) {
@ -2242,7 +2208,6 @@ class PDFViewer {
}
this._previousScrollMode = this._scrollMode;
this.clearSelection();
this._scrollMode = mode;
this.eventBus.dispatch("scrollmodechanged", { source: this, mode });
@ -2308,7 +2273,6 @@ class PDFViewer {
if (!isValidSpreadMode(mode)) {
throw new Error(`Invalid spread mode: ${mode}`);
}
this.clearSelection();
this._spreadMode = mode;
this.eventBus.dispatch("spreadmodechanged", { source: this, mode });

View File

@ -38,6 +38,7 @@ const {
getRGB,
getRGBA,
getUuid,
getXfaPageViewport,
GlobalWorkerOptions,
ImageKind,
InvalidPDFException,
@ -101,6 +102,7 @@ export {
getRGB,
getRGBA,
getUuid,
getXfaPageViewport,
GlobalWorkerOptions,
ImageKind,
InvalidPDFException,

View File

@ -13,7 +13,7 @@
* limitations under the License.
*/
import { PixelsPerInch, XfaLayer } from "pdfjs-lib";
import { getXfaPageViewport, PixelsPerInch } from "pdfjs-lib";
import { SimpleLinkService } from "./pdf_link_service.js";
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
@ -51,7 +51,7 @@ function getXfaHtmlForPrinting(printContainer, pdfDocument) {
linkService,
xfaHtml: xfaPage,
});
const viewport = XfaLayer.getPageViewport(xfaPage, { scale });
const viewport = getXfaPageViewport(xfaPage, { scale });
builder.render({ viewport, intent: "print" });
page.append(builder.div);

View File

@ -38,7 +38,6 @@
white-space: pre;
cursor: text;
transform-origin: 0% 0%;
user-select: text;
}
/* We multiply the font size by --min-font-size, and then scale the text
@ -116,7 +115,12 @@
}
::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 */

View File

@ -15,7 +15,7 @@
/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */
// 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
/** @typedef {import("../src/display/text_layer_images.js").TextLayerImages} TextLayerImages */
/** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */

View File

@ -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" />
<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-->
<!--#include viewer-snippet-firefox-extension.html-->
<!--#endif-->

View File

@ -4,7 +4,19 @@
users with recognizing which checkbox they have to click when they
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
<a id="chrome-link-to-extensions-page">chrome://extensions</a>
<br />

View File

@ -713,25 +713,6 @@ dialog :link {
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 {
cursor: grab !important;
}

View File

@ -29,35 +29,6 @@ See https://github.com/adobe-type-tools/cmap-resources
<!--#endif-->
<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-->
<!--#include viewer-snippet-firefox-extension.html-->
<!--#elif CHROME-->
@ -1266,7 +1237,7 @@ See https://github.com/adobe-type-tools/cmap-resources
</dialog>
<!--#if !MOZCENTRAL-->
<dialog id="printServiceDialog">
<dialog id="printServiceDialog" style="min-width: 200px">
<div class="row">
<span data-l10n-id="pdfjs-print-progress-message"></span>
</div>

View File

@ -632,15 +632,14 @@
pointer-events: none;
}
}
}
&.isDragging > .dragMarker,
&.isDraggingFile > .dragMarker {
position: absolute;
top: 0;
left: 0;
border: 2px solid var(--indicator-color);
contain: strict;
> .dragMarker {
position: absolute;
top: 0;
left: 0;
border: 2px solid var(--indicator-color);
contain: strict;
}
}
&.pasteMode {

View File

@ -17,7 +17,7 @@
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/annotation_storage").AnnotationStorage} AnnotationStorage */
// 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 */
import { XfaLayer } from "pdfjs-lib";