From e85c30e08a342f0518393ce98ee392c2a4fd7a4c Mon Sep 17 00:00:00 2001 From: calixteman Date: Sun, 15 Mar 2026 22:25:39 +0100 Subject: [PATCH 1/5] Add the possibility to skip some ops in the debug view The user has to click in the space before an op to add a breakpoint and click again in order to skip it. --- src/display/canvas.js | 12 ++++++--- web/debugger.mjs | 4 +++ web/internal/draw_ops_view.css | 21 +++++++++++++--- web/internal/draw_ops_view.js | 45 ++++++++++++++++++++++++---------- web/internal/page_view.js | 16 +++++++++--- 5 files changed, 76 insertions(+), 22 deletions(-) diff --git a/src/display/canvas.js b/src/display/canvas.js index eb3a11d14d8de..7cf64d640764a 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -787,9 +787,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/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/page_view.js b/web/internal/page_view.js index 5f294e3a8f132..ad179da9288a5 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; } } From c7837580b9ec79e6ea0f17176b1300b371f64a62 Mon Sep 17 00:00:00 2001 From: calixteman Date: Sun, 15 Mar 2026 22:45:48 +0100 Subject: [PATCH 2/5] (Debugger) Replace checkboxes in the search bar by toggle buttons --- web/internal/canvas_context_details_view.js | 4 +- web/internal/debugger.html | 10 ++-- web/internal/multiline_view.css | 9 ---- web/internal/multiline_view.js | 59 ++++++++++++--------- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/web/internal/canvas_context_details_view.js b/web/internal/canvas_context_details_view.js index 11bf1911dfcb6..391f8970dfd0e 100644 --- a/web/internal/canvas_context_details_view.js +++ b/web/internal/canvas_context_details_view.js @@ -284,7 +284,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 +292,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..2882996ddd96b 100644 --- a/web/internal/debugger.html +++ b/web/internal/debugger.html @@ -57,12 +57,12 @@

PDF.js — Debugging tools

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); } From fc286aac4ee56ccbc0c2992525395c5b08904f83 Mon Sep 17 00:00:00 2001 From: calixteman Date: Sun, 15 Mar 2026 23:20:02 +0100 Subject: [PATCH 3/5] (Debugger) Don't draw the checkerboard on the canvas but add it behind --- web/internal/canvas_context_details_view.js | 56 --------------------- web/internal/debugger.html | 4 +- web/internal/page_view.css | 13 ++++- web/internal/page_view.js | 5 +- 4 files changed, 19 insertions(+), 59 deletions(-) diff --git a/web/internal/canvas_context_details_view.js b/web/internal/canvas_context_details_view.js index 11bf1911dfcb6..29d1044eed163 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; diff --git a/web/internal/debugger.html b/web/internal/debugger.html index 1f5cfecd537ea..4d7c57688868f 100644 --- a/web/internal/debugger.html +++ b/web/internal/debugger.html @@ -66,7 +66,9 @@

PDF.js — Debugging tools

- +
+ +
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..6ff3a82f8530d 100644 --- a/web/internal/page_view.js +++ b/web/internal/page_view.js @@ -297,7 +297,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); From fa7ddbb9bc0b048efbf3e7c9a2f132199b29f81d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 16 Mar 2026 14:44:19 +0100 Subject: [PATCH 4/5] Simplify compilation of font paths (PR 20346 follow-up) Given that `CompiledFont.prototype.getPathJs` already returns data in the desired TypedArray format, we should be able to directly copy the font-path data which helps shorten the code a little bit (rather than the "manual" handling in PR 20346). To ensure that this keeps working as expected, a non-production `assert` is added to prevent any future surprises. --- src/core/obj_bin_transform_core.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) 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 { From 2cc53270f38d2279887da6f4a30872680ad3813d Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 16 Mar 2026 18:59:46 +0100 Subject: [PATCH 5/5] Re-factor the `CachedCanvases` class to use a `Map` internally --- src/display/canvas.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/display/canvas.js b/src/display/canvas.js index eb3a11d14d8de..072db136c4c4a 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(); } }