first commit
Some checks failed
Types tests / Test (lts/*) (push) Has been cancelled
Lint / Lint (lts/*) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CI / Test (20) (push) Has been cancelled
CI / Test (22) (push) Has been cancelled
CI / Test (24) (push) Has been cancelled

This commit is contained in:
2025-10-03 22:20:19 +08:00
commit 44db9807a1
2172 changed files with 526822 additions and 0 deletions

4
test/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
ref/
tmp/
*.log
test_snapshots/

74
test/add_test.mjs Normal file
View File

@@ -0,0 +1,74 @@
import crypto from "crypto";
import { execSync } from "child_process";
import fs from "fs";
const testManifest = "test/test_manifest.json";
const pdfFolder = "test/pdfs/";
const gitIgnore = "test/pdfs/.gitignore";
if (process.argv.length < 3) {
console.log("\nUsage: node add_test.js FILE\n");
console.log(
` Add a PDF as a reference test. FILE must be located in ${pdfFolder}`
);
process.exit(1);
}
const file = process.argv[2];
if (!file.startsWith(pdfFolder)) {
throw new Error(`PDF file must be in '${pdfFolder}' directory.`);
}
if (!fs.existsSync(file)) {
throw new Error(`PDF file does not exist '${file}'.`);
}
function calculateMD5(pdfFile, callback) {
const hash = crypto.createHash("md5");
const stream = fs.createReadStream(pdfFile);
stream.on("data", function (data) {
hash.update(data);
});
stream.on("error", function (err) {
callback(err);
});
stream.on("end", function () {
const result = hash.digest("hex");
callback(null, result);
});
}
function getRandomArbitrary(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
calculateMD5(file, (err, md5) => {
if (err) {
throw new Error(err);
}
let contents = fs.readFileSync(gitIgnore, "utf8").split("\n");
const randomLine = getRandomArbitrary(10, contents.length - 2);
contents.splice(
randomLine,
0,
"!" + file.substring(file.lastIndexOf("/") + 1)
);
fs.writeFileSync("test/pdfs/.gitignore", contents.join("\n"));
contents = fs.readFileSync(testManifest, "utf8");
const pdf = file.substring(file.lastIndexOf("/") + 1, file.length - 4);
const randomPoint = getRandomArbitrary(100, contents.length - 20);
const bracket = contents.indexOf("},\n", randomPoint);
const out =
contents.substring(0, bracket) +
"},\n" +
` { "id": "${pdf}",\n` +
` "file": "pdfs/${pdf}.pdf",\n` +
` "md5": "${md5}",\n` +
' "rounds": 1,\n' +
' "type": "eq"\n' +
" " +
contents.substring(bracket);
fs.writeFileSync("test/test_manifest.json", out);
execSync(`git add ${testManifest} ${gitIgnore}`);
execSync(`git add ${file}`);
});

View File

@@ -0,0 +1,71 @@
/* Copyright 2017 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.
*/
/* Overrides to make the annotation layer visible for reference testing. */
.annotationLayer {
position: absolute;
.wasCanvas {
width: 100%;
height: 100%;
position: absolute;
}
.buttonWidgetAnnotation:is(.checkBox, .radioButton) input {
-webkit-appearance: none;
}
:is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder) > a,
.popupTriggerArea::after,
.fileAttachmentAnnotation:not(.hasFillAlpha) .popupTriggerArea {
opacity: 0.2;
background: rgb(255 255 0);
box-shadow: 0 2px 10px rgb(255 255 0);
}
.fileAttachmentAnnotation.hasFillAlpha {
outline: 2px solid yellow;
}
.hasClipPath::after {
box-shadow: none;
}
.linkAnnotation.hasBorder {
background-color: rgb(255 255 0 / 0.2);
}
.popupTriggerArea::after {
display: block;
width: 100%;
height: 100%;
content: "";
}
.popup :is(h1, p) {
margin: 0;
padding: 0;
}
.annotationTextContent {
position: absolute;
width: 100%;
height: 100%;
opacity: 0.4;
background-color: transparent;
color: red;
font-size: 10px;
}
}

455
test/chromium/test-telemetry.js Executable file
View File

@@ -0,0 +1,455 @@
#!/usr/bin/env node
/* Copyright 2016 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.
*/
/* eslint-disable no-var */
import assert from "assert";
import fs from "fs";
import vm from "vm";
var SRC_DIR = __dirname + "/../../";
var telemetryJsPath = "extensions/chromium/telemetry.js";
var telemetryJsSource = fs.readFileSync(SRC_DIR + telemetryJsPath);
var telemetryScript = new vm.Script(telemetryJsSource, {
filename: telemetryJsPath,
displayErrors: true,
});
var LOG_URL = /LOG_URL = '([^']+)'/.exec(telemetryJsSource)[1];
// Create a minimal extension global that mocks the extension environment that
// is used by telemetry.js
function createExtensionGlobal() {
var window = {};
// Whenever a "request" was made, the extra headers are appended to this list.
var test_requests = (window.test_requests = []);
// Extension API mocks.
window.window = window;
window.chrome = {};
window.chrome.extension = {};
window.chrome.extension.inIncognitoContext = false;
window.chrome.runtime = {};
window.chrome.runtime.id = "oemmndcbldboiebfnladdacbdfmadadm";
window.chrome.runtime.getManifest = function () {
return { version: "1.0.0" };
};
function createStorageAPI() {
var storageArea = {};
storageArea.get = function (key, callback) {
assert.equal(key, "disableTelemetry");
// chrome.storage.*. is async, but we make it synchronous to ease testing.
callback(storageArea.mock_data);
};
return storageArea;
}
window.chrome.storage = {};
window.chrome.storage.managed = createStorageAPI();
window.chrome.storage.local = createStorageAPI();
window.chrome.storage.sync = createStorageAPI();
// Other DOM.
window.navigator = {};
window.navigator.onLine = true;
window.navigator.userAgent =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/50.0.2661.94 Safari/537.36";
window.localStorage = {};
var getRandomValues_state = 0;
window.crypto = {};
window.crypto.getRandomValues = function (buf) {
var state = getRandomValues_state++;
for (var i = 0; i < buf.length; ++i) {
// Totally random byte ;)
buf[i] = 0x42 + state;
}
return buf;
};
// Network-related mocks.
window.Request = {};
window.Request.prototype = {
get mode() {
throw new TypeError("Illegal invocation");
},
};
window.fetch = function (url, options) {
assert.equal(url, LOG_URL);
assert.equal(options.method, "POST");
assert.equal(options.mode, "cors");
assert.ok(!options.body);
test_requests.push(options.headers);
};
window.Headers = function (headers) {
headers = JSON.parse(JSON.stringify(headers)); // Clone.
Object.keys(headers).forEach(function (k) {
headers[k] = String(headers[k]);
});
return headers;
};
window.XMLHttpRequest = function () {
var invoked = {
open: false,
send: false,
};
var headers = {};
return {
open(method, url) {
assert.equal(invoked.open, false);
invoked.open = true;
assert.equal(method, "POST");
assert.equal(url, LOG_URL);
},
setRequestHeader(k, v) {
assert.equal(invoked.open, true);
headers[k] = String(v);
},
send(body) {
assert.equal(invoked.open, true);
assert.equal(invoked.send, false);
invoked.send = true;
assert.ok(!body);
test_requests.push(headers);
},
};
};
// Time-related logic.
var timers = [];
window.setInterval = function (callback, ms) {
assert.equal(typeof callback, "function");
timers.push(callback);
};
window.Date = {
test_now_value: Date.now(),
now() {
return window.Date.test_now_value;
},
};
window.test_fireTimers = function () {
assert.ok(timers.length);
timers.forEach(function (timer) {
timer();
});
};
return window;
}
// Simulate an update of the browser.
function updateBrowser(window) {
window.navigator.userAgent = window.navigator.userAgent.replace(
/Chrome\/(\d+)/,
function (_, v) {
return "Chrome/" + (parseInt(v) + 1);
}
);
}
var tests = [
function test_first_run() {
// Default settings, run extension for the first time.
var window = createExtensionGlobal();
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
},
function test_first_run_incognito() {
// The extension should not send any requests when in incognito mode.
var window = createExtensionGlobal();
window.chrome.extension.inIncognitoContext = true;
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, []);
},
function test_storage_managed_unavailable() {
var window = createExtensionGlobal();
delete window.chrome.storage.managed;
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
},
function test_managed_pref() {
var window = createExtensionGlobal();
window.chrome.storage.managed.mock_data = {
disableTelemetry: true,
};
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, []);
},
function test_local_pref() {
var window = createExtensionGlobal();
window.chrome.storage.local.mock_data = {
disableTelemetry: true,
};
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, []);
},
function test_managed_pref_is_overridden() {
var window = createExtensionGlobal();
window.chrome.storage.managed.mock_data = {
disableTelemetry: true,
};
window.chrome.storage.sync.mock_data = {
disableTelemetry: false,
};
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
},
function test_run_extension_again() {
var window = createExtensionGlobal();
telemetryScript.runInNewContext(window);
telemetryScript.runInNewContext(window);
// Only one request should be sent because of rate-limiting.
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
// Simulate that quite some hours passed, but it's still rate-limited.
window.Date.test_now_value += 11 * 36e5;
telemetryScript.runInNewContext(window);
// Only one request should be sent because of rate-limiting.
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
// Another hour passes and the request should not be rate-limited any more.
window.Date.test_now_value += 1 * 36e5;
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
},
function test_running_for_a_while() {
var window = createExtensionGlobal();
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
// Simulate that the timer fired 11 hours since the last ping. The request
// should still be rate-limited.
window.Date.test_now_value += 11 * 36e5;
window.test_fireTimers();
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
// Another hour passes and the request should not be rate-limited any more.
window.Date.test_now_value += 1 * 36e5;
window.test_fireTimers();
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
},
function test_browser_update() {
var window = createExtensionGlobal();
telemetryScript.runInNewContext(window);
updateBrowser(window);
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
{
// Generate a new ID for better privacy.
"Deduplication-Id": "4343434343",
"Extension-Version": "1.0.0",
},
]);
},
function test_browser_update_between_pref_toggle() {
var window = createExtensionGlobal();
telemetryScript.runInNewContext(window);
window.chrome.storage.local.mock_data = {
disableTelemetry: true,
};
updateBrowser(window);
telemetryScript.runInNewContext(window);
window.chrome.storage.local.mock_data = {
disableTelemetry: false,
};
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
{
// Generate a new ID for better privacy, even if the update happened
// while telemetry was disabled.
"Deduplication-Id": "4343434343",
"Extension-Version": "1.0.0",
},
]);
},
function test_extension_update() {
var window = createExtensionGlobal();
telemetryScript.runInNewContext(window);
window.chrome.runtime.getManifest = function () {
return { version: "1.0.1" };
};
window.Date.test_now_value += 12 * 36e5;
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
{
// The ID did not change because the browser version did not change.
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.1",
},
]);
},
function test_unofficial_build() {
var window = createExtensionGlobal();
var didWarn = false;
window.console = {};
window.console.warn = function () {
didWarn = true;
};
window.chrome.runtime.id = "abcdefghijklmnopabcdefghijklmnop";
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, []);
assert.ok(didWarn);
},
function test_fetch_is_supported() {
var window = createExtensionGlobal();
// XMLHttpRequest should not be called when fetch is available. So removing
// the XMLHttpRequest API should not change behavior.
delete window.XMLHttpRequest;
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
},
function test_fetch_not_supported() {
var window = createExtensionGlobal();
delete window.fetch;
delete window.Request;
delete window.Headers;
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
},
function test_fetch_mode_not_supported() {
var window = createExtensionGlobal();
delete window.Request.prototype.mode;
window.fetch = function () {
throw new Error("Unexpected call to fetch!");
};
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
},
function test_network_offline() {
var window = createExtensionGlobal();
// Simulate that the network is down for sure.
window.navigator.onLine = false;
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, []);
// Simulate that the network might be up.
window.navigator.onLine = true;
telemetryScript.runInNewContext(window);
assert.deepEqual(window.test_requests, [
{
"Deduplication-Id": "4242424242",
"Extension-Version": "1.0.0",
},
]);
},
];
var test_i = 0;
(function next() {
var test = tests[test_i++];
if (!test) {
console.log("All tests completed.");
return;
}
console.log("Running test " + test_i + "/" + tests.length + ": " + test.name);
test();
process.nextTick(next);
})();

155
test/downloadutils.mjs Normal file
View File

@@ -0,0 +1,155 @@
/*
* Copyright 2014 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 crypto from "crypto";
import fs from "fs";
import http from "http";
import https from "https";
import { resolve as urlResolve } from "url";
function rewriteWebArchiveUrl(url) {
// Web Archive URLs need to be transformed to add `if_` after the ID.
// Without this, an HTML page containing an iframe with the PDF file
// will be served instead (issue 8920).
const webArchiveRegex =
/(^https?:\/\/web\.archive\.org\/web\/)(\d+)(\/https?:\/\/.+)/g;
const urlParts = webArchiveRegex.exec(url);
if (urlParts) {
return `${urlParts[1]}${urlParts[2]}if_${urlParts[3]}`;
}
return url;
}
function downloadFile(file, url, redirects = 0) {
url = rewriteWebArchiveUrl(url);
const protocol = /^https:\/\//.test(url) ? https : http;
return new Promise((resolve, reject) => {
protocol
.get(url, async function (response) {
if ([301, 302, 307, 308].includes(response.statusCode)) {
if (redirects > 10) {
response.resume();
reject(new Error("Too many redirects"));
return;
}
const redirectTo = urlResolve(url, response.headers.location);
try {
await downloadFile(file, redirectTo, ++redirects);
resolve();
} catch (ex) {
response.resume();
reject(ex);
}
return;
}
if (response.statusCode !== 200) {
response.resume();
reject(new Error(`HTTP ${response.statusCode}`));
return;
}
const stream = fs.createWriteStream(file);
stream.on("error", error => reject(error));
stream.on("finish", () => {
stream.end();
resolve();
});
response.pipe(stream);
})
.on("error", error => reject(error));
});
}
async function downloadManifestFiles(manifest) {
const links = manifest
.filter(item => item.link && !fs.existsSync(item.file))
.map(item => {
let url = fs.readFileSync(`${item.file}.link`).toString();
url = url.replace(/\s+$/, "");
return { file: item.file, url };
});
for (const { file, url } of links) {
console.log(`Downloading ${url} to ${file}...`);
try {
await downloadFile(file, url);
} catch (ex) {
console.error(`Error during downloading of ${url}:`, ex);
fs.writeFileSync(file, ""); // making it empty file
fs.writeFileSync(`${file}.error`, ex.toString());
}
}
}
function calculateMD5(file) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash("md5");
const stream = fs.createReadStream(file);
stream.on("data", data => hash.update(data));
stream.on("error", error => reject(error));
stream.on("end", () => resolve(hash.digest("hex")));
});
}
async function verifyManifestFiles(manifest) {
let error = false;
for (const item of manifest) {
if (fs.existsSync(`${item.file}.error`)) {
console.error(
`WARNING: "${item.file}" was not downloaded; see "${item.file}.error" file.`
);
error = true;
continue;
}
if (item.link && !fs.existsSync(`${item.file}.link`)) {
console.error(
`WARNING: Unneeded \`"link": true\`-entry for the "${item.id}" test.`
);
error = true;
continue;
}
try {
const md5 = await calculateMD5(item.file);
if (!item.md5) {
console.error(
`WARNING: MD5 hash missing for "${item.file}" (computed "${md5}").`
);
error = true;
} else if (md5 !== item.md5) {
console.error(
`WARNING: MD5 hash mismatch for "${item.file}" (expected "${item.md5}", computed "${md5}").`
);
error = true;
}
} catch (ex) {
console.log(
`WARNING: MD5 hash calculation failed for "${item.file}" ("${ex}").`
);
error = true;
}
}
if (error) {
throw new Error("Manifest validation failed");
}
}
export { downloadManifestFiles, verifyManifestFiles };

39
test/draw_layer_test.css Normal file
View File

@@ -0,0 +1,39 @@
/* Copyright 2023 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.
*/
/* Used in 'highlight' tests */
.highlight {
position: absolute;
mix-blend-mode: multiply;
fill-rule: evenodd;
}
.highlightOutline {
position: absolute;
mix-blend-mode: normal;
fill-rule: evenodd;
fill: none;
.mainOutline {
stroke: white;
stroke-width: 4px;
}
.secondaryOutline {
stroke: blue;
stroke-width: 2px;
}
}

1324
test/driver.js Normal file

File diff suppressed because it is too large Load Diff

36
test/font/README.md Normal file
View File

