Syntax highlighting and zoomable view updates

This commit is contained in:
Kilian Schüttler 2025-03-25 17:51:35 +01:00
parent 4637c66185
commit fcfa06f691
13 changed files with 410 additions and 262 deletions

View File

@ -3,9 +3,10 @@
import { UploadOutline } from 'flowbite-svelte-icons'; import { UploadOutline } from 'flowbite-svelte-icons';
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor';
import type ContentModel from '../models/ContentModel.svelte'; import type ContentModel from '../models/ContentModel.svelte';
import { opLookup } from '../models/OperatorList'; import { getPDFOperator, opLookup } from '../models/OperatorList';
import { OperatorList } from '../models/OperatorList.js'; import { OperatorList } from '../models/OperatorList.js';
import type { PDFOperator } from '../models/OperatorList';
const opIdRegex = /opId-(\d+)/;
monaco.languages.register({ id: 'pdf-content-stream' }); monaco.languages.register({ id: 'pdf-content-stream' });
@ -85,7 +86,7 @@
minimap: { enabled: false }, minimap: { enabled: false },
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
fontSize: 14, fontSize: 14,
automaticLayout: true automaticLayout: true,
}); });
// Add change listener to detect edits // Add change listener to detect edits
@ -95,6 +96,25 @@
loadContents(contents); loadContents(contents);
editor.onMouseDown((e) => {
if (!e.target.position) return;
const decorations = editor.getModel()?.getDecorationsInRange({
startLineNumber: e.target.position.lineNumber,
startColumn: e.target.position.column,
endLineNumber: e.target.position.lineNumber,
endColumn: e.target.position.column
});
decorations?.forEach(d => {
const match = d.options.className?.match(opIdRegex);
if (!match) {
return;
}
console.log(contents.opList.fnArray[+match[1]]);
console.log(contents.opList.argsArray[+match[1]]);
})
});
return () => { return () => {
editor.dispose(); editor.dispose();
}; };
@ -104,6 +124,44 @@
loadContents(contents); loadContents(contents);
}); });
function addDecorations(contents: ContentModel) {
if (contents.opList && contents.opList.fnArray.length > 0) {
const decorations = [];
for (let i = 0; i < contents.opList.fnArray.length; i++) {
const fnId = contents.opList.fnArray[i];
const args = contents.opList.argsArray[i];
const range = contents.opList.rangeArray[i];
const operator = getPDFOperator(fnId);
const model = editor.getModel();
if (model && operator && range) {
const startPos = model.getPositionAt(range[0]);
const endPos = model.getPositionAt(range[1]);
const monacoRange = new monaco.Range(
startPos.lineNumber,
startPos.column,
endPos.lineNumber,
endPos.column
);
let className = `${operator.class} highlightOp opId-${i}`;
// Add class-based decoration for operator highlighting
decorations.push({
range: monacoRange,
options: {
className: className,
hoverMessage: { value: OperatorList.formatOperatorToMarkdown(fnId, args) }
}
});
}
}
// Apply all decorations at once
editor.createDecorationsCollection(decorations);
}
}
function loadContents(contents: ContentModel | undefined) { function loadContents(contents: ContentModel | undefined) {
if (!contents || !editor) return; if (!contents || !editor) return;
@ -114,36 +172,8 @@
lastContents = contents; lastContents = contents;
editor.setValue(contents.toDisplay()); editor.setValue(contents.toDisplay());
isEdited = false; isEdited = false;
// Add decorations for operator ranges if available
if (contents.opList && contents.opList.fnArray.length > 0) {
const decorations = [];
for (let i = 0; i < contents.opList.fnArray.length; i++) {
const fnId = contents.opList.fnArray[i];
const range = contents.opList.rangeArray[i];
const operator = opLookup[fnId];
if (operator && range) { addDecorations(contents);
const startPos = editor.getModel()!.getPositionAt(range[0]);
const endPos = editor.getModel()!.getPositionAt(range[1]);
const monacoRange = new monaco.Range(
startPos.lineNumber,
startPos.column,
endPos.lineNumber,
endPos.column);
if (fnId == 91) {
console.log(range, monacoRange)
}
decorations.push({
range: monacoRange,
options: {
className: operator.class,
hoverMessage: {value: OperatorList.formatOperatorToMarkdown(fnId)},
}
});
}
}
editor.createDecorationsCollection(decorations);
}
} }
</script> </script>
@ -170,4 +200,8 @@
background-color: #3c3d41; background-color: #3c3d41;
} }
:global(.highlightOp) {
@apply border border-forge-sec rounded;
}
</style> </style>

