diff --git a/src/core/obj_bin_transform_core.js b/src/core/obj_bin_transform_core.js
index b9cbd75acdb8c..362026da9aa30 100644
--- a/src/core/obj_bin_transform_core.js
+++ b/src/core/obj_bin_transform_core.js
@@ -389,20 +389,15 @@ function compilePatternInfo(ir) {
}
function compileFontPathInfo(path) {
- let data;
- let buffer;
- if (
- (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) ||
- FeatureTest.isFloat16ArraySupported
- ) {
- buffer = new ArrayBuffer(path.length * 2);
- data = new Float16Array(buffer);
- } else {
- buffer = new ArrayBuffer(path.length * 4);
- data = new Float32Array(buffer);
+ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
+ assert(
+ FeatureTest.isFloat16ArraySupported
+ ? path instanceof Float16Array
+ : path instanceof Float32Array,
+ "compileFontPathInfo: Unexpected path format."
+ );
}
- data.set(path);
- return buffer;
+ return path.slice().buffer;
}
export {
diff --git a/src/display/canvas.js b/src/display/canvas.js
index eb3a11d14d8de..2b4645bf9f592 100644
--- a/src/display/canvas.js
+++ b/src/display/canvas.js
@@ -205,33 +205,32 @@ function mirrorContextOperations(ctx, destCtx) {
}
class CachedCanvases {
+ #cache = new Map();
+
constructor(canvasFactory) {
this.canvasFactory = canvasFactory;
- this.cache = Object.create(null);
}
getCanvas(id, width, height) {
- let canvasEntry;
- if (this.cache[id] !== undefined) {
- canvasEntry = this.cache[id];
+ let canvasEntry = this.#cache.get(id);
+ if (canvasEntry) {
this.canvasFactory.reset(canvasEntry, width, height);
} else {
canvasEntry = this.canvasFactory.create(width, height);
- this.cache[id] = canvasEntry;
+ this.#cache.set(id, canvasEntry);
}
return canvasEntry;
}
delete(id) {
- delete this.cache[id];
+ this.#cache.delete(id);
}
clear() {
- for (const id in this.cache) {
- const canvasEntry = this.cache[id];
+ for (const canvasEntry of this.#cache.values()) {
this.canvasFactory.destroy(canvasEntry);
- delete this.cache[id];
}
+ this.#cache.clear();
}
}
@@ -787,9 +786,15 @@ class CanvasGraphics {
let fnId, fnArgs;
while (true) {
- if (stepper !== undefined && i === stepper.nextBreakPoint) {
- stepper.breakIt(i, continueCallback);
- return i;
+ if (stepper !== undefined) {
+ if (i === stepper.nextBreakPoint) {
+ stepper.breakIt(i, continueCallback);
+ return i;
+ }
+ if (stepper.shouldSkip(i)) {
+ i++;
+ continue;
+ }
}
if (!operationsFilter || operationsFilter(i)) {
diff --git a/web/debugger.mjs b/web/debugger.mjs
index efd36aea4ad82..5953b1791aff6 100644
--- a/web/debugger.mjs
+++ b/web/debugger.mjs
@@ -659,6 +659,10 @@ class Stepper {
this.goTo(idx);
}
+ shouldSkip(idx) {
+ return false;
+ }
+
goTo(idx) {
const allRows = this.panel.getElementsByClassName("line");
for (const row of allRows) {
diff --git a/web/internal/canvas_context_details_view.js b/web/internal/canvas_context_details_view.js
index 11bf1911dfcb6..df5c9c2d436ff 100644
--- a/web/internal/canvas_context_details_view.js
+++ b/web/internal/canvas_context_details_view.js
@@ -62,56 +62,6 @@ const COLOR_CTX_PROPS = new Set(["fillStyle", "shadowColor", "strokeStyle"]);
const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
-// Cached media queries used by drawCheckerboard.
-const _prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
-const _prefersHCM = window.matchMedia("(forced-colors: active)");
-
-/**
- * Draw a checkerboard pattern filling the canvas, to reveal transparency.
- * Mirrors the pattern used in src/display/editor/stamp.js.
- */
-function drawCheckerboard(ctx, width, height) {
- const isHCM = _prefersHCM.matches;
- const isDark = _prefersDark.matches;
- let light, dark;
- if (isHCM) {
- light = "white";
- dark = "black";
- } else if (isDark) {
- light = "#8f8f9d";
- dark = "#42414d";
- } else {
- light = "white";
- dark = "#cfcfd8";
- }
- const boxDim = 15;
- const pattern =
- typeof OffscreenCanvas !== "undefined"
- ? new OffscreenCanvas(boxDim * 2, boxDim * 2)
- : Object.assign(document.createElement("canvas"), {
- width: boxDim * 2,
- height: boxDim * 2,
- });
- const patternCtx = pattern.getContext("2d");
- if (!patternCtx) {
- return;
- }
- patternCtx.fillStyle = light;
- patternCtx.fillRect(0, 0, boxDim * 2, boxDim * 2);
- patternCtx.fillStyle = dark;
- patternCtx.fillRect(0, 0, boxDim, boxDim);
- patternCtx.fillRect(boxDim, boxDim, boxDim, boxDim);
- ctx.save();
- const fillPattern = ctx.createPattern(pattern, "repeat");
- if (!fillPattern) {
- ctx.restore();
- return;
- }
- ctx.fillStyle = fillPattern;
- ctx.fillRect(0, 0, width, height);
- ctx.restore();
-}
-
/**
* Tracks and displays the CanvasRenderingContext2D graphics state for all
* contexts created during a stepped render.
@@ -240,12 +190,6 @@ class CanvasContextDetailsView {
return ctx;
}
if (!wrappedCtx) {
- if (
- globalThis.StepperManager._active !== null &&
- args[0]?.alpha !== false
- ) {
- drawCheckerboard(ctx, canvas.width, canvas.height);
- }
wrappedCtx = this.wrapContext(ctx, label);
}
return wrappedCtx;
@@ -284,7 +228,7 @@ class CanvasContextDetailsView {
const prevBtn = document.createElement("button");
prevBtn.className = "gfx-state-stack-button";
- prevBtn.ariaLabel = "View older saved state";
+ prevBtn.title = "View older saved state";
prevBtn.textContent = "←";
const pos = document.createElement("span");
@@ -292,7 +236,7 @@ class CanvasContextDetailsView {
const nextBtn = document.createElement("button");
nextBtn.className = "gfx-state-stack-button";
- nextBtn.ariaLabel = "View newer saved state";
+ nextBtn.title = "View newer saved state";
nextBtn.textContent = "→";
navContainer.append(prevBtn, pos, nextBtn);
diff --git a/web/internal/debugger.html b/web/internal/debugger.html
index 1f5cfecd537ea..37ceb0a5f3382 100644
--- a/web/internal/debugger.html
+++ b/web/internal/debugger.html
@@ -57,16 +57,18 @@
PDF.js — Debugging tools
-
+
-
-
-
-
+
+
+
+
diff --git a/web/internal/draw_ops_view.css b/web/internal/draw_ops_view.css
index af20c0d9447be..d412c23b9bb4c 100644
--- a/web/internal/draw_ops_view.css
+++ b/web/internal/draw_ops_view.css
@@ -159,7 +159,7 @@
&::before {
content: "●";
color: var(--changed-color);
- font-size: 0.75em;
+ font-size: 0.9em;
opacity: 0;
}
@@ -167,9 +167,17 @@
opacity: 0.4;
}
- &.active::before {
+ &[data-bp="pause"]::before {
opacity: 1;
}
+
+ &[data-bp="skip"]::before {
+ content: "✕";
+ opacity: 1;
+ }
+}
+.op-line.op-skipped > :not(.bp-gutter) {
+ opacity: 0.4;
}
.op-line.paused {
background: var(--paused-bg);
@@ -194,9 +202,16 @@
.bp-gutter:hover::before {
color: ButtonBorder;
}
- .bp-gutter.active::before {
+ .bp-gutter[data-bp="pause"]::before {
color: ButtonText;
}
+ .bp-gutter[data-bp="skip"]::before {
+ color: ButtonText;
+ }
+ .op-line.op-skipped > :not(.bp-gutter) {
+ opacity: 1;
+ color: GrayText;
+ }
/* Color swatch preserves the actual PDF color value. */
.color-swatch {
diff --git a/web/internal/draw_ops_view.js b/web/internal/draw_ops_view.js
index c6b1fd25c570d..5d1d500c25ca3 100644
--- a/web/internal/draw_ops_view.js
+++ b/web/internal/draw_ops_view.js
@@ -23,6 +23,11 @@ for (const [name, id] of Object.entries(OPS)) {
OPS_TO_NAME[id] = name;
}
+const BreakpointType = {
+ PAUSE: 0,
+ SKIP: 1,
+};
+
// Single hidden color input reused for all swatch pickers.
const colorPickerInput = document.createElement("input");
colorPickerInput.type = "color";
@@ -408,7 +413,8 @@ class DrawOpsView {
#selectedLine = null;
- #breakpoints = new Set();
+ // Map
+ #breakpoints = new Map();
#originalColors = new Map();
@@ -553,27 +559,40 @@ class DrawOpsView {
line.ariaSelected = "false";
line.tabIndex = i === 0 ? 0 : -1;
- // Breakpoint gutter — click to toggle a red-bullet breakpoint.
+ // Breakpoint gutter — click cycles: none → pause (●) → skip (✕) → none.
const gutter = document.createElement("span");
gutter.className = "bp-gutter";
gutter.role = "checkbox";
gutter.tabIndex = 0;
gutter.ariaLabel = "Breakpoint";
- const isInitiallyActive = this.#breakpoints.has(i);
- gutter.ariaChecked = String(isInitiallyActive);
- if (isInitiallyActive) {
- gutter.classList.add("active");
+ const initBpType = this.#breakpoints.get(i);
+ if (initBpType === BreakpointType.PAUSE) {
+ gutter.dataset.bp = "pause";
+ gutter.ariaChecked = "true";
+ } else if (initBpType === BreakpointType.SKIP) {
+ gutter.dataset.bp = "skip";
+ gutter.ariaChecked = "mixed";
+ line.classList.add("op-skipped");
+ } else {
+ gutter.ariaChecked = "false";
}
gutter.addEventListener("click", e => {
e.stopPropagation();
- if (this.#breakpoints.has(i)) {
+ const current = this.#breakpoints.get(i);
+ if (current === undefined) {
+ this.#breakpoints.set(i, BreakpointType.PAUSE);
+ gutter.dataset.bp = "pause";
+ gutter.ariaChecked = "true";
+ } else if (current === BreakpointType.PAUSE) {
+ this.#breakpoints.set(i, BreakpointType.SKIP);
+ gutter.dataset.bp = "skip";
+ gutter.ariaChecked = "mixed";
+ line.classList.add("op-skipped");
+ } else {
this.#breakpoints.delete(i);
- gutter.classList.remove("active");
+ delete gutter.dataset.bp;
gutter.ariaChecked = "false";
- } else {
- this.#breakpoints.add(i);
- gutter.classList.add("active");
- gutter.ariaChecked = "true";
+ line.classList.remove("op-skipped");
}
});
gutter.addEventListener("keydown", e => {
@@ -656,4 +675,4 @@ class DrawOpsView {
}
}
-export { DrawOpsView };
+export { BreakpointType, DrawOpsView };
diff --git a/web/internal/multiline_view.css b/web/internal/multiline_view.css
index fec9d1f087670..9a8dc67ab78e5 100644
--- a/web/internal/multiline_view.css
+++ b/web/internal/multiline_view.css
@@ -145,15 +145,6 @@
white-space: nowrap;
min-width: 4ch;
}
-
- .mlc-check-label {
- display: flex;
- align-items: center;
- gap: 3px;
- font-size: 0.85em;
- cursor: pointer;
- white-space: nowrap;
- }
}
.mlc-num-item {
diff --git a/web/internal/multiline_view.js b/web/internal/multiline_view.js
index c53eb09647828..3b63750ff7b0e 100644
--- a/web/internal/multiline_view.js
+++ b/web/internal/multiline_view.js
@@ -75,9 +75,9 @@ class MultilineView {
#matchInfo;
- #ignoreCaseCb;
+ #ignoreCaseBtn;
- #regexCb;
+ #regexBtn;
/**
* @param {object} opts
@@ -362,32 +362,32 @@ class MultilineView {
const prevButton = (this.#prevButton = document.createElement("button"));
prevButton.className = "mlc-nav-button";
prevButton.textContent = "↑";
- prevButton.ariaLabel = "Previous match";
+ prevButton.title = "Previous match";
prevButton.disabled = true;
const nextButton = (this.#nextButton = document.createElement("button"));
nextButton.className = "mlc-nav-button";
nextButton.textContent = "↓";
- nextButton.ariaLabel = "Next match";
+ nextButton.title = "Next match";
nextButton.disabled = true;
const matchInfo = (this.#matchInfo = document.createElement("span"));
matchInfo.className = "mlc-match-info";
- const { label: ignoreCaseLabel, cb: ignoreCaseCb } =
- this.#makeCheckboxLabel("Ignore case");
- const { label: regexLabel, cb: regexCb } = this.#makeCheckboxLabel("Regex");
- this.#ignoreCaseCb = ignoreCaseCb;
- this.#regexCb = regexCb;
+ const ignoreCaseBtn = (this.#ignoreCaseBtn = this.#makeToggleButton(
+ "Aa",
+ "Ignore case"
+ ));
+ const regexBtn = (this.#regexBtn = this.#makeToggleButton(".*", "Regex"));
searchGroup.append(
searchInput,
searchError,
prevButton,
nextButton,
- matchInfo,
- ignoreCaseLabel,
- regexLabel
+ ignoreCaseBtn,
+ regexBtn,
+ matchInfo
);
const gotoInput = document.createElement("input");
@@ -412,8 +412,16 @@ class MultilineView {
});
prevButton.addEventListener("click", () => this.#navigateMatch(-1));
nextButton.addEventListener("click", () => this.#navigateMatch(1));
- this.#ignoreCaseCb.addEventListener("change", () => this.#runSearch());
- this.#regexCb.addEventListener("change", () => this.#runSearch());
+ this.#ignoreCaseBtn.addEventListener("click", () => {
+ this.#ignoreCaseBtn.ariaPressed =
+ this.#ignoreCaseBtn.ariaPressed === "true" ? "false" : "true";
+ this.#runSearch();
+ });
+ this.#regexBtn.addEventListener("click", () => {
+ this.#regexBtn.ariaPressed =
+ this.#regexBtn.ariaPressed === "true" ? "false" : "true";
+ this.#runSearch();
+ });
gotoInput.addEventListener("keydown", ({ key }) => {
if (key !== "Enter") {
@@ -432,13 +440,13 @@ class MultilineView {
return bar;
}
- #makeCheckboxLabel(text) {
- const label = document.createElement("label");
- label.className = "mlc-check-label";
- const cb = document.createElement("input");
- cb.type = "checkbox";
- label.append(cb, ` ${text}`);
- return { label, cb };
+ #makeToggleButton(text, title) {
+ const btn = document.createElement("button");
+ btn.className = "mlc-nav-button";
+ btn.textContent = text;
+ btn.title = title;
+ btn.ariaPressed = "false";
+ return btn;
}
#updateMatchInfo() {
@@ -466,9 +474,12 @@ class MultilineView {
}
let test;
- if (this.#regexCb.checked) {
+ if (this.#regexBtn.ariaPressed === "true") {
try {
- const re = new RegExp(query, this.#ignoreCaseCb.checked ? "i" : "");
+ const re = new RegExp(
+ query,
+ this.#ignoreCaseBtn.ariaPressed === "true" ? "i" : ""
+ );
test = str => re.test(str);
this.#searchInput.removeAttribute("aria-invalid");
this.#searchError.textContent = "";
@@ -479,7 +490,7 @@ class MultilineView {
return false;
}
} else {
- const ignoreCase = this.#ignoreCaseCb.checked;
+ const ignoreCase = this.#ignoreCaseBtn.ariaPressed === "true";
const needle = ignoreCase ? query.toLowerCase() : query;
test = str => (ignoreCase ? str.toLowerCase() : str).includes(needle);
}
diff --git a/web/internal/page_view.css b/web/internal/page_view.css
index 903b4a3e5bff4..656acefb5ec4d 100644
--- a/web/internal/page_view.css
+++ b/web/internal/page_view.css
@@ -124,7 +124,18 @@
color: var(--muted-color);
font-style: italic;
}
-.temp-canvas-wrapper canvas {
+.canvas-checker {
+ display: inline-block;
+ line-height: 0;
+ background-image: conic-gradient(
+ light-dark(#cfcfd8, #42414d) 25%,
+ light-dark(white, #8f8f9d) 25% 50%,
+ light-dark(#cfcfd8, #42414d) 50% 75%,
+ light-dark(white, #8f8f9d) 75%
+ );
+ background-size: 30px 30px;
+}
+.temp-canvas-wrapper .canvas-checker {
border: 1px solid var(--border-subtle-color);
zoom: calc(1 / var(--dpr, 1));
}
diff --git a/web/internal/page_view.js b/web/internal/page_view.js
index 5f294e3a8f132..4d9c55afa9a98 100644
--- a/web/internal/page_view.js
+++ b/web/internal/page_view.js
@@ -13,9 +13,9 @@
* limitations under the License.
*/
+import { BreakpointType, DrawOpsView } from "./draw_ops_view.js";
import { CanvasContextDetailsView } from "./canvas_context_details_view.js";
import { DOMCanvasFactory } from "pdfjs/display/canvas_factory.js";
-import { DrawOpsView } from "./draw_ops_view.js";
import { SplitView } from "./split_view.js";
// Stepper for pausing/stepping through op list rendering.
@@ -61,10 +61,20 @@ class ViewerStepper {
cb();
}
+ shouldSkip(i) {
+ return (
+ globalThis.StepperManager._breakpoints.get(i) === BreakpointType.SKIP
+ );
+ }
+
#findNextAfter(idx) {
let next = null;
- for (const bp of globalThis.StepperManager._breakpoints) {
- if (bp > idx && (next === null || bp < next)) {
+ for (const [bp, type] of globalThis.StepperManager._breakpoints) {
+ if (
+ type === BreakpointType.PAUSE &&
+ bp > idx &&
+ (next === null || bp < next)
+ ) {
next = bp;
}
}
@@ -297,7 +307,10 @@ class PageView {
const labelEl = document.createElement("div");
labelEl.className = "temp-canvas-label";
labelEl.textContent = `${ctxLabel} — ${width}×${height}`;
- wrapper.append(labelEl, canvasAndCtx.canvas);
+ const checker = document.createElement("div");
+ checker.className = "canvas-checker";
+ checker.append(canvasAndCtx.canvas);
+ wrapper.append(labelEl, checker);
const entry = { canvasAndCtx, wrapper, labelEl };
this.#alive.push(entry);
this.#attachWrapper(entry);