@@ -0,0 +1,36 @@
# Font tests
The font tests check if PDF.js can read font data correctly. For validation
the `ttx` tool (from the Python `fonttools` library) is used that can convert
font data to an XML format that we can easily use for assertions in the tests.
In the font tests we let PDF.js read font data and pass the PDF.js-interpreted
font data through `ttx` to check its correctness. The font tests are successful
if PDF.js can successfully read the font data and `ttx` can successfully read
the PDF.js-interpreted font data back, proving that PDF.js does not apply any
transformations that break the font data.
## Running the font tests
The font tests are run on GitHub Actions using the workflow defined in
`.github/workflows/font_tests.yml`, but it is also possible to run the font
tests locally. The current stable versions of the following dependencies are
required to be installed on the system:
- Python 3
- `fonttools` (see https://pypi.org/project/fonttools and https://github.com/fonttools/fonttools)
The recommended way of installing `fonttools` is using `pip` in a virtual
environment because it avoids having to do a system-wide installation and
therefore improves isolation, but any other way of installing `fonttools`
that makes `ttx` available in the `PATH` environment variable also works.
Using the virtual environment approach the font tests can be run locally by
creating and sourcing a virtual environment with `fonttools` installed in
it before running the font tests:
```
python3 -m venv venv
source venv/bin/activate
pip install fonttools
npx gulp fonttest
```

View File

@@ -0,0 +1,18 @@
import { decodeFontData, ttx, verifyTtxOutput } from "./fontutils.js";
describe("font1", function () {
const font1_1 = decodeFontData(
// eslint-disable-next-line max-len
"T1RUTwAJAIAAAwAQQ0ZGIP/t0rAAAACcAAADKU9TLzJDxycMAAADyAAAAGBjbWFwwFIBcgAABCgAAABUaGVhZKsnTJ4AAAR8AAAANmhoZWEDHvxTAAAEtAAAACRobXR4AAAAAAAABNgAAAA4bWF4cAAOUAAAAAUQAAAABm5hbWX8Fq+xAAAFGAAAAfhwb3N0AAMAAAAABxAAAAAgAQAEAgABAQEMS0hQRkxFK01UU1kAAQEBOfgeAPgfAfggAvghA/gXBIv+Tvqn+bAFHQAAAMgPHQAAAL0QHQAAANsRHQAAACcdAAADARL4IAwWAAcBAQgUGx5TV19yYWRpY2FsY2lyY2xlY29weXJ0c2ltaWxhcjEuMUNvcHlyaWdodCAoQykgMTk5MiwgMTk5MyBUaGUgVGVYcGxvcmF0b3JzIENvcnBvcmF0aW9uTVRTWU1hdGhUaW1lAAAAAAkAAg0YQ0RmZ3AAAKYAqAGIAYkADAAeAFwAXgGHAAoCAAEAAwAWAFoAtgDxARcBNgGKAd4CDiAO93W9Ad/4+AP5TPd1Fb38+FkHDvfslp/3PtH3Pp8B9xjR9zDQ9zDRFPz4P/eAFfd193UFRQb7UvtS+1L3UgVFBvd1+3X7dvt1BdIG91L3UvdS+1IF0gYO+MT7ZbP5vLMBw7P5vLMD+kT3fxX3iPtc91z7iPuI+1z7XPuI+4j3XPtc94j3iPdc91z3iB78UPwoFft0+0f3SPd093T3R/dI93T3dPdI+0j7dPt0+0j7SPt0Hw73Zb33Br0Bw/kwA/ln+C8VT3o8Lz8hMvc4+xYbP0E/WncfQIwH3KLi0Mb3AuL7OPcUG9nc272ZH9IHDjig97O997SfAfgBvQP5aPd1Fb37yffIWfvI+8lZ98n7yL33yAcO9MP3JsMBw/kwA/lo98cVw/0wUwf5MPteFcP9MFMHDkX7SaD4JJ/4JJ8B9yXVA/dv9w0V0n6yPZwejQfZnZiy0hr3PAfQn7HSmx6WByRNd/sLH/tGB0t7bEZ5HtB4m2xLGvtFB/sMyXfyHpYHRJt3sdAaDkX7SaD4JJ/4JJ8B9yvVA/d19xwVy5uq0J4eRp17qssa90UH9wxNnyQegAfSe59lRhr7PAdEmGTZeh6JBz15fmREGvs8B0Z3ZUR7HoAH8smf9wsfDvgq/k6g99/k+LCfAcD5yAP4Kf5OFZUG+F76fQVWBvwe/fT7cffE+yz7KJp23dsFDnie+GWenJD3K54G+2WiBx4KBI8MCb0KvQufqQwMqZ8MDfmgFPhMFQAAAAAAAwIkAfQABQAAAooCuwAAAIwCigK7AAAB3wAxAQIAAAAABgAAAAAAAAAAAAABEAAAAAAAAAAAAAAAKjIxKgAAAEPgBwMc/EYAZAMcA7oAAAAAAAAAAAAAAAAAAABDAAMAAAABAAMAAQAAAAwABABIAAAACgAIAAIAAgBEAGcAcOAH//8AAABDAGYAcOAA////wv+h/5kAAAABAAAAAAAAAAQAAAABAAEAAgACAAMAAwAEAAQAAQAAAAAQAAAAAABfDzz1AAAD6AAAAACeC34nAAAAAJ4LficAAPxGD/8DHAAAABEAAAAAAAAAAAABAAADHPxGAAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAOAAAAAAAUAPYAAQAAAAAAAAAQAAAAAQAAAAAAAQALABAAAQAAAAAAAgAHABsAAQAAAAAAAwAIACIAAQAAAAAABAALACoAAQAAAAAABQAMADUAAQAAAAAABgAAAEEAAQAAAAAABwAHAEEAAQAAAAAACAAHAEgAAQAAAAAACQAHAE8AAwABBAkAAAAgAFYAAwABBAkAAQAWAHYAAwABBAkAAgAOAIwAAwABBAkAAwAQAJoAAwABBAkABAAWAKoAAwABBAkABQAYAMAAAwABBAkABgAAANgAAwABBAkABwAOANgAAwABBAkACAAOAOYAAwABBAkACQAOAPRPcmlnaW5hbCBsaWNlbmNlS0hQRkxFK01UU1lVbmtub3dudW5pcXVlSURLSFBGTEUrTVRTWVZlcnNpb24gMC4xMVVua25vd25Vbmtub3duVW5rbm93bgBPAHIAaQBnAGkAbgBhAGwAIABsAGkAYwBlAG4AYwBlAEsASABQAEYATABFACsATQBUAFMAWQBVAG4AawBuAG8AdwBuAHUAbgBpAHEAdQBlAEkARABLAEgAUABGAEwARQArAE0AVABTAFkAVgBlAHIAcwBpAG8AbgAgADAALgAxADEAVQBuAGsAbgBvAHcAbgBVAG4AawBuAG8AdwBuAFUAbgBrAG4AbwB3AG4AAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
);
describe("test harness testing", function () {
it("returns output", async function () {
const output = await ttx(font1_1);
verifyTtxOutput(output);
expect(/<ttFont /.test(output)).toEqual(true);
expect(/<\/ttFont>/.test(output)).toEqual(true);
});
});
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

25
test/font/font_test.html Normal file
View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>PDF.js font tests</title>
<link rel="stylesheet" type="text/css" href="../../node_modules/jasmine-core/lib/jasmine-core/jasmine.css">
<script src="../../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="../../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script type="importmap">
{
"imports": {
"pdfjs/": "../../src/",
"pdfjs-lib": "../../src/pdf.js",
"pdfjs-web/": "../../web/",
"pdfjs-test/": "../"
}
}
</script>
<script src="jasmine-boot.js" type="module"></script>
</head>
<body>
</body>
</html>

48
test/font/fontutils.js Normal file
View File

@@ -0,0 +1,48 @@
/*
* Copyright 2013 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 { bytesToString, stringToBytes } from "../../src/shared/util.js";
function decodeFontData(base64) {
const str = atob(base64);
return stringToBytes(str);
}
function encodeFontData(data) {
const str = bytesToString(data);
return btoa(str);
}
async function ttx(data) {
const response = await fetch("/ttx", {
method: "POST",
body: encodeFontData(data),
});
if (!response.ok) {
throw new Error(response.statusText);
}
return response.text();
}
function verifyTtxOutput(output) {
const m = /^<error>(.*?)<\/error>/.exec(output);
if (m) {
throw m[1];
}
}
export { decodeFontData, encodeFontData, ttx, verifyTtxOutput };

158
test/font/jasmine-boot.js Normal file
View File

@@ -0,0 +1,158 @@
/* Copyright 2016 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.
*/
/*
Copyright (c) 2008-2016 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* globals jasmineRequire */
// Modified jasmine's boot.js file to load PDF.js libraries async.
"use strict";
import { TestReporter } from "../reporter.js";
async function initializePDFJS(callback) {
await Promise.all(
[
"pdfjs-test/font/font_core_spec.js",
"pdfjs-test/font/font_os2_spec.js",
"pdfjs-test/font/font_post_spec.js",
"pdfjs-test/font/font_fpgm_spec.js",
].map(moduleName => import(moduleName)) // eslint-disable-line no-unsanitized/method
);
callback();
}
(function () {
window.jasmine = jasmineRequire.core(jasmineRequire);
jasmineRequire.html(jasmine);
const env = jasmine.getEnv();
const jasmineInterface = jasmineRequire.interface(jasmine, env);
extend(window, jasmineInterface);
// Runner Parameters
const queryString = new jasmine.QueryString({
getWindowLocation() {
return window.location;
},
});
const config = {
failFast: queryString.getParam("failFast"),
oneFailurePerSpec: queryString.getParam("oneFailurePerSpec"),
hideDisabled: queryString.getParam("hideDisabled"),
};
const random = queryString.getParam("random");
if (random !== undefined && random !== "") {
config.random = random;
}
const seed = queryString.getParam("seed");
if (seed) {
config.seed = seed;
}
// Reporters
const htmlReporter = new jasmine.HtmlReporter({
env,
navigateWithNewParam(key, value) {
return queryString.navigateWithNewParam(key, value);
},
addToExistingQueryString(key, value) {
return queryString.fullStringWithNewParam(key, value);
},
getContainer() {
return document.body;
},
createElement() {
return document.createElement(...arguments);
},
createTextNode() {
return document.createTextNode(...arguments);
},
timer: new jasmine.Timer(),
});
env.addReporter(htmlReporter);
if (queryString.getParam("browser")) {
const testReporter = new TestReporter(queryString.getParam("browser"));
env.addReporter(testReporter);
}
// Filter which specs will be run by matching the start of the full name
// against the `spec` query param.
const specFilter = new jasmine.HtmlSpecFilter({
filterString() {
return queryString.getParam("spec");
},
});
config.specFilter = function (spec) {
return specFilter.matches(spec.getFullName());
};
env.configure(config);
// Sets longer timeout.
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
function extend(destination, source) {
for (const property in source) {
destination[property] = source[property];
}
return destination;
}
function fontTestInit() {
initializePDFJS(function () {
htmlReporter.initialize();
env.execute();
});
}
if (
document.readyState === "interactive" ||
document.readyState === "complete"
) {
fontTestInit();
} else {
document.addEventListener("DOMContentLoaded", fontTestInit, true);
}
})();

71
test/font/ttxdriver.mjs Normal file
View File

@@ -0,0 +1,71 @@
/*
* Copyright 2014 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 fs from "fs";
import os from "os";
import path from "path";
import { spawn } from "child_process";
let ttxTaskId = Date.now();
function runTtx(fontPath) {
return new Promise((resolve, reject) => {
const ttx = spawn("ttx", [fontPath], { stdio: "ignore" });
ttx.on("error", () => {
reject(
new Error(
"Unable to execute `ttx`; make sure the `fonttools` dependency is installed"
)
);
});
ttx.on("close", () => {
resolve();
});
});
}
async function translateFont(content) {
const buffer = Buffer.from(content, "base64");
const taskId = (ttxTaskId++).toString();
const fontPath = path.join(os.tmpdir(), `pdfjs-font-test-${taskId}.otf`);
const resultPath = path.join(os.tmpdir(), `pdfjs-font-test-${taskId}.ttx`);
// Write the font data to a temporary file on disk (because TTX only accepts
// files as input).
fs.writeFileSync(fontPath, buffer);
// Run TTX on the temporary font file.
let ttxError;
try {
await runTtx(fontPath);
} catch (error) {
ttxError = error;
}
// Remove the temporary font/result files and report on the outcome.
fs.unlinkSync(fontPath);
if (ttxError) {
throw ttxError;
}
if (!fs.existsSync(resultPath)) {
throw new Error("TTX did not generate output");
}
const xml = fs.readFileSync(resultPath);
fs.unlinkSync(resultPath);
return xml;
}
export { translateFont };

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

BIN
test/images/red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
test/images/samplesignature.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,305 @@
/* Copyright 2021 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,
loadAndWait,
waitForPageRendered,
} from "./test_utils.mjs";
const isStructTreeVisible = async page => {
await page.waitForSelector(".structTree");
return page.evaluate(() => {
let elem = document.querySelector(".structTree");
while (elem) {
if (elem.getAttribute("aria-hidden") === "true") {
return false;
}
elem = elem.parentElement;
}
return true;
});
};
describe("accessibility", () => {
describe("structure tree", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("structure_simple.pdf", ".structTree");
});
afterEach(async () => {
await closePages(pages);
});
it("must build structure that maps to text layer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
expect(await isStructTreeVisible(page))
.withContext(`In ${browserName}`)
.toBeTrue();
// Check the headings match up.
const head1 = await page.$eval(
".structTree [role='heading'][aria-level='1'] span",
el =>
document.getElementById(el.getAttribute("aria-owns")).textContent
);
expect(head1).withContext(`In ${browserName}`).toEqual("Heading 1");
const head2 = await page.$eval(
".structTree [role='heading'][aria-level='2'] span",
el =>
document.getElementById(el.getAttribute("aria-owns")).textContent
);
expect(head2).withContext(`In ${browserName}`).toEqual("Heading 2");
// Check the order of the content.
const texts = await page.$$eval(".structTree [aria-owns]", nodes =>
nodes.map(
el =>
document.getElementById(el.getAttribute("aria-owns"))
.textContent
)
);
expect(texts)
.withContext(`In ${browserName}`)
.toEqual([
"Heading 1",
"This paragraph 1.",
"Heading 2",
"This paragraph 2.",
]);
})
);
});
it("must check that the struct tree is still there after zooming", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
for (let i = 0; i < 8; i++) {
expect(await isStructTreeVisible(page))
.withContext(`In ${browserName}`)
.toBeTrue();
const handle = await waitForPageRendered(page);
await page.click(`#zoom${i < 4 ? "In" : "Out"}Button`);
await awaitPromise(handle);
}
})
);
});
});
describe("Annotation", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey_a11y.pdf",
".textLayer .endOfContent"
);
});
afterEach(async () => {
await closePages(pages);
});
function getSpans(page) {
return page.evaluate(() => {
const elements = document.querySelectorAll(
`.textLayer span[aria-owns]:not([role="presentation"])`
);
const results = [];
for (const element of elements) {
results.push(element.innerText);
}
return results;
});
}
it("must check that some spans are linked to some annotations thanks to aria-owns", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const spanContents = await getSpans(page);
expect(spanContents)
.withContext(`In ${browserName}`)
.toEqual(["Languages", "@intel.com", "Abstract", "Introduction"]);
})
);
});
});
describe("Annotations order", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("fields_order.pdf", ".annotationLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the text fields are in the visual order", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const ids = await page.evaluate(() => {
const elements = document.querySelectorAll(
".annotationLayer .textWidgetAnnotation"
);
const results = [];
for (const element of elements) {
results.push(element.getAttribute("data-annotation-id"));
}
return results;
});
expect(ids)
.withContext(`In ${browserName}`)
.toEqual(["32R", "30R", "31R", "34R", "29R", "33R"]);
})
);
});
});
describe("Stamp annotation accessibility", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tagged_stamp.pdf", ".annotationLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check the id in aria-controls", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(".annotationLayer");
const stampId = "pdfjs_internal_id_20R";
await page.click(`#${stampId}`);
const controlledId = await page.$eval(
"#pdfjs_internal_id_21R",
el => document.getElementById(el.getAttribute("aria-controls")).id
);
expect(controlledId)
.withContext(`In ${browserName}`)
.toEqual(stampId);
})
);
});
it("must check the aria-label linked to the stamp annotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(".annotationLayer");
const ariaLabel = await page.$eval(
".annotationLayer section[role='img']",
el => el.getAttribute("aria-label")
);
expect(ariaLabel)
.withContext(`In ${browserName}`)
.toEqual("Secondary text for stamp");
})
);
});
it("must check that the stamp annotation is linked to the struct tree", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(".structTree");
const isLinkedToStampAnnotation = await page.$eval(
".structTree [role='figure']",
el =>
document
.getElementById(el.getAttribute("aria-owns"))
.classList.contains("stampAnnotation")
);
expect(isLinkedToStampAnnotation)
.withContext(`In ${browserName}`)
.toEqual(true);
})
);
});
});
describe("Figure in the content stream", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1708040.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that an image is correctly inserted in the text layer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
expect(await isStructTreeVisible(page))
.withContext(`In ${browserName}`)
.toBeTrue();
const spanId = await page.evaluate(() => {
const el = document.querySelector(
`.structTree span[role="figure"]`
);
return el.getAttribute("aria-owns") || null;
});
expect(spanId).withContext(`In ${browserName}`).not.toBeNull();
const ariaLabel = await page.evaluate(id => {
const img = document.querySelector(`#${id} > span[role="img"]`);
return img.getAttribute("aria-label");
}, spanId);
expect(ariaLabel)
.withContext(`In ${browserName}`)
.toEqual("A logo of a fox and a globe");
})
);
});
});
describe("No undefined id", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue20102.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that span hasn't an 'undefined' id", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const id = await page.$eval("span.markedContent", span => span.id);
expect(id).withContext(`In ${browserName}`).toBe("");
})
);
});
});
});

View File

@@ -0,0 +1,881 @@
/* Copyright 2020 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,
getAnnotationSelector,
getQuerySelector,
getRect,
getSelector,
loadAndWait,
} from "./test_utils.mjs";
describe("Annotation highlight", () => {
describe("annotation-highlight.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"annotation-highlight.pdf",
getAnnotationSelector("19R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check the popup position in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const highlightSelector = getAnnotationSelector("19R");
const popupSelector = getAnnotationSelector("21R");
const areSiblings = await page.evaluate(
(highlightSel, popupSel) => {
const highlight = document.querySelector(highlightSel);
const popup = document.querySelector(popupSel);
return highlight.nextElementSibling === popup;
},
highlightSelector,
popupSelector
);
expect(areSiblings).withContext(`In ${browserName}`).toEqual(true);
})
);
});
it("must show a popup on mouseover", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
let hidden = await page.$eval(
getAnnotationSelector("21R"),
el => el.hidden
);
expect(hidden).withContext(`In ${browserName}`).toEqual(true);
await page.hover(getAnnotationSelector("19R"));
await page.waitForSelector(getAnnotationSelector("21R"), {
visible: true,
timeout: 0,
});
hidden = await page.$eval(
getAnnotationSelector("21R"),
el => el.hidden
);
expect(hidden).withContext(`In ${browserName}`).toEqual(false);
})
);
});
});
describe("Check that widget annotations are in front of highlight ones", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1883609.pdf", getAnnotationSelector("23R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must click on widget annotations", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
for (const i of [23, 22, 14]) {
await page.click(getAnnotationSelector(`${i}R`));
await page.waitForSelector(`#pdfjs_internal_id_${i}R:focus`);
}
})
);
});
});
});
describe("Checkbox annotation", () => {
describe("issue12706.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue12706.pdf", getAnnotationSelector("63R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must let checkboxes with the same name behave like radio buttons", async () => {
const selectors = [63, 70, 79].map(n => getAnnotationSelector(`${n}R`));
await Promise.all(
pages.map(async ([browserName, page]) => {
for (const selector of selectors) {
await page.click(selector);
await page.waitForFunction(
`document.querySelector('${selector} > :first-child').checked`
);
for (const otherSelector of selectors) {
const checked = await page.$eval(
`${otherSelector} > :first-child`,
el => el.checked
);
expect(checked)
.withContext(`In ${browserName}`)
.toBe(selector === otherSelector);
}
}
})
);
});
});
describe("issue15597.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue15597.pdf", getAnnotationSelector("7R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must check the checkbox", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selector = getAnnotationSelector("7R");
await page.click(selector);
await page.waitForFunction(
`document.querySelector('${selector} > :first-child').checked`
);
expect(true).withContext(`In ${browserName}`).toEqual(true);
})
);
});
});
describe("bug1847733.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1847733.pdf", getAnnotationSelector("18R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must check the checkbox", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selectors = [18, 30, 42, 54].map(id =>
getAnnotationSelector(`${id}R`)
);
for (const selector of selectors) {
await page.click(selector);
await page.waitForFunction(
`document.querySelector('${selector} > :first-child').checked`
);
}
})
);
});
});
});
describe("Text widget", () => {
describe("issue13271.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue13271.pdf", getAnnotationSelector("24R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must update all the fields with the same value", async () => {
const base = "hello world";
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.type(getSelector("25R"), base);
await page.waitForFunction(`${getQuerySelector("24R")}.value !== ""`);
await page.waitForFunction(`${getQuerySelector("26R")}.value !== ""`);
let text = await page.$eval(getSelector("24R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual(base);
text = await page.$eval(getSelector("26R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual(base);
})
);
});
});
describe("issue16473.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue16473.pdf", getAnnotationSelector("22R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must reset a formatted value after a change", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.type(getSelector("22R"), "a");
await page.keyboard.press("Tab");
await page.waitForFunction(
`${getQuerySelector("22R")}.value !== "Hello world"`
);
const text = await page.$eval(getSelector("22R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("aHello World");
})
);
});
});
});
describe("Link annotations with internal destinations", () => {
describe("bug1708041.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1708041.pdf",
".page[data-page-number='1'] .annotationLayer"
);
});
afterEach(async () => {
await closePages(pages);
});
it("must click on a link and check if it navigates to the correct page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const pageOneSelector = ".page[data-page-number='1']";
const linkSelector = `${pageOneSelector} #pdfjs_internal_id_42R`;
await page.waitForSelector(linkSelector);
const linkTitle = await page.$eval(linkSelector, el => el.title);
expect(linkTitle)
.withContext(`In ${browserName}`)
.toEqual("Go to the last page");
await page.click(linkSelector);
const pageSixTextLayerSelector =
".page[data-page-number='6'] .textLayer";
await page.waitForSelector(pageSixTextLayerSelector, {
visible: true,
});
await page.waitForFunction(
sel => {
const textLayer = document.querySelector(sel);
return document.activeElement === textLayer;
},
{},
pageSixTextLayerSelector
);
})
);
});
});
});
describe("Annotation and storage", () => {
describe("issue14023.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue14023.pdf", getAnnotationSelector("64R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must let checkboxes with the same name behave like radio buttons", async () => {
const text1 = "hello world!";
const text2 = "!dlrow olleh";
await Promise.all(
pages.map(async ([browserName, page]) => {
// Text field.
await page.type(getSelector("64R"), text1);
// Checkbox.
await page.click(getAnnotationSelector("65R"));
// Radio.
await page.click(getAnnotationSelector("67R"));
for (const [pageNumber, textId, checkId, radio1Id, radio2Id] of [
[2, "18R", "19R", "21R", "20R"],
[5, "23R", "24R", "22R", "25R"],
]) {
await page.evaluate(n => {
window.document
.querySelectorAll(`[data-page-number="${n}"][class="page"]`)[0]
.scrollIntoView();
}, pageNumber);
// Need to wait to have a displayed text input.
await page.waitForSelector(getSelector(textId), {
timeout: 0,
});
const text = await page.$eval(getSelector(textId), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual(text1);
let checked = await page.$eval(
getSelector(checkId),
el => el.checked
);
expect(checked).toEqual(true);
checked = await page.$eval(getSelector(radio1Id), el => el.checked);
expect(checked).toEqual(false);
checked = await page.$eval(getSelector(radio2Id), el => el.checked);
expect(checked).toEqual(false);
}
// Change data on page 5 and check that other pages changed.
// Text field.
await page.type(getSelector("23R"), text2);
// Checkbox.
await page.click(getAnnotationSelector("24R"));
// Radio.
await page.click(getAnnotationSelector("25R"));
for (const [pageNumber, textId, checkId, radio1Id, radio2Id] of [
[1, "64R", "65R", "67R", "68R"],
[2, "18R", "19R", "21R", "20R"],
]) {
await page.evaluate(n => {
window.document
.querySelectorAll(`[data-page-number="${n}"][class="page"]`)[0]
.scrollIntoView();
}, pageNumber);
// Need to wait to have a displayed text input.
await page.waitForSelector(getSelector(textId), {
timeout: 0,
});
const text = await page.$eval(getSelector(textId), el => el.value);
expect(text)
.withContext(`In ${browserName}`)
.toEqual(text2 + text1);
let checked = await page.$eval(
getSelector(checkId),
el => el.checked
);
expect(checked).toEqual(false);
checked = await page.$eval(getSelector(radio1Id), el => el.checked);
expect(checked).toEqual(false);
checked = await page.$eval(getSelector(radio2Id), el => el.checked);
expect(checked).toEqual(false);
}
})
);
});
});
});
describe("ResetForm action", () => {
describe("resetform.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("resetform.pdf", getAnnotationSelector("63R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must reset all fields", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const base = "hello world";
for (let i = 63; i <= 67; i++) {
await page.type(getSelector(`${i}R`), base);
}
const selectors = [69, 71, 75].map(n =>
getAnnotationSelector(`${n}R`)
);
for (const selector of selectors) {
await page.click(selector);
}
await page.select(getSelector("78R"), "b");
await page.select(getSelector("81R"), "f");
await page.click(getAnnotationSelector("82R"));
await page.waitForFunction(`${getQuerySelector("63R")}.value === ""`);
for (let i = 63; i <= 68; i++) {
const text = await page.$eval(getSelector(`${i}R`), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("");
}
const ids = [69, 71, 72, 73, 74, 75, 76, 77];
for (const id of ids) {
const checked = await page.$eval(
getSelector(`${id}R`),
el => el.checked
);
expect(checked).withContext(`In ${browserName}`).toEqual(false);
}
let selected = await page.$eval(
`${getSelector("78R")} [value="a"]`,
el => el.selected
);
expect(selected).withContext(`In ${browserName}`).toEqual(true);
selected = await page.$eval(
`${getSelector("81R")} [value="d"]`,
el => el.selected
);
expect(selected).withContext(`In ${browserName}`).toEqual(true);
})
);
});
it("must reset some fields", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const base = "hello world";
for (let i = 63; i <= 68; i++) {
await page.type(getSelector(`${i}R`), base);
}
const selectors = [69, 71, 72, 73, 75].map(n =>
getAnnotationSelector(`${n}R`)
);
for (const selector of selectors) {
await page.click(selector);
}
await page.select(getSelector("78R"), "b");
await page.select(getSelector("81R"), "f");
await page.click(getAnnotationSelector("84R"));
await page.waitForFunction(`${getQuerySelector("63R")}.value === ""`);
for (let i = 63; i <= 68; i++) {
const expected = (i - 3) % 2 === 0 ? "" : base;
const text = await page.$eval(getSelector(`${i}R`), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual(expected);
}
let ids = [69, 72, 73, 74, 76, 77];
for (const id of ids) {
const checked = await page.$eval(
getSelector(`${id}R`),
el => el.checked
);
expect(checked)
.withContext(`In ${browserName + id}`)
.toEqual(false);
}
ids = [71, 75];
for (const id of ids) {
const checked = await page.$eval(
getSelector(`${id}R`),
el => el.checked
);
expect(checked).withContext(`In ${browserName}`).toEqual(true);
}
let selected = await page.$eval(
`${getSelector("78R")} [value="a"]`,
el => el.selected
);
expect(selected).withContext(`In ${browserName}`).toEqual(true);
selected = await page.$eval(
`${getSelector("81R")} [value="f"]`,
el => el.selected
);
expect(selected).withContext(`In ${browserName}`).toEqual(true);
})
);
});
});
describe("FreeText widget", () => {
describe("issue14438.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"issue14438.pdf",
getAnnotationSelector("10R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the FreeText annotation has a popup", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selector = getAnnotationSelector("10R");
await page.click(selector);
await page.waitForFunction(
`document.querySelector('${selector}').hidden === false`
);
})
);
});
});
});
describe("Ink widget and its popup after editing", () => {
describe("annotation-caret-ink.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"annotation-caret-ink.pdf",
getAnnotationSelector("25R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the Ink annotation has a popup", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selector = getAnnotationSelector("25R");
await page.waitForFunction(
`document.querySelector('${selector}').hidden === false`
);
await page.click("#editorFreeText");
await page.waitForFunction(
`document.querySelector('${selector}').hidden === true`
);
await page.click("#editorFreeText");
await page.waitForFunction(
`document.querySelector('${selector}').hidden === false`
);
})
);
});
});
});
describe("Don't use AP when /NeedAppearances is true", () => {
describe("bug1844583.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1844583.pdf",
getAnnotationSelector("8R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check the content of the text field", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(getSelector("8R"), el => el.value);
expect(text)
.withContext(`In ${browserName}`)
.toEqual("Hello World");
})
);
});
});
});
describe("Toggle popup with keyboard", () => {
describe("tagged_stamp.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tagged_stamp.pdf",
getAnnotationSelector("20R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the popup has the correct visibility", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selector = getAnnotationSelector("21R");
let hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(true);
await page.focus(getAnnotationSelector("20R"));
await page.keyboard.press("Enter");
await page.waitForFunction(
`document.querySelector('${selector}').hidden !== true`
);
hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(false);
await page.keyboard.press("Enter");
await page.waitForFunction(
`document.querySelector('${selector}').hidden !== false`
);
hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(true);
await page.keyboard.press("Enter");
await page.waitForFunction(
`document.querySelector('${selector}').hidden !== true`
);
hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(false);
await page.keyboard.press("Escape");
await page.waitForFunction(
`document.querySelector('${selector}').hidden !== false`
);
hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(true);
})
);
});
});
});
describe("Annotation with empty popup and aria", () => {
describe("issue14438.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"highlights.pdf",
getAnnotationSelector("693R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the highlight annotation has no popup and no aria-haspopup attribute", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const highlightSelector = getAnnotationSelector("693R");
const popupSelector = getAnnotationSelector("694R");
await page.waitForFunction(
// No aria-haspopup attribute,
`document.querySelector('${highlightSelector}').ariaHasPopup === null ` +
// and no popup.
`&& document.querySelector('${popupSelector}') === null`
);
})
);
});
});
});
describe("Rotated annotation and its clickable area", () => {
describe("rotated_ink.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"rotated_ink.pdf",
getAnnotationSelector("18R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the clickable area has been rotated", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const rect = await getRect(page, getAnnotationSelector("18R"));
const promisePopup = page.waitForSelector(
getAnnotationSelector("19R"),
{ visible: true }
);
await page.mouse.move(
rect.x + rect.width * 0.1,
rect.y + rect.height * 0.9
);
await promisePopup;
})
);
});
});
});
describe("Text under some annotations", () => {
describe("bug1885505.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1885505.pdf",
":is(" +
[56, 58, 60, 65]
.map(id => getAnnotationSelector(`${id}R`))
.join(", ") +
")"
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the text under a highlight annotation exist in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(
`${getAnnotationSelector("56R")} mark`,
el => el.textContent
);
expect(text).withContext(`In ${browserName}`).toEqual("Languages");
})
);
});
it("must check that the text under an underline annotation exist in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(
`${getAnnotationSelector("58R")} u`,
el => el.textContent
);
expect(text).withContext(`In ${browserName}`).toEqual("machine");
})
);
});
it("must check that the text under a squiggly annotation exist in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(
`${getAnnotationSelector("60R")} u`,
el => el.textContent
);
expect(text).withContext(`In ${browserName}`)
.toEqual(`paths through nested loops. We have implemented
a dynamic compiler for JavaScript based on our`);
})
);
});
it("must check that the text under a strikeout annotation exist in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(
`${getAnnotationSelector("65R")} s`,
el => el.textContent
);
expect(text)
.withContext(`In ${browserName}`)
.toEqual("Experimentation,");
})
);
});
});
});
describe("Annotation without popup and enableComment set to true", () => {
describe("annotation-text-without-popup.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"annotation-text-without-popup.pdf",
getAnnotationSelector("4R"),
"page-fit",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the popup is shown", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const rect = await getRect(page, getAnnotationSelector("4R"));
// Hover the annotation, the popup should be visible.
let promisePopup = page.waitForSelector("#commentPopup", {
visible: true,
});
await page.mouse.move(
rect.x + rect.width / 2,
rect.y + rect.height / 2
);
await promisePopup;
// Move the mouse away, the popup should be hidden.
promisePopup = page.waitForSelector("#commentPopup", {
visible: false,
});
await page.mouse.move(
rect.x - rect.width / 2,
rect.y - rect.height / 2
);
await promisePopup;
// Click the annotation, the popup should be visible.
promisePopup = page.waitForSelector("#commentPopup", {
visible: true,
});
await page.mouse.click(
rect.x + rect.width / 2,
rect.y + rect.height / 2
);
await promisePopup;
// Click again, the popup should be hidden.
promisePopup = page.waitForSelector("#commentPopup", {
visible: false,
});
await page.mouse.click(
rect.x + rect.width / 2,
rect.y + rect.height / 2
);
await promisePopup;
})
);
});
});
});
});

View File

@@ -0,0 +1,262 @@
/* Copyright 2025 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,
} from "./test_utils.mjs";
function waitForLinkAnnotations(page, pageNumber) {
return page.evaluateHandle(
number => [
new Promise(resolve => {
const { eventBus } = window.PDFViewerApplication;
eventBus.on("linkannotationsadded", function listener(e) {
if (number === undefined || e.pageNumber === number) {
resolve();
eventBus.off("linkannotationsadded", listener);
}
});
}),
],
pageNumber
);
}
function recordInitialLinkAnnotationsEvent(eventBus) {
globalThis.initialLinkAnnotationsEventFired = false;
eventBus.on(
"linkannotationsadded",
() => {
globalThis.initialLinkAnnotationsEventFired = true;
},
{ once: true }
);
}
function waitForInitialLinkAnnotations(page) {
return createPromise(page, resolve => {
if (globalThis.initialLinkAnnotationsEventFired) {
resolve();
return;
}
window.PDFViewerApplication.eventBus.on("linkannotationsadded", resolve, {
once: true,
});
});
}
describe("autolinker", function () {
describe("bug1019475_2.pdf", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1019475_2.pdf",
".annotationLayer",
null,
null,
{
enableAutoLinking: true,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must appropriately add link annotations when relevant", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForLinkAnnotations(page);
const url = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations => annotations.map(a => a.href)
);
expect(url.length).withContext(`In ${browserName}`).toEqual(1);
expect(url[0])
.withContext(`In ${browserName}`)
.toEqual("http://www.mozilla.org/");
})
);
});
});
describe("bug1019475_1.pdf", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1019475_1.pdf",
".annotationLayer",
null,
null,
{
enableAutoLinking: true,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must not add links when unnecessary", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForLinkAnnotations(page);
const linkIds = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations =>
annotations.map(a => a.getAttribute("data-element-id"))
);
expect(linkIds.length).withContext(`In ${browserName}`).toEqual(3);
linkIds.forEach(id =>
expect(id)
.withContext(`In ${browserName}`)
.not.toContain("inferred_link_")
);
})
);
});
});
describe("pr19449.pdf", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait("pr19449.pdf", ".annotationLayer", null, null, {
docBaseUrl: "http://example.com",
enableAutoLinking: true,
});
});
afterEach(async () => {
await closePages(pages);
});
it("must not add links that overlap even if the URLs are different", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForLinkAnnotations(page);
const linkIds = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations =>
annotations.map(a => a.getAttribute("data-element-id"))
);
expect(linkIds.length).withContext(`In ${browserName}`).toEqual(1);
linkIds.forEach(id =>
expect(id)
.withContext(`In ${browserName}`)
.not.toContain("inferred_link_")
);
})
);
});
});
describe("PR 19470", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1019475_2.pdf",
".annotationLayer",
null,
null,
{
enableAutoLinking: true,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must not repeatedly add link annotations redundantly", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForLinkAnnotations(page);
let url = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations => annotations.map(a => a.href)
);
expect(url.length).withContext(`In ${browserName}`).toEqual(1);
await page.evaluate(() =>
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 2,
})
);
await waitForLinkAnnotations(page);
url = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations => annotations.map(a => a.href)
);
expect(url.length).withContext(`In ${browserName}`).toEqual(1);
})
);
});
});
describe("when highlighting search results", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"issue3115r.pdf",
".annotationLayer",
null,
{ eventBusSetup: recordInitialLinkAnnotationsEvent },
{ enableAutoLinking: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must find links that overlap with search results", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await awaitPromise(await waitForInitialLinkAnnotations(page));
const linkAnnotationsPromise = await waitForLinkAnnotations(page, 36);
// Search for "rich.edu"
await page.click("#viewFindButton");
await page.waitForSelector("#viewFindButton", { hidden: false });
await page.type("#findInput", "rich.edu");
await page.waitForSelector(".textLayer .highlight");
await awaitPromise(linkAnnotationsPromise);
const urls = await page.$$eval(
".page[data-page-number='36'] > .annotationLayer > .linkAnnotation > a",
annotations => annotations.map(a => a.href)
);
expect(urls)
.withContext(`In ${browserName}`)
.toContain(jasmine.stringContaining("rich.edu"));
})
);
});
});
});

View File

@@ -0,0 +1,97 @@
/* Copyright 2021 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, getRect, loadAndWait } from "./test_utils.mjs";
const waitForSelectionChange = (page, selection) =>
page.waitForFunction(
// We need to replace EOL on Windows to make the test pass.
sel => document.getSelection().toString().replaceAll("\r\n", "\n") === sel,
{},
selection
);
describe("Caret browsing", () => {
describe("Selection", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("must move the caret down and check the selection", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const spanRect = await getRect(
page,
`.page[data-page-number="1"] > .textLayer > span`
);
await page.mouse.click(
spanRect.x + 1,
spanRect.y + spanRect.height / 2,
{ count: 2 }
);
await page.keyboard.down("Shift");
for (let i = 0; i < 6; i++) {
await page.keyboard.press("ArrowRight");
}
await page.keyboard.up("Shift");
await waitForSelectionChange(page, "Trace-based");
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Shift");
// The caret is just before Languages.
await waitForSelectionChange(
page,
"Trace-based Just-in-Time Type Specialization for Dynamic\n"
);
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Shift");
// The caret is just before Mike Shaver.
await waitForSelectionChange(
page,
"Trace-based Just-in-Time Type Specialization for Dynamic\nLanguages\nAndreas Gal+, Brendan Eich, "
);
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowUp");
await page.keyboard.up("Shift");
// The caret is just before Languages.
await waitForSelectionChange(
page,
"Trace-based Just-in-Time Type Specialization for Dynamic\n"
);
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowUp");
await page.keyboard.up("Shift");
// The caret is in the middle of Time.
await waitForSelectionChange(page, "Trace-based Just-in-Tim");
})
);
});
});
});

View File

@@ -0,0 +1,627 @@
/* Copyright 2025 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,
dragAndDrop,
getEditorSelector,
getRect,
getSpanRectFromText,
loadAndWait,
scrollIntoView,
selectEditor,
switchToEditor,
waitAndClick,
waitForSerialized,
} from "./test_utils.mjs";
const switchToHighlight = switchToEditor.bind(null, "Highlight");
const switchToStamp = switchToEditor.bind(null, "Stamp");
const switchToComment = switchToEditor.bind(null, "Comment");
const highlightSpan = async (page, pageIndex, text) => {
const rect = await getSpanRectFromText(page, pageIndex, text);
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
};
const editComment = async (page, editorSelector, comment) => {
const commentButtonSelector = `${editorSelector} button.comment`;
await waitAndClick(page, commentButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, comment);
await waitAndClick(page, "#commentManagerSaveButton");
await page.waitForSelector("#commentManagerDialog", {
visible: false,
});
};
describe("Comment", () => {
describe("Comment edit dialog must be visible in ltr", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1989304.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must set the comment dialog in the viewport (LTR)", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
await scrollIntoView(page, ".textLayer span:last-of-type");
const rect = await getSpanRectFromText(page, 1, "...");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
// Here and elsewhere, we add a small delay between press and release
// to make sure that a pointerup event is triggered after
// selectionchange.
// It works with a value of 1ms, but we use 100ms to be sure.
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
const commentButtonSelector = `${getEditorSelector(0)} button.comment`;
await waitAndClick(page, commentButtonSelector);
await page.waitForSelector("#commentManagerDialog", {
visible: true,
});
const dialogRect = await getRect(page, "#commentManagerDialog");
const viewport = await page.evaluate(() => ({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
}));
expect(dialogRect.x + dialogRect.width)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(viewport.width + 1);
expect(dialogRect.y + dialogRect.height)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(viewport.height + 1);
})
);
});
});
describe("Comment edit dialog must be visible in rtl", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1989304.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true, localeProperties: "ar" }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must set the comment dialog in the viewport (RTL)", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
await scrollIntoView(page, ".textLayer span:nth-of-type(4)");
const rect = await getSpanRectFromText(page, 1, "World");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
const commentButtonSelector = `${getEditorSelector(0)} button.comment`;
await waitAndClick(page, commentButtonSelector);
await page.waitForSelector("#commentManagerDialog", {
visible: true,
});
const dialogRect = await getRect(page, "#commentManagerDialog");
const viewport = await page.evaluate(() => ({
height: window.innerHeight,
}));
expect(dialogRect.x + dialogRect.width)
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(-1);
expect(dialogRect.y + dialogRect.height)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(viewport.height + 1);
})
);
});
});
describe("Update comment position and color in reading mode", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"comments.pdf",
".annotationEditorLayer",
"page-fit",
null,
{
enableComment: true,
highlightEditorColors:
"yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0000",
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must set the comment button at the right place", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
const stampSelector = getEditorSelector(8);
await selectEditor(page, stampSelector);
await dragAndDrop(page, stampSelector, [[100, 100]]);
await waitForSerialized(page, 1);
const rectCommentButton = await getRect(
page,
`${stampSelector} .annotationCommentButton`
);
await switchToStamp(page, /* disable = */ true);
const rectCommentButtonAfter = await getRect(
page,
`#pdfjs_internal_id_713R + .annotationCommentButton`
);
expect(Math.abs(rectCommentButtonAfter.x - rectCommentButton.x))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(Math.abs(rectCommentButtonAfter.y - rectCommentButton.y))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(
Math.abs(rectCommentButtonAfter.width - rectCommentButton.width)
)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(
Math.abs(rectCommentButtonAfter.height - rectCommentButton.height)
)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
})
);
});
it("must set the right color to the comment button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const highlightSelector = getEditorSelector(0);
await selectEditor(page, highlightSelector);
const colorButtonSelector = `${highlightSelector} .editToolbar button`;
await page.waitForSelector(`${colorButtonSelector}.colorPicker`);
await page.click(`${colorButtonSelector}.colorPicker`);
await page.waitForSelector(`${colorButtonSelector}[title = "Red"]`);
await page.click(`${colorButtonSelector}[title = "Red"]`);
await page.waitForSelector(
`.page[data-page-number = "1"] svg.highlight[fill = "#FF0000"]`
);
const commentButtonColor = await page.evaluate(selector => {
const button = document.querySelector(
`${selector} .annotationCommentButton`
);
return window.getComputedStyle(button).backgroundColor;
}, highlightSelector);
await switchToHighlight(page, /* disable = */ true);
const commentButtonColorAfter = await page.evaluate(() => {
const button = document.querySelector(
"section[data-annotation-id='612R'] + .annotationCommentButton"
);
return window.getComputedStyle(button).backgroundColor;
});
expect(commentButtonColorAfter)
.withContext(`In ${browserName}`)
.toEqual(commentButtonColor);
})
);
});
});
describe("Comment buttons", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the comment button has a title", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Languages");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
let commentButtonSelector = `${getEditorSelector(0)} button.comment`;
await page.waitForSelector(commentButtonSelector, { visible: true });
let title = await page.evaluate(
selector => document.querySelector(selector).title,
commentButtonSelector
);
expect(title)
.withContext(`In ${browserName}`)
.toEqual("Edit comment");
await page.click(commentButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, "Hello world!");
await page.click("#commentManagerSaveButton");
commentButtonSelector = `${getEditorSelector(0)} button.annotationCommentButton`;
await page.waitForSelector(commentButtonSelector, {
visible: true,
});
title = await page.evaluate(selector => {
const button = document.querySelector(selector);
return button.title;
}, commentButtonSelector);
expect(title)
.withContext(`In ${browserName}`)
.toEqual("Show comment");
})
);
});
it("must check that the comment button is added in the annotation layer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
const comment = "Hello world!";
await editComment(page, getEditorSelector(0), comment);
await page.hover("#editorHighlightButton");
let buttonSelector =
".annotationEditorLayer .annotationCommentButton";
await page.waitForSelector(buttonSelector, { visible: true });
await page.hover(buttonSelector);
const popupSelector = "#commentPopup";
await page.waitForSelector(popupSelector, {
visible: true,
});
let popupText = await page.evaluate(
selector => document.querySelector(selector).textContent,
`${popupSelector} .commentPopupText`
);
expect(popupText).withContext(`In ${browserName}`).toEqual(comment);
await page.hover("#editorHighlightButton");
await switchToHighlight(page, /* disable = */ true);
buttonSelector = ".annotationLayer .annotationCommentButton";
await page.waitForSelector(buttonSelector, {
visible: true,
});
await page.hover(buttonSelector);
await page.waitForSelector(popupSelector, {
visible: true,
});
popupText = await page.evaluate(
selector => document.querySelector(selector).textContent,
`${popupSelector} .commentPopupText`
);
expect(popupText).withContext(`In ${browserName}`).toEqual(comment);
})
);
});
});
describe("Focused element after editing", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the focus is moved on the comment button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Languages");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
const commentButtonSelector = `${getEditorSelector(0)} button.comment`;
await waitAndClick(page, commentButtonSelector);
await page.waitForSelector("#commentManagerCancelButton", {
visible: true,
});
const handle = await createPromise(page, resolve => {
document
.querySelector("button.comment")
.addEventListener("focus", resolve, { once: true });
});
await page.click("#commentManagerCancelButton");
await awaitPromise(handle);
await waitAndClick(page, commentButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, "Hello world!");
await page.click("#commentManagerSaveButton");
await page.waitForSelector("button.annotationCommentButton", {
visible: true,
});
await page.waitForFunction(
sel => document.activeElement === document.querySelector(sel),
{},
"button.annotationCommentButton"
);
})
);
});
});
describe("Focused element after editing in reading mode", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"comments.pdf",
".annotationLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the focus is moved on the comment button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const commentButtonSelector = `[data-annotation-id="612R"] + button.annotationCommentButton`;
await waitAndClick(page, commentButtonSelector);
const commentPopupSelector = "#commentPopup";
const editButtonSelector = `${commentPopupSelector} button.commentPopupEdit`;
await waitAndClick(page, editButtonSelector);
await page.waitForSelector("#commentManagerCancelButton", {
visible: true,
});
let handle = await createPromise(page, resolve => {
document
.querySelector(
`[data-annotation-id="612R"] + button.annotationCommentButton`
)
.addEventListener("focus", resolve, { once: true });
});
await page.click("#commentManagerCancelButton");
await awaitPromise(handle);
await waitAndClick(page, commentButtonSelector);
await waitAndClick(page, editButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, "Hello world!");
handle = await createPromise(page, resolve => {
document
.querySelector(
`[data-annotation-id="612R"] + button.annotationCommentButton`
)
.addEventListener("focus", resolve, { once: true });
});
await page.click("#commentManagerSaveButton");
await awaitPromise(handle);
})
);
});
});
describe("Comment sidebar", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"comments.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the comment sidebar is resizable", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToComment(page);
const sidebarSelector = "#editorCommentParamsToolbar";
for (const extraWidth of [100, -100]) {
const rect = await getRect(page, sidebarSelector);
const resizerRect = await getRect(
page,
"#editorCommentsSidebarResizer"
);
const startX = resizerRect.x + resizerRect.width / 2;
const startY = resizerRect.y + 2;
await page.mouse.move(startX, startY);
await page.mouse.down();
const steps = 20;
await page.mouse.move(startX - extraWidth, startY, { steps });
await page.mouse.up();
const rectAfter = await getRect(page, sidebarSelector);
expect(Math.abs(rectAfter.width - (rect.width + extraWidth)))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(Math.abs(rectAfter.x - (rect.x - extraWidth)))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
}
})
);
});
it("must check that comments are in chronological order", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToComment(page);
const checkDates = async () => {
const dates = await page.evaluate(() =>
Array.from(
document.querySelectorAll(
`#editorCommentParamsToolbar ul > li > time`
)
).map(time => new Date(time.getAttribute("datetime")))
);
for (let i = 0; i < dates.length - 1; i++) {
expect(dates[i])
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(dates[i + 1]);
}
};
await checkDates();
// Add an highlight with a comment and check the order again.
await switchToHighlight(page);
await highlightSpan(page, 1, "Languages");
const editorSelector = getEditorSelector(9);
await page.waitForSelector(editorSelector);
const commentButtonSelector = `${editorSelector} button.comment`;
await waitAndClick(page, commentButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, "Hello world!");
await page.click("#commentManagerSaveButton");
await waitForSerialized(page, 1);
await switchToComment(page);
await checkDates();
})
);
});
it("must check that comments can be selected/unselected", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToComment(page);
const firstElementSelector =
"#editorCommentsSidebarList li:first-child";
await waitAndClick(page, firstElementSelector);
const popupSelector = "#commentPopup";
await page.waitForSelector(popupSelector, { visible: true });
const popupTextSelector = `${popupSelector} .commentPopupText`;
await page.waitForSelector(popupTextSelector, {
visible: true,
});
const popupText = await page.evaluate(
selector => document.querySelector(selector).textContent,
popupTextSelector
);
expect(popupText)
.withContext(`In ${browserName}`)
.toEqual("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
// Click again to unselect the comment.
await waitAndClick(page, firstElementSelector);
await page.waitForSelector(popupSelector, { visible: false });
})
);
});
});
});

