diff --git a/src/core/internal_viewer_utils.js b/src/core/internal_viewer_utils.js index f5c8877d80582..0b5cd4cef41df 100644 --- a/src/core/internal_viewer_utils.js +++ b/src/core/internal_viewer_utils.js @@ -56,6 +56,7 @@ const InternalViewerUtils = { const refs = Array.isArray(contentsVal) ? contentsVal : [contentsVal]; const rawContents = []; const tokens = []; + const rawBytesArr = []; for (const rawRef of refs) { if (rawRef instanceof Ref) { rawContents.push({ num: rawRef.num, gen: rawRef.gen }); @@ -64,10 +65,21 @@ const InternalViewerUtils = { if (!(stream instanceof BaseStream)) { continue; } - tokens.push(...this.tokenizeStream(stream, xref)); + rawBytesArr.push(stream.getString()); + stream.reset(); + for (const token of this.tokenizeStream(stream, xref)) { + tokens.push(token); + } } + const rawBytes = rawBytesArr.join("\n"); const { instructions, cmdNames } = this.groupIntoInstructions(tokens); - return { contentStream: true, instructions, cmdNames, rawContents }; + return { + contentStream: true, + instructions, + cmdNames, + rawContents, + rawBytes, + }; }, // Lazily-built reverse map: OPS numeric id → property name string. diff --git a/web/pdf_internal_viewer.css b/web/pdf_internal_viewer.css index 553ef45b15ac3..67019554e9c37 100644 --- a/web/pdf_internal_viewer.css +++ b/web/pdf_internal_viewer.css @@ -15,18 +15,166 @@ :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; + } + + /* Resizer hover: accent color → CanvasText is wrong for a background. */ + #render-resizer, + #op-resizer, + #op-gfx-state-resizer { + &:hover, + &.dragging { + background: Highlight; + } + } + + /* Opacity trick for breakpoint glyph visibility → use Canvas color to hide. */ + .bp-gutter::before { + opacity: 1; + color: Canvas; + } + .bp-gutter:hover::before { + color: ButtonBorder; + } + .bp-gutter.active::before { + color: ButtonText; + } + + /* Opacity-only disabled style → explicit GrayText. */ + button:disabled, + input:disabled { + color: GrayText; + border-color: GrayText; + opacity: 1; + } + + /* Color swatch preserves the actual PDF color value. */ + .color-swatch { + forced-color-adjust: none; + } } + * { 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: light-dark(#fff, #1e1e1e); - color: light-dark(#1e1e1e, #d4d4d4); + 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; @@ -35,7 +183,7 @@ body { margin-bottom: 12px; h1 { - color: light-dark(#0070c1, #9cdcfe); + color: var(--accent-color); font-size: 1.2em; margin: 0; } @@ -44,13 +192,13 @@ body { font-family: system-ui, sans-serif; font-size: 1.15em; font-weight: 500; - color: light-dark(#1e1e1e, #d4d4d4); + color: var(--text-color); } } #password-dialog { - background: light-dark(#fff, #2d2d2d); - color: light-dark(#1e1e1e, #d4d4d4); - border: 1px solid light-dark(#ccc, #555); + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border-color); border-radius: 6px; padding: 20px; min-width: 320px; @@ -67,9 +215,9 @@ body { display: block; width: 100%; margin-top: 4px; - background: light-dark(#fff, #3c3c3c); - color: light-dark(#1e1e1e, #d4d4d4); - border: 1px solid light-dark(#c8c8c8, #555); + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border-color); border-radius: 3px; padding: 4px 8px; font-family: inherit; @@ -85,15 +233,15 @@ body { button { padding: 4px 14px; border-radius: 3px; - border: 1px solid light-dark(#c8c8c8, #555); - background: light-dark(#f3f3f3, #3c3c3c); + border: 1px solid var(--input-border-color); + background: var(--button-bg); color: inherit; cursor: pointer; font-family: inherit; font-size: inherit; &:hover { - background: light-dark(#e0e0e0, #4a4a4a); + background: var(--button-hover-bg); } } } @@ -108,26 +256,26 @@ body { gap: 12px; margin-bottom: 16px; padding: 10px 14px; - background: light-dark(#f3f3f3, #252526); + background: var(--surface-bg); border-radius: 4px; - border: 1px solid light-dark(#e0e0e0, #3c3c3c); + border: 1px solid var(--border-color); label { display: flex; align-items: center; gap: 4px; - color: light-dark(#6e6e6e, #888); + color: var(--muted-color); } #github-link { margin-inline-start: auto; display: flex; align-items: center; - color: light-dark(#6e6e6e, #aaa); + color: var(--muted-color); text-decoration: none; &:hover { - color: light-dark(#1e1e1e, #fff); + color: var(--text-color); } svg { @@ -138,9 +286,9 @@ body { } } #goto-input { - background: light-dark(#fff, #3c3c3c); - color: light-dark(#1e1e1e, #d4d4d4); - border: 1px solid light-dark(#c8c8c8, #555); + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border-color); border-radius: 3px; padding: 2px 6px; font-family: inherit; @@ -150,18 +298,21 @@ body { opacity: 0.4; } &[aria-invalid="true"] { - border-color: #f66; + border-color: var(--changed-color); } } #status { - color: light-dark(#6e6e6e, #888); + color: var(--muted-color); font-style: italic; } +#tree.loading { + pointer-events: none; +} #tree { padding: 8px 12px; - background: light-dark(#f3f3f3, #252526); + background: var(--surface-bg); border-radius: 4px; - border: 1px solid light-dark(#e0e0e0, #3c3c3c); + border: 1px solid var(--border-color); min-height: 60px; .node { @@ -169,23 +320,23 @@ body { padding: 1px 0; } .key { - color: light-dark(#0070c1, #9cdcfe); + color: var(--accent-color); } .separator { - color: light-dark(#6e6e6e, #888); + color: var(--muted-color); } [role="button"] { display: inline-block; width: 14px; font-size: 0.7em; - color: light-dark(#666, #aaa); + color: var(--muted-color); cursor: pointer; user-select: none; vertical-align: middle; } [role="group"] { padding-left: 20px; - border-left: 1px dashed light-dark(#d0d0d0, #444); + border-left: 1px dashed var(--border-subtle-color); margin-left: 2px; &.hidden { @@ -193,31 +344,31 @@ body { } } .ref { - color: light-dark(#007b6e, #4ec9b0); + color: var(--ref-color); cursor: pointer; text-decoration: underline dotted; &:hover { - color: light-dark(#065, #89d9c8); + color: var(--ref-hover-color); } } .str-value { - color: light-dark(#a31515, #ce9178); + color: var(--string-color); } .num-value { - color: light-dark(#098658, #b5cea8); + color: var(--number-color); } .bool-value { - color: light-dark(#00f, #569cd6); + color: var(--bool-color); } .null-value { - color: light-dark(#767676, #808080); + color: var(--null-color); } .name-value { - color: light-dark(#795e26, #dcdcaa); + color: var(--name-color); } .bracket { - color: light-dark(#6e6e6e, #888); + color: var(--muted-color); cursor: pointer; user-select: none; @@ -226,79 +377,630 @@ body { } } .stream-label { - color: light-dark(#af00db, #c586c0); + color: var(--stream-color); font-style: italic; } [role="status"] { - color: light-dark(#6e6e6e, #888); + color: var(--muted-color); font-style: italic; } [role="alert"] { - color: #f66; + color: var(--changed-color); } .bytes-content { padding-left: 20px; white-space: pre-wrap; font-size: 1em; opacity: 0.85; - color: light-dark(#a31515, #ce9178); + color: var(--string-color); } .bytes-hex { font-family: monospace; - color: light-dark(#00f, #569cd6); + color: var(--bool-color); } .image-preview { display: block; margin-top: 4px; max-width: 40%; + height: auto; image-rendering: pixelated; - border: 1px solid light-dark(#ccc, #444); - } - .content-stream-parsed { - display: none; - } - .content-stream-raw { - display: inline; + border: 1px solid var(--border-subtle-color); } - &.parse-cs-active { - .content-stream-parsed { - display: inline; - } - .content-stream-raw { - display: none; - } + .content-stm-scroll { + display: flex; + flex-direction: column; + max-height: 60vh; + border: 1px solid var(--border-subtle-color); + border-radius: 3px; + overflow: hidden; } - .content-stream { - line-height: 1.8; - } - .cs-instruction { - display: block; - white-space: nowrap; + .content-stm-load-sentinel { + height: 1px; } .token-cmd { - color: light-dark(#0070c1, #9cdcfe); + color: var(--accent-color); font-weight: bold; } .token-num { - color: light-dark(#098658, #b5cea8); + color: var(--number-color); } .token-str { - color: light-dark(#a31515, #ce9178); + color: var(--string-color); } .token-name { - color: light-dark(#795e26, #dcdcaa); + color: var(--name-color); } .token-bool { - color: light-dark(#00f, #569cd6); + color: var(--bool-color); } .token-null { - color: light-dark(#767676, #808080); + color: var(--null-color); } .token-ref { - color: light-dark(#007b6e, #4ec9b0); + color: var(--ref-color); } .token-array, .token-dict { - color: light-dark(#1e1e1e, #d4d4d4); + color: var(--text-color); + } +} +#debug-btn, +#debug-back-btn { + 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); + } +} +#debug-view { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + gap: 10px; + + &[hidden] { + display: none; + } +} +#render-panels { + flex: 1; + min-height: 0; + display: flex; + flex-direction: row; + gap: 0; + align-items: stretch; +} +#render-resizer { + flex-shrink: 0; + width: 6px; + align-self: stretch; + cursor: col-resize; + background: var(--border-color); + + &:hover, + &.dragging { + background: var(--accent-color); + } +} +/* ── Shared code-view layout (content stream and op-list) ──────────────── */ +/* Row wrapper that sits between the toolbar and the scrollable content. + Hosts the frozen line-number column and the actual scroll container. */ +.content-stm-body, +.op-list-body { + flex: 1; + display: flex; + flex-direction: row; + min-height: 0; +} +.content-stm-body { + line-height: 1.8; +} +/* The line-number column lives *outside* the scroll container so it is + never affected by horizontal or vertical scroll. Its scrollTop is kept + in sync with the adjacent scroll container via a JS scroll listener. */ +.cs-line-nums-col { + overflow: hidden; + flex-shrink: 0; + background: var(--surface-bg); + border-inline-end: 1px solid var(--border-subtle-color); +} +.content-stm-inner { + flex: 1; + overflow: auto; + /* Disable scroll anchoring so manual scrollTop corrections aren't doubled. */ + overflow-anchor: none; +} +.content-stm-instruction { + display: block; + white-space: nowrap; + padding-inline-start: 0.5em; +} +.raw-bytes-stream { + color: var(--string-color); +} +/* ── Shared search/goto toolbar (used in .content-stm-scroll and #op-list-panel) ── */ +.cs-goto-bar { + position: sticky; + top: 0; + display: flex; + align-items: center; + gap: 8px; + padding: 3px 4px; + background: var(--surface-bg); + border-bottom: 1px solid var(--border-subtle-color); + z-index: 1; + + .cs-search-group { + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; + } + + .cs-search-input, + .cs-goto { + font: inherit; + font-size: 0.85em; + padding: 2px 6px; + border: 1px solid var(--input-border-color); + border-radius: 3px; + background: var(--input-bg); + color: var(--text-color); + + &:focus { + outline: 2px solid var(--accent-color); + outline-offset: 0; + } + + &[aria-invalid="true"] { + border-color: red; + } + } + + .cs-search-input { + width: 140px; + } + + .cs-goto { + width: 110px; + margin-inline-start: auto; + } + + .cs-nav-btn { + font: inherit; + font-size: 0.85em; + padding: 1px 6px; + border: 1px solid var(--input-border-color); + border-radius: 3px; + background: var(--button-bg); + color: var(--text-color); + cursor: pointer; + line-height: 1.4; + + &:hover:not(:disabled) { + background: var(--button-hover-bg); + } + + &:disabled { + opacity: 0.4; + cursor: default; + } + + &[aria-pressed="true"] { + background: var(--accent-color); + color: light-dark(white, black); + border-color: var(--accent-color); + } + } + + .cs-match-info { + font-size: 0.8em; + color: var(--muted-color); + white-space: nowrap; + min-width: 4ch; + } + + .cs-check-label { + display: flex; + align-items: center; + gap: 3px; + font-size: 0.85em; + cursor: pointer; + white-space: nowrap; + } +} +.cs-num-item { + display: block; + white-space: nowrap; +} +/* Op-list num items need padding/spacing to match .op-line and #op-list-panel. */ +.op-list-body .cs-num-item { + padding: 1px 0; +} +.op-list-body .cs-line-nums-col { + padding-block: 8px; +} +.cs-num-item.cs-match { + background: var(--match-bg); +} +.cs-line-num { + display: inline-block; + min-width: var(--line-num-width, 3ch); + padding-inline: 0.4em; + text-align: right; + font-family: monospace; + font-size: 0.8em; + color: var(--muted-color); + user-select: none; +} +.cs-match { + background: var(--match-bg); + outline: 1px solid var(--match-outline-color); + border-radius: 2px; +} + +#op-left-col { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} +#op-resizer { + flex-shrink: 0; + height: 6px; + cursor: row-resize; + background: var(--border-color); + + &:hover, + &.dragging { + background: var(--accent-color); + } +} +#op-top-row { + flex: 4 1 0; + display: flex; + flex-direction: row; + min-height: 0; +} +/* Wrapper injected by showRenderView() to host the toolbar above #op-list-panel + without it being part of the horizontal-scroll area. */ +.op-list-panel-wrapper { + flex: 1 1 0; + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + + & > .cs-goto-bar { + position: static; + border: 1px solid var(--border-color); + border-bottom-color: var(--border-subtle-color); + border-radius: 4px 4px 0 0; + } + + & > .op-list-body > #op-list-panel { + flex: 1 1 0; + border-radius: 0 0 4px 4px; + border-top: none; + } +} +#op-list-panel { + flex: 1 1 0; + overflow: auto; + min-width: 0; + min-height: 0; + padding: 8px 12px; + background: var(--surface-bg); + border-radius: 4px; + border: 1px solid var(--border-color); +} +#op-list { + min-width: max-content; +} +#op-gfx-state-resizer { + flex-shrink: 0; + width: 6px; + align-self: stretch; + cursor: col-resize; + background: var(--border-color); + + &:hover, + &.dragging { + background: var(--accent-color); + } +} +#gfx-state-panel { + flex: 0 0 auto; + width: 40ch; + overflow: auto; + min-height: 0; + padding: 8px 12px; + background: var(--surface-bg); + border-radius: 4px; + border: 1px solid var(--border-color); + + .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; + + &[hidden] { + display: none; + } + } + .gfx-state-stack-btn { + padding: 0 3px; + border: 1px solid currentcolor; + border-radius: 2px; + background: transparent; + color: inherit; + cursor: pointer; + line-height: 1.3; + + &:disabled { + opacity: 0.35; + cursor: default; + } + } + .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; + } +} +#op-detail-panel { + flex: 1 1 0; + container-type: size; + overflow: auto; + min-height: 0; + padding: 8px 12px; + background: var(--surface-bg); + border-radius: 4px; + border: 1px solid var(--border-color); + + .detail-name { + color: var(--accent-color); + font-weight: bold; + margin-bottom: 4px; + } + .detail-empty { + color: var(--muted-color); + font-style: italic; + } + .detail-row { + display: flex; + align-items: center; + gap: 8px; + padding: 1px 0; } + .detail-idx { + color: var(--muted-color); + flex-shrink: 0; + } + .detail-val { + color: var(--number-color); + white-space: pre-wrap; + word-break: break-all; + } + .detail-body { + display: flex; + flex-direction: row; + gap: 12px; + align-items: flex-start; + } + .detail-args-col { + flex: 1; + min-width: 0; + } + .detail-img-col { + flex-shrink: 0; + max-width: 45%; + overflow: hidden; + + .image-preview { + height: 90cqh; + width: auto; + max-width: 100%; + margin-top: 0; + } + } + .path-preview { + flex-shrink: 0; + border: 1px solid var(--border-subtle-color); + border-radius: 3px; + background: var(--bg-color); + } +} +#canvas-panel { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + background: var(--surface-bg); + border-radius: 4px; + border: 1px solid var(--border-color); +} +#canvas-toolbar { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 4px 8px; + border-bottom: 1px solid var(--border-color); + + button { + padding: 1px 8px; + border-radius: 3px; + border: 1px solid var(--input-border-color); + background: var(--button-bg); + color: inherit; + cursor: pointer; + font-family: inherit; + font-size: 1.1em; + line-height: 1.4; + + &:hover { + background: var(--button-hover-bg); + } + &:disabled { + opacity: 0.4; + cursor: default; + } + } + + #zoom-level { + min-width: 4ch; + text-align: center; + } +} +#canvas-scroll { + flex: 1; + overflow: auto; + padding: 8px 12px; + min-height: 0; + background: var(--clr-canvas-bg); + display: flex; + flex-direction: column; + align-items: safe center; + gap: 12px; +} +#canvas-wrapper { + position: relative; + display: inline-block; + line-height: 0; +} +.temp-canvas-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + cursor: pointer; +} +.temp-canvas-label { + font-size: 0.85em; + color: var(--muted-color); + font-style: italic; +} +.temp-canvas-wrapper canvas { + border: 1px solid var(--border-subtle-color); + zoom: calc(1 / var(--dpr, 1)); +} +#render-canvas { + cursor: pointer; +} +#highlight-canvas { + position: absolute; + top: 0; + left: 0; + pointer-events: none; +} +.op-line { + display: flex; + align-items: center; + gap: 0.5ch; + white-space: nowrap; + padding: 1px 0; + cursor: pointer; + + &.selected { + text-decoration: underline; + } + + &:hover { + background: var(--hover-bg); + color: var(--hover-color); + } +} +.color-swatch { + width: 14px; + height: 14px; + border-radius: 50%; + border: 1px solid var(--muted-color); + flex-shrink: 0; +} +.op-name { + color: var(--accent-color); + font-weight: bold; +} +.op-arg { + color: var(--number-color); +} +.changed-value { + font-weight: bold; + background: var(--changed-bg); + color: var(--changed-color); +} +.bp-gutter { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + flex-shrink: 0; + cursor: pointer; + user-select: none; + + &::before { + content: "●"; + color: var(--changed-color); + font-size: 0.75em; + opacity: 0; + } + + &:hover::before { + opacity: 0.4; + } + + &.active::before { + opacity: 1; + } +} +.op-line.paused { + background: var(--paused-bg); + color: var(--paused-color); + outline: 1px solid var(--paused-outline-color); + outline-offset: -1px; } diff --git a/web/pdf_internal_viewer.html b/web/pdf_internal_viewer.html index b31ae57bdccce..e6156f630c0d5 100644 --- a/web/pdf_internal_viewer.html +++ b/web/pdf_internal_viewer.html @@ -18,12 +18,12 @@
-