Syntax highlighting and zoomable view updates
This commit is contained in:
parent
4637c66185
commit
fcfa06f691
@ -3,9 +3,10 @@
|
||||
import { UploadOutline } from 'flowbite-svelte-icons';
|
||||
import * as monaco from 'monaco-editor';
|
||||
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 type { PDFOperator } from '../models/OperatorList';
|
||||
|
||||
const opIdRegex = /opId-(\d+)/;
|
||||
|
||||
monaco.languages.register({ id: 'pdf-content-stream' });
|
||||
|
||||
@ -85,7 +86,7 @@
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 14,
|
||||
automaticLayout: true
|
||||
automaticLayout: true,
|
||||
});
|
||||
|
||||
// Add change listener to detect edits
|
||||
@ -95,6 +96,25 @@
|
||||
|
||||
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 () => {
|
||||
editor.dispose();
|
||||
};
|
||||
@ -104,6 +124,44 @@
|
||||
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) {
|
||||
if (!contents || !editor) return;
|
||||
|
||||
@ -114,36 +172,8 @@
|
||||
lastContents = contents;
|
||||
editor.setValue(contents.toDisplay());
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
addDecorations(contents);
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -170,4 +200,8 @@
|
||||
background-color: #3c3d41;
|
||||
}
|
||||
|
||||
:global(.highlightOp) {
|
||||
@apply border border-forge-sec rounded;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -12,7 +12,8 @@
|
||||
<RefreshOutline class="animate-spin" size="xl" />
|
||||
</div>
|
||||
{:else}
|
||||
<ZoomableContainer imgUrl={img} {height} />
|
||||
<ZoomableContainer imgUrl={img} {height}>
|
||||
</ZoomableContainer>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,48 +1,66 @@
|
||||
<script lang="ts">
|
||||
import {Pane, Splitpanes} from "svelte-splitpanes";
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import StreamEditor from "./StreamEditor.svelte";
|
||||
import ContentModel from "../models/ContentModel.svelte";
|
||||
import type DocumentWorker from '../models/Document.svelte';
|
||||
import ContentEditor from './ContentEditor.svelte';
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||
import type FileViewState from '../models/FileViewState.svelte';
|
||||
import ContentModel from '../models/ContentModel.svelte';
|
||||
import type DocumentWorker from '../models/Document.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 renderer: DocumentWorker = $derived(fState.document);
|
||||
let pageNum = $derived(fState.currentPageNumber);
|
||||
let contents: ContentModel | undefined = $state(undefined)
|
||||
let { fState, height }: { fState: FileViewState, height: number } = $props();
|
||||
let renderer: DocumentWorker = $derived(fState.document);
|
||||
let pageNum = $derived(fState.currentPageNumber);
|
||||
let contents: ContentModel | undefined = $state(undefined);
|
||||
|
||||
$effect(() => {
|
||||
if (pageNum) {
|
||||
renderer.renderPage(pageNum, display);
|
||||
renderer.getContents(pageNum).then(parts => {
|
||||
contents = parts;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
contents = undefined;
|
||||
}
|
||||
})
|
||||
$effect(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
function refresh() {
|
||||
if (pageNum) {
|
||||
renderer.getContents(pageNum).then(parts => {
|
||||
contents = parts;
|
||||
renderer.renderPage(pageNum, display);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
contents = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function update(newData: string) {
|
||||
if (pageNum) {
|
||||
await fState.document.updateContents(pageNum, newData);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
{#if pageNum}
|
||||
<Splitpanes theme="forge-movable">
|
||||
<Pane minSize={1}>
|
||||
<div class="overflow-hidden">
|
||||
{#if contents}
|
||||
<ContentEditor save={(newData) => console.log("Save")} contents={contents} height={height - 1}></ContentEditor>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane minSize={1}>
|
||||
<canvas bind:this={display}></canvas>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
<Splitpanes theme="forge-movable">
|
||||
<Pane minSize={1}>
|
||||
<div class="overflow-hidden">
|
||||
{#if contents}
|
||||
<ContentEditor save={(newData) => update(newData)} contents={contents} height={height - 1}></ContentEditor>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane minSize={1}>
|
||||
<ZoomableContainer bind:canvas={display} {height}></ZoomableContainer>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{:else }
|
||||
<h1>Please select a Page!</h1>
|
||||
<h1>Please select a Page!</h1>
|
||||
{/if}
|
||||
<style lang="postcss">
|
||||
|
||||
.scroller {
|
||||
@apply w-full h-full bg-forge-dark;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
}
|
||||
.container {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
@ -99,11 +99,6 @@
|
||||
|
||||
<div class="relative">
|
||||
<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>
|
||||
|
||||
<style lang="postcss">
|
||||
|
||||
@ -1,162 +1,177 @@
|
||||
<script lang="ts">
|
||||
import ZoomControls from "../components/ZoomControls.svelte";
|
||||
import ZoomControls from '../components/ZoomControls.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
type ZoomableProps = {
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
zoomStep?: number;
|
||||
imgUrl: string;
|
||||
height?: string | number;
|
||||
};
|
||||
type ZoomableProps = {
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
zoomStep?: number;
|
||||
imgUrl?: string;
|
||||
canvas?: HTMLCanvasElement;
|
||||
height?: string | number;
|
||||
};
|
||||
|
||||
let {
|
||||
minZoom = 0.5,
|
||||
maxZoom = 10,
|
||||
zoomStep = 0.1,
|
||||
imgUrl,
|
||||
height,
|
||||
}: ZoomableProps = $props();
|
||||
let {
|
||||
minZoom = 0.5,
|
||||
maxZoom = 10,
|
||||
zoomStep = 0.1,
|
||||
imgUrl,
|
||||
canvas = $bindable(),
|
||||
height
|
||||
}: ZoomableProps = $props();
|
||||
|
||||
let scale = $state(1);
|
||||
let currentImgUrl = $state(imgUrl);
|
||||
|
||||
// DOM refs
|
||||
let container = $state<HTMLElement>();
|
||||
let image = $state<HTMLElement>();
|
||||
type ViewState = {
|
||||
x: number;
|
||||
y: number;
|
||||
scale: number;
|
||||
}
|
||||
|
||||
// Drag state
|
||||
type DragState = {
|
||||
startX: number;
|
||||
startY: number;
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
};
|
||||
let viewState: ViewState = $state({
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1
|
||||
});
|
||||
|
||||
let dragState: DragState | undefined = $state({
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
scrollLeft: 0,
|
||||
scrollTop: 0,
|
||||
});
|
||||
// DOM refs
|
||||
let container = $state<HTMLElement>();
|
||||
|
||||
// Zoom handlers
|
||||
function handleZoom(event: WheelEvent) {
|
||||
if (!container || !image || !event.ctrlKey) return;
|
||||
// Drag state
|
||||
type DragState = {
|
||||
startX: number;
|
||||
startY: number;
|
||||
};
|
||||
|
||||
event.preventDefault();
|
||||
const delta = -Math.sign(event.deltaY);
|
||||
const newScale = Math.min(
|
||||
Math.max(scale + delta * zoomStep, minZoom),
|
||||
maxZoom,
|
||||
);
|
||||
let dragState: DragState | undefined = $state(undefined);
|
||||
|
||||
if (newScale !== scale) {
|
||||
const rect = image.getBoundingClientRect();
|
||||
const x = event.clientX;
|
||||
const y = event.clientY;
|
||||
// Zoom handlers
|
||||
function handleZoom(event: WheelEvent) {
|
||||
if (!container || !event.ctrlKey) return;
|
||||
|
||||
const scaleChange = newScale / scale;
|
||||
container.scrollLeft = x * scaleChange - x + container.scrollLeft;
|
||||
container.scrollTop = y * scaleChange - y + container.scrollTop;
|
||||
event.preventDefault();
|
||||
const delta = -Math.sign(event.deltaY);
|
||||
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
|
||||
function handleDragStart(event: MouseEvent) {
|
||||
if (!container) return;
|
||||
const scaleChange = newScale / viewState.scale;
|
||||
container.scrollLeft = x * scaleChange - x + container.scrollLeft;
|
||||
container.scrollTop = y * scaleChange - y + container.scrollTop;
|
||||
viewState.scale = newScale;
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
dragState = {
|
||||
startX: event.pageX - container.offsetLeft,
|
||||
startY: event.pageY - container.offsetTop,
|
||||
scrollLeft: container.scrollLeft,
|
||||
scrollTop: container.scrollTop,
|
||||
};
|
||||
}
|
||||
// Drag handlers
|
||||
function handleDragStart(event: MouseEvent) {
|
||||
if (!container || event.button) return;
|
||||
|
||||
function handleDragMove(event: MouseEvent) {
|
||||
if (!container || !dragState) return;
|
||||
event.preventDefault();
|
||||
dragState = {
|
||||
startX: event.pageX - viewState.x,
|
||||
startY: event.pageY - viewState.y
|
||||
};
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const x = event.pageX - container.offsetLeft;
|
||||
const y = event.pageY - container.offsetTop;
|
||||
function handleDragMove(event: MouseEvent) {
|
||||
if (!container || !dragState) return;
|
||||
|
||||
container.scrollLeft = dragState.scrollLeft - (x - dragState.startX);
|
||||
container.scrollTop = dragState.scrollTop - (y - dragState.startY);
|
||||
}
|
||||
event.preventDefault();
|
||||
const x = event.pageX - container.offsetLeft;
|
||||
const y = event.pageY - container.offsetTop;
|
||||
|
||||
function handleDragEnd(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
dragState = undefined;
|
||||
}
|
||||
viewState.x = (x - dragState.startX);
|
||||
viewState.y = (y - dragState.startY);
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
scale = 1;
|
||||
if (container) {
|
||||
container.scrollLeft = 0;
|
||||
container.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
function handleDragEnd(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
dragState = undefined;
|
||||
}
|
||||
|
||||
// Event listener management
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
container.addEventListener("wheel", handleZoom, { passive: false });
|
||||
return () => container.removeEventListener("wheel", handleZoom);
|
||||
}
|
||||
});
|
||||
function resetZoom() {
|
||||
viewState.scale = 1;
|
||||
if (container) {
|
||||
viewState.x = 0;
|
||||
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>
|
||||
|
||||
<div class="relative w-full h-full">
|
||||
<ZoomControls
|
||||
{scale}
|
||||
onZoomIn={() => (scale = Math.min(scale + zoomStep, maxZoom))}
|
||||
onZoomOut={() => (scale = Math.max(scale - zoomStep, minZoom))}
|
||||
{resetZoom}
|
||||
/>
|
||||
<div
|
||||
class="container"
|
||||
bind:this={container}
|
||||
onmousedown={handleDragStart}
|
||||
onmousemove={handleDragMove}
|
||||
onmouseup={handleDragEnd}
|
||||
onmouseleave={handleDragEnd}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="image-container"
|
||||
bind:this={image}
|
||||
style:transform="scale({scale})"
|
||||
style:height={height + "px"}
|
||||
style:max-height={height + "px"}
|
||||
>
|
||||
|
||||
<img
|
||||
alt="rendered-page"
|
||||
src={imgUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ZoomControls
|
||||
scale={viewState.scale}
|
||||
onZoomIn={() => (viewState.scale = Math.min(viewState.scale + zoomStep, maxZoom))}
|
||||
onZoomOut={() => (viewState.scale = Math.max(viewState.scale - zoomStep, minZoom))}
|
||||
{resetZoom}
|
||||
/>
|
||||
<div
|
||||
class="container"
|
||||
bind:this={container}
|
||||
onmousedown={handleDragStart}
|
||||
onmousemove={handleDragMove}
|
||||
onmouseup={handleDragEnd}
|
||||
onmouseleave={handleDragEnd}
|
||||
role="presentation"
|
||||
style:height={height + "px"}
|
||||
style:max-height={height + "px"}
|
||||
>
|
||||
{#if imgUrl}
|
||||
<img
|
||||
class="zoomimage"
|
||||
alt="rendered-page"
|
||||
src={currentImgUrl}
|
||||
style:transform="translate({viewState.x}px, {viewState.y}px) scale({viewState.scale})"
|
||||
style:max-height={height + "px"}
|
||||
/>
|
||||
{:else}
|
||||
<canvas
|
||||
bind:this={canvas}
|
||||
class="zoomimage"
|
||||
style:transform="translate({viewState.x}px, {viewState.y}px) scale({viewState.scale})"
|
||||
>
|
||||
</canvas>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.container {
|
||||
@apply w-full h-full bg-forge-dark;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
transform-origin: 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.zoomimage {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -1,24 +1,49 @@
|
||||
import type { OperatorList } from './OperatorList';
|
||||
|
||||
export default class ContentModel {
|
||||
parts: string[];
|
||||
content: string;
|
||||
opList: OperatorList;
|
||||
|
||||
constructor(parts: string[], opList: OperatorList) {
|
||||
this.parts = parts;
|
||||
constructor(content: string, opList: OperatorList) {
|
||||
this.content = content;
|
||||
this.opList = opList;
|
||||
}
|
||||
|
||||
public toDisplay(): string {
|
||||
|
||||
let text = "";
|
||||
if (this.parts.length > 1) {
|
||||
for (const part of this.parts) {
|
||||
text += part;
|
||||
// PDF allows for \n\r and \n intermixed. Monaco does not.
|
||||
// So we to recalculate the content and ranges to contain \n only
|
||||
private unifyEOL() {
|
||||
const newRanges: number[][] = [];
|
||||
const result = this.replaceCRLF(this.content);
|
||||
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) {
|
||||
text = this.parts[0];
|
||||
newRanges.push([result.lookup[oldRange[0]], result.lookup[oldRange[1]]]);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -62,24 +62,22 @@ export default class DocumentWorker {
|
||||
|
||||
public async getContents(pageNumber: number): Promise<ContentModel> {
|
||||
try {
|
||||
const pathStem = `/Page${pageNumber}/Contents`;
|
||||
const contents = await this.doc.getPrimitiveByPath(pathStem);
|
||||
const promises = [];
|
||||
if (contents && contents.ptype === "Array") {
|
||||
for (const child of contents.children) {
|
||||
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));
|
||||
const page = await this.doc.getPage(pageNumber);
|
||||
const contentsPromise = page.getContents();
|
||||
const operatorList = await page.getOperatorList(pageNumber)
|
||||
const opList = OperatorList.createUnfoldedOpList(operatorList.fnArray, operatorList.argsArray, operatorList.rangeArray);
|
||||
return new ContentModel(await contentsPromise, opList);
|
||||
} catch (error: unknown) {
|
||||
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() {
|
||||
this.doc.destroy();
|
||||
}
|
||||
|
||||
@ -19,15 +19,15 @@ export default class FileViewState {
|
||||
|
||||
public currentPageNumber: number | undefined = $state();
|
||||
|
||||
public treeMode: boolean = $state(true);
|
||||
public pageMode: boolean = $state(false);
|
||||
public treeMode: boolean = $state(false);
|
||||
public pageMode: boolean = $state(true);
|
||||
public xRefShowing: boolean = $state(false);
|
||||
public notificationsShowing: boolean = $state(false);
|
||||
public changesShowing: boolean = $state(false);
|
||||
public canForward: boolean = $derived(this.pathHistory.canForward);
|
||||
public canBack: boolean = $derived(this.pathHistory.canBack);
|
||||
|
||||
public path: string[] = $state(['Trailer']);
|
||||
public path: string[] = $state(['Page1']);
|
||||
public container_prim: Primitive | undefined = $state();
|
||||
public selected_leaf_prim: Primitive | undefined = $state();
|
||||
public xref_entries: XRefEntry[] = $state([]);
|
||||
@ -214,6 +214,8 @@ export default class FileViewState {
|
||||
if (!this.canBack) return;
|
||||
const prevPath = this.pathHistory.goBack();
|
||||
if (prevPath) {
|
||||
// prevent browser from leaving page
|
||||
history.pushState(null, "", location.href);
|
||||
this.selectPath(prevPath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,32 +8,93 @@ export interface PDFOperator {
|
||||
}
|
||||
|
||||
export class OperatorList {
|
||||
fnArray: [];
|
||||
fnArray: number[];
|
||||
argsArray: [];
|
||||
rangeArray: [];
|
||||
rangeArray: number[][];
|
||||
constructor(fnArray: [], argsArray: [], rangeArray: []) {
|
||||
this.fnArray = fnArray;
|
||||
this.argsArray = argsArray;
|
||||
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;
|
||||
if (!operator) {
|
||||
return `Unknown operator: ${operatorId}`;
|
||||
}
|
||||
|
||||
const keyword = operator.keyword ? `\`${operator.keyword}\`` : 'N/A';
|
||||
const args = operator.numArgs !== undefined ?
|
||||
`Arguments: ${operator.numArgs}${operator.variableArgs ? ' (variable)' : ''}` :
|
||||
'No arguments';
|
||||
|
||||
return `**${operator.name}** (${keyword}) ` +
|
||||
`${operator.description} ` +
|
||||
`${args}`;
|
||||
const numArgs =
|
||||
operator.numArgs
|
||||
? `Arguments: ${operator.numArgs}${operator.variableArgs ? ' (variable)' : ''}`
|
||||
: 'No arguments';
|
||||
if (operator.keyword === "Tj" || operator.keyword === "TJ" && Array.isArray(args)) {
|
||||
let content = '';
|
||||
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 = {
|
||||
'1': {
|
||||
name: 'dependency',
|
||||
@ -129,7 +190,7 @@ export const opLookup = {
|
||||
description: 'Modifies the current transformation matrix (CTM)',
|
||||
numArgs: 6,
|
||||
variableArgs: false,
|
||||
class: 'matrix'
|
||||
class: 'operator'
|
||||
},
|
||||
'13': {
|
||||
name: 'moveTo',
|
||||
@ -369,7 +430,7 @@ export const opLookup = {
|
||||
description: 'Sets the text matrix and text line matrix',
|
||||
numArgs: 6,
|
||||
variableArgs: false,
|
||||
class: 'matrix'
|
||||
class: 'text-positioning'
|
||||
},
|
||||
'43': {
|
||||
name: 'nextLine',
|
||||
@ -382,7 +443,7 @@ export const opLookup = {
|
||||
'44': {
|
||||
name: 'showText',
|
||||
keyword: 'Tj',
|
||||
description: 'Shows a text string',
|
||||
description: 'Shows the text string',
|
||||
numArgs: 1,
|
||||
variableArgs: false,
|
||||
class: 'text-render'
|
||||
@ -390,7 +451,7 @@ export const opLookup = {
|
||||
'45': {
|
||||
name: 'showSpacedText',
|
||||
keyword: 'TJ',
|
||||
description: 'Shows a text string with individual glyph positioning',
|
||||
description: 'Shows the text string with individual glyph positioning',
|
||||
numArgs: 1,
|
||||
variableArgs: false,
|
||||
class: 'text-render'
|
||||
@ -669,9 +730,11 @@ export const opLookup = {
|
||||
},
|
||||
'85': {
|
||||
name: 'paintImageXObject',
|
||||
keyword: null,
|
||||
description: 'Paints an image XObject',
|
||||
class: 'named-element'
|
||||
keyword: 'Do',
|
||||
description: 'Paints an image XObject from ressources',
|
||||
numArgs: 1,
|
||||
variableArgs: false,
|
||||
class: 'named-element',
|
||||
},
|
||||
'86': {
|
||||
name: 'paintInlineImageXObject',
|
||||
@ -707,7 +770,7 @@ export const opLookup = {
|
||||
name: 'constructPath',
|
||||
keyword: null,
|
||||
description: 'Constructs a path from the specified segments',
|
||||
class: 'path-construct'
|
||||
class: 'constructPath'
|
||||
},
|
||||
'92': {
|
||||
name: 'setStrokeTransparent',
|
||||
|
||||
0
src/models/PageViewState.svelte.ts
Normal file
0
src/models/PageViewState.svelte.ts
Normal file
@ -1,5 +1,8 @@
|
||||
export default class TreeViewRequest {
|
||||
|
||||
static TRAILER = new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [])]);
|
||||
static PAGE = new TreeViewRequest("Page1", []);
|
||||
|
||||
public key: string;
|
||||
public children: TreeViewRequest[];
|
||||
public displayName: string;
|
||||
@ -8,7 +11,6 @@ export default class TreeViewRequest {
|
||||
constructor(
|
||||
key: string,
|
||||
children: TreeViewRequest[],
|
||||
expand: boolean = false,
|
||||
) {
|
||||
if (key.startsWith("Page")) {
|
||||
this.displayName = "Page " + key.slice(4);
|
||||
@ -17,21 +19,19 @@ export default class TreeViewRequest {
|
||||
}
|
||||
this.key = key;
|
||||
this.children = children;
|
||||
this.expand = expand;
|
||||
this.expand = true;
|
||||
}
|
||||
|
||||
static fromPageCount(pageCount: number) {
|
||||
let roots = [];
|
||||
|
||||
static initialRequest(pageCount: number) {
|
||||
const initialRequest = [TreeViewRequest.TRAILER];
|
||||
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 {
|
||||
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) {
|
||||
@ -40,7 +40,7 @@ export default class TreeViewRequest {
|
||||
|
||||
public addChild(key: string) {
|
||||
this.expand = true;
|
||||
let child = new TreeViewRequest(key, [], true)
|
||||
const child = new TreeViewRequest(key, [])
|
||||
this.children.push(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
@ -20,12 +20,9 @@ export default class TreeViewState {
|
||||
private height = 100;
|
||||
|
||||
constructor(doc: DocumentWorker) {
|
||||
this.initialRequest = [TreeViewRequest.TRAILER];
|
||||
this.initialRequest = this.initialRequest.concat(
|
||||
TreeViewRequest.fromPageCount(+doc.getNumberOfPages())
|
||||
);
|
||||
this.initialRequest = TreeViewRequest.initialRequest(doc.getNumberOfPages());
|
||||
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() {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user