View File

@@ -0,0 +1,181 @@
/* Copyright 2023 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,
copy,
kbSelectAll,
loadAndWait,
mockClipboard,
waitForEvent,
} from "./test_utils.mjs";
const selectAll = async page => {
await waitForEvent({
page,
eventName: "selectionchange",
action: () => kbSelectAll(page),
});
await page.waitForFunction(() => {
const selection = document.getSelection();
const hiddenCopyElement = document.getElementById("hiddenCopyElement");
return selection.containsNode(hiddenCopyElement);
});
};
describe("Copy and paste", () => {
describe("all text", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", "#hiddenCopyElement", 100);
await mockClipboard(pages);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that we've all the contents on copy/paste", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(
".page[data-page-number='1'] .textLayer .endOfContent"
);
await selectAll(page);
await copy(page);
await page.waitForFunction(
`document.querySelector('#viewerContainer').style.cursor !== "wait"`
);
await page.waitForFunction(
async () =>
!!(await navigator.clipboard.readText())?.includes(
"Dynamic languages such as JavaScript"
)
);
const text = await page.evaluate(() =>
navigator.clipboard.readText()
);
expect(
text.includes("This section provides an overview of our system")
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"are represented by function calls. This makes the LIR used by"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes("When compiling loops, we consult the oracle before")
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(text.includes("Nested Trace Tree Formation"))
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"An important detail is that the call to the inner trace"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(text.includes("When trace recording is completed, nanojit"))
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"SpiderMonkey, like many VMs, needs to preempt the user program"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"Using similar computations, we find that trace recording takes"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"specialization algorithm. We also described our trace compiler"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"dynamic optimization system. In Proceedings of the ACM SIGPLAN"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
})
);
});
});
describe("Copy/paste and ligatures", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"copy_paste_ligatures.pdf",
"#hiddenCopyElement",
100
);
await mockClipboard(pages);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the ligatures have been removed when the text has been copied", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(
".page[data-page-number='1'] .textLayer .endOfContent"
);
await selectAll(page);
await copy(page);
await page.waitForFunction(
`document.querySelector('#viewerContainer').style.cursor !== "wait"`
);
await page.waitForFunction(
async () => !!(await navigator.clipboard.readText())
);
const text = await page.evaluate(() =>
navigator.clipboard.readText()
);
expect(text)
.withContext(`In ${browserName}`)
.toEqual("abcdeffffiflffifflſtstghijklmno");
})
);
});
});
});

View File

@@ -0,0 +1,97 @@
/* Copyright 2024 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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, FSI, loadAndWait, PDI } from "./test_utils.mjs";
const FIELDS = [
"fileName",
"fileSize",
"title",
"author",
"subject",
"keywords",
"creationDate",
"modificationDate",
"creator",
"producer",
"version",
"pageCount",
"pageSize",
"linearized",
];
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;
beforeEach(async () => {
pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the document properties dialog has the correct information", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryToolbar", { hidden: false });
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",
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",
});
})
);
});
});
});

View File

@@ -0,0 +1,180 @@
/* Copyright 2021 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, FSI, loadAndWait, PDI } from "./test_utils.mjs";
function fuzzyMatch(a, b, browserName, pixelFuzz = 3) {
expect(a)
.withContext(`In ${browserName}`)
.toBeLessThan(b + pixelFuzz);
expect(a)
.withContext(`In ${browserName}`)
.toBeGreaterThan(b - pixelFuzz);
}
describe("find bar", () => {
describe("highlight all", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("find_all.pdf", ".textLayer", 100);
});
afterEach(async () => {
await closePages(pages);
});
it("must highlight text in the right position", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Highlight all occurrences of the letter A (case insensitive).
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "a");
await page.click("#findHighlightAll + label");
await page.waitForSelector(".textLayer .highlight");
// The PDF file contains the text 'AB BA' in a monospace font on a
// single line. Check if the two occurrences of A are highlighted.
const highlights = await page.$$(".textLayer .highlight");
expect(highlights.length).withContext(`In ${browserName}`).toEqual(2);
// Normalize the highlight's height. The font data in the PDF sets the
// size of the glyphs (and therefore the size of the highlights), but
// the viewer applies extra padding to them. For the comparison we
// therefore use the unpadded, glyph-sized parent element's height.
const parentSpan = (await highlights[0].$$("xpath/.."))[0];
const parentBox = await parentSpan.boundingBox();
const firstA = await highlights[0].boundingBox();
const secondA = await highlights[1].boundingBox();
firstA.height = parentBox.height;
secondA.height = parentBox.height;
// Check if the vertical position of the highlights is correct. Both
// should be on a single line.
expect(firstA.y).withContext(`In ${browserName}`).toEqual(secondA.y);
// Check if the height of the two highlights is correct. Both should
// match the font size.
const fontSize = 26.66; // From the PDF.
fuzzyMatch(firstA.height, fontSize, browserName);
fuzzyMatch(secondA.height, fontSize, browserName);
// Check if the horizontal position of the highlights is correct. The
// second occurrence should be four glyph widths (three letters and
// one space) away from the first occurrence.
const pageDiv = await page.$(".page canvas");
const pageBox = await pageDiv.boundingBox();
const expectedFirstAX = 30; // From the PDF.
const glyphWidth = 15.98; // From the PDF.
fuzzyMatch(firstA.x, pageBox.x + expectedFirstAX, browserName);
fuzzyMatch(secondA.x, firstA.x + glyphWidth * 4, browserName);
})
);
});
});
describe("highlight all (XFA)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("xfa_imm5257e.pdf", ".xfaLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must search xfa correctly", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "preferences");
await page.waitForSelector("#findInput[data-status='']");
await page.waitForSelector(".xfaLayer .highlight");
await page.waitForFunction(
() => !!document.querySelector("#findResultsCount")?.textContent
);
const resultElement = await page.waitForSelector("#findResultsCount");
const resultText = await resultElement.evaluate(el => el.textContent);
expect(resultText).toEqual(`${FSI}1${PDI} of ${FSI}1${PDI} match`);
const selectedElement = await page.waitForSelector(
".highlight.selected"
);
const selectedText = await selectedElement.evaluate(
el => el.textContent
);
expect(selectedText).toEqual("Preferences");
})
);
});
});
describe("issue19207.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue19207.pdf", ".textLayer", 200);
});
afterEach(async () => {
await closePages(pages);
});
it("must scroll to the search result text", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Search for "40"
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "40");
const highlight = await page.waitForSelector(".textLayer .highlight");
expect(await highlight.isIntersectingViewport()).toBeTrue();
})
);
});
});
describe("scrolls to the search result text for smaller viewports", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".textLayer", 100);
});
afterEach(async () => {
await closePages(pages);
});
it("must scroll to the search result text", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Set a smaller viewport to simulate a mobile device
await page.setViewport({ width: 350, height: 600 });
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "productivity");
const highlight = await page.waitForSelector(".textLayer .highlight");
expect(await highlight.isIntersectingViewport()).toBeTrue();
})
);
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
/* Copyright 2020 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.
*/
/* eslint-disable no-console */
import Jasmine from "jasmine";
async function runTests(results) {
const jasmine = new Jasmine();
jasmine.exitOnCompletion = false;
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
jasmine.loadConfig({
random: true,
spec_dir: "integration",
spec_files: [
"accessibility_spec.mjs",
"annotation_spec.mjs",
"autolinker_spec.mjs",
"caret_browsing_spec.mjs",
"comment_spec.mjs",
"copy_paste_spec.mjs",
"document_properties_spec.mjs",
"find_spec.mjs",
"freetext_editor_spec.mjs",
"highlight_editor_spec.mjs",
"ink_editor_spec.mjs",
"scripting_spec.mjs",
"signature_editor_spec.mjs",
"stamp_editor_spec.mjs",
"text_field_spec.mjs",
"text_layer_spec.mjs",
"thumbnail_view_spec.mjs",
"viewer_spec.mjs",
],
});
jasmine.addReporter({
jasmineDone(suiteInfo) {},
jasmineStarted(suiteInfo) {},
specDone(result) {
// Report on the result of individual tests.
++results.runs;
if (result.failedExpectations.length > 0) {
++results.failures;
console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`);
} else {
console.log(`TEST-PASSED | ${result.description}`);
}
},
specStarted(result) {},
suiteDone(result) {
// Report on the result of `afterAll` invocations.
if (result.failedExpectations.length > 0) {
++results.failures;
console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`);
}
},
suiteStarted(result) {
// Report on the result of `beforeAll` invocations.
if (result.failedExpectations.length > 0) {
++results.failures;
console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`);
}
},
});
return jasmine.execute();
}
export { runTests };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,774 @@
/* Copyright 2025 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,
copy,
FSI,
getEditorSelector,
getRect,
loadAndWait,
paste,
PDI,
switchToEditor,
waitForPointerUp,
waitForTimeout,
} from "./test_utils.mjs";
import fs from "fs";
import path from "path";
import { PNG } from "pngjs";
const __dirname = import.meta.dirname;
const switchToSignature = switchToEditor.bind(null, "Signature");
describe("Signature Editor", () => {
const descriptionInputSelector = "#addSignatureDescription > input";
const addButtonSelector = "#addSignatureAddButton";
describe("Basic operations", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the editor has been removed when the dialog is cancelled", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
// An invisible editor is created but invisible.
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: false });
await page.click("#addSignatureCancelButton");
// The editor should have been removed.
await page.waitForSelector(`:not(${editorSelector})`);
})
);
});
it("must check that the basic and common elements are working as expected", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.waitForSelector(
"#addSignatureTypeButton[aria-selected=true]"
);
await page.click("#addSignatureTypeInput");
await page.waitForSelector(
"#addSignatureSaveContainer > input:disabled"
);
let description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("");
await page.waitForSelector(`${addButtonSelector}:disabled`);
await page.waitForSelector("#addSignatureDescInput:disabled");
await page.type("#addSignatureTypeInput", "PDF.js");
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.waitForSelector("#addSignatureDescInput:not(:disabled)");
// The save button should be enabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:not(:disabled)"
);
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
// The description has been filled in automatically.
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("PDF.js");
// Clear the description.
await page.click("#addSignatureDescription > button");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value === ""`
);
// Clear the text for the signature.
await page.click("#clearSignatureButton");
await page.waitForFunction(
`document.querySelector("#addSignatureTypeInput").value === ""`
);
// The save button should be disabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:disabled"
);
await page.waitForSelector(`${addButtonSelector}:disabled`);
await page.type("#addSignatureTypeInput", "PDF.js");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
// Clearing the signature type should clear the description.
await page.click("#clearSignatureButton");
await page.waitForFunction(
`document.querySelector("#addSignatureTypeInput").value === ""`
);
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value === ""`
);
// Add a signature and change the description.
await page.type("#addSignatureTypeInput", "PDF.js");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
await page.click("#addSignatureDescription > button");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value === ""`
);
await page.type(descriptionInputSelector, "Hello World");
await page.type("#addSignatureTypeInput", "Hello");
// The description mustn't be changed.
// eslint-disable-next-line no-restricted-syntax
await waitForTimeout(100);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("Hello World");
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(
`.canvasWrapper > svg use[href="#path_p1_0"]`,
{ visible: true }
);
await page.waitForFunction(
`document.getElementById("viewer-alert").textContent === "Signature added"`
);
// Check the tooltip.
await page.waitForSelector(
`.altText.editDescription[title="Hello World"]`
);
// Check the aria description.
await page.waitForSelector(
`${editorSelector}[aria-description="Signature editor: ${FSI}Hello World${PDI}"]`
);
// Edit the description.
await page.click(`.altText.editDescription`);
await page.waitForSelector("#editSignatureDescriptionDialog", {
visible: true,
});
await page.waitForSelector("#editSignatureUpdateButton:disabled");
await page.waitForSelector(
`#editSignatureDescriptionDialog svg[aria-label="Hello World"]`
);
const editDescriptionInput = "#editSignatureDescription > input";
description = await page.$eval(editDescriptionInput, el => el.value);
expect(description).withContext(browserName).toEqual("Hello World");
await page.click("#editSignatureDescription > button");
await page.waitForFunction(
`document.querySelector("${editDescriptionInput}").value === ""`
);
await page.waitForSelector(
"#editSignatureUpdateButton:not(:disabled)"
);
await page.type(editDescriptionInput, "Hello PDF.js World");
await page.waitForSelector(
"#editSignatureUpdateButton:not(:disabled)"
);
await page.click("#editSignatureUpdateButton");
// Check the tooltip.
await page.waitForSelector(
`.altText.editDescription[title="Hello PDF.js World"]`
);
})
);
});
it("must check drawing with the mouse", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureDrawButton");
const drawSelector = "#addSignatureDraw";
await page.waitForSelector(drawSelector, { visible: true });
let description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("");
await page.waitForSelector(`${addButtonSelector}:disabled`);
const { x, y, width, height } = await getRect(page, drawSelector);
const clickHandle = await waitForPointerUp(page);
await page.mouse.move(x + 0.1 * width, y + 0.1 * height);
await page.mouse.down();
await page.mouse.move(x + 0.3 * width, y + 0.3 * height);
await page.mouse.up();
await awaitPromise(clickHandle);
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
// The save button should be enabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:not(:disabled)"
);
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
// The description has been filled in automatically.
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("Signature");
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
await page.waitForSelector(
".canvasWrapper > svg use[href='#path_p1_0']"
);
})
);
});
it("must check adding an image", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureImageButton");
await page.waitForSelector("#addSignatureImagePlaceholder", {
visible: true,
});
let description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("");
await page.waitForSelector(`${addButtonSelector}:disabled`);
const input = await page.$("#addSignatureFilePicker");
await input.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.png")}`
);
await page.waitForSelector(`#addSignatureImage > path:not([d=""])`);
// The save button should be enabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:not(:disabled)"
);
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
// The description has been filled in automatically.
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description)
.withContext(browserName)
.toEqual("firefox_logo.png");
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
await page.waitForSelector(
".canvasWrapper > svg use[href='#path_p1_0']"
);
})
);
});
it("must check copy and paste", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.type("#addSignatureTypeInput", "Hello");
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.click("#addSignatureAddButton");
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
const originalRect = await getRect(page, editorSelector);
const originalDescription = await page.$eval(
`${editorSelector} .altText.editDescription`,
el => el.title
);
const originalL10nParameter = await page.$eval(editorSelector, el =>
el.getAttribute("data-l10n-args")
);
await copy(page);
await paste(page);
const pastedEditorSelector = getEditorSelector(1);
await page.waitForSelector(pastedEditorSelector, { visible: true });
const pastedRect = await getRect(page, pastedEditorSelector);
const pastedDescription = await page.$eval(
`${pastedEditorSelector} .altText.editDescription`,
el => el.title
);
const pastedL10nParameter = await page.$eval(pastedEditorSelector, el =>
el.getAttribute("data-l10n-args")
);
expect(pastedRect)
.withContext(`In ${browserName}`)
.not.toEqual(originalRect);
expect(pastedDescription)
.withContext(`In ${browserName}`)
.toEqual(originalDescription);
expect(pastedL10nParameter)
.withContext(`In ${browserName}`)
.toEqual(originalL10nParameter);
}
});
});
describe("Bug 1948741", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the editor isn't too large", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.type(
"#addSignatureTypeInput",
"[18:50:03] asset pdf.scripting.mjs 105 KiB [emitted] [javascript module] (name: main)"
);
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.click("#addSignatureAddButton");
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(
`.canvasWrapper > svg use[href="#path_p1_0"]`,
{ visible: true }
);
const { width } = await getRect(page, editorSelector);
const { width: pageWidth } = await getRect(page, ".page");
expect(width).toBeLessThanOrEqual(pageWidth);
})
);
});
});
describe("Bug 1949201", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the error panel is correctly removed", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureImageButton");
await page.waitForSelector("#addSignatureImagePlaceholder", {
visible: true,
});
const input = await page.$("#addSignatureFilePicker");
await input.uploadFile(
`${path.join(__dirname, "./signature_editor_spec.mjs")}`
);
await page.waitForSelector("#addSignatureError", { visible: true });
await page.click("#addSignatureErrorCloseButton");
await page.waitForSelector("#addSignatureError", { visible: false });
await input.uploadFile(
`${path.join(__dirname, "./stamp_editor_spec.mjs")}`
);
await page.waitForSelector("#addSignatureError", { visible: true });
await page.click("#addSignatureTypeButton");
await page.waitForSelector(
"#addSignatureTypeButton[aria-selected=true]"
);
await page.waitForSelector("#addSignatureError", { visible: false });
await page.click("#addSignatureCancelButton");
})
);
});
});
describe("viewerCssTheme (light)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"empty.pdf",
".annotationEditorLayer",
null,
null,
{ viewerCssTheme: "1" }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the signature has the correct color with the light theme", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
const colorTheme = await page.evaluate(() => {
const html = document.querySelector("html");
const style = getComputedStyle(html);
return style.getPropertyValue("color-scheme");
});
expect(colorTheme).toEqual("light");
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.type("#addSignatureTypeInput", "Should be black.");
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.click("#addSignatureAddButton");
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(
`.canvasWrapper > svg use[href="#path_p1_0"]`,
{ visible: true }
);
const color = await page.evaluate(() => {
const use = document.querySelector(
`.canvasWrapper > svg use[href="#path_p1_0"]`
);
return use.parentNode.getAttribute("fill");
});
expect(color).toEqual("#000000");
})
);
});
});
describe("viewerCssTheme (dark)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"empty.pdf",
".annotationEditorLayer",
null,
null,
{ viewerCssTheme: "2" }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the signature has the correct color with the dark theme", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
const colorTheme = await page.evaluate(() => {
const html = document.querySelector("html");
const style = getComputedStyle(html);
return style.getPropertyValue("color-scheme");
});
expect(colorTheme).toEqual("dark");
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.type("#addSignatureTypeInput", "Should be black.");
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.click("#addSignatureAddButton");
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(
`.canvasWrapper > svg use[href="#path_p1_0"]`,
{ visible: true }
);
const color = await page.evaluate(() => {
const use = document.querySelector(
`.canvasWrapper > svg use[href="#path_p1_0"]`
);
return use.parentNode.getAttribute("fill");
});
expect(color).toEqual("#000000");
})
);
});
});
describe("Check the aspect ratio (bug 1962819)", () => {
let pages, contentWidth, contentHeight;
function getContentAspectRatio(png) {
const { width, height } = png;
const buffer = new Uint32Array(png.data.buffer);
let x0 = width;
let y0 = height;
let x1 = 0;
let y1 = 0;
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (buffer[width * i + j] !== 0) {
x0 = Math.min(x0, j);
y0 = Math.min(y0, i);
x1 = Math.max(x1, j);
y1 = Math.max(y1, i);
}
}
}
contentWidth = x1 - x0;
contentHeight = y1 - y0;
}
beforeAll(() => {
const data = fs.readFileSync(
path.join(__dirname, "../images/samplesignature.png")
);
const png = PNG.sync.read(data);
getContentAspectRatio(png);
});
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the signature has the correct aspect ratio", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureImageButton");
await page.waitForSelector("#addSignatureImagePlaceholder", {
visible: true,
});
await page.waitForSelector(`${addButtonSelector}:disabled`);
const input = await page.$("#addSignatureFilePicker");
await input.uploadFile(
`${path.join(__dirname, "../images/samplesignature.png")}`
);
await page.waitForSelector(`#addSignatureImage > path:not([d=""])`);
// The save button should be enabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:not(:disabled)"
);
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
const { width, height } = await getRect(
page,
".canvasWrapper > svg use[href='#path_p1_0']"
);
expect(Math.abs(contentWidth / width - contentHeight / height))
.withContext(
`In ${browserName} (${contentWidth}x${contentHeight} vs ${width}x${height})`
)
.toBeLessThan(0.25);
})
);
});
});
describe("Bug 1974257", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the signature save checkbox is disabled if storage is full", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
for (let i = 0; i < 6; i++) {
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureTypeInput");
await page.type("#addSignatureTypeInput", `PDF.js ${i}`);
if (i === 5) {
await page.waitForSelector(
"#addSignatureSaveCheckbox:not(checked)"
);
await page.waitForSelector("#addSignatureSaveCheckbox:disabled");
} else {
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
await page.waitForSelector(
"#addSignatureSaveCheckbox:not(:disabled)"
);
}
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
}
})
);
});
});
describe("Bug 1975719", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that an error is displayed with a monochrome image", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureImageButton");
await page.waitForSelector("#addSignatureImagePlaceholder", {
visible: true,
});
const input = await page.$("#addSignatureFilePicker");
await input.uploadFile(
`${path.join(__dirname, "../images/red.png")}`
);
await page.waitForSelector("#addSignatureError", { visible: true });
await page.waitForSelector(
"#addSignatureErrorTitle[data-l10n-id='pdfjs-editor-add-signature-image-no-data-error-title']"
);
await page.waitForSelector(
"#addSignatureErrorDescription[data-l10n-id='pdfjs-editor-add-signature-image-no-data-error-description']"
);
await page.click("#addSignatureErrorCloseButton");
await page.waitForSelector("#addSignatureError", { visible: false });
})
);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,964 @@
/* Copyright 2020 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 os from "os";
const isMac = os.platform() === "darwin";
function loadAndWait(filename, selector, zoom, setups, options, viewport) {
return Promise.all(
global.integrationSessions.map(async session => {
const page = await session.browser.newPage();
if (viewport) {
await page.setViewport(viewport);
}
// In order to avoid errors because of checks which depend on
// a locale.
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "language", {
get() {
return "en-US";
},
});
Object.defineProperty(navigator, "languages", {
get() {
return ["en-US", "en"];
},
});
});
let app_options = "";
if (options) {
const optionsObject =
typeof options === "function"
? await options(page, session.name)
: options;
// Options must be handled in app.js::_parseHashParams.
for (const [key, value] of Object.entries(optionsObject)) {
app_options += `&${key}=${encodeURIComponent(value)}`;
}
}
const fileParam = filename.startsWith("http")
? filename
: `/test/pdfs/${filename}`;
const url = `${global.integrationBaseUrl}?file=${fileParam}#zoom=${zoom ?? "page-fit"}${app_options}`;
if (setups) {
// page.evaluateOnNewDocument allows us to run code before the
// first js script is executed.
// The idea here is to set up some setters for PDFViewerApplication
// and EventBus, so we can inject some code to do whatever we want
// soon enough especially before the first event in the eventBus is
// dispatched.
const { prePageSetup, appSetup, earlySetup, eventBusSetup } = setups;
await prePageSetup?.(page);
if (earlySetup || appSetup || eventBusSetup) {
await page.evaluateOnNewDocument(
(eaSetup, aSetup, evSetup) => {
if (eaSetup) {
// eslint-disable-next-line no-eval
eval(`(${eaSetup})`)();
}
let app;
let eventBus;
Object.defineProperty(window, "PDFViewerApplication", {
get() {
return app;
},
set(newValue) {
app = newValue;
if (aSetup) {
// eslint-disable-next-line no-eval
eval(`(${aSetup})`)(app);
}
Object.defineProperty(app, "eventBus", {
get() {
return eventBus;
},
set(newV) {
eventBus = newV;
if (evSetup) {
// eslint-disable-next-line no-eval
eval(`(${evSetup})`)(eventBus);
}
},
});
},
});
},
earlySetup?.toString(),
appSetup?.toString(),
eventBusSetup?.toString()
);
}
}
await page.goto(url);
await setups?.postPageSetup?.(page);
await page.bringToFront();
if (selector) {
await page.waitForSelector(selector, {
timeout: 0,
});
}
return [session.name, page];
})
);
}
function createPromise(page, callback) {
return page.evaluateHandle(
// eslint-disable-next-line no-eval
cb => [new Promise(eval(`(${cb})`))],
callback.toString()
);
}
function awaitPromise(promise) {
return promise.evaluate(([p]) => p);
}
function closePages(pages) {
return Promise.all(pages.map(([_, page]) => closeSinglePage(page)));
}
async function closeSinglePage(page) {
// Avoid to keep something from a previous test.
await page.evaluate(async () => {
await window.PDFViewerApplication.testingClose();
window.localStorage.clear();
});
await page.close({ runBeforeUnload: false });
}
async function waitForSandboxTrip(page) {
const handle = await page.evaluateHandle(() => [
new Promise(resolve => {
window.addEventListener("sandboxtripend", resolve, { once: true });
window.PDFViewerApplication.pdfScriptingManager.sandboxTrip();
}),
]);
await awaitPromise(handle);
}
function waitForTimeout(milliseconds) {
/**
* Wait for the given number of milliseconds.
*
* Note that waiting for an arbitrary time in tests is discouraged because it
* can easily cause intermittent failures, which is why this functionality is
* no longer provided by Puppeteer 22+ and we have to implement it ourselves
* for the remaining callers in the integration tests. We should avoid
* creating new usages of this function; instead please refer to the better
* alternatives at https://github.com/puppeteer/puppeteer/pull/11780.
*/
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
async function clearInput(page, selector, waitForInputEvent = false) {
const action = async () => {
await page.click(selector);
await kbSelectAll(page);
await page.keyboard.press("Backspace");
await page.waitForFunction(
`document.querySelector('${selector}').value === ""`
);
};
return waitForInputEvent
? waitForEvent({
page,
eventName: "input",
action,
selector,
})
: action();
}
async function waitAndClick(page, selector, clickOptions = {}) {
await page.waitForSelector(selector, { visible: true });
await page.click(selector, clickOptions);
}
function waitForPointerUp(page) {
return createPromise(page, resolve => {
window.addEventListener("pointerup", resolve, { once: true });
});
}
function getSelector(id) {
return `[data-element-id="${id}"]`;
}
async function getRect(page, selector) {
// In Chrome something is wrong when serializing a `DomRect`,
// so we extract the values and return them ourselves.
await page.waitForSelector(selector, { visible: true });
return page.$eval(selector, el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
}
function getQuerySelector(id) {
return `document.querySelector('${getSelector(id)}')`;
}
function getComputedStyleSelector(id) {
return `getComputedStyle(${getQuerySelector(id)})`;
}
function getEditorSelector(n) {
return `#pdfjs_internal_editor_${n}`;
}
function getAnnotationSelector(id) {
return `[data-annotation-id="${id}"]`;
}
async function getSpanRectFromText(page, pageNumber, text) {
await page.waitForSelector(
`.page[data-page-number="${pageNumber}"] > .textLayer .endOfContent`
);
return page.evaluate(
(number, content) => {
for (const el of document.querySelectorAll(
`.page[data-page-number="${number}"] > .textLayer span:not(:has(> span))`
)) {
if (el.textContent === content) {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
}
}
return null;
},
pageNumber,
text
);
}
async function waitForEvent({
page,
eventName,
action,
selector = null,
validator = null,
timeout = 5000,
}) {
const handle = await page.evaluateHandle(
(name, sel, validate, timeOut) => {
let callback = null,
timeoutId = null;
const element = sel ? document.querySelector(sel) : document;
return [
Promise.race([
new Promise(resolve => {
// The promise is resolved if the event fired in the context of the
// selector and, if a validator is defined, the event data satisfies
// the conditions of the validator function.
callback = e => {
if (timeoutId) {
clearTimeout(timeoutId);
}
// eslint-disable-next-line no-eval
resolve(validate ? eval(`(${validate})`)(e) : true);
};
element.addEventListener(name, callback, { once: true });
}),
new Promise(resolve => {
timeoutId = setTimeout(() => {
element.removeEventListener(name, callback);
resolve(null);
}, timeOut);
}),
]),
];
},
eventName,
selector,
validator ? validator.toString() : null,
timeout
);
await action();
const success = await awaitPromise(handle);
if (success === null) {
console.warn(
`waitForEvent: ${eventName} didn't trigger within the timeout`
);
} else if (!success) {
console.warn(`waitForEvent: ${eventName} triggered, but validation failed`);
}
}
async function waitForStorageEntries(page, nEntries) {
return page.waitForFunction(
n => window.PDFViewerApplication.pdfDocument.annotationStorage.size === n,
{},
nEntries
);
}
async function waitForSerialized(page, nEntries) {
return page.waitForFunction(
n => {
try {
return (
(window.PDFViewerApplication.pdfDocument.annotationStorage
.serializable.map?.size ?? 0) === n
);
} catch {
// When serializing a stamp annotation with a SVG, the transfer
// can fail because of the SVG, so we just retry.
return false;
}
},
{},
nEntries
);
}
async function applyFunctionToEditor(page, editorId, func) {
return page.evaluate(
(id, f) => {
const editor =
window.PDFViewerApplication.pdfDocument.annotationStorage.getRawValue(
id
);
// eslint-disable-next-line no-eval
eval(`(${f})`)(editor);
},
editorId,
func.toString()
);
}
async function selectEditor(page, selector, count = 1) {
const editorRect = await getRect(page, selector);
await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2,
{ count }
);
await waitForSelectedEditor(page, selector);
}
async function waitForSelectedEditor(page, selector) {
return page.waitForSelector(`${selector}.selectedEditor`);
}
async function unselectEditor(page, selector) {
await page.keyboard.press("Escape");
await waitForUnselectedEditor(page, selector);
}
async function waitForUnselectedEditor(page, selector) {
return page.waitForSelector(`${selector}:not(.selectedEditor)`);
}
async function mockClipboard(pages) {
return Promise.all(
pages.map(async ([_, page]) => {
await page.evaluate(() => {
let data = null;
const clipboard = {
writeText: async text => (data = text),
readText: async () => data,
};
Object.defineProperty(navigator, "clipboard", { value: clipboard });
});
})
);
}
async function copy(page) {
await waitForEvent({
page,
eventName: "copy",
action: () => kbCopy(page),
});
}
async function copyToClipboard(page, data) {
await page.evaluate(async dat => {
const items = Object.create(null);
for (const [type, value] of Object.entries(dat)) {
if (value.startsWith("data:")) {
const resp = await fetch(value);
items[type] = await resp.blob();
} else {
items[type] = new Blob([value], { type });
}
}
await navigator.clipboard.write([new ClipboardItem(items)]);
}, data);
}
async function paste(page) {
await waitForEvent({
page,
eventName: "paste",
action: () => kbPaste(page),
});
}
async function pasteFromClipboard(page, selector = null) {
const validator = e => e.clipboardData.items.length !== 0;
await waitForEvent({
page,
eventName: "paste",
action: () => kbPaste(page),
selector,
validator,
});
}
async function getSerialized(page, filter = undefined) {
const values = await page.evaluate(() => {
const { map } =
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
if (!map) {
return [];
}
const vals = Array.from(map.values());
for (const value of vals) {
for (const [k, v] of Object.entries(value)) {
// Puppeteer don't serialize typed array correctly, so we convert them
// to arrays.
if (ArrayBuffer.isView(v)) {
value[k] = Array.from(v);
}
}
}
return vals;
});
return filter ? values.map(filter) : values;
}
async function getFirstSerialized(page, filter = undefined) {
return (await getSerialized(page, filter))[0];
}
function getAnnotationStorage(page) {
return page.evaluate(() =>
Object.fromEntries(
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable
.map || []
)
);
}
function waitForEntryInStorage(page, key, value, checker = (x, y) => x === y) {
return page.waitForFunction(
(k, v, c) => {
const { map } =
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
// eslint-disable-next-line no-eval
return map && eval(`(${c})`)(JSON.stringify(map.get(k)), v);
},
{},
key,
JSON.stringify(value),
checker.toString()
);
}
function getEditors(page, kind) {
return page.evaluate(aKind => {
const elements = document.querySelectorAll(`.${aKind}Editor`);
const results = [];
for (const { id } of elements) {
results.push(parseInt(id.split("_").at(-1)));
}
results.sort();
return results;
}, kind);
}
function getEditorDimensions(page, selector) {
return page.evaluate(sel => {
const { style } = document.querySelector(sel);
return {
left: style.left,
top: style.top,
width: style.width,
height: style.height,
};
}, selector);
}
async function serializeBitmapDimensions(page) {
await page.waitForFunction(() => {
try {
const map =
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable
.map;
return !!map;
} catch {
return false;
}
});
return page.evaluate(() => {
const { map } =
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
return map
? Array.from(map.values(), x => ({
width: x.bitmap.width,
height: x.bitmap.height,
}))
: [];
});
}
async function dragAndDrop(page, selector, translations, steps = 1) {
const rect = await getRect(page, selector);
const startX = rect.x + rect.width / 2;
const startY = rect.y + rect.height / 2;
await page.mouse.move(startX, startY);
await page.mouse.down();
for (const [tX, tY] of translations) {
await page.mouse.move(startX + tX, startY + tY, { steps });
}
await page.mouse.up();
await page.waitForSelector("#viewer:not(.noUserSelect)");
}
function waitForPageChanging(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on("pagechanging", resolve, {
once: true,
});
});
}
function waitForAnnotationEditorLayer(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"annotationeditorlayerrendered",
resolve,
{ once: true }
);
});
}
function waitForAnnotationModeChanged(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"annotationeditormodechanged",
resolve,
{ once: true }
);
});
}
function waitForPageRendered(page, pageNumber) {
return page.evaluateHandle(
number => [
new Promise(resolve => {
const { eventBus } = window.PDFViewerApplication;
eventBus.on("pagerendered", function handler(e) {
if (
!e.isDetailView &&
(number === undefined || e.pageNumber === number)
) {
resolve();
eventBus.off("pagerendered", handler);
}
});
}),
],
pageNumber
);
}
function waitForEditorMovedInDOM(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on("editormovedindom", resolve, {
once: true,
});
});
}
async function scrollIntoView(page, selector) {
const handle = await page.evaluateHandle(
sel => [
new Promise(resolve => {
const container = document.getElementById("viewerContainer");
if (container.scrollHeight <= container.clientHeight) {
resolve();
return;
}
container.addEventListener("scrollend", resolve, { once: true });
const element = document.querySelector(sel);
element.scrollIntoView({ behavior: "instant", block: "start" });
}),
],
selector
);
return awaitPromise(handle);
}
async function firstPageOnTop(page) {
const handle = await page.evaluateHandle(() => [
new Promise(resolve => {
const container = document.getElementById("viewerContainer");
if (container.scrollTop === 0 && container.scrollLeft === 0) {
resolve();
return;
}
container.addEventListener("scrollend", resolve, { once: true });
container.scrollTo(0, 0);
}),
]);
return awaitPromise(handle);
}
async function setCaretAt(page, pageNumber, text, position) {
await page.evaluate(
(pageN, string, pos) => {
for (const el of document.querySelectorAll(
`.page[data-page-number="${pageN}"] > .textLayer > span`
)) {
if (el.textContent === string) {
window.getSelection().setPosition(el.firstChild, pos);
break;
}
}
},
pageNumber,
text,
position
);
}
const modifier = isMac ? "Meta" : "Control";
async function kbCopy(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("c", { commands: ["Copy"] });
await page.keyboard.up(modifier);
}
async function kbPaste(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("v", { commands: ["Paste"] });
await page.keyboard.up(modifier);
}
async function kbUndo(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("z");
await page.keyboard.up(modifier);
}
async function kbRedo(page) {
if (isMac) {
await page.keyboard.down("Meta");
await page.keyboard.down("Shift");
await page.keyboard.press("z");
await page.keyboard.up("Shift");
await page.keyboard.up("Meta");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("y");
await page.keyboard.up("Control");
}
}
async function kbSelectAll(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("a", { commands: ["SelectAll"] });
await page.keyboard.up(modifier);
}
async function kbModifierDown(page) {
await page.keyboard.down(modifier);
}
async function kbModifierUp(page) {
await page.keyboard.up(modifier);
}
async function kbGoToEnd(page) {
if (isMac) {
await page.keyboard.down("Meta");
await page.keyboard.press("ArrowDown", {
commands: ["MoveToEndOfDocument"],
});
await page.keyboard.up("Meta");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("End");
await page.keyboard.up("Control");
}
}
async function kbGoToBegin(page) {
if (isMac) {
await page.keyboard.down("Meta");
await page.keyboard.press("ArrowUp", {
commands: ["MoveToBeginningOfDocument"],
});
await page.keyboard.up("Meta");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("Home");
await page.keyboard.up("Control");
}
}
async function kbBigMoveLeft(page) {
if (isMac) {
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowLeft");
await page.keyboard.up("Shift");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("ArrowLeft");
await page.keyboard.up("Control");
}
}
async function kbBigMoveRight(page) {
if (isMac) {
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowRight");
await page.keyboard.up("Shift");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("ArrowRight");
await page.keyboard.up("Control");
}
}
async function kbBigMoveUp(page) {
if (isMac) {
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowUp");
await page.keyboard.up("Shift");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("ArrowUp");
await page.keyboard.up("Control");
}
}
async function kbBigMoveDown(page) {
if (isMac) {
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Shift");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Control");
}
}
async function kbDeleteLastWord(page) {
if (isMac) {
await page.keyboard.down("Alt");
await page.keyboard.press("Backspace");
await page.keyboard.up("Alt");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("Backspace");
await page.keyboard.up("Control");
}
}
async function kbFocusNext(page) {
const handle = await createPromise(page, resolve => {
window.addEventListener("focusin", resolve, { once: true });
});
await page.keyboard.press("Tab");
await awaitPromise(handle);
}
async function kbFocusPrevious(page) {
const handle = await createPromise(page, resolve => {
window.addEventListener("focusin", resolve, { once: true });
});
await page.keyboard.down("Shift");
await page.keyboard.press("Tab");
await page.keyboard.up("Shift");
await awaitPromise(handle);
}
async function kbSave(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("s");
await page.keyboard.up(modifier);
}
async function switchToEditor(name, page, disable = false) {
const modeChangedHandle = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"annotationeditormodechanged",
resolve,
{ once: true }
);
});
await page.click(`#editor${name}Button`);
name = name.toLowerCase();
await page.waitForSelector(
".annotationEditorLayer" +
(disable ? `:not(.${name}Editing)` : `.${name}Editing`)
);
await awaitPromise(modeChangedHandle);
}
async function selectEditors(name, page) {
await kbSelectAll(page);
await page.waitForFunction(
() => !document.querySelector(`.${name}Editor:not(.selectedEditor)`)
);
}
async function clearEditors(name, page) {
await selectEditors(name, page);
await page.keyboard.press("Backspace");
await waitForStorageEntries(page, 0);
}
function waitForNoElement(page, selector) {
return page.waitForFunction(
sel => !document.querySelector(sel),
{},
selector
);
}
function isCanvasMonochrome(page, pageNumber, rectangle, color) {
return page.evaluate(
(rect, pageN, col) => {
const canvas = document.querySelector(
`.page[data-page-number = "${pageN}"] .canvasWrapper canvas`
);
const canvasRect = canvas.getBoundingClientRect();
const ctx = canvas.getContext("2d");
rect ||= canvasRect;
const { data } = ctx.getImageData(
rect.x - canvasRect.x,
rect.y - canvasRect.y,
rect.width,
rect.height
);
return new Uint32Array(data.buffer).every(x => x === col);
},
rectangle,
pageNumber,
color
);
}
async function getXY(page, selector) {
const rect = await getRect(page, selector);
return `${rect.x}::${rect.y}`;
}
function waitForPositionChange(page, selector, xy) {
return page.waitForFunction(
(sel, currentXY) => {
const bbox = document.querySelector(sel).getBoundingClientRect();
return `${bbox.x}::${bbox.y}` !== currentXY;
},
{},
selector,
xy
);
}
async function moveEditor(page, selector, n, pressKey) {
let xy = await getXY(page, selector);
for (let i = 0; i < n; i++) {
const handle = await waitForEditorMovedInDOM(page);
await pressKey();
await awaitPromise(handle);
await waitForPositionChange(page, selector, xy);
xy = await getXY(page, selector);
}
}
// Unicode bidi isolation characters, Fluent adds these markers to the text.
const FSI = "\u2068";
const PDI = "\u2069";
export {
applyFunctionToEditor,
awaitPromise,
clearEditors,
clearInput,
closePages,
closeSinglePage,
copy,
copyToClipboard,
createPromise,
dragAndDrop,
firstPageOnTop,
FSI,
getAnnotationSelector,
getAnnotationStorage,
getComputedStyleSelector,
getEditorDimensions,
getEditors,
getEditorSelector,
getFirstSerialized,
getQuerySelector,
getRect,
getSelector,
getSerialized,
getSpanRectFromText,
getXY,
isCanvasMonochrome,
kbBigMoveDown,
kbBigMoveLeft,
kbBigMoveRight,
kbBigMoveUp,
kbDeleteLastWord,
kbFocusNext,
kbFocusPrevious,
kbGoToBegin,
kbGoToEnd,
kbModifierDown,
kbModifierUp,
kbRedo,
kbSave,
kbSelectAll,
kbUndo,
loadAndWait,
mockClipboard,
moveEditor,
paste,
pasteFromClipboard,
PDI,
scrollIntoView,
selectEditor,
selectEditors,
serializeBitmapDimensions,
setCaretAt,
switchToEditor,
unselectEditor,
waitAndClick,
waitForAnnotationEditorLayer,
waitForAnnotationModeChanged,
waitForEntryInStorage,
waitForEvent,
waitForNoElement,
waitForPageChanging,
waitForPageRendered,
waitForPointerUp,
waitForSandboxTrip,
waitForSelectedEditor,
waitForSerialized,
waitForStorageEntries,
waitForTimeout,
waitForUnselectedEditor,
};

