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 { 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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
0
src/models/PageViewState.svelte.ts
Normal file
0
src/models/PageViewState.svelte.ts
Normal 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
Loading…
x
Reference in New Issue
Block a user