View File

@ -12,7 +12,8 @@
<RefreshOutline class="animate-spin" size="xl" /> <RefreshOutline class="animate-spin" size="xl" />
</div> </div>
{:else} {:else}
<ZoomableContainer imgUrl={img} {height} /> <ZoomableContainer imgUrl={img} {height}>
</ZoomableContainer>
{/if} {/if}
</div> </div>

View File

@ -1,48 +1,66 @@
<script lang="ts"> <script lang="ts">
import {Pane, Splitpanes} from "svelte-splitpanes"; import { Pane, Splitpanes } from 'svelte-splitpanes';
import type FileViewState from "../models/FileViewState.svelte"; import type FileViewState from '../models/FileViewState.svelte';
import StreamEditor from "./StreamEditor.svelte"; import ContentModel from '../models/ContentModel.svelte';
import ContentModel from "../models/ContentModel.svelte"; import type DocumentWorker from '../models/Document.svelte';
import type DocumentWorker from '../models/Document.svelte'; import ContentEditor from './ContentEditor.svelte';
import ContentEditor from './ContentEditor.svelte'; import ZoomableContainer from './ZoomableContainer.svelte';
let display: HTMLCanvasElement; let display: HTMLCanvasElement;
let {fState, height}: {fState: FileViewState, height: number} = $props(); let { fState, height }: { fState: FileViewState, height: number } = $props();
let renderer: DocumentWorker = $derived(fState.document); let renderer: DocumentWorker = $derived(fState.document);
let pageNum = $derived(fState.currentPageNumber); let pageNum = $derived(fState.currentPageNumber);
let contents: ContentModel | undefined = $state(undefined) let contents: ContentModel | undefined = $state(undefined);
$effect(() => { $effect(() => {
if (pageNum) { refresh();
renderer.renderPage(pageNum, display); });
renderer.getContents(pageNum).then(parts => {
contents = parts; function refresh() {
} if (pageNum) {
); renderer.getContents(pageNum).then(parts => {
} else { contents = parts;
contents = undefined; renderer.renderPage(pageNum, display);
} }
}) );
} else {
contents = undefined;
}
}
async function update(newData: string) {
if (pageNum) {
await fState.document.updateContents(pageNum, newData);
refresh();
}
}
</script> </script>
{#if pageNum} {#if pageNum}
<Splitpanes theme="forge-movable"> <Splitpanes theme="forge-movable">
<Pane minSize={1}> <Pane minSize={1}>
<div class="overflow-hidden"> <div class="overflow-hidden">
{#if contents} {#if contents}
<ContentEditor save={(newData) => console.log("Save")} contents={contents} height={height - 1}></ContentEditor> <ContentEditor save={(newData) => update(newData)} contents={contents} height={height - 1}></ContentEditor>
{/if} {/if}
</div> </div>
</Pane> </Pane>
<Pane minSize={1}> <Pane minSize={1}>
<canvas bind:this={display}></canvas> <ZoomableContainer bind:canvas={display} {height}></ZoomableContainer>
</Pane> </Pane>
</Splitpanes> </Splitpanes>
{:else } {:else }
<h1>Please select a Page!</h1> <h1>Please select a Page!</h1>
{/if} {/if}
<style lang="postcss"> <style lang="postcss">
.scroller {
@apply w-full h-full bg-forge-dark;
overflow: auto;
display: flex;
}
.container {
display: contents;
}
</style> </style>

View File

@ -99,11 +99,6 @@
<div class="relative"> <div class="relative">
<div bind:this={editorContainer} style:height={height + "px"}></div> <div bind:this={editorContainer} style:height={height + "px"}></div>
{#if isEdited}
<button onclick={() => save(editor.getValue())} class="save-button">
<UploadOutline size="xl" />
</button>
{/if}
</div> </div>
<style lang="postcss"> <style lang="postcss">

View File

@ -1,162 +1,177 @@
<script lang="ts"> <script lang="ts">
import ZoomControls from "../components/ZoomControls.svelte"; import ZoomControls from '../components/ZoomControls.svelte';
import { onMount } from 'svelte';
type ZoomableProps = { type ZoomableProps = {
minZoom?: number; minZoom?: number;
maxZoom?: number; maxZoom?: number;
zoomStep?: number; zoomStep?: number;
imgUrl: string; imgUrl?: string;
height?: string | number; canvas?: HTMLCanvasElement;
}; height?: string | number;
};
let { let {
minZoom = 0.5, minZoom = 0.5,
maxZoom = 10, maxZoom = 10,
zoomStep = 0.1, zoomStep = 0.1,
imgUrl, imgUrl,
height, canvas = $bindable(),
}: ZoomableProps = $props(); height
}: ZoomableProps = $props();
let scale = $state(1); let currentImgUrl = $state(imgUrl);
// DOM refs type ViewState = {
let container = $state<HTMLElement>(); x: number;
let image = $state<HTMLElement>(); y: number;
scale: number;
}
// Drag state let viewState: ViewState = $state({
type DragState = { x: 0,
startX: number; y: 0,
startY: number; scale: 1
scrollLeft: number; });
scrollTop: number;
};
let dragState: DragState | undefined = $state({ // DOM refs
startX: 0, let container = $state<HTMLElement>();
startY: 0,
scrollLeft: 0,
scrollTop: 0,
});
// Zoom handlers // Drag state
function handleZoom(event: WheelEvent) { type DragState = {
if (!container || !image || !event.ctrlKey) return; startX: number;
startY: number;
};
event.preventDefault(); let dragState: DragState | undefined = $state(undefined);
const delta = -Math.sign(event.deltaY);
const newScale = Math.min(
Math.max(scale + delta * zoomStep, minZoom),
maxZoom,
);
if (newScale !== scale) { // Zoom handlers
const rect = image.getBoundingClientRect(); function handleZoom(event: WheelEvent) {
const x = event.clientX; if (!container || !event.ctrlKey) return;
const y = event.clientY;
const scaleChange = newScale / scale; event.preventDefault();
container.scrollLeft = x * scaleChange - x + container.scrollLeft; const delta = -Math.sign(event.deltaY);
container.scrollTop = y * scaleChange - y + container.scrollTop; const newScale = Math.min(
Math.max(viewState.scale + delta * zoomStep * viewState.scale, minZoom),
maxZoom
);
scale = newScale; if (newScale !== viewState.scale) {
} const x = event.clientX;
} const y = event.clientY;
// Drag handlers const scaleChange = newScale / viewState.scale;
function handleDragStart(event: MouseEvent) { container.scrollLeft = x * scaleChange - x + container.scrollLeft;
if (!container) return; container.scrollTop = y * scaleChange - y + container.scrollTop;
viewState.scale = newScale;
}
}
event.preventDefault(); // Drag handlers
dragState = { function handleDragStart(event: MouseEvent) {
startX: event.pageX - container.offsetLeft, if (!container || event.button) return;
startY: event.pageY - container.offsetTop,
scrollLeft: container.scrollLeft,
scrollTop: container.scrollTop,
};
}
function handleDragMove(event: MouseEvent) { event.preventDefault();
if (!container || !dragState) return; dragState = {
startX: event.pageX - viewState.x,
startY: event.pageY - viewState.y
};
}
event.preventDefault(); function handleDragMove(event: MouseEvent) {
const x = event.pageX - container.offsetLeft; if (!container || !dragState) return;
const y = event.pageY - container.offsetTop;
container.scrollLeft = dragState.scrollLeft - (x - dragState.startX); event.preventDefault();
container.scrollTop = dragState.scrollTop - (y - dragState.startY); const x = event.pageX - container.offsetLeft;
} const y = event.pageY - container.offsetTop;
function handleDragEnd(event: MouseEvent) { viewState.x = (x - dragState.startX);
event.preventDefault(); viewState.y = (y - dragState.startY);
dragState = undefined; }
}
function resetZoom() { function handleDragEnd(event: MouseEvent) {
scale = 1; event.preventDefault();
if (container) { dragState = undefined;
container.scrollLeft = 0; }
container.scrollTop = 0;
}
}
// Event listener management function resetZoom() {
$effect(() => { viewState.scale = 1;
if (container) { if (container) {
container.addEventListener("wheel", handleZoom, { passive: false }); viewState.x = 0;
return () => container.removeEventListener("wheel", handleZoom); viewState.y = 0;
} }
}); }
// Event listener management
$effect(() => {
if (container) {
container.addEventListener('wheel', handleZoom, { passive: false });
return () => container?.removeEventListener('wheel', handleZoom);
}
});
$effect(() => {
if (canvas) {
currentImgUrl = canvas.toDataURL();
} else if (imgUrl) {
currentImgUrl = imgUrl;
}
});
</script> </script>
<div class="relative w-full h-full"> <div class="relative w-full h-full">
<ZoomControls <ZoomControls
{scale} scale={viewState.scale}
onZoomIn={() => (scale = Math.min(scale + zoomStep, maxZoom))} onZoomIn={() => (viewState.scale = Math.min(viewState.scale + zoomStep, maxZoom))}
onZoomOut={() => (scale = Math.max(scale - zoomStep, minZoom))} onZoomOut={() => (viewState.scale = Math.max(viewState.scale - zoomStep, minZoom))}
{resetZoom} {resetZoom}
/> />
<div <div
class="container" class="container"
bind:this={container} bind:this={container}
onmousedown={handleDragStart} onmousedown={handleDragStart}
onmousemove={handleDragMove} onmousemove={handleDragMove}
onmouseup={handleDragEnd} onmouseup={handleDragEnd}
onmouseleave={handleDragEnd} onmouseleave={handleDragEnd}
role="presentation" role="presentation"
> style:height={height + "px"}
<div style:max-height={height + "px"}
class="image-container" >
bind:this={image} {#if imgUrl}
style:transform="scale({scale})" <img
style:height={height + "px"} class="zoomimage"
style:max-height={height + "px"} alt="rendered-page"
> src={currentImgUrl}
style:transform="translate({viewState.x}px, {viewState.y}px) scale({viewState.scale})"
<img style:max-height={height + "px"}
alt="rendered-page" />
src={imgUrl} {:else}
/> <canvas
</div> bind:this={canvas}
</div> class="zoomimage"
style:transform="translate({viewState.x}px, {viewState.y}px) scale({viewState.scale})"
>
</canvas>
{/if}
</div>
</div> </div>
<style lang="postcss"> <style lang="postcss">
.container { .container {
@apply w-full h-full bg-forge-dark; @apply w-full h-full bg-forge-dark;
overflow: auto; overflow: hidden;
cursor: grab; cursor: grab;
display: flex;
justify-content: center;
align-items: center;
} }
.container:active { .container:active {
cursor: grabbing; cursor: grabbing;
} }
.image-container { .zoomimage {
transform-origin: 0 0; position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
} }
</style> </style>

View File

@ -1,24 +1,49 @@
import type { OperatorList } from './OperatorList'; import type { OperatorList } from './OperatorList';
export default class ContentModel { export default class ContentModel {
parts: string[]; content: string;
opList: OperatorList; opList: OperatorList;
constructor(parts: string[], opList: OperatorList) { constructor(content: string, opList: OperatorList) {
this.parts = parts; this.content = content;
this.opList = opList; this.opList = opList;
} }
public toDisplay(): string { // PDF allows for \n\r and \n intermixed. Monaco does not.
// So we to recalculate the content and ranges to contain \n only
let text = ""; private unifyEOL() {
if (this.parts.length > 1) { const newRanges: number[][] = [];
for (const part of this.parts) { const result = this.replaceCRLF(this.content);
text += part; for (let i = 0; i < this.opList.rangeArray.length; i++){
const oldRange: number[] = this.opList.rangeArray[i];
if (!oldRange) {
newRanges.push([0, 0]);
continue;
} }
} else if (this.parts.length == 1) { newRanges.push([result.lookup[oldRange[0]], result.lookup[oldRange[1]]]);
text = this.parts[0];
} }
return text; this.content = result.newStr;
this.opList.setRangeArray(newRanges);
}
private replaceCRLF(str: string) {
let newStr = "";
let count = 0;
const lookup = [];
for (let i = 0; i < str.length; i++) {
if (str[i] !== "\r") {
newStr += str[i];
}
if (i > 0 && str[i-1] === "\r") {
count++;
}
lookup.push(i - count);
}
return {newStr: newStr, lookup: lookup};
}
public toDisplay(): string {
this.unifyEOL();
return this.content;
} }
} }

View File

@ -62,24 +62,22 @@ export default class DocumentWorker {
public async getContents(pageNumber: number): Promise<ContentModel> { public async getContents(pageNumber: number): Promise<ContentModel> {
try { try {
const pathStem = `/Page${pageNumber}/Contents`; const page = await this.doc.getPage(pageNumber);
const contents = await this.doc.getPrimitiveByPath(pathStem); const contentsPromise = page.getContents();
const promises = []; const operatorList = await page.getOperatorList(pageNumber)
if (contents && contents.ptype === "Array") { const opList = OperatorList.createUnfoldedOpList(operatorList.fnArray, operatorList.argsArray, operatorList.rangeArray);
for (const child of contents.children) { return new ContentModel(await contentsPromise, opList);
promises.push(this.doc.getStreamAsString(`${pathStem}/${child.key}/Data`));
}
} else {
promises.push(this.doc.getStreamAsString(`${pathStem}/Data`));
}
const parts = await Promise.all(promises);
return new ContentModel(parts, await this.getOperatorList(pageNumber));
} catch (error: unknown) { } catch (error: unknown) {
console.error(error); console.error(error);
return new ContentModel([], new OperatorList([], [], [])); return new ContentModel("", new OperatorList([], [], []));
} }
} }
public async updateContents(pageNumber: number, newContents: string) {
const page = await this.doc.getPage(pageNumber);
await page.updateContents(newContents);
}
public dispose() { public dispose() {
this.doc.destroy(); this.doc.destroy();
} }

View File

@ -19,15 +19,15 @@ export default class FileViewState {
public currentPageNumber: number | undefined = $state(); public currentPageNumber: number | undefined = $state();
public treeMode: boolean = $state(true); public treeMode: boolean = $state(false);
public pageMode: boolean = $state(false); public pageMode: boolean = $state(true);
public xRefShowing: boolean = $state(false); public xRefShowing: boolean = $state(false);
public notificationsShowing: boolean = $state(false); public notificationsShowing: boolean = $state(false);
public changesShowing: boolean = $state(false); public changesShowing: boolean = $state(false);
public canForward: boolean = $derived(this.pathHistory.canForward); public canForward: boolean = $derived(this.pathHistory.canForward);
public canBack: boolean = $derived(this.pathHistory.canBack); public canBack: boolean = $derived(this.pathHistory.canBack);
public path: string[] = $state(['Trailer']); public path: string[] = $state(['Page1']);
public container_prim: Primitive | undefined = $state(); public container_prim: Primitive | undefined = $state();
public selected_leaf_prim: Primitive | undefined = $state(); public selected_leaf_prim: Primitive | undefined = $state();
public xref_entries: XRefEntry[] = $state([]); public xref_entries: XRefEntry[] = $state([]);
@ -214,6 +214,8 @@ export default class FileViewState {
if (!this.canBack) return; if (!this.canBack) return;
const prevPath = this.pathHistory.goBack(); const prevPath = this.pathHistory.goBack();
if (prevPath) { if (prevPath) {
// prevent browser from leaving page
history.pushState(null, "", location.href);
this.selectPath(prevPath); this.selectPath(prevPath);
} }
} }

View File

@ -8,32 +8,93 @@ export interface PDFOperator {
} }
export class OperatorList { export class OperatorList {
fnArray: []; fnArray: number[];
argsArray: []; argsArray: [];
rangeArray: []; rangeArray: number[][];
constructor(fnArray: [], argsArray: [], rangeArray: []) { constructor(fnArray: [], argsArray: [], rangeArray: []) {
this.fnArray = fnArray; this.fnArray = fnArray;
this.argsArray = argsArray; this.argsArray = argsArray;
this.rangeArray = rangeArray; this.rangeArray = rangeArray;
} }
public static formatOperatorToMarkdown(operatorId: keyof typeof opLookup): string { public setRangeArray(rangeArray: number[][]) {
this.rangeArray = rangeArray;
}
public static createUnfoldedOpList(fnArray: [], argsArray: [], rangeArray: []) {
const unfoldedFnArray = [];
const unfoldedArgsArray = [];
const unfoldedRangeArray = [];
for (let i = 0; i < fnArray.length; i++) {
unfoldedFnArray.push(fnArray[i]);
unfoldedArgsArray.push(argsArray[i]);
unfoldedRangeArray.push(rangeArray[i]);
if (fnArray[i] == 91) {
const [subFnArray, subArgsArray, subRangeArray] = OperatorList.unfoldConstructPath(
argsArray[i]
);
unfoldedFnArray.push(...subFnArray);
unfoldedArgsArray.push(...subArgsArray);
unfoldedRangeArray.push(...subRangeArray);
}
}
return new OperatorList(unfoldedFnArray, unfoldedArgsArray, unfoldedRangeArray);
}
private static unfoldConstructPath(args: [[], [], [], []]) {
const [subFnArray, subArgs, , subRanges] = args;
const subArgsArray = [];
const subRangesArray = [];
let argsOffset = 0;
for (let i = 0; i < subFnArray.length; i++) {
const op = getPDFOperator(subFnArray[i]);
const rangeOffset = i * 2;
const range = subRanges.slice(rangeOffset, rangeOffset + 2);
subRangesArray.push(range);
const newSubArgs = [];
for (let j = 0; j < (op.numArgs ?? 0); j++) {
newSubArgs.push(subArgs[argsOffset++]);
}
subArgsArray.push(newSubArgs);
}
return [subFnArray, subArgsArray, subRangesArray];
}
public static formatOperatorToMarkdown(operatorId: number, args: any): string {
const operator = opLookup[operatorId] as PDFOperator; const operator = opLookup[operatorId] as PDFOperator;
if (!operator) { if (!operator) {
return `Unknown operator: ${operatorId}`; return `Unknown operator: ${operatorId}`;
} }
const keyword = operator.keyword ? `\`${operator.keyword}\`` : 'N/A'; const keyword = operator.keyword ? `\`${operator.keyword}\`` : 'N/A';
const args = operator.numArgs !== undefined ? const numArgs =
`Arguments: ${operator.numArgs}${operator.variableArgs ? ' (variable)' : ''}` : operator.numArgs
'No arguments'; ? `Arguments: ${operator.numArgs}${operator.variableArgs ? ' (variable)' : ''}`
: 'No arguments';
return `**${operator.name}** (${keyword}) ` + if (operator.keyword === "Tj" || operator.keyword === "TJ" && Array.isArray(args)) {
`${operator.description} ` + let content = '';
`${args}`; content += "`"
for (const arg of args[0]) {
if (arg.unicode) {
content += arg.unicode;
}
}
content += "`"
return `**${operator.name}** (${keyword})
${operator.description}:
${content}
${numArgs}`;
}
return `**${operator.name}** (${keyword})
${operator.description}
${numArgs}`;
} }
} }
export function getPDFOperator(id: number | string): PDFOperator {
return opLookup[id];
}
export const opLookup = { export const opLookup = {
'1': { '1': {
name: 'dependency', name: 'dependency',
@ -129,7 +190,7 @@ export const opLookup = {
description: 'Modifies the current transformation matrix (CTM)', description: 'Modifies the current transformation matrix (CTM)',
numArgs: 6, numArgs: 6,
variableArgs: false, variableArgs: false,
class: 'matrix' class: 'operator'
}, },
'13': { '13': {
name: 'moveTo', name: 'moveTo',
@ -369,7 +430,7 @@ export const opLookup = {
description: 'Sets the text matrix and text line matrix', description: 'Sets the text matrix and text line matrix',
numArgs: 6, numArgs: 6,
variableArgs: false, variableArgs: false,
class: 'matrix' class: 'text-positioning'
}, },
'43': { '43': {
name: 'nextLine', name: 'nextLine',
@ -382,7 +443,7 @@ export const opLookup = {
'44': { '44': {
name: 'showText', name: 'showText',
keyword: 'Tj', keyword: 'Tj',
description: 'Shows a text string', description: 'Shows the text string',
numArgs: 1, numArgs: 1,
variableArgs: false, variableArgs: false,
class: 'text-render' class: 'text-render'
@ -390,7 +451,7 @@ export const opLookup = {
'45': { '45': {
name: 'showSpacedText', name: 'showSpacedText',
keyword: 'TJ', keyword: 'TJ',
description: 'Shows a text string with individual glyph positioning', description: 'Shows the text string with individual glyph positioning',
numArgs: 1, numArgs: 1,
variableArgs: false, variableArgs: false,
class: 'text-render' class: 'text-render'
@ -669,9 +730,11 @@ export const opLookup = {
}, },
'85': { '85': {
name: 'paintImageXObject', name: 'paintImageXObject',
keyword: null, keyword: 'Do',
description: 'Paints an image XObject', description: 'Paints an image XObject from ressources',
class: 'named-element' numArgs: 1,
variableArgs: false,
class: 'named-element',
}, },
'86': { '86': {
name: 'paintInlineImageXObject', name: 'paintInlineImageXObject',
@ -707,7 +770,7 @@ export const opLookup = {
name: 'constructPath', name: 'constructPath',
keyword: null, keyword: null,
description: 'Constructs a path from the specified segments', description: 'Constructs a path from the specified segments',
class: 'path-construct' class: 'constructPath'
}, },
'92': { '92': {
name: 'setStrokeTransparent', name: 'setStrokeTransparent',

View File

View File

@ -1,5 +1,8 @@
export default class TreeViewRequest { export default class TreeViewRequest {
static TRAILER = new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [])]);
static PAGE = new TreeViewRequest("Page1", []);
public key: string; public key: string;
public children: TreeViewRequest[]; public children: TreeViewRequest[];
public displayName: string; public displayName: string;
@ -8,7 +11,6 @@ export default class TreeViewRequest {
constructor( constructor(
key: string, key: string,
children: TreeViewRequest[], children: TreeViewRequest[],
expand: boolean = false,
) { ) {
if (key.startsWith("Page")) { if (key.startsWith("Page")) {
this.displayName = "Page " + key.slice(4); this.displayName = "Page " + key.slice(4);
@ -17,21 +19,19 @@ export default class TreeViewRequest {
} }
this.key = key; this.key = key;
this.children = children; this.children = children;
this.expand = expand; this.expand = true;
} }
static fromPageCount(pageCount: number) { static initialRequest(pageCount: number) {
let roots = []; const initialRequest = [TreeViewRequest.TRAILER];
for (let i = 0; i < pageCount; i++) { for (let i = 0; i < pageCount; i++) {
roots.push(new TreeViewRequest("Page" + (i + 1), [])); initialRequest.push(new TreeViewRequest("Page" + (i + 1), []));
} }
return roots; return initialRequest;
} }
static TRAILER = new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [], true)], true);
public clone(): TreeViewRequest { public clone(): TreeViewRequest {
return new TreeViewRequest(this.key, this.children.map(child => child.clone()), this.expand); return new TreeViewRequest(this.key, this.children.map(child => child.clone()));
} }
public getChild(key: string) { public getChild(key: string) {
@ -40,7 +40,7 @@ export default class TreeViewRequest {
public addChild(key: string) { public addChild(key: string) {
this.expand = true; this.expand = true;
let child = new TreeViewRequest(key, [], true) const child = new TreeViewRequest(key, [])
this.children.push(child); this.children.push(child);
return child; return child;
} }

View File

@ -20,12 +20,9 @@ export default class TreeViewState {
private height = 100; private height = 100;
constructor(doc: DocumentWorker) { constructor(doc: DocumentWorker) {
this.initialRequest = [TreeViewRequest.TRAILER]; this.initialRequest = TreeViewRequest.initialRequest(doc.getNumberOfPages());
this.initialRequest = this.initialRequest.concat(
TreeViewRequest.fromPageCount(+doc.getNumberOfPages())
);
this.doc = doc; this.doc = doc;
this.updateTreeViewRequest([this.initialRequest[0]]).then(() => this.updateTreeView(0, 1080)); this.updateTreeViewRequest(this.initialRequest.slice(0, 2)).then(() => this.updateTreeView(0, 1080));
} }
public getEntryCount() { public getEntryCount() {

File diff suppressed because one or more lines are too long