View File

@@ -0,0 +1,39 @@
/* Copyright 2024 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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, getSelector, loadAndWait } from "./test_utils.mjs";
describe("Text field", () => {
describe("Empty text field", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("file_pdfjs_form.pdf", getSelector("7R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the field is empty although its appearance contains a white space", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(getSelector("7R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("");
})
);
});
});
});

View File

@@ -0,0 +1,571 @@
/* Copyright 2024 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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,
closeSinglePage,
getSpanRectFromText,
loadAndWait,
waitForEvent,
} from "./test_utils.mjs";
import { startBrowser } from "../test.mjs";
describe("Text layer", () => {
describe("Text selection", () => {
// page.mouse.move(x, y, { steps: ... }) doesn't work in Firefox, because
// puppeteer will send fractional intermediate positions and Firefox doesn't
// support them. Use this function to round each intermediate position to an
// integer.
async function moveInSteps(page, from, to, steps) {
const deltaX = to.x - from.x;
const deltaY = to.y - from.y;
for (let i = 0; i <= steps; i++) {
const x = Math.round(from.x + (deltaX * i) / steps);
const y = Math.round(from.y + (deltaY * i) / steps);
await page.mouse.move(x, y);
}
}
function middlePosition(rect) {
return {
x: Math.round(rect.x + rect.width / 2),
y: Math.round(rect.y + rect.height / 2),
};
}
function middleLeftPosition(rect) {
return {
x: Math.round(rect.x + 1),
y: Math.round(rect.y + rect.height / 2),
};
}
function belowEndPosition(rect) {
return {
x: Math.round(rect.x + rect.width),
y: Math.round(rect.y + rect.height * 1.5),
};
}
beforeEach(() => {
jasmine.addAsyncMatchers({
// Check that a page has a selection containing the given text, with
// some tolerance for extra characters before/after.
toHaveRoughlySelected({ pp }) {
return {
async compare(page, expected) {
const TOLERANCE = 10;
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) {
const match = expected.exec(actual);
start = -1;
if (match) {
start = match.index;
end = start + match[0].length;
}
} else {
start = actual.indexOf(expected);
if (start !== -1) {
end = start + expected.length;
}
}
const pass =
start !== -1 &&
start < TOLERANCE &&
end > actual.length - TOLERANCE;
return {
pass,
message: `Expected ${pp(
actual.length > 200
? actual.slice(0, 100) + "[...]" + actual.slice(-100)
: actual
)} to ${pass ? "not " : ""}roughly match ${pp(expected)}.`,
};
},
};
},
});
});
describe("using mouse", () => {
describe("doesn't jump when hovering on an empty area", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
`.page[data-page-number = "1"] .endOfContent`
);
});
afterEach(async () => {
await closePages(pages);
});
it("in a single page", 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();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected(
"code sequences, records\n" +
"them, and compiles them to fast native code. We call suc"
);
})
);
});
it("across multiple pages", 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,
positionEndPage1,
positionStartPage2,
positionEndPage2,
] = await Promise.all([
getSpanRectFromText(
page,
1,
"Each compiled trace covers one path through the program with"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"or that the same types will occur in subsequent loop iterations."
).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),
]);
await page.mouse.move(positionStartPage1.x, positionStartPage1.y);
await page.mouse.down();
await moveInSteps(page, positionStartPage1, positionEndPage1, 20);
await moveInSteps(page, positionEndPage1, positionStartPage2, 20);
await expectAsync(page)
.withContext(`In ${browserName}, first selection`)
.toHaveRoughlySelected(
/path through the program .*Hence, recording a/s
);
await moveInSteps(page, positionStartPage2, positionEndPage2, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}, second selection`)
.toHaveRoughlySelected(
/path through.*Hence, recording and .* tracing, and give/s
);
})
);
});
});
describe("doesn't jump when hovering on an empty area, with .markedContent", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"chrome-text-selection-markedContent.pdf",
`.page[data-page-number = "1"] .endOfContent`
);
});
afterEach(async () => {
await closePages(pages);
});
it("in per-character selection mode", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"strengthen in the coming quarters as the railway projects under"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"development enter the construction phase (estimated at around"
).then(belowEndPosition),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected(
"rs as the railway projects under\n" +
"development enter the construction phase (estimated at "
);
})
);
});
it("in per-word selection mode", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"strengthen in the coming quarters as the railway projects under"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"development enter the construction phase (estimated at around"
).then(belowEndPosition),
]);
// Puppeteer doesn't support emulating "double click and hold" for
// WebDriver BiDi, so we must manually dispatch a protocol action
// (see https://github.com/puppeteer/puppeteer/issues/13745).
await page.mainFrame().browsingContext.performActions([
{
type: "pointer",
id: "__puppeteer_mouse",
actions: [
{ type: "pointerMove", ...positionStart },
{ type: "pointerDown", button: 0 },
{ type: "pointerUp", button: 0 },
{ type: "pointerDown", button: 0 },
],
},
]);
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected(
"quarters as the railway projects under\n" +
"development enter the construction phase (estimated at around"
);
})
);
});
});
describe("when selecting over a link", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"annotation-link-text-popup.pdf",
`.page[data-page-number = "1"] .endOfContent`
);
});
afterEach(async () => {
await closePages(pages);
});
it("allows selecting within the link", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected("Link\nmozil");
})
);
});
it("allows selecting within the link when going backwards", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Text").then(middlePosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected("a.org\nTe");
})
);
});
it("allows clicking the link after selecting", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await waitForEvent({
page,
eventName: "click",
action: () => page.mouse.click(positionEnd.x, positionEnd.y),
selector: "#pdfjs_internal_id_8R",
validator: e => {
// Don't navigate to the link destination: the `click` event
// firing is enough validation that the link can be clicked.
e.preventDefault();
return true;
},
});
})
);
});
it("allows clicking the link after changing selection with the keyboard", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowRight");
await page.keyboard.up("Shift");
await waitForEvent({
page,
eventName: "click",
action: () => page.mouse.click(positionEnd.x, positionEnd.y),
selector: "#pdfjs_internal_id_8R",
validator: e => {
// Don't navigate to the link destination: the `click` event
// firing is enough validation that the link can be clicked.
e.preventDefault();
return true;
},
});
})
);
});
});
});
describe("using selection carets", () => {
let browser;
let page;
beforeEach(async () => {
// Chrome does not support simulating caret-based selection, so this
// test only runs in Firefox.
browser = await startBrowser({
browserName: "firefox",
startUrl: "",
extraPrefsFirefox: {
"layout.accessiblecaret.enabled": true,
"layout.accessiblecaret.hide_carets_for_mouse_input": false,
},
});
page = await browser.newPage();
await page.goto(
`${global.integrationBaseUrl}?file=/test/pdfs/tracemonkey.pdf#zoom=page-fit`
);
await page.bringToFront();
await page.waitForSelector(
`.page[data-page-number = "1"] .endOfContent`,
{ timeout: 0 }
);
});
afterEach(async () => {
await closeSinglePage(page);
await browser.close();
});
it("doesn't jump when moving selection", async () => {
const [initialStart, initialEnd, finalEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"(frequently executed) bytecode sequences, records"
).then(middleLeftPosition),
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(initialStart.x, initialStart.y);
await page.mouse.down();
await moveInSteps(page, initialStart, initialEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`first selection`)
.toHaveRoughlySelected("frequently executed) byt");
const initialCaretPos = {
x: initialEnd.x,
y: initialEnd.y + 10,
};
const intermediateCaretPos = {
x: finalEnd.x,
y: finalEnd.y + 5,
};
const finalCaretPos = {
x: finalEnd.x + 20,
y: finalEnd.y + 5,
};
await page.mouse.move(initialCaretPos.x, initialCaretPos.y);
await page.mouse.down();
await moveInSteps(page, initialCaretPos, intermediateCaretPos, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`second selection`)
.toHaveRoughlySelected(/frequently .* We call such a s/s);
await page.mouse.down();
await moveInSteps(page, intermediateCaretPos, finalCaretPos, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`third selection`)
.toHaveRoughlySelected(/frequently .* We call such a s/s);
});
});
});
describe("when the browser enforces a minimum font size", () => {
let browser;
let page;
beforeEach(async () => {
// Only testing in Firefox because, while Chrome has a setting similar to
// font.minimum-size.x-western, it is not exposed through its API.
browser = await startBrowser({
browserName: "firefox",
startUrl: "",
extraPrefsFirefox: { "font.minimum-size.x-western": 40 },
});
page = await browser.newPage();
await page.goto(
`${global.integrationBaseUrl}?file=/test/pdfs/tracemonkey.pdf#zoom=100`
);
await page.bringToFront();
await page.waitForSelector(
`.page[data-page-number = "1"] .endOfContent`,
{ timeout: 0 }
);
});
afterEach(async () => {
await closeSinglePage(page);
await browser.close();
});
it("renders spans with the right size", async () => {
const rect = await getSpanRectFromText(
page,
1,
"Dynamic languages such as JavaScript are more difficult to com-"
);
// The difference between `a` and `b`, as a percentage of the lower one
const getPercentDiff = (a, b) => Math.max(a, b) / Math.min(a, b) - 1;
expect(getPercentDiff(rect.width, 315)).toBeLessThan(0.03);
expect(getPercentDiff(rect.height, 12)).toBeLessThan(0.03);
});
});
});

View File

@@ -0,0 +1,35 @@
import { closePages, loadAndWait } from "./test_utils.mjs";
describe("PDF Thumbnail View", () => {
describe("Works without errors", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", "#sidebarToggleButton");
});
afterEach(async () => {
await closePages(pages);
});
it("should render thumbnails without errors", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#sidebarToggleButton");
const thumbSelector = "#thumbnailView .thumbnailImage";
await page.waitForSelector(thumbSelector, { visible: true });
await page.waitForSelector(
'#thumbnailView .thumbnail[data-loaded="true"]'
);
const src = await page.$eval(thumbSelector, el => el.src);
expect(src)
.withContext(`In ${browserName}`)
.toMatch(/^data:image\//);
})
);
});
});
});

File diff suppressed because it is too large Load Diff

1
test/pdfs/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.pdf binary

748
test/pdfs/.gitignore vendored Normal file
View File

@@ -0,0 +1,748 @@
*.pdf
*.error
!boundingBox_invalid.pdf
!pdkids.pdf
!tracemonkey.pdf
!TrueType_without_cmap.pdf
!franz.pdf
!franz_2.pdf
!fraction-highlight.pdf
!german-umlaut-r.pdf
!issue13269.pdf
!xref_command_missing.pdf
!issue1155r.pdf
!issue2017r.pdf
!bug1727053.pdf
!issue18408_reduced.pdf
!bug1907000_reduced.pdf
!bug1953099.pdf
!issue11913.pdf
!issue2391-1.pdf
!issue2391-2.pdf
!issue14046.pdf
!issue7891_bc1.pdf
!issue14881.pdf
!issue3214.pdf
!issue4665.pdf
!checkbox-bad-appearance.pdf
!issue4684.pdf
!issue8092.pdf
!issue19802.pdf
!issue5256.pdf
!issue5801.pdf
!issue5946.pdf
!issue5972.pdf
!issue5874.pdf
!issue5808.pdf
!issue6179_reduced.pdf
!issue6204.pdf
!issue6342.pdf
!issue6652.pdf
!issue6782.pdf
!issue6901.pdf
!issue6961.pdf
!issue6962.pdf
!issue7020.pdf
!issue7101.pdf
!issue7115.pdf
!issue7180.pdf
!issue7769.pdf
!issue7200.pdf
!issue7229.pdf
!issue7403.pdf
!issue7406.pdf
!issue7426.pdf
!issue7439.pdf
!issue7847_radial.pdf
!issue8844.pdf
!issue17056.pdf
!issue17679.pdf
!issue17679_2.pdf
!issue18030.pdf
!issue18042.pdf
!issue18059.pdf
!issue14953.pdf
!issue15367.pdf
!issue15372.pdf
!issue7446.pdf
!issue7492.pdf
!issue7544.pdf
!issue7507.pdf
!issue6931_reduced.pdf
!issue14847.pdf
!issue17846.pdf
!doc_actions.pdf
!issue7580.pdf
!issue7598.pdf
!issue12750.pdf
!issue7665.pdf
!issue7696.pdf
!issue7835.pdf
!issue11922_reduced.pdf
!issue7855.pdf
!issue11144_reduced.pdf
!issue7872.pdf
!issue7901.pdf
!issue8061.pdf
!bug1721218_reduced.pdf
!issue8088.pdf
!issue8125.pdf
!issue8229.pdf
!issue8276_reduced.pdf
!issue8372.pdf
!issue9713.pdf
!xfa_filled_imm1344e.pdf
!issue8424.pdf
!issue8480.pdf
!bug1650302_reduced.pdf
!issue18816.pdf
!issue8570.pdf
!issue8697.pdf
!issue8702.pdf
!structure_simple.pdf
!issue12823.pdf
!issue8707.pdf
!issue8798r.pdf
!issue8823.pdf
!issue9084.pdf
!issue12963.pdf
!issue9105_reduced.pdf
!issue19484_2.pdf
!issue9105_other.pdf
!issue9252.pdf
!issue9262_reduced.pdf
!issue9291.pdf
!issue9418.pdf
!issue9458.pdf
!issue9655_reduced.pdf
!issue9915_reduced.pdf
!bug854315.pdf
!issue9940.pdf
!issue10388_reduced.pdf
!issue10438_reduced.pdf
!issue10529.pdf
!issue10542_reduced.pdf
!issue10665_reduced.pdf
!issue11016_reduced.pdf
!issue19532.pdf
!issue15516_reduced.pdf
!issue11045.pdf
!bug1057544.pdf
!issue11150_reduced.pdf
!issue6127.pdf
!issue7891_bc0.pdf
!issue11242_reduced.pdf
!issue16176.pdf
!issue16287.pdf
!issue17064_readonly.pdf
!issue11279.pdf
!issue11362.pdf
!issue13325_reduced.pdf
!issue11578_reduced.pdf
!issue11651.pdf
!issue11878.pdf
!issue13916.pdf
!issue14023.pdf
!issue14438.pdf
!issue14999_reduced.pdf
!bad-PageLabels.pdf
!decodeACSuccessive.pdf
!issue13003.pdf
!filled-background.pdf
!ArabicCIDTrueType.pdf
!ThuluthFeatures.pdf
!arial_unicode_ab_cidfont.pdf
!arial_unicode_en_cidfont.pdf
!asciihexdecode.pdf
!bug766086.pdf
!bug793632.pdf
!issue14821.pdf
!bug1020858.pdf
!prefilled_f1040.pdf
!bug1050040.pdf
!issue18986.pdf
!bug1200096.pdf
!bug1068432.pdf
!issue12295.pdf
!bug1146106.pdf
!issue13447.pdf
!bug1245391_reduced.pdf
!bug1252420.pdf
!bug1513120_reduced.pdf
!bug1538111.pdf
!bug1552113.pdf
!issue6132.pdf
!issue9949.pdf
!bug1308536.pdf
!bug1337429.pdf
!bug1606566.pdf
!issue5564_reduced.pdf
!canvas.pdf
!bug1132849.pdf
!issue6894.pdf
!issue5804.pdf
!issue11131_reduced.pdf
!Pages-tree-refs.pdf
!ShowText-ShadingPattern.pdf
!complex_ttf_font.pdf
!issue3694_reduced.pdf
!extgstate.pdf
!issue4706.pdf
!rotation.pdf
!simpletype3font.pdf
!Type3WordSpacing.pdf
!IndexedCS_negative_and_high.pdf
!sizes.pdf
!javauninstall-7r.pdf
!file_url_link.pdf
!multiple-filters-length-zero.pdf
!non-embedded-NuptialScript.pdf
!issue3205r.pdf
!issue3207r.pdf
!issue3263r.pdf
!issue3879r.pdf
!issue5686.pdf
!issue3928.pdf
!issue8565.pdf
!clippath.pdf
!issue19800.pdf
!issue8795_reduced.pdf
!bug1755507.pdf
!close-path-bug.pdf
!issue6019.pdf
!issue6621.pdf
!issue6286.pdf
!issue13107_reduced.pdf
!issue1055r.pdf
!issue11713.pdf
!issue1293r.pdf
!issue11931.pdf
!issue1655r.pdf
!issue6541.pdf
!issue10640.pdf
!issue2948.pdf
!issue6231_1.pdf
!issue10402.pdf
!issue7074_reduced.pdf
!issue6413.pdf
!issue4630.pdf
!issue4909.pdf
!scorecard_reduced.pdf
!issue5084.pdf
!issue8960_reduced.pdf
!issue5202.pdf
!images_1bit_grayscale.pdf
!issue5280.pdf
!issue12399_reduced.pdf
!annotation-ink-without-appearance.pdf
!issue5677.pdf
!issue5954.pdf
!issue6612.pdf
!alphatrans.pdf
!issue14200.pdf
!pattern_text_embedded_font.pdf
!devicen.pdf
!cmykjpeg.pdf
!issue840.pdf
!160F-2019.pdf
!issue4402_reduced.pdf
!issue16263.pdf
!issue845r.pdf
!issue3405r.pdf
!issue14130.pdf
!issue7339_reduced.pdf
!issue3438.pdf
!issue11403_reduced.pdf
!ContentStreamNoCycleType3insideType3.pdf
!ContentStreamCycleType3insideType3.pdf
!issue2074.pdf
!issue18117.pdf
!scan-bad.pdf
!issue13561_reduced.pdf
!bug847420.pdf
!bug860632.pdf
!bug894572.pdf
!bug911034.pdf
!issue19695.pdf
!bug1108301.pdf
!issue10301.pdf
!bug1157493.pdf
!issue15910.pdf
!issue4260_reduced.pdf
!bug1250079.pdf
!bug1473809.pdf
!issue12120_reduced.pdf
!pdfjsbad1586.pdf
!standard_fonts.pdf
!freeculture.pdf
!issue14802.pdf
!issue6006.pdf
!pdfkit_compressed.pdf
!TAMReview.pdf
!pr4922.pdf
!pr6531_1.pdf
!pr6531_2.pdf
!pr7352.pdf
!pr19449.pdf
!bug900822.pdf
!bug1392647.pdf
!issue918.pdf
!bug920426.pdf
!issue1905.pdf
!issue2833.pdf
!issue2931.pdf
!issue3323.pdf
!issue4304.pdf
!issue9017_reduced.pdf
!issue4379.pdf
!issue4550.pdf
!issue13316_reduced.pdf
!issue15977_reduced.pdf
!issue4575.pdf
!colorspace_atan.pdf
!bug1011159.pdf
!issue5734.pdf
!issue4875.pdf
!issue11740_reduced.pdf
!issue12705.pdf
!issue4881.pdf
!issue5994.pdf
!issue6151.pdf
!rotated.pdf
!issue1249.pdf
!issue1171.pdf
!smaskdim.pdf
!endchar.pdf
!type4psfunc.pdf
!issue1350.pdf
!S2.pdf
!glyph_accent.pdf
!personwithdog.pdf
!find_all.pdf
!helloworld-bad.pdf
!zerowidthline.pdf
!colorspace_cos.pdf
!issue17554.pdf
!issue13242.pdf
!js-colors.pdf
!annotation-line-without-appearance-empty-Rect.pdf
!issue12841_reduced.pdf
!bug868745.pdf
!mmtype1.pdf
!issue4436r.pdf
!issue5704.pdf
!issue5751.pdf
!bug893730.pdf
!bug864847.pdf
!issue15629.pdf
!issue1002.pdf
!issue925.pdf
!issue2840.pdf
!issue4061.pdf
!issue4668.pdf
!issue13226.pdf
!PDFJS-7562-reduced.pdf
!issue11768_reduced.pdf
!issue5039.pdf
!issue14117.pdf
!issue5070.pdf
!issue5238.pdf
!jp2k-resetprob.pdf
!issue5244.pdf
!issue5291.pdf
!issue4398.pdf
!issue5421.pdf
!issue5470.pdf
!issue5501.pdf
!issue5599.pdf
!issue17147.pdf
!issue5747.pdf
!issue6099.pdf
!issue6336.pdf
!issue6387.pdf
!issue6410.pdf
!issue11124.pdf
!issue8586.pdf
!jbig2_symbol_offset.pdf
!gradientfill.pdf
!bug903856.pdf
!issue14618.pdf
!bug850854.pdf
!issue12810.pdf
!bug866395.pdf
!issue12010_reduced.pdf
!issue10572.pdf
!issue11718_reduced.pdf
!bug1027533.pdf
!bug1028735.pdf
!bug1046314.pdf
!bug1065245.pdf
!issue6769.pdf
!bug1151216.pdf
!issue8111.pdf
!bug1175962.pdf
!bug1020226.pdf
!issue9534_reduced.pdf
!attachment.pdf
!basicapi.pdf
!issue15590.pdf
!issue15594_reduced.pdf
!issue2884_reduced.pdf
!mixedfonts.pdf
!shading_extend.pdf
!noembed-identity.pdf
!noembed-identity-2.pdf
!noembed-jis7.pdf
!issue12504.pdf
!noembed-eucjp.pdf
!bug1627427_reduced.pdf
!noembed-sjis.pdf
!vertical.pdf
!issue13343.pdf
!ZapfDingbats.pdf
!bug878026.pdf
!issue1045.pdf
!issue5010.pdf
!issue10339_reduced.pdf
!issue15557.pdf
!issue4934.pdf
!issue4650.pdf
!issue6721_reduced.pdf
!issue3025.pdf
!bug1365930.pdf
!french_diacritics.pdf
!issue2099-1.pdf
!issue3371.pdf
!issue2956.pdf
!issue2537r.pdf
!issue269_1.pdf
!bug946506.pdf
!issue3885.pdf
!issue11697_reduced.pdf
!bug859204.pdf
!annotation-tx.pdf
!annotation-tx2.pdf
!annotation-tx3.pdf
!coons-allflags-withfunction.pdf
!tensor-allflags-withfunction.pdf
!issue10084_reduced.pdf
!issue4246.pdf
!issue11915.pdf
!js-authors.pdf
!issue4461.pdf
!issue4573.pdf
!issue4722.pdf
!bug1811668_reduced.pdf
!issue4800.pdf
!issue9243.pdf
!issue13147.pdf
!issue11477_reduced.pdf
!text_clip_cff_cid.pdf
!issue4801.pdf
!issue5334.pdf
!annotation-caret-ink.pdf
!bug1186827.pdf
!issue12706.pdf
!issue215.pdf
!issue5044.pdf
!issue1512r.pdf
!issue2128r.pdf
!bug1703683_page2_reduced.pdf
!issue5540.pdf
!issue15893_reduced.pdf
!issue5549.pdf
!visibility_expressions.pdf
!issue5475.pdf
!issue10519_reduced.pdf
!annotation-border-styles.pdf
!colorspace_sin.pdf
!IdentityToUnicodeMap_charCodeOf.pdf
!PDFJS-9279-reduced.pdf
!issue5481.pdf
!resetform.pdf
!issue5567.pdf
!issue5701.pdf
!issue6769_no_matrix.pdf
!issue12007_reduced.pdf
!issue5896.pdf
!issue6010_1.pdf
!issue6010_2.pdf
!issue6068.pdf
!issue6081.pdf
!issue6069.pdf
!issue6106.pdf
!issue6296.pdf
!issue19848.pdf
!bug852992_reduced.pdf
!issue13271.pdf
!issue6298.pdf
!issue6889.pdf
!issue11473.pdf
!bug1001080.pdf
!issue15716.pdf
!bug1671312_reduced.pdf
!bug1671312_ArialNarrow.pdf
!issue17848.pdf
!issue6108.pdf
!issue6113.pdf
!openoffice.pdf
!js-buttons.pdf
!issue7014.pdf
!issue19484_1.pdf
!issue8187.pdf
!annotation-link-text-popup.pdf
!issue9278.pdf
!annotation-text-without-popup.pdf
!annotation-underline.pdf
!issue13193.pdf
!annotation-underline-without-appearance.pdf
!issue269_2.pdf
!issue13372.pdf
!annotation-strikeout.pdf
!annotation-strikeout-without-appearance.pdf
!annotation-squiggly.pdf
!issue14256.pdf
!annotation-squiggly-without-appearance.pdf
!annotation-highlight.pdf
!annotation-highlight-without-appearance.pdf
!issue12418_reduced.pdf
!annotation-freetext.pdf
!annotation-line.pdf
!evaljs.pdf
!issue12798_page1_reduced.pdf
!annotation-line-without-appearance.pdf
!bug1669099.pdf
!annotation-square-circle.pdf
!bug1942064.pdf
!annotation-square-circle-without-appearance.pdf
!annotation-stamp.pdf
!issue14048.pdf
!issue11656.pdf
!annotation-fileattachment.pdf
!annotation-text-widget.pdf
!issue7454.pdf
!issue15443.pdf
!annotation-choice-widget.pdf
!issue10900.pdf
!annotation-button-widget.pdf
!annotation-polyline-polygon.pdf
!annotation-polyline-polygon-without-appearance.pdf
!zero_descent.pdf
!operator-in-TJ-array.pdf
!issue7878.pdf
!font_ascent_descent.pdf
!listbox_actions.pdf
!issue11442_reduced.pdf
!issue11549_reduced.pdf
!issue8097_reduced.pdf
!issue15262.pdf
!issue17904.pdf
!bug1743245.pdf
!quadpoints.pdf
!transparent.pdf
!issue19326.pdf
!issue13931.pdf
!issue19474.pdf
!xobject-image.pdf
!issue15441.pdf
!issue6605.pdf
!ccitt_EndOfBlock_false.pdf
!issue9972-1.pdf
!issue9972-2.pdf
!issue9972-3.pdf
!tiling-pattern-box.pdf
!tiling-pattern-large-steps.pdf
!issue13201.pdf
!issue14462_reduced.pdf
!issue11555.pdf
!issue12337.pdf
!pr12564.pdf
!pr12828.pdf
!secHandler.pdf
!issue14297.pdf
!rc_annotation.pdf
!issue14267.pdf
!PDFBOX-4352-0.pdf
!REDHAT-1531897-0.pdf
!xfa_issue14315.pdf
!poppler-67295-0.pdf
!poppler-85140-0.pdf
!issue15012.pdf
!issue19176.pdf
!bug1947248_text.pdf
!bug1947248_forms.pdf
!issue15150.pdf
!poppler-395-0-fuzzed.pdf
!issue14165.pdf
!GHOSTSCRIPT-698804-1-fuzzed.pdf
!issue14814.pdf
!poppler-91414-0-53.pdf
!poppler-91414-0-54.pdf
!poppler-742-0-fuzzed.pdf
!poppler-937-0-fuzzed.pdf
!PDFBOX-3148-2-fuzzed.pdf
!poppler-90-0-fuzzed.pdf
!issue14415.pdf
!issue14307.pdf
!issue18645.pdf
!issue14497.pdf
!bug1799927.pdf
!issue14502.pdf
!issue13211.pdf
!issue14627.pdf
!issue14862.pdf
!issue14705.pdf
!bug1771477.pdf
!bug1724918.pdf
!issue15053.pdf
!bug1675139.pdf
!issue15092.pdf
!bug1782186.pdf
!tracemonkey_a11y.pdf
!bug1782564.pdf
!issue15340.pdf
!bug1795263.pdf
!issue15597.pdf
!bug1796741.pdf
!textfields.pdf
!freetext_no_appearance.pdf
!issue15690.pdf
!bug1802888.pdf
!issue15759.pdf
!issue18823.pdf
!pr20043.pdf
!issue15753.pdf
!issue15789.pdf
!fields_order.pdf
!issue15815.pdf
!issue15818.pdf
!autoprint.pdf
!bug1811694.pdf
!bug1811510.pdf
!bug1815476.pdf
!issue16021.pdf
!bug1770750.pdf
!issue19971.pdf
!issue16063.pdf
!issue19389.pdf
!issue16067.pdf
!bug1820909.1.pdf
!issue16221.pdf
!issue16224.pdf
!issue16278.pdf
!copy_paste_ligatures.pdf
!issue16316.pdf
!issue14565.pdf
!multiline.pdf
!bug1825002.pdf
!issue14755.pdf
!issue16473.pdf
!bug1529502.pdf
!issue16500.pdf
!issue16538.pdf
!freetexts.pdf
!issue16553.pdf
!empty.pdf
!rotated_freetexts.pdf
!issue16633.pdf
!bug1844576.pdf
!bug1844583.pdf
!annotation_hidden_print.pdf
!annotation_hidden_noview.pdf
!widget_hidden_print.pdf
!empty_protected.pdf
!tagged_stamp.pdf
!bug1851498.pdf
!issue17065.pdf
!issue17069.pdf
!issue17215.pdf
!bug1863910.pdf
!bug1865341.pdf
!bug1872721.pdf
!bug1871353.pdf
!bug1871353.1.pdf
!file_pdfjs_form.pdf
!issue17492.pdf
!issue17540.pdf
!bug1669097.pdf
!issue17671.pdf
!bug1868759.pdf
!issue17730.pdf
!bug1883609.pdf
!issue19550.pdf
!issue17808.pdf
!issue17871_bottom_right.pdf
!issue17871_top_right.pdf
!bug1889122.pdf
!issue17929.pdf
!issue12213.pdf
!tracemonkey_freetext.pdf
!issue17998.pdf
!pdfjs_wikipedia.pdf
!bug1539074.pdf
!bug1539074.1.pdf
!issue18305.pdf
!issue18360.pdf
!issue18099_reduced.pdf
!file_pdfjs_test.pdf
!issue18536.pdf
!issue18561.pdf
!highlights.pdf
!highlight.pdf
!bug1708040.pdf
!issue18694.pdf
!issue18693.pdf
!bug1918115.pdf
!bug1919513.pdf
!issue16038.pdf
!highlight_popup.pdf
!issue18072.pdf
!stamps.pdf
!issue15096.pdf
!issue18036.pdf
!issue18894.pdf
!bug1922766.pdf
!issue18956.pdf
!issue19083.pdf
!issue19120.pdf
!bug1934157.pdf
!rotated_ink.pdf
!inks.pdf
!inks_basic.pdf
!issue19182.pdf
!issue18911.pdf
!issue19207.pdf
!issue19239.pdf
!issue19360.pdf
!bug1019475_1.pdf
!bug1019475_2.pdf
!issue19505.pdf
!colors.pdf
!red_stamp.pdf
!issue19633.pdf
!issue19424.pdf
!issue18529.pdf
!issue16742.pdf
!chrome-text-selection-markedContent.pdf
!bug1963407.pdf
!issue19517.pdf
!empty#hash.pdf
!bug1885505.pdf
!bug1974436.pdf
!firefox_logo.pdf
!issue20062.pdf
!issue20102.pdf
!issue20065.pdf
!bug1708041.pdf
!bug1978317.pdf
!dates.pdf
!dates_save.pdf
!print_protection.pdf
!tracemonkey_with_annotations.pdf
!tracemonkey_with_editable_annotations.pdf
!bug1980958.pdf
!tracemonkey_annotation_on_page_8.pdf
!issue20232.pdf
!bug1989304.pdf
!comments.pdf

BIN
test/pdfs/160F-2019.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
https://web.archive.org/web/20160111221513/http://www.courts.go.jp/app/files/hanrei_jp/014/083014_hanrei.pdf

Binary file not shown.

View File

@@ -0,0 +1,122 @@
%PDF-1.6
%µ¶
1 0 obj
<</Type/Catalog/Outlines 2 0 R/Pages 3 0 R>>
endobj
2 0 obj
<</Type/Outlines/Count 0>>
endobj
3 0 obj
<</Type/Pages/Kids[4 0 R]/Count 1>>
endobj
4 0 obj
<</Type/Page/Parent 3 0 R/MediaBox[0 0 600 840]/Contents 5 0 R/Resources<</Font<</FType3A 6 0 R>>>>>>
endobj
5 0 obj
<</Length 123>>
stream
BT
1 0 0 1 100 500 Tm
2 Tr % Fill then Stroke Text render mode
/FType3A 20 Tf
20 20 Td
50 Tw
(ab) Tj
ET
endstream
endobj
6 0 obj
<</Type/Font/Subtype/Type3/Name/FType3A/Widths[1000 1000]/FontBBox[0 0 750 750]/FontMatrix[.01 0 0 .01 0 0]/Encoding<</Type/Encoding/Differences[97/rect/triangle]>>/Resources<</Font<</FType3B 9 0 R>>>>/CharProcs<</rect 7 0 R/triangle 8 0 R>>/FirstChar 97/LastChar 98>>
endobj
7 0 obj
<</Length 179>>
stream
%%% FType3A stroked red 'rect' = 97 or 'a'
1000 0 d0
20 w
1 0 0 RG
0 0 750 750 re s
BT % This is where a Type3 calls into another Type 3!
/FType3B 50 Tf
(ccc) Tj
ET
endstream
endobj
8 0 obj
<</Length 99>>
stream
%%% FType3A stroked green 'triangle' = 98 'b'
1000 0 d0
20 w
0 1 0 RG
0 0 m 375 750 l 750 0 l s
endstream
endobj
9 0 obj
<</Type/Font/Subtype/Type3/Name/FType3B/FontBBox[0 0 750 750]/FontMatrix[.004 0 0 .004 0 0]/Encoding<</Type/Encoding/Differences[99/fontinside]>>/Widths[900]/Resources<</Font<</F1 11 0 R>>/Pattern<</P1 12 0 R>>>>/CharProcs<</fontinside 10 0 R>>/FirstChar 99/LastChar 99>>
endobj
10 0 obj
<</Length 265>>
stream
900 0 d0
%%% FType3B Helv text for 'c'
2 Tr % Fill then Stroke Text render mode
10 w % stroke width 10
0 0 1 RG % Blue stroke
/Pattern cs /P1 scn % Set /P1 pattern as fill (magenta text)
BT
/F1 60 Tf
30 0 0 30 15 15 Tm -3 Tc 0 Tw
(ab) Tj
ET
endstream
endobj
11 0 obj
<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>
endobj
12 0 obj
<</Type/Pattern/PaintType 1/PatternType 1/TilingType 2/Matrix[0.9 0 0 0.9 0 0]/XStep 55/YStep 32/BBox[0 0 60 60]/Resources<</Font<</CyclicFont 6 0 R>>>>/Length 100>>
stream
BT
/CyclicFont 4 Tf
10 0 0 10 1 1 Tm
0 Tc
0 Tw
1 0 1 rg % Magenta fill
(ba) Tj
ET
endstream
endobj
xref
0 13
0000000000 65536 f
0000000016 00000 n
0000000077 00000 n
0000000120 00000 n
0000000172 00000 n
0000000290 00000 n
0000000464 00000 n
0000000749 00000 n
0000000978 00000 n
0000001127 00000 n
0000001419 00000 n
0000001736 00000 n
0000001802 00000 n
trailer
<</Size 13/Root 1 0 R>>
startxref
2111
%%EOF

View File

@@ -0,0 +1,122 @@
%PDF-1.6
%µ¶
1 0 obj
<</Type/Catalog/Outlines 2 0 R/Pages 3 0 R>>
endobj
2 0 obj
<</Type/Outlines/Count 0>>
endobj
3 0 obj
<</Type/Pages/Kids[4 0 R]/Count 1>>
endobj
4 0 obj
<</Type/Page/Parent 3 0 R/MediaBox[0 0 600 840]/Contents 5 0 R/Resources<</Font<</FType3A 6 0 R>>>>>>
endobj
5 0 obj
<</Length 123>>
stream
BT
1 0 0 1 100 500 Tm
2 Tr % Fill then Stroke Text render mode
/FType3A 20 Tf
20 20 Td
50 Tw
(ab) Tj
ET
endstream
endobj
6 0 obj
<</Type/Font/Subtype/Type3/Name/FType3A/Widths[1000 1000]/FontBBox[0 0 750 750]/FontMatrix[.01 0 0 .01 0 0]/Encoding<</Type/Encoding/Differences[97/rect/triangle]>>/Resources<</Font<</FType3B 9 0 R>>>>/CharProcs<</rect 7 0 R/triangle 8 0 R>>/FirstChar 97/LastChar 98>>
endobj
7 0 obj
<</Length 179>>
stream
%%% FType3A stroked red 'rect' = 97 or 'a'
1000 0 d0
20 w
1 0 0 RG
0 0 750 750 re s
BT % This is where a Type3 calls into another Type 3!
/FType3B 50 Tf
(ccc) Tj
ET
endstream
endobj
8 0 obj
<</Length 99>>
stream
%%% FType3A stroked green 'triangle' = 98 'b'
1000 0 d0
20 w
0 1 0 RG
0 0 m 375 750 l 750 0 l s
endstream
endobj
9 0 obj
<</Type/Font/Subtype/Type3/Name/FType3B/FontBBox[0 0 750 750]/FontMatrix[.004 0 0 .004 0 0]/Encoding<</Type/Encoding/Differences[99/fontinside]>>/Widths[900]/Resources<</Font<</F1 11 0 R>>/Pattern<</P1 12 0 R>>>>/CharProcs<</fontinside 10 0 R>>/FirstChar 99/LastChar 99>>
endobj
10 0 obj
<</Length 265>>
stream
900 0 d0
%%% FType3B Helv text for 'c'
2 Tr % Fill then Stroke Text render mode
10 w % stroke width 10
0 0 1 RG % Blue stroke
/Pattern cs /P1 scn % Set /P1 pattern as fill (magenta text)
BT
/F1 60 Tf
30 0 0 30 15 15 Tm -3 Tc 0 Tw
(ab) Tj
ET
endstream
endobj
11 0 obj
<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>
endobj
12 0 obj
<</Type/Pattern/PaintType 1/PatternType 1/TilingType 2/Matrix[0.9 0 0 0.9 0 0]/XStep 55/YStep 32/BBox[0 0 60 60]/Resources<</Font<</CyclicFont 11 0 R>>>>/Length 100>>
stream
BT
/CyclicFont 4 Tf
10 0 0 10 1 1 Tm
0 Tc
0 Tw
1 0 1 rg % Magenta fill
(ba) Tj
ET
endstream
endobj
xref
0 13
0000000000 65536 f
0000000016 00000 n
0000000077 00000 n
0000000120 00000 n
0000000172 00000 n
0000000290 00000 n
0000000464 00000 n
0000000749 00000 n
0000000978 00000 n
0000001127 00000 n
0000001419 00000 n
0000001736 00000 n
0000001802 00000 n
trailer
<</Size 13/Root 1 0 R>>
startxref
2111
%%EOF

View File

@@ -0,0 +1,69 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<<
/Type /Catalog
/Outline 2 0 R
/Pages 3 0 R
>>
endobj
2 0 obj
<<
/Type /Outlines
/Count 0
>>
endobj
3 0 obj
<<
/Type /Pages
/Kids [ 4 0 R ]
/Count 1
>>
endobj
4 0 obj
<<
/Type /Page
/Parent 3 0 R
/MediaBox [ 0 0 612 792 ]
/Contents 5 0 R
/Resources <<
/ProcSet 6 0 R
>>
>>
endobj
5 0 obj
<<
/Length 0
>>
stream
endstream
endobj
6 0 obj
[ /PDF ]
endobj
xref
0 2
0000000000 65536 f
0000000016 00000 n
00000004294967296 3
0000000138 00000 n
0000000204 00000 n
0000000342 00000 n
trailer
<<
/Size 7
/Root 1 0 R
>>
startxref
418
%%EOF

View File

@@ -0,0 +1 @@
https://web.archive.org/web/20150927202748/http://www2.emersonprocess.com/siteadmincenter/PM%20Micro%20Motion%20Documents/High-Pressure-Measurement-WP-001287.pdf

View File

@@ -0,0 +1,68 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<<
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Kids [3 0 R]
/Count 1
/Type /Pages
>>
endobj
3 0 obj
<<
/Parent 2 0 R
/MediaBox [0 0 200 50]
/Resources
<<
/Font
<<
/F1 4 0 R
>>
>>
/Contents 5 0 R
/Type /Page
>>
endobj
4 0 obj
<<
/BaseFont /Times-Roman
/Subtype /Type1
/ToUnicode /Identity-H
/Encoding /WinAnsiEncoding
/Type /Font
>>
endobj
5 0 obj
<<
/Length 37
>>
stream
BT
10 20 TD
/F1 20 Tf
(ABCdef) Tj
ET
endstream
endobj xref
0 6
0000000000 65535 f
0000000015 00000 n
0000000066 00000 n
0000000125 00000 n
0000000254 00000 n
0000000378 00000 n
trailer
<<
/Root 1 0 R
/Size 6
>>
startxref
467
%%EOF

View File

@@ -0,0 +1,87 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<<
/Type /Pages
/Kids [ 3 0 R ]
/Count 1
>>
endobj
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/MediaBox [ 0 0 500 100 ]
/Contents 5 0 R
/Resources <<
/ColorSpace << /Cs1 4 0 R >>
>>
>>
endobj
4 0 obj
[
/Indexed /DeviceRGB
7 % "hival"
< 008000 FF0000 00FF00 0000FF 00FFFF FF00FF FFFF00 F380FF > % indices 0-7 inclusive
]
endobj
5 0 obj
<<
/Length 1037
>>
stream
% Reference
0 0.5 0 rg 5 10 20 20 re f % 008000
0 0.5 0 rg 30 10 20 20 re f % 008000
1 0 0 rg 55 10 20 20 re f % FF0000
0 1 0 rg 80 10 20 20 re f % 00FF00
0 0 1 rg 105 10 20 20 re f % 0000FF
0 1 1 rg 130 10 20 20 re f % 00FFFF
1 0 1 rg 155 10 20 20 re f % FF00FF
1 1 0 rg 180 10 20 20 re f % FFFF00
0.95 0.5 1 rg 205 10 20 20 re f % F380FF
0.95 0.5 1 rg 230 10 20 20 re f % F380FF
0.95 0.5 1 rg 255 10 20 20 re f % F380FF
% Test patches
/Cs1 cs
-17 sc 5 50 20 20 re f % Below 0 --> snap to 0
0 sc 30 50 20 20 re f
1 sc 55 50 20 20 re f
2 sc 80 50 20 20 re f
3 sc 105 50 20 20 re f
4 sc 130 50 20 20 re f
5 sc 155 50 20 20 re f
6 sc 180 50 20 20 re f
7 sc 205 50 20 20 re f % Same as "Hival" = OK
6.5 sc 230 50 20 20 re f % Needs to round up --> snap to "hival"
17 sc 255 50 20 20 re f % Clearly out of range! --> snap to "hival"
endstream
endobj
xref
0 6
0000000000 65535 f
0000000019 00000 n
0000000083 00000 n
0000000162 00000 n
0000000332 00000 n
0000000503 00000 n
trailer
<<
/Root 1 0 R
/Size 6
>>
startxref
1600
%%EOF

View File

@@ -0,0 +1 @@
https://web.archive.org/web/20090612045731/http://kanji.zinbun.kyoto-u.ac.jp/~yasuoka/publications/JST2007-5.pdf

View File

@@ -0,0 +1 @@
https://web.archive.org/web/20150329232430/http://image.haier.com/manual/japan/wash_machine/201211/P020121130574743273239.pdf

File diff suppressed because it is too large Load Diff

BIN
test/pdfs/PDFBOX-4352-0.pdf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,84 @@
%PDF-1.7
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<< /Type /Pages
/Kids [6 0 R 3 0 R]
/Count 2
/MediaBox [0 0 595 842]
>>
endobj
3 0 obj
<< /Type /Pages
/Kids [4 0 R]
/Count 1
/MediaBox [0 0 595 842]
>>
endobj
4 0 obj
<< /Type /Pages
/Kids [5 0 R]
/Count 1
/MediaBox [0 0 595 842]
>>
endobj
5 0 obj
<< /Type /Pages
/Kids [3 0 R]
/Count 1
/MediaBox [0 0 595 842]
>>
endobj
6 0 obj
<< /Type /Page
/Parent 2 0 R
/Resources
<< /Font
<< /F1
<< /Type /Font
/Subtype /Type1
/BaseFont /Courier
>>
>>
>>
/Contents [7 0 R]
>>
endobj
7 0 obj
<< /Length 69 >>
stream
BT
/F1 22 Tf
30 800 Td
(Testcase: 'Pages loop') Tj
ET
endstream
endobj
xref
0 8
0000000000 65535 f
0000000010 00000 n
0000000069 00000 n
0000000176 00000 n
0000000277 00000 n
0000000378 00000 n
0000000479 00000 n
0000000744 00000 n
trailer
<< /Root 1 0 R
/Size 8
>>
startxref
866
%%EOF

Binary file not shown.

2549
test/pdfs/S2.pdf Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
https://web.archive.org/web/20150307061027/http://www.project2061.org/publications/sfaa/SFAA_Japanese.pdf

Binary file not shown.

BIN
test/pdfs/TAMReview.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
https://web.archive.org/web/20150805202858/http://blogs.adobe.com/CCJKType/files/2012/07/TaroUTR50SortedList112.pdf

BIN
test/pdfs/ThuluthFeatures.pdf Executable file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,139 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<< /Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<< /Type /Pages
/Kids [ 3 0 R ]
/Count 1
>>
endobj
3 0 obj
<< /Type /Page
/Parent 2 0 R
/MediaBox [ 0 0 300 80 ]
/Contents 4 0 R
/Resources <<
/Font << /FTyp3 5 0 R >>
>>
>>
endobj
4 0 obj
<<
/Length 715
>>
stream
0 0 1 rg % Blue non-stroking (fill)
1 0 0 RG % Red stroking
BT
/FTyp3 10 Tf % Select Type 3 font
0 Tr % Text render mode: 0=fill, 1=stroke, 2=fill then stroke
0 10 TD
50 Tw % Adjust this to: 0, 50, etc
( ab ba abba) Tj
T*
40 Tw % Adjust this to: 0, 50, etc
( ab ba abba) Tj
T*
30 Tw % Adjust this to: 0, 50, etc
( ab ba abba) Tj
T*
20 Tw % Adjust this to: 0, 50, etc
( ab ba abba) Tj
T*
10 Tw % Adjust this to: 0, 50, etc
( ab ba abba) Tj
T*
0 Tw % Adjust this to: 0, 50, etc
( ab ba abba) Tj
ET
endstream
endobj
5 0 obj
<< /Type /Font
/Subtype /Type3
/FontBBox [ 0 0 750 750 ]
/FontMatrix [ 0.001 0 0 0.001 0 0 ]
/CharProcs 7 0 R
/Encoding 6 0 R
/FirstChar 97 % "a"
/LastChar 98 % "b"
/Widths [ 1000 1000 ]
>>
endobj
6 0 obj
<< /Type /Encoding
/Differences [ 97 /square /triangle ]
>>
endobj
7 0 obj
<< /square 8 0 R
/triangle 9 0 R
>>
endobj
% Type 3 SQUARE glyph description = "a"
8 0 obj
<<
/Length 273
>>
stream
1000 0 0 0 750 750 d1 % NO explicit colour
50 w % Explicitly set line width as depend on it for stroking
[ 150 150 ] 0 d % Explicitly set dashing as depend on it for stroking
30 30 720 720 re S % Stroke (only) with inherited colour
endstream
endobj
% Type 3 TRIANGLE glyph description = "b"
9 0 obj
<<
/Length 316
>>
stream
1000 0 0 0 750 750 d1 % NO explicit colour
20 w % Explicitly set line width as depend on it for stroking
[ 100 100 ] 0 d % Explicitly set dashing as depend on it for stroking
20 20 m
350 730 l
730 20 l
b* % Close then Fill then Stroke with inherited colours
endstream
endobj
xref
0 10
0000000000 65535 f
0000000019 00000 n
0000000087 00000 n
0000000168 00000 n
0000000353 00000 n
0000001130 00000 n
0000001439 00000 n
0000001539 00000 n
0000001657 00000 n
0000002035 00000 n
trailer
<<
/Root 1 0 R
/Size 10
>>
startxref
2413
%%EOF

BIN
test/pdfs/ZapfDingbats.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
https://web.archive.org/web/20160112115354/http://www.fao.org/fileadmin/user_upload/tci/docs/2_About%20Stacks.pdf

BIN
test/pdfs/alphatrans.pdf Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
https://bugzilla.mozilla.org/attachment.cgi?id=611315

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
test/pdfs/annotation-freetext.pdf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,207 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<<
/Kids [2 0 R]
/Type /Pages
/Count 1
>>
endobj
2 0 obj
<<
/Parent 1 0 R
/Annots 3 0 R
/MediaBox [0 0 612 792]
/Resources 4 0 R
/Contents 5 0 R
/Type /Page
>>
endobj
3 0 obj [6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R]
endobj
4 0 obj
<<
/Font
<<
/F1 12 0 R
>>
/ProcSet [/PDF /Text]
>>
endobj
5 0 obj
<<
/Length 71
>>
stream
BT
50 700 TD
/F1 20 Tf
(InkAnnotation without appearance stream) Tj
ET
endstream
endobj
6 0 obj
<<
/CA 1
/P 2 0 R
/InkList [[124.66666666666667 769.3333333333334 124.66666666666667 768.6666666666666 124.66666666666667 766.6666666666666 124.66666666666667 763.3333333333334 124.66666666666667 757.3333333333334 124.66666666666667 748.6666666666666 122.66666666666667 738 120.66666666666667 727.3333333333334 119.33333333333333 718.6666666666666 118.66666666666667 710.6666666666666 118.66666666666667 706 118.66666666666667 703.3333333333334 118.66666666666667 703.3333333333334 118.66666666666667 703.3333333333334 117.33333333333333 706.6666666666666 116.66666666666667 708.6666666666666]]
/M (D:20210426091448)
/Contents (Contents)
/F 132
/BS
<<
/D [3]
/W 1
>>
/C [0 0 0]
/Subtype /Ink
/Type /Annot
/T (Title)
/Border [1 0 0]
/Rect [129.66666666666669 698.3333333333334 111.66666666666667 774.3333333333334]
>>
endobj
7 0 obj
<<
/CA 1
/P 2 0 R
/InkList [[68.66666666666667 764.6666666666666 70.66666666666667 765.3333333333334 76 766.6666666666666 83.33333333333333 768.6666666666666 94.66666666666667 771.3333333333334 112 773.3333333333334 132.66666666666666 776.6666666666666 153.33333333333334 777.3333333333334 171.33333333333334 778 187.33333333333334 778 197.33333333333334 778 200.66666666666666 778 200.66666666666666 778]]
/M (D:20210426091449)
/Contents (Contents)
/F 132
/BS
<<
/D [3]
/W 1
>>
/C [0 0 0]
/Subtype /Ink
/Type /Annot
/T (Title)
/Border [1 0 0]
/Rect [205.66666666666666 759.6666666666666 63.66666666666667 783]
>>
endobj
8 0 obj
<<
/CA 1
/P 2 0 R
/InkList [[165.33333333333334 727.3333333333334 168 727.3333333333334 175.33333333333334 727.3333333333334 184.66666666666666 727.3333333333334 196 728.6666666666666 206 730 216 731.3333333333334 221.33333333333334 732.6666666666666 222.66666666666666 734 222.66666666666666 734.6666666666666 222.66666666666666 736 222 737.3333333333334 219.33333333333334 739.3333333333334 214 742.6666666666666 205.33333333333334 744.6666666666666 192 747.3333333333334 178.66666666666666 748.6666666666666 170 748.6666666666666 166 748 163.33333333333334 746 161.33333333333334 742.6666666666666 160 734.6666666666666 158.66666666666666 726.6666666666666 158.66666666666666 719.3333333333334 159.33333333333334 715.3333333333334 164.66666666666666 705.3333333333334 169.33333333333334 701.3333333333334 175.33333333333334 699.3333333333334 183.33333333333334 698 192 697.3333333333334 201.33333333333334 697.3333333333334 210.66666666666666 697.3333333333334 221.33333333333334 697.3333333333334 235.33333333333334 702 244 705.3333333333334 250.66666666666666 709.3333333333334 254 712.6666666666666 256 716 257.3333333333333 718.6666666666666 258.6666666666667 720.6666666666666 260.6666666666667 722.6666666666666 261.3333333333333 724 263.3333333333333 726 264.6666666666667 726.6666666666666 264.6666666666667 728 266 730 266 732 267.3333333333333 733.3333333333334 267.3333333333333 734.6666666666666 267.3333333333333 735.3333333333334]]
/M (D:20210426091451)
/Contents (Contents)
/F 132
/BS
<<
/D [3]
/W 1
>>
/C [0 0 0]
/Subtype /Ink
/Type /Annot
/T (Title)
/Border [1 0 0]
/Rect [272.3333333333333 692.3333333333334 153.66666666666666 753.6666666666666]
>>
endobj
9 0 obj
<<
/CA 1
/P 2 0 R
/InkList [[322.6666666666667 742.6666666666666 314.6666666666667 743.3333333333334 304.6666666666667 744.6666666666666 295.3333333333333 744.6666666666666 288 744.6666666666666 286 744 285.3333333333333 744 284.6666666666667 743.3333333333334 286 740 291.3333333333333 735.3333333333334 299.3333333333333 730 308 724.6666666666666 313.3333333333333 721.3333333333334 316.6666666666667 719.3333333333334 317.3333333333333 718.6666666666666 317.3333333333333 718 317.3333333333333 717.3333333333334 316.6666666666667 716.6666666666666 316 715.3333333333334 313.3333333333333 714 308 712.6666666666666 302.6666666666667 712.6666666666666 294.6666666666667 712.6666666666666 288.6666666666667 712.6666666666666 284.6666666666667 712 284 712 283.3333333333333 712 283.3333333333333 712 283.3333333333333 712 282 712.6666666666666 282 712.6666666666666]]
/M (D:20210426091452)
/Contents (Contents)
/F 132
/BS
<<
/D [3]
/W 1
>>
/C [0 0 0]
/Subtype /Ink
/Type /Annot
/T (Title)
/Border [1 0 0]
/Rect [327.6666666666667 707 277 749.6666666666666]
>>
endobj
10 0 obj
<<
/CA 1
/P 2 0 R
/InkList [[356.6666666666667 763.3333333333334 356.6666666666667 760 357.3333333333333 752 357.3333333333333 742.6666666666666 358 732 358 721.3333333333334 358 712 358 707.3333333333334 358 705.3333333333334 358 704 358 703.3333333333334 358 702.6666666666666 357.3333333333333 704]]
/M (D:20210426091453)
/Contents (Contents)
/F 132
/BS
<<
/D [3]
/W 1
>>
/C [0 0 0]
/Subtype /Ink
/Type /Annot
/T (Title)
/Border [1 0 0]
/Rect [363 697.6666666666666 351.6666666666667 768.3333333333334]
>>
endobj
11 0 obj
<<
/CA 1
/P 2 0 R
/InkList [[342.6666666666667 740 342.6666666666667 740 344 740.6666666666666 346 740.6666666666666 350.6666666666667 741.3333333333334 366.6666666666667 743.3333333333334 376 744.6666666666666 392.6666666666667 744.6666666666666 402 745.3333333333334 407.3333333333333 746.6666666666666 408 746.6666666666666 408 746.6666666666666 408 747.3333333333334]]
/M (D:20210426091454)
/Contents (Contents)
/F 132
/BS
<<
/D [3]
/W 1
>>
/C [0 0 0]
/Subtype /Ink
/Type /Annot
/T (Title)
/Border [1 0 0]
/Rect [413 735 337.6666666666667 752.3333333333334]
>>
endobj
12 0 obj
<<
/BaseFont /Times-Roman
/Subtype /Type1
/Type /Font
/Encoding /WinAnsiEncoding
>>
endobj
13 0 obj
<<
/Pages 1 0 R
/Type /Catalog
>>
endobj xref
0 14
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000194 00000 n
0000000250 00000 n
0000000319 00000 n
0000000443 00000 n
0000001278 00000 n
0000001910 00000 n
0000003598 00000 n
0000004675 00000 n
0000005203 00000 n
0000005787 00000 n
0000005889 00000 n
trailer
<<
/Root 13 0 R
/Size 14
>>
startxref
5940
%%EOF

View File

@@ -0,0 +1,92 @@
%PDF-1.5
%âãÏÓ
1 0 obj
<<
/W 2
>>
endobj
2 0 obj
<<
/Rotate 0
/L [24 572 594 572]
/Contents (Contents)
/F 4
/BS 1 0 R
/NM (e6bb78a9c-0418-56ae-c91e-bb07d06b496)
/C [1 0 0]
/Subtype /Line
/LE [/None /None]
/Page 0
/Type /Annot
/T (Title)
/Rect [0 0 0 0]
>>
endobj
3 0 obj
<<
/Pages 4 0 R
/Type /Catalog
>>
endobj
4 0 obj
<<
/Kids [5 0 R]
/Count 1
/Type /Pages
>>
endobj
5 0 obj
<<
/Parent 4 0 R
/Annots [2 0 R]
/MediaBox [0 0 650 792]
/Resources
<<
/Font
<<
/F1 6 0 R
>>
>>
/Contents 7 0 R
/Type /Page
>>
endobj
6 0 obj
<<
/BaseFont /Times-Roman
/Subtype /Type1
/Encoding /WinAnsiEncoding
/Type /Font
>>
endobj
7 0 obj
<<
/Length 69
>>
stream
BT
50 700 TD
/F1 20 Tf
(LineAnnotation with empty /Rect-entry) Tj
ET
endstream
endobj xref
0 8
0000000000 65535 f
0000000015 00000 n
0000000043 00000 n
0000000267 00000 n
0000000318 00000 n
0000000377 00000 n
0000000523 00000 n
0000000624 00000 n
trailer
<<
/Root 3 0 R
/Size 8
>>
startxref
745
%%EOF

Binary file not shown.

BIN
test/pdfs/annotation-line.pdf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
test/pdfs/annotation-tx.pdf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More