diff --git a/README.md b/README.md index 41f6d7d4dfb51..612a076b437b1 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ directory `build/chromium`. ### PDF debugger -Browser the internal structure of a PDF document with https://mozilla.github.io/pdf.js/internal-viewer/web/pdf_internal_viewer.html +Browser the internal structure of a PDF document with https://mozilla.github.io/pdf.js/internal-viewer/web/debugger.html ## Getting the Code diff --git a/gulpfile.mjs b/gulpfile.mjs index db428b7da57ef..422c26707b3fc 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -2372,13 +2372,13 @@ gulp.task("check_l10n", function (done) { function createInternalViewerBundle(defines) { const viewerFileConfig = createWebpackConfig(defines, { - filename: "pdf_internal_viewer.mjs", + filename: "debugger.mjs", library: { type: "module", }, }); return gulp - .src("./web/pdf_internal_viewer.js", { encoding: false }) + .src("./web/internal/debugger.js", { encoding: false }) .pipe(webpack2Stream(viewerFileConfig)); } @@ -2389,10 +2389,10 @@ function buildInternalViewer(defines, dir) { createMainBundle(defines).pipe(gulp.dest(dir + "build")), createWorkerBundle(defines).pipe(gulp.dest(dir + "build")), createInternalViewerBundle(defines).pipe(gulp.dest(dir + "web")), - preprocessHTML("web/pdf_internal_viewer.html", defines).pipe( + preprocessHTML("web/internal/debugger.html", defines).pipe( gulp.dest(dir + "web") ), - preprocessCSS("web/pdf_internal_viewer.css", defines) + preprocessCSS("web/internal/debugger.css", defines) .pipe( postcss([ postcssDirPseudoClass(), diff --git a/package-lock.json b/package-lock.json index 0148d72f7d5f2..a8e5952e3d0c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2630,9 +2630,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2654,9 +2651,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2678,9 +2672,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2702,9 +2693,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2726,9 +2714,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -13630,9 +13615,9 @@ } }, "node_modules/undici": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.21.0.tgz", - "integrity": "sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.2.tgz", + "integrity": "sha512-P9J1HWYV/ajFr8uCqk5QixwiRKmB1wOamgS0e+o2Z4A44Ej2+thFVRLG/eA7qprx88XXhnV5Bl8LHXTURpzB3Q==", "dev": true, "license": "MIT", "engines": { diff --git a/pdfjs.config b/pdfjs.config index 93df9428ab059..e183e76f7a455 100644 --- a/pdfjs.config +++ b/pdfjs.config @@ -1,5 +1,5 @@ { "stableVersion": "5.5.207", - "baseVersion": "025d658a9c66d10825ce19bc830e52a8c6347e61", - "versionPrefix": "5.5." + "baseVersion": "315491dd3224d957f203f813f880b26cc3251bae", + "versionPrefix": "5.6." } diff --git a/src/core/worker.js b/src/core/worker.js index 44e438a772182..4aef9e996e1e3 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -196,7 +196,6 @@ class WorkerMessageHandler { password, disableAutoFetch, rangeChunkSize, - length, docBaseUrl, enableXfa, evaluatorOptions, @@ -209,7 +208,7 @@ class WorkerMessageHandler { enableXfa, evaluatorOptions, handler, - length, + length: 0, password, rangeChunkSize, }; @@ -287,14 +286,9 @@ class WorkerMessageHandler { } if (!newPdfManager) { - const pdfFile = arrayBuffersToBytes(cachedChunks); + pdfManagerArgs.source = arrayBuffersToBytes(cachedChunks); cachedChunks = null; - if (length && pdfFile.length !== length) { - warn("reported HTTP length is different from actual"); - } - pdfManagerArgs.source = pdfFile; - newPdfManager = new LocalPdfManager(pdfManagerArgs); resolve(newPdfManager); } diff --git a/src/display/api.js b/src/display/api.js index 9b452aa124f4a..136ac1fe955ef 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -124,8 +124,6 @@ const RENDERING_CANCELLED_TIMEOUT = 100; // ms * cross-site Access-Control requests should be made using credentials such * as cookies or authorization headers. The default is `false`. * @property {string} [password] - For decrypting password-protected PDFs. - * @property {number} [length] - The PDF file length. It's used for progress - * reports and range requests operations. * @property {PDFDataRangeTransport} [range] - Allows for using a custom range * transport implementation. * @property {number} [rangeChunkSize] - Specify maximum number of bytes fetched @@ -353,7 +351,6 @@ function getDocument(src = {}) { const pagesMapper = src.pagesMapper || new PagesMapper(); // Parameters whose default values depend on other parameters. - const length = rangeTransport ? rangeTransport.length : (src.length ?? NaN); const useSystemFonts = typeof src.useSystemFonts === "boolean" ? src.useSystemFonts @@ -425,7 +422,6 @@ function getDocument(src = {}) { password, disableAutoFetch, rangeChunkSize, - length, docBaseUrl, enableXfa, evaluatorOptions: { @@ -497,7 +493,6 @@ function getDocument(src = {}) { networkStream = new NetworkStream({ url, - length, httpHeaders, withCredentials, rangeChunkSize, diff --git a/src/display/fetch_stream.js b/src/display/fetch_stream.js index 82270d7df0302..179f0a3291708 100644 --- a/src/display/fetch_stream.js +++ b/src/display/fetch_stream.js @@ -86,15 +86,12 @@ class PDFFetchStreamReader extends BasePDFStreamReader { const { disableRange, disableStream, - length, rangeChunkSize, url, withCredentials, } = stream._source; - this._contentLength = length; this._isStreamingSupported = !disableStream; - this._isRangeSupported = !disableRange; // Always create a copy of the headers. const headers = new Headers(stream.headers); @@ -114,10 +111,8 @@ class PDFFetchStreamReader extends BasePDFStreamReader { rangeChunkSize, disableRange, }); - + this._contentLength = contentLength; this._isRangeSupported = isRangeSupported; - // Setting right content length. - this._contentLength = contentLength || this._contentLength; this._filename = extractFilenameFromHeader(responseHeaders); diff --git a/src/display/network.js b/src/display/network.js index 86548c41f9ca6..a4d94be884903 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -187,9 +187,6 @@ class PDFNetworkStreamReader extends BasePDFStreamReader { constructor(stream) { super(stream); - const { length } = stream._source; - - this._contentLength = length; // Note that `XMLHttpRequest` doesn't support streaming, and range requests // will be enabled (if supported) in `this.#onHeadersReceived` below. @@ -229,12 +226,8 @@ class PDFNetworkStreamReader extends BasePDFStreamReader { rangeChunkSize, disableRange, }); - - if (isRangeSupported) { - this._isRangeSupported = true; - } - // Setting right content length. - this._contentLength = contentLength || this._contentLength; + this._contentLength = contentLength; + this._isRangeSupported = isRangeSupported; this._filename = extractFilenameFromHeader(responseHeaders); diff --git a/src/display/network_utils.js b/src/display/network_utils.js index 67b2eb61a2b41..bd2779706d4f8 100644 --- a/src/display/network_utils.js +++ b/src/display/network_utils.js @@ -50,7 +50,7 @@ function validateRangeRequestCapabilities({ ); } const rv = { - contentLength: undefined, + contentLength: 0, isRangeSupported: false, }; diff --git a/src/display/node_stream.js b/src/display/node_stream.js index 0d57ee789dc70..0b4c9454ebc8b 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -62,12 +62,9 @@ class PDFNodeStreamReader extends BasePDFStreamReader { constructor(stream) { super(stream); - const { disableRange, disableStream, length, rangeChunkSize, url } = - stream._source; + const { disableRange, disableStream, rangeChunkSize, url } = stream._source; - this._contentLength = length; this._isStreamingSupported = !disableStream; - this._isRangeSupported = !disableRange; const fs = process.getBuiltinModule("fs"); fs.promises @@ -79,13 +76,10 @@ class PDFNodeStreamReader extends BasePDFStreamReader { this._reader = readableStream.getReader(); const { size } = stat; - if (size <= 2 * rangeChunkSize) { - // The file size is smaller than the size of two chunks, so it doesn't - // make any sense to abort the request and retry with a range request. - this._isRangeSupported = false; - } - // Setting right content length. this._contentLength = size; + // When the file size is smaller than the size of two chunks, it doesn't + // make any sense to abort the request and retry with a range request. + this._isRangeSupported = !disableRange && size > 2 * rangeChunkSize; // We need to stop reading when range is supported and streaming is // disabled. diff --git a/test/unit/network_utils_spec.js b/test/unit/network_utils_spec.js index 7890fcfc65860..89d20f1813f49 100644 --- a/test/unit/network_utils_spec.js +++ b/test/unit/network_utils_spec.js @@ -153,7 +153,7 @@ describe("network_utils", function () { }) ).toEqual({ isRangeSupported: false, - contentLength: undefined, + contentLength: 0, }); }); diff --git a/web/internal/canvas_context_details_view.css b/web/internal/canvas_context_details_view.css new file mode 100644 index 0000000000000..fbd560bfbdd1c --- /dev/null +++ b/web/internal/canvas_context_details_view.css @@ -0,0 +1,85 @@ +/* Copyright 2026 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.gfx-state-section { + padding-inline: 12px; +} + +.gfx-state-section + .gfx-state-section { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-color); +} + +.gfx-state-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + color: var(--accent-color); + font-weight: bold; + margin-bottom: 4px; +} + +.gfx-state-stack-nav { + display: flex; + align-items: center; + gap: 2px; + font-weight: normal; + font-size: 0.8em; +} + +.gfx-state-stack-button { + padding: 0 3px; + border: 1px solid currentcolor; + border-radius: 2px; + background: transparent; + color: inherit; + cursor: pointer; + line-height: 1.3; + + &:disabled { + cursor: default; + opacity: 0.35; + } +} + +.gfx-state-stack-pos { + min-width: 4ch; + text-align: center; + font-variant-numeric: tabular-nums; +} + +.gfx-state-row { + display: flex; + align-items: center; + gap: 8px; + padding: 1px 0; +} + +.gfx-state-key { + color: var(--muted-color); + flex-shrink: 0; + min-width: 20ch; +} + +.gfx-state-val { + color: var(--number-color); + flex: 1 1 0; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/web/internal/canvas_context_details_view.js b/web/internal/canvas_context_details_view.js new file mode 100644 index 0000000000000..11bf1911dfcb6 --- /dev/null +++ b/web/internal/canvas_context_details_view.js @@ -0,0 +1,533 @@ +/* Copyright 2026 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Properties of CanvasRenderingContext2D that we track while stepping. +const TRACKED_CTX_PROPS = new Set([ + "direction", + "fillStyle", + "filter", + "font", + "globalAlpha", + "globalCompositeOperation", + "imageSmoothingEnabled", + "lineCap", + "lineDashOffset", + "lineJoin", + "lineWidth", + "miterLimit", + "strokeStyle", + "textAlign", + "textBaseline", +]); + +// Methods that modify the current transform matrix. +const TRANSFORM_METHODS = new Set([ + "resetTransform", + "rotate", + "scale", + "setTransform", + "transform", + "translate", +]); + +// Maps every tracked context property to a function that reads its current +// value from a CanvasRenderingContext2D. Covers directly-readable properties +// (TRACKED_CTX_PROPS) and method-read ones (lineDash, transform). +const CTX_PROP_READERS = new Map([ + ...Array.from(TRACKED_CTX_PROPS, p => [p, ctx => ctx[p]]), + ["lineDash", ctx => ctx.getLineDash()], + [ + "transform", + ctx => { + const { a, b, c, d, e, f } = ctx.getTransform(); + return { a, b, c, d, e, f }; + }, + ], +]); + +// Color properties whose value is rendered as a swatch. +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. + * + * @param {HTMLElement} panelEl The #gfx-state-panel DOM element. + */ +class CanvasContextDetailsView { + #panel; + + // Map> — live graphics state per tracked context. + #ctxStates = new Map(); + + // Map>> — save() stack snapshots per context. + #ctxStateStacks = new Map(); + + // Map — which stack frame is shown; null = live/current. + #ctxStackViewIdx = new Map(); + + // Map> — DOM elements for live updates. + #gfxStateValueElements = new Map(); + + // Map — stack-nav DOM elements. + #gfxStateNavElements = new Map(); + + constructor(panelEl) { + this.#panel = panelEl; + } + + /** + * Wrap a CanvasRenderingContext2D to track its graphics state. + * Returns a Proxy that keeps internal state in sync and updates the DOM. + */ + wrapContext(ctx, label) { + const state = new Map(); + for (const [prop, read] of CTX_PROP_READERS) { + state.set(prop, read(ctx)); + } + this.#ctxStates.set(label, state); + this.#ctxStateStacks.set(label, []); + this.#ctxStackViewIdx.set(label, null); + // If the panel is already visible (stepping in progress), rebuild it so + // the new context section is added and its live-update entries are + // registered. + if (this.#gfxStateValueElements.size > 0) { + this.build(); + } + + return new Proxy(ctx, { + set: (target, prop, value) => { + target[prop] = value; + if (TRACKED_CTX_PROPS.has(prop)) { + state.set(prop, value); + this.#updatePropEl(label, prop, value); + } + return true; + }, + get: (target, prop) => { + const val = target[prop]; + if (typeof val !== "function") { + return val; + } + if (prop === "save") { + return (...args) => { + const result = val.apply(target, args); + this.#ctxStateStacks.get(label).push(this.#copyState(state)); + this.#updateStackNav(label); + return result; + }; + } + if (prop === "restore") { + return (...args) => { + const result = val.apply(target, args); + for (const [p, read] of CTX_PROP_READERS) { + const v = read(target); + state.set(p, v); + this.#updatePropEl(label, p, v); + } + const stack = this.#ctxStateStacks.get(label); + if (stack.length > 0) { + stack.pop(); + // If the viewed frame was just removed, fall back to current. + const viewIndex = this.#ctxStackViewIdx.get(label); + if (viewIndex !== null && viewIndex >= stack.length) { + this.#ctxStackViewIdx.set(label, null); + this.#showState(label); + } + this.#updateStackNav(label); + } + return result; + }; + } + if (prop === "setLineDash") { + return segments => { + val.call(target, segments); + const dash = target.getLineDash(); + state.set("lineDash", dash); + this.#updatePropEl(label, "lineDash", dash); + }; + } + if (TRANSFORM_METHODS.has(prop)) { + return (...args) => { + const result = val.apply(target, args); + const { a, b, c, d, e, f } = target.getTransform(); + const tf = { a, b, c, d, e, f }; + state.set("transform", tf); + this.#updatePropEl(label, "transform", tf); + return result; + }; + } + return val.bind(target); + }, + }); + } + + /** + * Override canvas.getContext to return a tracked proxy for "2d" contexts. + * Caches the proxy so repeated getContext("2d") calls return the same + * wrapper. + */ + wrapCanvasGetContext(canvas, label) { + let wrappedCtx = null; + const origGetContext = canvas.getContext.bind(canvas); + canvas.getContext = (type, ...args) => { + const ctx = origGetContext(type, ...args); + if (type !== "2d") { + 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; + }; + return canvas.getContext("2d"); + } + + /** + * Rebuild the graphics-state panel DOM for all currently tracked contexts. + * Shows the panel if it was hidden. + */ + build() { + this.#panel.hidden = false; + this.#panel.replaceChildren(); + this.#gfxStateValueElements.clear(); + this.#gfxStateNavElements.clear(); + + for (const [ctxLabel, state] of this.#ctxStates) { + const propEls = new Map(); + this.#gfxStateValueElements.set(ctxLabel, propEls); + + const section = document.createElement("div"); + section.className = "gfx-state-section"; + section.dataset.ctxLabel = ctxLabel; + + // Title row with label and stack-navigation arrows. + const title = document.createElement("div"); + title.className = "gfx-state-title"; + + const titleLabel = document.createElement("span"); + titleLabel.textContent = ctxLabel; + + const navContainer = document.createElement("span"); + navContainer.className = "gfx-state-stack-nav"; + navContainer.hidden = true; + + const prevBtn = document.createElement("button"); + prevBtn.className = "gfx-state-stack-button"; + prevBtn.ariaLabel = "View older saved state"; + prevBtn.textContent = "←"; + + const pos = document.createElement("span"); + pos.className = "gfx-state-stack-pos"; + + const nextBtn = document.createElement("button"); + nextBtn.className = "gfx-state-stack-button"; + nextBtn.ariaLabel = "View newer saved state"; + nextBtn.textContent = "→"; + + navContainer.append(prevBtn, pos, nextBtn); + title.append(titleLabel, navContainer); + section.append(title); + + this.#gfxStateNavElements.set(ctxLabel, { + container: navContainer, + prevBtn, + pos, + nextBtn, + }); + + prevBtn.addEventListener("click", () => this.#navigate(ctxLabel, -1)); + nextBtn.addEventListener("click", () => this.#navigate(ctxLabel, +1)); + + for (const [prop, value] of state) { + const row = document.createElement("div"); + row.className = "gfx-state-row"; + + const key = document.createElement("span"); + key.className = "gfx-state-key"; + key.textContent = prop; + + row.append(key); + + if (prop === "transform") { + const { math, mnEls } = this.#buildTransformMathML(value); + row.append(math); + propEls.set(prop, { valEl: math, swatchEl: null, mnEls }); + } else { + const val = document.createElement("span"); + val.className = "gfx-state-val"; + const text = this.#formatCtxValue(value); + val.textContent = text; + val.title = text; + let swatchEl = null; + if (COLOR_CTX_PROPS.has(prop)) { + swatchEl = document.createElement("span"); + swatchEl.className = "color-swatch"; + swatchEl.style.background = String(value); + row.append(swatchEl); + } + row.append(val); + propEls.set(prop, { valEl: val, swatchEl }); + } + section.append(row); + } + this.#panel.append(section); + + // Apply the correct state for the current view index (may be a saved + // frame). + this.#showState(ctxLabel); + this.#updateStackNav(ctxLabel); + } + } + + /** Hide the panel. */ + hide() { + this.#panel.hidden = true; + } + + /** + * Scroll the panel to bring the section for the given context label into + * view. + */ + scrollToSection(label) { + this.#panel + .querySelector(`[data-ctx-label="${CSS.escape(label)}"]`) + ?.scrollIntoView({ block: "nearest" }); + } + + /** + * Clear all tracked state and reset the panel DOM. + * Called when the debug view is reset between pages. + */ + clear() { + this.#ctxStates.clear(); + this.#ctxStateStacks.clear(); + this.#ctxStackViewIdx.clear(); + this.#gfxStateValueElements.clear(); + this.#gfxStateNavElements.clear(); + this.#panel.replaceChildren(); + } + + #formatCtxValue(value) { + return Array.isArray(value) ? `[${value.join(", ")}]` : String(value); + } + + // Shallow-copy a state Map (arrays and plain objects are cloned one level + // deep). + #copyState(state) { + const clone = v => { + if (Array.isArray(v)) { + return [...v]; + } + if (typeof v === "object" && v !== null) { + return { ...v }; + } + return v; + }; + return new Map([...state].map(([k, v]) => [k, clone(v)])); + } + + // Apply a single (label, prop, value) update to the DOM unconditionally. + #applyPropEl(label, prop, value) { + const entry = this.#gfxStateValueElements.get(label)?.get(prop); + if (!entry) { + return; + } + if (entry.mnEls) { + for (const k of ["a", "b", "c", "d", "e", "f"]) { + entry.mnEls[k].textContent = this.#formatMatrixValue(value[k]); + } + return; + } + const text = this.#formatCtxValue(value); + entry.valEl.textContent = text; + entry.valEl.title = text; + if (entry.swatchEl) { + entry.swatchEl.style.background = String(value); + } + } + + // Update DOM for a live setter — skipped when the user is browsing a saved + // state so that live updates don't overwrite the frozen view. + #updatePropEl(label, prop, value) { + if (this.#ctxStackViewIdx.get(label) !== null) { + return; + } + this.#applyPropEl(label, prop, value); + } + + // Re-render all value DOM elements for label using the currently-viewed + // state. + #showState(label) { + const viewIdx = this.#ctxStackViewIdx.get(label); + const stateToShow = + viewIdx === null + ? this.#ctxStates.get(label) + : this.#ctxStateStacks.get(label)?.[viewIdx]; + if (!stateToShow) { + return; + } + for (const [prop, value] of stateToShow) { + this.#applyPropEl(label, prop, value); + } + } + + // Sync the stack-nav button states and position counter for a context. + #updateStackNav(label) { + const nav = this.#gfxStateNavElements.get(label); + if (!nav) { + return; + } + const stack = this.#ctxStateStacks.get(label) ?? []; + const viewIdx = this.#ctxStackViewIdx.get(label); + nav.container.hidden = stack.length === 0; + if (stack.length === 0) { + return; + } + nav.prevBtn.disabled = viewIdx === 0; + nav.nextBtn.disabled = viewIdx === null; + nav.pos.textContent = + viewIdx === null ? "cur" : `${viewIdx + 1}/${stack.length}`; + } + + // Navigate the save/restore stack view for a context. + // delta = -1 → older (prev) frame; +1 → newer (next) frame. + #navigate(label, delta) { + const stack = this.#ctxStateStacks.get(label) ?? []; + const viewIndex = this.#ctxStackViewIdx.get(label); + let newViewIndex; + if (delta < 0) { + newViewIndex = viewIndex === null ? stack.length - 1 : viewIndex - 1; + if (newViewIndex < 0) { + return; + } + } else { + if (viewIndex === null) { + return; + } + newViewIndex = viewIndex >= stack.length - 1 ? null : viewIndex + 1; + } + this.#ctxStackViewIdx.set(label, newViewIndex); + this.#showState(label); + this.#updateStackNav(label); + } + + #mEl(tag, ...children) { + const el = document.createElementNS(MATHML_NS, tag); + el.append(...children); + return el; + } + + #formatMatrixValue(v) { + return Number.isInteger(v) ? String(v) : String(parseFloat(v.toFixed(4))); + } + + #buildTransformMathML({ a, b, c, d, e, f }) { + const mnEls = {}; + for (const [k, v] of Object.entries({ a, b, c, d, e, f })) { + mnEls[k] = this.#mEl("mn", this.#formatMatrixValue(v)); + } + const math = this.#mEl( + "math", + this.#mEl( + "mrow", + this.#mEl("mo", "["), + this.#mEl( + "mtable", + this.#mEl( + "mtr", + this.#mEl("mtd", mnEls.a), + this.#mEl("mtd", mnEls.c), + this.#mEl("mtd", mnEls.e) + ), + this.#mEl( + "mtr", + this.#mEl("mtd", mnEls.b), + this.#mEl("mtd", mnEls.d), + this.#mEl("mtd", mnEls.f) + ), + this.#mEl( + "mtr", + this.#mEl("mtd", this.#mEl("mn", "0")), + this.#mEl("mtd", this.#mEl("mn", "0")), + this.#mEl("mtd", this.#mEl("mn", "1")) + ) + ), + this.#mEl("mo", "]") + ) + ); + return { math, mnEls }; + } +} + +export { CanvasContextDetailsView }; diff --git a/web/internal/debugger.css b/web/internal/debugger.css new file mode 100644 index 0000000000000..730eb1583b0f3 --- /dev/null +++ b/web/internal/debugger.css @@ -0,0 +1,302 @@ +/* Copyright 2026 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import url(canvas_context_details_view.css); +@import url(draw_ops_view.css); +@import url(multiline_view.css); +@import url(page_view.css); +@import url(split_view.css); +@import url(tree_view.css); + +:root { + color-scheme: light dark; + + /* Backgrounds */ + --bg-color: light-dark(#fff, #1e1e1e); + --surface-bg: light-dark(#f3f3f3, #252526); + --input-bg: light-dark(#fff, #3c3c3c); + --button-bg: light-dark(#f3f3f3, #3c3c3c); + --button-hover-bg: light-dark(#e0e0e0, #4a4a4a); + --clr-canvas-bg: var(--surface-bg); + + /* Text */ + --text-color: light-dark(#1e1e1e, #d4d4d4); + --muted-color: light-dark(#6e6e6e, #888); + --accent-color: light-dark(#0070c1, #9cdcfe); + + /* Borders */ + --border-color: light-dark(#e0e0e0, #3c3c3c); + --border-subtle-color: light-dark(#d0d0d0, #444); + --input-border-color: light-dark(#c8c8c8, #555); + + /* Interactive states */ + --hover-bg: light-dark(rgb(0 0 0 / 0.05), rgb(255 255 255 / 0.05)); + --hover-color: currentColor; + --paused-bg: light-dark(rgb(255 165 0 / 0.15), rgb(255 165 0 / 0.2)); + --paused-outline-color: rgb(255 140 0 / 0.6); + --paused-color: currentColor; + + /* Semantic */ + --ref-color: light-dark(#007b6e, #4ec9b0); + --ref-hover-color: light-dark(#065, #89d9c8); + --changed-bg: transparent; + --changed-color: light-dark(#c00, #f66); + --match-bg: light-dark(rgb(255 200 0 / 0.35), rgb(255 200 0 / 0.25)); + --match-outline-color: light-dark(rgb(200 140 0 / 0.8), rgb(255 200 0 / 0.6)); + + /* Syntax highlighting */ + --string-color: light-dark(#a31515, #ce9178); + --number-color: light-dark(#098658, #b5cea8); + --bool-color: light-dark(#00f, #569cd6); + --null-color: light-dark(#767676, #808080); + --name-color: light-dark(#795e26, #dcdcaa); + --stream-color: light-dark(#af00db, #c586c0); +} + +@media (forced-colors: active) { + :root { + /* Backgrounds */ + --bg-color: Canvas; + --surface-bg: Canvas; + --input-bg: Field; + --button-bg: ButtonFace; + --button-hover-bg: Highlight; + --clr-canvas-bg: var(--surface-bg); + + /* Text */ + --text-color: CanvasText; + --muted-color: GrayText; + --accent-color: CanvasText; + + /* Borders */ + --border-color: ButtonBorder; + --border-subtle-color: ButtonBorder; + --input-border-color: ButtonBorder; + + /* Interactive states */ + --hover-bg: Highlight; + --hover-color: HighlightText; + --paused-bg: Mark; + --paused-outline-color: ButtonBorder; + --paused-color: MarkText; + + /* Semantic */ + --ref-color: LinkText; + --ref-hover-color: ActiveText; + --changed-bg: Mark; + --changed-color: MarkText; + --match-bg: Mark; + --match-outline-color: ButtonBorder; + + /* Syntax highlighting — replaced by plain text in HCM */ + --string-color: CanvasText; + --number-color: CanvasText; + --bool-color: CanvasText; + --null-color: GrayText; + --name-color: CanvasText; + --stream-color: CanvasText; + } + + /* Opacity-only disabled style → explicit GrayText. */ + button:disabled, + input:disabled { + color: GrayText; + border-color: GrayText; + opacity: 1; + } +} + +* { + box-sizing: border-box; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +body.loading { + cursor: wait; +} +html { + height: 100%; +} +body { + font-family: "Courier New", Courier, monospace; + margin: 0; + padding: 16px; + background: var(--bg-color); + color: var(--text-color); + font-size: 13px; + line-height: 1.5; + display: flex; + flex-direction: column; +} + +/* In debug mode the body must be viewport-height so #debug-view can fill it. + In tree mode body is auto-height so the tree can grow and the page scrolls. */ +body:has(#debug-view:not([hidden])) { + height: 100%; + overflow: hidden; +} +#header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 12px; + + h1 { + color: var(--accent-color); + font-size: 1.2em; + margin: 0; + } + + #pdf-info { + font-family: system-ui, sans-serif; + font-size: 1.15em; + font-weight: 500; + color: var(--text-color); + } +} +#password-dialog { + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border-color); + border-radius: 6px; + padding: 20px; + min-width: 320px; + + &::backdrop { + background: rgb(0 0 0 / 0.4); + } + + p { + margin: 0 0 12px; + } + + input { + display: block; + width: 100%; + margin-top: 4px; + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border-color); + border-radius: 3px; + padding: 4px 8px; + font-family: inherit; + font-size: inherit; + } + + .password-dialog-buttons { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 16px; + + button { + padding: 4px 14px; + border-radius: 3px; + border: 1px solid var(--input-border-color); + background: var(--button-bg); + color: inherit; + cursor: pointer; + font-family: inherit; + font-size: inherit; + + &:hover { + background: var(--button-hover-bg); + } + } + } +} +#controls { + position: sticky; + top: 0; + z-index: 1; + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding: 10px 14px; + background: var(--surface-bg); + border-radius: 4px; + border: 1px solid var(--border-color); + + label { + display: flex; + align-items: center; + gap: 4px; + color: var(--muted-color); + } + + #github-link { + margin-inline-start: auto; + display: flex; + align-items: center; + color: var(--muted-color); + text-decoration: none; + + &:hover { + color: var(--text-color); + } + + svg { + width: 20px; + height: 20px; + fill: currentColor; + } + } +} +#goto-input { + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border-color); + border-radius: 3px; + padding: 2px 6px; + font-family: inherit; + font-size: inherit; + + &:disabled { + opacity: 0.4; + } + &[aria-invalid="true"] { + border-color: var(--changed-color); + } +} +#status { + color: var(--muted-color); + font-style: italic; +} +#debug-button, +#debug-back-button { + padding: 4px 12px; + border-radius: 3px; + border: 1px solid var(--input-border-color); + background: var(--button-bg); + color: inherit; + cursor: pointer; + font-family: inherit; + font-size: inherit; + + &:hover { + background: var(--button-hover-bg); + } +} diff --git a/web/pdf_internal_viewer.html b/web/internal/debugger.html similarity index 67% rename from web/pdf_internal_viewer.html rename to web/internal/debugger.html index e6156f630c0d5..1f5cfecd537ea 100644 --- a/web/pdf_internal_viewer.html +++ b/web/internal/debugger.html @@ -19,7 +19,7 @@ PDF.js — Debugging tools - +