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