added debugger
This commit is contained in:
parent
5227c0acea
commit
223c5f76e0
@ -35,7 +35,6 @@
|
||||
reader.onload = (e) => {
|
||||
const data = e.target?.result;
|
||||
if (!data) return;
|
||||
console.log(file, data);
|
||||
createFileViewState(file.name, data)
|
||||
};
|
||||
reader.onerror = () => {
|
||||
|
||||
@ -3,9 +3,8 @@
|
||||
import { UploadOutline } from 'flowbite-svelte-icons';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import type ContentModel from '../models/ContentModel.svelte';
|
||||
import { getPDFOperator, opLookup } from '../models/OperatorList';
|
||||
import { OperatorList } from '../models/OperatorList.js';
|
||||
|
||||
import { opLookup } from '../models/OperatorList';
|
||||
import type FileViewState from '../models/FileViewState.svelte';
|
||||
const opIdRegex = /opId-(\d+)/;
|
||||
|
||||
monaco.languages.register({ id: 'pdf-content-stream' });
|
||||
@ -63,20 +62,31 @@
|
||||
});
|
||||
|
||||
let {
|
||||
fState,
|
||||
contentsUpdatedEvent,
|
||||
height,
|
||||
save,
|
||||
contents
|
||||
onBreakpointsChange = (breakpoints: Set<number>) => {},
|
||||
currentOgIdx = -1
|
||||
}: {
|
||||
fState: FileViewState;
|
||||
contentsUpdatedEvent: () => void;
|
||||
height: number;
|
||||
save: (updated_data: string) => void;
|
||||
contents: ContentModel;
|
||||
onBreakpointsChange?: (breakpoints: Set<number>) => void;
|
||||
currentOgIdx?: number;
|
||||
} = $props();
|
||||
|
||||
let editorContainer: HTMLElement;
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
|
||||
let isEdited = $state(false);
|
||||
let lastContents: ContentModel | undefined = $state(undefined);
|
||||
let contents: ContentModel | undefined;
|
||||
let isEdited = false;
|
||||
let loaded = false;
|
||||
|
||||
let breakpoints: Set<number> = $state(new Set());
|
||||
let breakpointDecorations: monaco.editor.IEditorDecorationsCollection | null = null;
|
||||
let currentOperationDecoration: monaco.editor.IEditorDecorationsCollection | null = null;
|
||||
let lineToOperations: Map<number, number[]> = new Map();
|
||||
let ogIdxToEditorIdx: Map<number, number> = new Map();
|
||||
|
||||
onMount(() => {
|
||||
editor = monaco.editor.create(editorContainer, {
|
||||
@ -87,18 +97,71 @@
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 14,
|
||||
automaticLayout: true,
|
||||
lineNumbers: 'on',
|
||||
lineDecorationsWidth: 8, // Further reducing margin after line numbers
|
||||
lineNumbersMinChars: 3, // Controls minimum width of line number area
|
||||
glyphMargin: true, // Enable the gutter for breakpoints
|
||||
});
|
||||
|
||||
// Add change listener to detect edits
|
||||
editor.onDidChangeModelContent(() => {
|
||||
isEdited = true;
|
||||
isEdited = loaded;
|
||||
updateContents();
|
||||
});
|
||||
|
||||
loadContents(contents);
|
||||
const gutterElement = editorContainer.querySelector('.margin');
|
||||
if (gutterElement) {
|
||||
gutterElement.classList.add('breakpoint-hover-area');
|
||||
}
|
||||
|
||||
let lastHoveredLine: number | null = null;
|
||||
let hoverDecorationIds: string[] = [];
|
||||
|
||||
function clearHoverIndicators() {
|
||||
if (hoverDecorationIds.length > 0) {
|
||||
editor.removeDecorations(hoverDecorationIds);
|
||||
hoverDecorationIds = [];
|
||||
lastHoveredLine = null;
|
||||
}
|
||||
}
|
||||
|
||||
editorContainer.addEventListener('mouseleave', clearHoverIndicators);
|
||||
|
||||
editor.onMouseMove((e) => {
|
||||
const currentLineNumber = e.target.position?.lineNumber || null;
|
||||
|
||||
if ((e.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS ||
|
||||
e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN) &&
|
||||
currentLineNumber) {
|
||||
|
||||
if (currentLineNumber !== lastHoveredLine || hoverDecorationIds.length === 0) {
|
||||
clearHoverIndicators();
|
||||
|
||||
if (!breakpoints.has(currentLineNumber)) {
|
||||
hoverDecorationIds = editor.deltaDecorations([], [{
|
||||
range: new monaco.Range(currentLineNumber, 1, currentLineNumber, 1),
|
||||
options: {
|
||||
isWholeLine: false,
|
||||
glyphMarginClassName: 'breakpoint-glyph-hover'
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
lastHoveredLine = currentLineNumber;
|
||||
}
|
||||
} else {
|
||||
clearHoverIndicators();
|
||||
}
|
||||
});
|
||||
|
||||
editor.onMouseDown((e) => {
|
||||
if (!e.target.position) return;
|
||||
|
||||
if (e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN ||
|
||||
e.target.type === monaco.editor.MouseTargetType.GUTTER_LINE_NUMBERS) {
|
||||
toggleBreakpointAtLine(e.target.position.lineNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
const decorations = editor.getModel()?.getDecorationsInRange({
|
||||
startLineNumber: e.target.position.lineNumber,
|
||||
startColumn: e.target.position.column,
|
||||
@ -110,34 +173,111 @@
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
console.log(contents.opList.fnArray[+match[1]]);
|
||||
console.log(contents.opList.argsArray[+match[1]]);
|
||||
})
|
||||
});
|
||||
|
||||
return () => {
|
||||
editorContainer.removeEventListener('mouseleave', clearHoverIndicators);
|
||||
editor.dispose();
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
loadContents(contents);
|
||||
loadContents();
|
||||
});
|
||||
|
||||
function addDecorations(contents: ContentModel) {
|
||||
|
||||
$effect(() => {
|
||||
if (currentOgIdx >= 0) {
|
||||
highlightCurrentOperation(currentOgIdx);
|
||||
} else {
|
||||
clearCurrentOperationHighlight();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function toggleBreakpointAtLine(lineNumber: number) {
|
||||
if (!contents?.opList) return;
|
||||
|
||||
|
||||
const operations = lineToOperations.get(lineNumber) || [];
|
||||
|
||||
if (operations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasBreakpoint = operations.some(ogIdx => breakpoints.has(ogIdx));
|
||||
|
||||
if (hasBreakpoint) {
|
||||
operations.forEach(ogIdx => {
|
||||
breakpoints.delete(ogIdx);
|
||||
});
|
||||
} else {
|
||||
operations.forEach(ogIdx => {
|
||||
breakpoints.add(ogIdx);
|
||||
});
|
||||
}
|
||||
|
||||
updateBreakpointDecorations();
|
||||
|
||||
breakpoints = new Set([...breakpoints]);
|
||||
onBreakpointsChange(breakpoints);
|
||||
}
|
||||
|
||||
function updateBreakpointDecorations() {
|
||||
const model = editor.getModel();
|
||||
if (!model || !contents?.opList) return;
|
||||
|
||||
const decorationsArray = [];
|
||||
|
||||
const linesWithBreakpoints = new Set<number>();
|
||||
|
||||
for (let i = 0; i < contents.opList.fnArray.length; i++) {
|
||||
const operator = contents.opList.getOperatorAt(i);
|
||||
|
||||
if (breakpoints.has(operator.ogIdx) && operator.range) {
|
||||
const startPos = model.getPositionAt(operator.range.start);
|
||||
linesWithBreakpoints.add(startPos.lineNumber);
|
||||
|
||||
decorationsArray.push({
|
||||
range: new monaco.Range(startPos.lineNumber, 1, startPos.lineNumber, 1),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: 'breakpoint-line',
|
||||
glyphMarginClassName: 'breakpoint-glyph',
|
||||
glyphMarginHoverMessage: { value: `Breakpoint on operation: ${operator.getPDFOperator?.().name || 'Unknown'}` }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (breakpointDecorations) {
|
||||
breakpointDecorations.clear();
|
||||
}
|
||||
|
||||
breakpointDecorations = editor.createDecorationsCollection(decorationsArray);
|
||||
}
|
||||
|
||||
function removeAllDecorations() {
|
||||
const ids = editor.getModel()?.getAllDecorations().map(d => d.id) ?? [];
|
||||
editor.removeDecorations(ids);
|
||||
}
|
||||
|
||||
function updateDecorations(contents: ContentModel) {
|
||||
removeAllDecorations();
|
||||
if (contents.opList && contents.opList.fnArray.length > 0) {
|
||||
const decorations = [];
|
||||
|
||||
lineToOperations.clear();
|
||||
ogIdxToEditorIdx.clear();
|
||||
|
||||
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 operator = contents.opList.getOperatorAt(i);
|
||||
|
||||
const model = editor.getModel();
|
||||
if (model && operator && range) {
|
||||
const startPos = model.getPositionAt(range[0]);
|
||||
const endPos = model.getPositionAt(range[1]);
|
||||
if (model && operator && operator.range) {
|
||||
const startPos = model.getPositionAt(operator.range.start);
|
||||
const endPos = model.getPositionAt(operator.range.end);
|
||||
const monacoRange = new monaco.Range(
|
||||
startPos.lineNumber,
|
||||
startPos.column,
|
||||
@ -145,45 +285,132 @@
|
||||
endPos.column
|
||||
);
|
||||
|
||||
let className = `${operator.class} highlightOp opId-${i}`;
|
||||
// Add class-based decoration for operator highlighting
|
||||
ogIdxToEditorIdx.set(operator.ogIdx, i);
|
||||
|
||||
let className = `${operator.getPDFOperator?.().class || 'operator'} highlightOp opId-${i}`;
|
||||
|
||||
decorations.push({
|
||||
range: monacoRange,
|
||||
options: {
|
||||
className: className,
|
||||
hoverMessage: { value: OperatorList.formatOperatorToMarkdown(fnId, args) }
|
||||
hoverMessage: { value: operator.toMarkdown?.() || `Operator Index: ${i}, Original Index: ${operator.ogIdx}` }
|
||||
}
|
||||
});
|
||||
|
||||
const lineNumber = startPos.lineNumber;
|
||||
if (!lineToOperations.has(lineNumber)) {
|
||||
lineToOperations.set(lineNumber, []);
|
||||
}
|
||||
lineToOperations.get(lineNumber)?.push(operator.ogIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply all decorations at once
|
||||
editor.createDecorationsCollection(decorations);
|
||||
|
||||
updateBreakpointDecorations();
|
||||
|
||||
if (currentOgIdx >= 0) {
|
||||
highlightCurrentOperation(currentOgIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadContents(contents: ContentModel | undefined) {
|
||||
if (!contents || !editor) return;
|
||||
async function loadContents() {
|
||||
if (!fState.currentPageNumber || !editor || fState.currentPageNumber === contents?.pageNum) return;
|
||||
loaded = false;
|
||||
contents = await fState.document.getContents(fState.currentPageNumber);
|
||||
|
||||
if (lastContents === contents) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastContents = contents;
|
||||
editor.setValue(contents.toDisplay());
|
||||
|
||||
lineToOperations.clear();
|
||||
ogIdxToEditorIdx.clear();
|
||||
|
||||
clearCurrentOperationHighlight();
|
||||
breakpointDecorations?.clear();
|
||||
|
||||
updateDecorations(contents);
|
||||
isEdited = false;
|
||||
|
||||
addDecorations(contents);
|
||||
loaded = true;
|
||||
breakpoints = new Set();
|
||||
onBreakpointsChange(breakpoints);
|
||||
}
|
||||
</script>
|
||||
|
||||
async function updateContents() {
|
||||
if (!fState.currentPageNumber || !loaded || !isEdited) return;
|
||||
await fState.document.updateContents(fState.currentPageNumber, editor.getValue())
|
||||
const contents = await fState.document.getContents(fState.currentPageNumber);
|
||||
updateDecorations(contents);
|
||||
isEdited = false;
|
||||
contentsUpdatedEvent();
|
||||
}
|
||||
|
||||
function highlightCurrentOperation(ogIdx: number) {
|
||||
if (!contents?.opList || !editor.getModel()) return;
|
||||
|
||||
clearCurrentOperationHighlight();
|
||||
|
||||
const editorIdx = findEditorIdxForOgIdx(ogIdx);
|
||||
if (editorIdx === undefined) return;
|
||||
|
||||
try {
|
||||
const operator = contents.opList.getOperatorAt(editorIdx);
|
||||
if (!operator || !operator.range) return;
|
||||
|
||||
const model = editor.getModel();
|
||||
if (!model) return;
|
||||
|
||||
const startPos = model.getPositionAt(operator.range.start);
|
||||
const endPos = model.getPositionAt(operator.range.end);
|
||||
|
||||
currentOperationDecoration = editor.createDecorationsCollection([{
|
||||
range: new monaco.Range(
|
||||
startPos.lineNumber,
|
||||
startPos.column,
|
||||
endPos.lineNumber,
|
||||
endPos.column
|
||||
),
|
||||
options: {
|
||||
className: 'current-operation-highlight',
|
||||
isWholeLine: false,
|
||||
inlineClassName: 'current-operation-inline',
|
||||
hoverMessage: { value: 'Current operation (execution paused)' }
|
||||
}
|
||||
}]);
|
||||
|
||||
editor.revealPositionInCenter(startPos);
|
||||
} catch (error) {
|
||||
console.error('Error highlighting current operation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function findEditorIdxForOgIdx(ogIdx: number): number | undefined {
|
||||
return ogIdxToEditorIdx.get(ogIdx);
|
||||
}
|
||||
|
||||
function clearCurrentOperationHighlight() {
|
||||
if (currentOperationDecoration) {
|
||||
currentOperationDecoration.clear();
|
||||
currentOperationDecoration = null;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<div class="relative">
|
||||
<div class="breakpoints-list">
|
||||
<h3>Breakpoints</h3>
|
||||
<ul>
|
||||
{#each Array.from(breakpoints).sort((a, b) => a - b) as opIdx}
|
||||
<li>Operation {opIdx}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div bind:this={editorContainer} style:height={height + "px"}></div>
|
||||
{#if isEdited}
|
||||
<button onclick={() => save(editor.getValue())} class="save-button">
|
||||
<button onclick={() => updateContents()} class="save-button">
|
||||
<UploadOutline size="xl" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
@ -191,7 +418,7 @@
|
||||
@apply p-2 bg-forge-sec text-forge-text border-t border-forge-bound cursor-pointer;
|
||||
position: absolute;
|
||||
right: 2em;
|
||||
bottom: 0px;
|
||||
bottom: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
@ -203,5 +430,71 @@
|
||||
:global(.highlightOp) {
|
||||
@apply border border-forge-sec rounded;
|
||||
}
|
||||
|
||||
|
||||
:global(.current-operation-highlight) {
|
||||
@apply border-2 border-orange-400 rounded bg-opacity-20 bg-orange-400;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
:global(.current-operation-inline) {
|
||||
@apply font-bold text-orange-300;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
background-color: rgba(251, 146, 60, 0.1);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(251, 146, 60, 0.3);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(251, 146, 60, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.breakpoint-glyph) {
|
||||
margin-left: 3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 6px solid #c65252;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.breakpoint-glyph-hover) {
|
||||
margin-left: 3px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 6px solid rgba(198, 82, 82, 0.4);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.breakpoint-line) {
|
||||
background-color: rgba(198, 82, 82, 0.2); /* Light red background */
|
||||
}
|
||||
|
||||
:global(.breakpoint-hover-area) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.breakpoints-list {
|
||||
@apply p-3 bg-forge-prim text-forge-text border border-forge-bound rounded;
|
||||
position: absolute;
|
||||
right: 2em;
|
||||
top: 2em;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.breakpoints-list h3 {
|
||||
@apply font-bold mb-2;
|
||||
}
|
||||
|
||||
.breakpoints-list ul {
|
||||
@apply list-disc pl-4;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,27 +1,169 @@
|
||||
<script lang="ts">
|
||||
import { RefreshOutline } from "flowbite-svelte-icons";
|
||||
import ZoomableContainer from './ZoomableContainer.svelte';
|
||||
import ZoomControls from '../components/ZoomControls.svelte';
|
||||
import type { ViewState } from '../models/ViewState';
|
||||
import {
|
||||
RefreshOutline
|
||||
} from 'flowbite-svelte-icons';
|
||||
type ZoomableProps = {
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
zoomStep?: number;
|
||||
imgUrl?: string;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
let {
|
||||
minZoom = 0.1,
|
||||
maxZoom = 30,
|
||||
zoomStep = 0.1,
|
||||
imgUrl,
|
||||
height
|
||||
}: ZoomableProps = $props();
|
||||
|
||||
let container = $state<HTMLDivElement>();
|
||||
|
||||
let viewState: ViewState = $state({
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1
|
||||
});
|
||||
|
||||
type DragState = {
|
||||
startX: number;
|
||||
startY: number;
|
||||
};
|
||||
|
||||
let dragState: DragState | undefined = $state(undefined);
|
||||
|
||||
function handleZoom(event: WheelEvent) {
|
||||
if (!container || !event.ctrlKey) return;
|
||||
|
||||
event.preventDefault();
|
||||
const delta = -Math.sign(event.deltaY);
|
||||
const newScale = Math.min(
|
||||
Math.max(viewState.scale + delta * zoomStep * viewState.scale, minZoom),
|
||||
maxZoom
|
||||
);
|
||||
|
||||
if (newScale !== viewState.scale) {
|
||||
const x = event.clientX;
|
||||
const y = event.clientY;
|
||||
|
||||
const scaleChange = newScale / viewState.scale;
|
||||
container.scrollLeft = x * scaleChange - x + container.scrollLeft;
|
||||
container.scrollTop = y * scaleChange - y + container.scrollTop;
|
||||
viewState.scale = newScale;
|
||||
}
|
||||
}
|
||||
|
||||
// Drag handlers
|
||||
function handleDragStart(event: MouseEvent) {
|
||||
if (!container || event.button) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragState = {
|
||||
startX: event.pageX - viewState.x,
|
||||
startY: event.pageY - viewState.y
|
||||
};
|
||||
}
|
||||
|
||||
function handleDragMove(event: MouseEvent) {
|
||||
if (!container || !dragState) return;
|
||||
|
||||
event.preventDefault();
|
||||
const x = event.pageX - container.offsetLeft;
|
||||
const y = event.pageY - container.offsetTop;
|
||||
|
||||
viewState.x = (x - dragState.startX);
|
||||
viewState.y = (y - dragState.startY);
|
||||
}
|
||||
|
||||
function handleDragEnd(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
dragState = undefined;
|
||||
}
|
||||
|
||||
|
||||
function zoomIn() {
|
||||
const newScale = Math.min(viewState.scale + (zoomStep * viewState.scale), maxZoom);
|
||||
if (newScale !== viewState.scale) {
|
||||
viewState.scale = newScale;
|
||||
}
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
const newScale = Math.max(viewState.scale - (zoomStep * viewState.scale), minZoom);
|
||||
if (newScale !== viewState.scale) {
|
||||
viewState.scale = newScale;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
let { img, height }: { img: string | undefined; height: number } =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<div class="page-container" style:height={height + "px"}>
|
||||
{#if !img}
|
||||
<div class="loading-container">
|
||||
<RefreshOutline class="animate-spin" size="xl" />
|
||||
</div>
|
||||
{:else}
|
||||
<ZoomableContainer imgUrl={img} {height}>
|
||||
</ZoomableContainer>
|
||||
{/if}
|
||||
<div class="relative w-full h-full">
|
||||
<ZoomControls
|
||||
scale={viewState.scale}
|
||||
onZoomIn={() => zoomIn()}
|
||||
onZoomOut={() => zoomOut()}
|
||||
{resetZoom}
|
||||
showFit={false}
|
||||
/>
|
||||
<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}
|
||||
<RefreshOutline class="animate-spin" size="xl"></RefreshOutline>
|
||||
{:else}
|
||||
<img
|
||||
class="zoomimage"
|
||||
alt="xobject"
|
||||
src={imgUrl}
|
||||
style:transform="translate({viewState.x}px, {viewState.y}px) scale({viewState.scale})"
|
||||
style:max-height={height + "px"}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.page-container {
|
||||
@apply w-full bg-forge-dark relative m-auto;
|
||||
.container {
|
||||
@apply w-full h-full bg-forge-dark;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.loading-container {
|
||||
@apply flex items-center justify-center h-full;
|
||||
|
||||
.container:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.zoomimage {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -1,43 +1,51 @@
|
||||
<script lang="ts">
|
||||
import ZoomControls from '../components/ZoomControls.svelte';
|
||||
import type FileViewState from '../models/FileViewState.svelte';
|
||||
import { PageRenderState } from '../models/PageRenderState.svelte';
|
||||
|
||||
type ZoomableProps = {
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
zoomStep?: number;
|
||||
imgUrl?: string;
|
||||
canvas?: HTMLCanvasElement;
|
||||
height?: string | number;
|
||||
fState: FileViewState;
|
||||
height?: number;
|
||||
render: () => void;
|
||||
};
|
||||
|
||||
let {
|
||||
minZoom = 0.1,
|
||||
maxZoom = 10,
|
||||
maxZoom = 20,
|
||||
zoomStep = 0.1,
|
||||
imgUrl,
|
||||
canvas = $bindable(),
|
||||
height
|
||||
fState,
|
||||
height,
|
||||
render = $bindable()
|
||||
}: ZoomableProps = $props();
|
||||
|
||||
let currentImgUrl = $state(imgUrl);
|
||||
let image: HTMLImageElement | undefined = $state(undefined);
|
||||
let container = $state<HTMLDivElement>();
|
||||
let canvas = $state<HTMLCanvasElement>();
|
||||
|
||||
type ViewState = {
|
||||
x: number;
|
||||
y: number;
|
||||
scale: number;
|
||||
}
|
||||
let renderState: PageRenderState | undefined = $derived(canvas && fState ? new PageRenderState(fState, canvas) : undefined)
|
||||
|
||||
let viewState: ViewState = $state({
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1
|
||||
let scale = $derived(renderState ? renderState.scale : 1);
|
||||
|
||||
$effect(() => {
|
||||
if (renderState) {
|
||||
render = () => {renderState.renderFrame();};
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (renderState && fState.currentPageNumber) {
|
||||
renderState.updatePage(fState.currentPageNumber).then(() => fitCanvasToPage())
|
||||
}
|
||||
});
|
||||
|
||||
// DOM refs
|
||||
let viewState = $state({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
// Drag state
|
||||
type DragState = {
|
||||
startX: number;
|
||||
startY: number;
|
||||
@ -45,25 +53,24 @@
|
||||
|
||||
let dragState: DragState | undefined = $state(undefined);
|
||||
|
||||
// Zoom handlers
|
||||
function handleZoom(event: WheelEvent) {
|
||||
if (!container || !event.ctrlKey) return;
|
||||
|
||||
event.preventDefault();
|
||||
const delta = -Math.sign(event.deltaY);
|
||||
const newScale = Math.min(
|
||||
Math.max(viewState.scale + delta * zoomStep * viewState.scale, minZoom),
|
||||
Math.max(scale + delta * zoomStep * scale, minZoom),
|
||||
maxZoom
|
||||
);
|
||||
|
||||
if (newScale !== viewState.scale) {
|
||||
if (newScale !== scale) {
|
||||
const x = event.clientX;
|
||||
const y = event.clientY;
|
||||
|
||||
const scaleChange = newScale / viewState.scale;
|
||||
const scaleChange = newScale / scale;
|
||||
container.scrollLeft = x * scaleChange - x + container.scrollLeft;
|
||||
container.scrollTop = y * scaleChange - y + container.scrollTop;
|
||||
viewState.scale = newScale;
|
||||
renderState?.updateScale(newScale);
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,25 +102,40 @@
|
||||
}
|
||||
|
||||
function fitCanvasToPage() {
|
||||
if (!canvas || !container) return;
|
||||
const w = canvas.width;
|
||||
const h = canvas.height;
|
||||
if (!renderState || !container) return;
|
||||
const w = renderState.width ?? 682;
|
||||
const h = renderState.height ?? 798;
|
||||
const targetWidth = container.offsetWidth;
|
||||
const targetHeight = container.offsetHeight;
|
||||
|
||||
const wScale = targetWidth / w;
|
||||
const hScale = targetHeight / h;
|
||||
viewState.scale = Math.min(maxZoom, Math.max(minZoom, Math.min(wScale, hScale)));
|
||||
const newScale = Math.min(maxZoom, Math.max(minZoom, Math.min(wScale, hScale)));
|
||||
viewState.x = 0;
|
||||
viewState.y = 0;
|
||||
renderState?.updateScale(newScale);
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
const newScale = Math.min(scale + (zoomStep * scale), maxZoom);
|
||||
if (newScale !== scale) {
|
||||
renderState?.updateScale(newScale);
|
||||
}
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
const newScale = Math.max(scale - (zoomStep * scale), minZoom);
|
||||
if (newScale !== scale) {
|
||||
renderState?.updateScale(newScale);
|
||||
}
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
viewState.scale = 1;
|
||||
if (container) {
|
||||
viewState.x = 0;
|
||||
viewState.y = 0;
|
||||
}
|
||||
renderState?.updateScale(1);
|
||||
}
|
||||
|
||||
// Event listener management
|
||||
@ -124,20 +146,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (canvas) {
|
||||
currentImgUrl = canvas.toDataURL();
|
||||
} else if (imgUrl) {
|
||||
currentImgUrl = imgUrl;
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
<div class="relative w-full h-full">
|
||||
<ZoomControls
|
||||
scale={viewState.scale}
|
||||
onZoomIn={() => (viewState.scale = Math.min(viewState.scale + (zoomStep * viewState.scale), maxZoom))}
|
||||
onZoomOut={() => (viewState.scale = Math.max(viewState.scale - (zoomStep * viewState.scale), minZoom))}
|
||||
scale={scale}
|
||||
onZoomIn={() => zoomIn()}
|
||||
onZoomOut={() => zoomOut()}
|
||||
{resetZoom}
|
||||
fit={fitCanvasToPage}
|
||||
showFit={!image}
|
||||
@ -153,23 +167,11 @@
|
||||
style:height={height + "px"}
|
||||
style:max-height={height + "px"}
|
||||
>
|
||||
{#if imgUrl}
|
||||
<img
|
||||
class="zoomimage"
|
||||
bind:this={image}
|
||||
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}
|
||||
<canvas
|
||||
bind:this={canvas}
|
||||
style:transform="translate({viewState.x}px, {viewState.y}px)"
|
||||
>
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -187,8 +189,4 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.zoomimage {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -1,66 +1,177 @@
|
||||
<script lang="ts">
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||
import { BugOutline } from "flowbite-svelte-icons";
|
||||
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;
|
||||
import PageRenderView from './PageRenderView.svelte';
|
||||
import { DebugStepper } from '../models/Stepper.svelte.js';
|
||||
|
||||
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 render: () => void = $state(() => {});
|
||||
let stepper: DebugStepper | undefined = $state();
|
||||
let breakpoints: Set<number> = $state(new Set<number>());
|
||||
|
||||
function contentsUpdatedEvent() {
|
||||
render();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
refresh();
|
||||
});
|
||||
function startDebug() {
|
||||
if (!pageNum) return;
|
||||
stepper = new DebugStepper(pageNum, breakpoints);
|
||||
fState.document.stepperManager.register(stepper);
|
||||
render();
|
||||
}
|
||||
|
||||
function handleBreakpointsChange(newBreakpoints: Set<number>) {
|
||||
breakpoints = newBreakpoints;
|
||||
stepper?.updateBreakPoints(breakpoints);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (pageNum) {
|
||||
renderer.getContents(pageNum).then(parts => {
|
||||
contents = parts;
|
||||
renderer.renderPage(pageNum, display);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
contents = undefined;
|
||||
function continueExecution() {
|
||||
if (stepper) {
|
||||
stepper.continue();
|
||||
}
|
||||
}
|
||||
|
||||
async function update(newData: string) {
|
||||
if (pageNum) {
|
||||
await fState.document.updateContents(pageNum, newData);
|
||||
refresh();
|
||||
function stepExecution() {
|
||||
if (stepper) {
|
||||
stepper.step();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function stopDebugging() {
|
||||
if (stepper) {
|
||||
fState.document.stepperManager.unregister(stepper);
|
||||
stepper = undefined;
|
||||
render();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{#if pageNum}
|
||||
<Splitpanes theme="forge-movable">
|
||||
<Pane minSize={1}>
|
||||
<div class="overflow-hidden">
|
||||
{#if contents}
|
||||
<ContentEditor save={(newData) => update(newData)} contents={contents} height={height - 1}></ContentEditor>
|
||||
<div class="controls">
|
||||
<h1 class="text-forge-text font-semibold">Contents</h1>
|
||||
<button class="debugButton m-1" on:click={startDebug}>
|
||||
<BugOutline size="md"></BugOutline>
|
||||
</button>
|
||||
{#if breakpoints.size > 0}
|
||||
<span class="breakpoint-count">{breakpoints.size}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="overflow-hidden relative h-full">
|
||||
<ContentEditor
|
||||
{fState}
|
||||
height={stepper ? (height - 101) : (height - 1)}
|
||||
{contentsUpdatedEvent}
|
||||
onBreakpointsChange={handleBreakpointsChange}
|
||||
currentOgIdx={stepper?.currentIdx}
|
||||
></ContentEditor>
|
||||
{#if stepper}
|
||||
<div class="debug-terminal">
|
||||
<div class="debug-terminal-header">
|
||||
<h2 class="text-forge-text font-semibold">Debug Terminal</h2>
|
||||
<div class="debug-info">
|
||||
{#if stepper.onBreak}
|
||||
<span class="break-indicator">Paused at operation {stepper.currentIdx}</span>
|
||||
{:else}
|
||||
<span class="running-indicator">Running...</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="debug-controls">
|
||||
<button class="debug-button continue" on:click={continueExecution}>
|
||||
Continue
|
||||
</button>
|
||||
<button class="debug-button step" on:click={stepExecution}>
|
||||
Step
|
||||
</button>
|
||||
<button class="debug-button stop" on:click={stopDebugging}>
|
||||
Stop Debugging
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane minSize={1}>
|
||||
<ZoomableContainer bind:canvas={display} {height}></ZoomableContainer>
|
||||
<PageRenderView bind:render {fState} {height}></PageRenderView>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{:else }
|
||||
<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;
|
||||
}
|
||||
|
||||
.controls {
|
||||
@apply bg-forge-dark border-forge-prim border-b flex flex-row;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
}
|
||||
.debugButton {
|
||||
@apply bg-forge-prim rounded-sm hover:bg-forge-acc text-green-500 p-1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.breakpoint-count {
|
||||
@apply bg-red-600 text-white text-xs font-bold px-1.5 py-0.5 rounded-full;
|
||||
position: absolute;
|
||||
right: 2.5rem;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.debug-terminal {
|
||||
@apply bg-forge-dark border-forge-prim border-t;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.debug-terminal-header {
|
||||
@apply bg-forge-prim flex flex-row justify-between items-center px-3 py-1;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.debug-info {
|
||||
@apply flex flex-row items-center;
|
||||
}
|
||||
|
||||
.break-indicator {
|
||||
@apply text-yellow-300 text-sm;
|
||||
}
|
||||
|
||||
.running-indicator {
|
||||
@apply text-green-400 text-sm;
|
||||
}
|
||||
|
||||
.debug-controls {
|
||||
@apply flex flex-row p-2 space-x-2;
|
||||
}
|
||||
|
||||
.debug-button {
|
||||
@apply px-4 py-2 rounded-md font-medium text-white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.debug-button.continue {
|
||||
@apply bg-green-600 hover:bg-green-700;
|
||||
}
|
||||
|
||||
.debug-button.step {
|
||||
@apply bg-blue-600 hover:bg-blue-700;
|
||||
}
|
||||
|
||||
.debug-button.stop {
|
||||
@apply bg-red-600 hover:bg-red-700;
|
||||
}
|
||||
</style>
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
{#if streamData}
|
||||
{#if streamData.type === "Image"}
|
||||
<ImageViewer img={streamData.imageData} {height}
|
||||
<ImageViewer imgUrl={streamData.imageData} {height}
|
||||
></ImageViewer>
|
||||
{:else if streamData.textData}
|
||||
<StreamEditor
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { UploadOutline } from "flowbite-svelte-icons";
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
// Define a custom theme for PDF content stream highlighting
|
||||
@ -46,7 +45,7 @@
|
||||
onMount(() => {
|
||||
editor = monaco.editor.create(editorContainer, {
|
||||
value: "",
|
||||
language: 'pdf-content-stream',
|
||||
language: 'plaintext',
|
||||
theme: "forge-dark",
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
|
||||
@ -4,8 +4,7 @@
|
||||
let {upload} = $props();
|
||||
let result_message = $state("");
|
||||
</script>
|
||||
|
||||
<h1 class="text-xl m-5">Welcome to PDF Forge!</h1>
|
||||
<div class="w-full h-[20%]"></div>
|
||||
<div class="row" on:click={upload}>
|
||||
<img src="/pdf-forge-logo-bg.png" class="logo forge" alt="PDF Forge Logo"/>
|
||||
</div>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
onZoomIn: () => void;
|
||||
onZoomOut: () => void;
|
||||
resetZoom: () => void;
|
||||
fit: () => void;
|
||||
fit?: () => void;
|
||||
showFit: boolean;
|
||||
} = $props();
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import type { OperatorList } from './OperatorList';
|
||||
|
||||
export default class ContentModel {
|
||||
pageNum: number;
|
||||
content: string;
|
||||
opList: OperatorList;
|
||||
|
||||
constructor(content: string, opList: OperatorList) {
|
||||
constructor(pageNum: number, content: string, opList: OperatorList) {
|
||||
this.pageNum = pageNum;
|
||||
this.content = content;
|
||||
this.opList = opList;
|
||||
}
|
||||
@ -17,7 +19,7 @@ export default class ContentModel {
|
||||
for (let i = 0; i < this.opList.rangeArray.length; i++){
|
||||
const oldRange: number[] = this.opList.rangeArray[i];
|
||||
if (!oldRange) {
|
||||
newRanges.push([0, 0]);
|
||||
newRanges.push(null);
|
||||
continue;
|
||||
}
|
||||
newRanges.push([result.lookup[oldRange[0]], result.lookup[oldRange[1]]]);
|
||||
|
||||
@ -5,130 +5,150 @@ import type { TreeViewModel } from './TreeViewModel';
|
||||
import type XRefTable from './XRefTable';
|
||||
import ContentModel from './ContentModel.svelte';
|
||||
import { OperatorList } from './OperatorList';
|
||||
import { StepperManager } from './Stepper.svelte';
|
||||
|
||||
export default class DocumentWorker {
|
||||
doc: PDFDocumentProxy;
|
||||
stepperManager: StepperManager;
|
||||
renderingTask: { promise: Promise<void>; cancel: () => void } | null;
|
||||
|
||||
doc: PDFDocumentProxy;
|
||||
constructor(doc: PDFDocumentProxy, stepperManager: StepperManager) {
|
||||
this.doc = doc;
|
||||
this.renderingTask = null;
|
||||
this.stepperManager = stepperManager;
|
||||
}
|
||||
|
||||
constructor(doc: PDFDocumentProxy) {
|
||||
this.doc = doc;
|
||||
}
|
||||
|
||||
static async load(data: string | ArrayBuffer) {
|
||||
console.log("Initializing pdf worker")
|
||||
GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
||||
const loadingTask = getDocument({ data });
|
||||
const doc = await loadingTask.promise;
|
||||
return new DocumentWorker(doc);
|
||||
}
|
||||
static async load(data: string | ArrayBuffer) {
|
||||
GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
||||
const stepperManager = new StepperManager();
|
||||
// @ts-expect-error interface to untyped js
|
||||
globalThis.StepperManager = stepperManager;
|
||||
const loadingTask = getDocument({ data, pdfBug: true });
|
||||
const doc = await loadingTask.promise;
|
||||
return new DocumentWorker(doc, stepperManager);
|
||||
}
|
||||
|
||||
public async save() {
|
||||
return this.doc.saveDocument();
|
||||
}
|
||||
public async save() {
|
||||
return this.doc.saveDocument();
|
||||
}
|
||||
|
||||
public async getTitle(): Promise<string | undefined> {
|
||||
let title = this.doc._pdfInfo.Title;
|
||||
console.log(this.doc._pdfInfo);
|
||||
if (title && title !== "") {
|
||||
return title;
|
||||
}
|
||||
const metadata = await this.doc.getMetadata()
|
||||
public async getTitle(): Promise<string | undefined> {
|
||||
let title = this.doc._pdfInfo.Title;
|
||||
if (title && title !== '') {
|
||||
return title;
|
||||
}
|
||||
const metadata = await this.doc.getMetadata();
|
||||
|
||||
title = metadata?.info?.Title;
|
||||
return title === "" ? undefined : title;
|
||||
}
|
||||
title = metadata?.info?.Title;
|
||||
return title === '' ? undefined : title;
|
||||
}
|
||||
|
||||
public getNumberOfPages(): number {
|
||||
return this.doc.numPages;
|
||||
}
|
||||
public getNumberOfPages(): number {
|
||||
return this.doc.numPages;
|
||||
}
|
||||
|
||||
public async getPrimByPath(path: string): Promise<PrimitiveModel> {
|
||||
return await this.doc.getPrimitiveByPath(path);
|
||||
}
|
||||
public async getPrimByPath(path: string): Promise<PrimitiveModel> {
|
||||
return await this.doc.getPrimitiveByPath(path);
|
||||
}
|
||||
|
||||
public async getPrimTreeByPath(request: TreeViewRequest[]): Promise<TreeViewModel[]> {
|
||||
|
||||
return await this.doc.getPrimitiveTree(request);
|
||||
}
|
||||
public async getPrimTreeByPath(request: TreeViewRequest[]): Promise<TreeViewModel[]> {
|
||||
return await this.doc.getPrimitiveTree(request);
|
||||
}
|
||||
|
||||
public async getXrefEntries(): Promise<XRefTable> {
|
||||
|
||||
return await this.doc.getXRefEntries();
|
||||
}
|
||||
public async getXrefEntries(): Promise<XRefTable> {
|
||||
return await this.doc.getXRefEntries();
|
||||
}
|
||||
|
||||
public async getStreamAsString(path: string): Promise<string> {
|
||||
return await this.doc.getStreamAsString(path);
|
||||
}
|
||||
public async getStreamAsString(path: string): Promise<string> {
|
||||
return await this.doc.getStreamAsString(path);
|
||||
}
|
||||
|
||||
public async getContents(pageNumber: number): Promise<ContentModel> {
|
||||
try {
|
||||
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([], [], []));
|
||||
}
|
||||
}
|
||||
public async getContents(pageNumber: number): Promise<ContentModel> {
|
||||
try {
|
||||
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(pageNumber, await contentsPromise, opList);
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
return new ContentModel(pageNumber, '', new OperatorList([], [], [], []));
|
||||
}
|
||||
}
|
||||
|
||||
public async updateContents(pageNumber: number, newContents: string) {
|
||||
const page = await this.doc.getPage(pageNumber);
|
||||
await page.updateContents(newContents);
|
||||
}
|
||||
public async updateContents(pageNumber: number, newContents: string) {
|
||||
if (this.renderingTask) {
|
||||
await this.renderingTask.promise;
|
||||
}
|
||||
const page = await this.doc.getPage(pageNumber);
|
||||
await page.updateContents(newContents);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.doc.destroy();
|
||||
}
|
||||
public dispose() {
|
||||
this.doc.destroy();
|
||||
}
|
||||
|
||||
public async getImageAsUrl(path: string): Promise<string> {
|
||||
|
||||
const imageData = await this.doc.getImageDataByPath(path);
|
||||
public async getImageAsUrl(path: string): Promise<string> {
|
||||
const imageData = await this.doc.getImageDataByPath(path);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = imageData.width;
|
||||
canvas.height = imageData.height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Could not get canvas context');
|
||||
}
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.putImageData(new ImageData(imageData.data, imageData.width, imageData.height), 0, 0);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Could not get canvas context');
|
||||
}
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.putImageData(new ImageData(imageData.data, imageData.width, imageData.height), 0, 0);
|
||||
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
public async getOperatorList(num: number) {
|
||||
const page = await this.doc.getPage(num);
|
||||
return await page.getOperatorList();
|
||||
}
|
||||
public async getOperatorList(num: number) {
|
||||
const page = await this.doc.getPage(num);
|
||||
return await page.getOperatorList();
|
||||
}
|
||||
|
||||
public async renderPage(num: number, canvas: HTMLCanvasElement) {
|
||||
const page = await this.doc.getPage(num);
|
||||
const scale = 1.5;
|
||||
const viewport = page.getViewport({ scale: scale, });
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
canvas.style.width = Math.floor(canvas.width) + "px";
|
||||
canvas.style.height = Math.floor(canvas.height) + "px";
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) return "";
|
||||
const transform = [1, 0, 0, 1, 0, 0];
|
||||
// viewport = page.getViewport({ scale: 0.7, offsetX: canvas.width / 4, offsetY: canvas.height / 4});
|
||||
public async renderPage(
|
||||
num: number,
|
||||
canvas: HTMLCanvasElement,
|
||||
scale: number
|
||||
) {
|
||||
if (this.renderingTask) {
|
||||
this.renderingTask.cancel();
|
||||
this.renderingTask = null;
|
||||
}
|
||||
const page = await this.doc.getPage(num);
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
transform: transform,
|
||||
viewport: viewport,
|
||||
};
|
||||
const renderTask = page.render(renderContext);
|
||||
const viewport = page.getViewport({ scale: scale });
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
canvas.style.width = Math.floor(canvas.width) + 'px';
|
||||
canvas.style.height = Math.floor(canvas.height) + 'px';
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) return '';
|
||||
const transform = [1, 0, 0, 1, 0, 0];
|
||||
|
||||
context.fillStyle = '#ffffff';
|
||||
context.fillRect(viewport.offsetX, viewport.offsetY, viewport.width, viewport.height);
|
||||
await renderTask.promise
|
||||
}
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
transform: transform,
|
||||
viewport: viewport
|
||||
};
|
||||
this.renderingTask = page.render(renderContext);
|
||||
|
||||
context.fillStyle = '#ffffff';
|
||||
context.fillRect(viewport.offsetX, viewport.offsetY, viewport.width, viewport.height);
|
||||
await this.renderingTask?.promise;
|
||||
this.renderingTask = null;
|
||||
}
|
||||
|
||||
public async getPage(pageNum: number) {
|
||||
return await this.doc.getPage(pageNum);
|
||||
}
|
||||
}
|
||||
@ -7,63 +7,33 @@ export interface PDFOperator {
|
||||
class: string;
|
||||
}
|
||||
|
||||
export class OperatorList {
|
||||
fnArray: number[];
|
||||
argsArray: [];
|
||||
rangeArray: number[][];
|
||||
constructor(fnArray: [], argsArray: [], rangeArray: []) {
|
||||
this.fnArray = fnArray;
|
||||
this.argsArray = argsArray;
|
||||
this.rangeArray = rangeArray;
|
||||
export interface Range {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export class Operator {
|
||||
fnId: keyof typeof opLookup;
|
||||
args: [] | null;
|
||||
range: Range | null;
|
||||
ogIdx: number;
|
||||
|
||||
constructor(fnId: number, args: [] | null, range: Range | null, ogIdx: number) {
|
||||
// @ts-expect-error can not happen
|
||||
this.fnId = fnId;
|
||||
this.args = args;
|
||||
this.range = range;
|
||||
this.ogIdx = ogIdx;
|
||||
}
|
||||
|
||||
public setRangeArray(rangeArray: number[][]) {
|
||||
this.rangeArray = rangeArray;
|
||||
getPDFOperator(): PDFOperator {
|
||||
return opLookup[this.fnId];
|
||||
}
|
||||
|
||||
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;
|
||||
toMarkdown() {
|
||||
const operator = this.getPDFOperator();
|
||||
if (!operator) {
|
||||
return `Unknown operator: ${operatorId}`;
|
||||
return `Unknown operator: ${this.fnId}`;
|
||||
}
|
||||
|
||||
const keyword = operator.keyword ? `\`${operator.keyword}\`` : 'N/A';
|
||||
@ -71,12 +41,16 @@ export class OperatorList {
|
||||
operator.numArgs
|
||||
? `Arguments: ${operator.numArgs}${operator.variableArgs ? ' (variable)' : ''}`
|
||||
: 'No arguments';
|
||||
if (operator.keyword === "Tj" || operator.keyword === "TJ" && Array.isArray(args)) {
|
||||
if ((operator.keyword === "Tj" || operator.keyword === "TJ") && Array.isArray(this.args)) {
|
||||
let content = '';
|
||||
content += "`"
|
||||
for (const arg of args[0]) {
|
||||
if (arg.unicode) {
|
||||
content += arg.unicode;
|
||||
const textArgs = this.args.at(0)
|
||||
if (textArgs && Array.isArray(textArgs)) {
|
||||
// @ts-expect-error I don't know how to properly type this
|
||||
for (const arg of textArgs) {
|
||||
if (arg.unicode) {
|
||||
content += arg.unicode;
|
||||
}
|
||||
}
|
||||
}
|
||||
content += "`"
|
||||
@ -91,7 +65,110 @@ export class OperatorList {
|
||||
}
|
||||
}
|
||||
|
||||
export interface OperatorListModel {
|
||||
fnArray: number[];
|
||||
argsArray: [];
|
||||
rangeArray: number[][];
|
||||
}
|
||||
|
||||
export class OperatorList {
|
||||
|
||||
fnArray: number[];
|
||||
argsArray: [];
|
||||
rangeArray: number[][];
|
||||
ogIdx: number[];
|
||||
|
||||
constructor(fnArray: [], argsArray: [], rangeArray: [], ogIdx: number[]) {
|
||||
this.fnArray = fnArray;
|
||||
this.argsArray = argsArray;
|
||||
this.rangeArray = rangeArray;
|
||||
this.ogIdx = ogIdx;
|
||||
}
|
||||
|
||||
public length() {
|
||||
return this.fnArray.length;
|
||||
}
|
||||
|
||||
public ogLength() {
|
||||
const l = this.ogIdx.at(-1);
|
||||
if (l === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return l + 1;
|
||||
}
|
||||
|
||||
public setRangeArray(rangeArray: number[][]) {
|
||||
this.rangeArray = rangeArray;
|
||||
}
|
||||
|
||||
public static fromModel(opList: OperatorListModel): OperatorList {
|
||||
// @ts-expect-error impossibly difficult to type
|
||||
return this.createUnfoldedOpList(opList.fnArray, opList.argsArray, opList.rangeArray);
|
||||
}
|
||||
|
||||
public static createUnfoldedOpList(fnArray: [], argsArray: [], rangeArray: []) {
|
||||
const unfoldedFnArray = [];
|
||||
const unfoldedArgsArray = [];
|
||||
const unfoldedRangeArray = [];
|
||||
const ogIdxArray = [];
|
||||
for (let i = 0; i < fnArray.length; i++) {
|
||||
if (fnArray[i] == 91) {
|
||||
const [subFnArray, subArgsArray, subRangeArray] = OperatorList.unfoldConstructPath(
|
||||
argsArray[i]
|
||||
);
|
||||
unfoldedFnArray.push(...subFnArray);
|
||||
unfoldedArgsArray.push(...subArgsArray);
|
||||
unfoldedRangeArray.push(...subRangeArray);
|
||||
for (let j = 0; j < subFnArray.length; j++) {
|
||||
ogIdxArray.push(i);
|
||||
}
|
||||
} else {
|
||||
unfoldedFnArray.push(fnArray[i]);
|
||||
unfoldedArgsArray.push(argsArray[i]);
|
||||
unfoldedRangeArray.push(rangeArray[i]);
|
||||
ogIdxArray.push(i);
|
||||
}
|
||||
}
|
||||
// @ts-expect-error I don't know how to properly type this
|
||||
return new OperatorList(unfoldedFnArray, unfoldedArgsArray, unfoldedRangeArray, ogIdxArray);
|
||||
}
|
||||
|
||||
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);
|
||||
// @ts-expect-error I don't know how to properly type this
|
||||
const newSubArgs = [];
|
||||
for (let j = 0; j < (op.numArgs ?? 0); j++) {
|
||||
newSubArgs.push(subArgs[argsOffset++]);
|
||||
}
|
||||
// @ts-expect-error I don't know how to properly type this
|
||||
subArgsArray.push(newSubArgs);
|
||||
}
|
||||
return [subFnArray, subArgsArray, subRangesArray];
|
||||
}
|
||||
|
||||
public getOperatorAt(i: number) {
|
||||
const fnId = this.fnArray.at(i);
|
||||
const args = this.argsArray.at(i) ?? null;
|
||||
const range = this.rangeArray.at(i);
|
||||
const ogIdx = this.ogIdx.at(i);
|
||||
if (fnId && ogIdx !== undefined) {
|
||||
return new Operator(fnId, args, Array.isArray(range) ? {start: range[0], end: range[1]} : null, ogIdx);
|
||||
} else {
|
||||
throw new Error(`Operator List Index not found at ${i}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getPDFOperator(id: number | string): PDFOperator {
|
||||
// @ts-expect-error I don't know how to properly type this
|
||||
return opLookup[id];
|
||||
}
|
||||
|
||||
|
||||
45
src/models/PageRenderState.svelte.ts
Normal file
45
src/models/PageRenderState.svelte.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type DocumentWorker from './Document.svelte';
|
||||
import type FileViewState from './FileViewState.svelte';
|
||||
|
||||
export class PageRenderState {
|
||||
canvas: HTMLCanvasElement;
|
||||
doc: DocumentWorker;
|
||||
fState: FileViewState;
|
||||
|
||||
pageNum: number | undefined;
|
||||
width: number | undefined;
|
||||
height: number | undefined;
|
||||
scale: number = $state(1);
|
||||
|
||||
constructor(fState: FileViewState, canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
this.doc = fState.document;
|
||||
this.fState = fState;
|
||||
this.scale = 1;
|
||||
}
|
||||
|
||||
async updatePage(pageNum: number) {
|
||||
if (pageNum === this.pageNum) {
|
||||
return;
|
||||
}
|
||||
|
||||
const page = await this.doc.getPage(pageNum);
|
||||
const viewPort = page.getViewport({ scale: 1, });
|
||||
this.pageNum = pageNum;
|
||||
this.width = viewPort.width;
|
||||
this.height = viewPort.height;
|
||||
}
|
||||
|
||||
updateScale(scale: number) {
|
||||
|
||||
this.scale = scale;
|
||||
return this.renderFrame();
|
||||
}
|
||||
|
||||
public async renderFrame() {
|
||||
if (this.fState.currentPageNumber) {
|
||||
await this.doc.renderPage(this.fState.currentPageNumber, this.canvas, this.scale);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
90
src/models/Stepper.svelte.ts
Normal file
90
src/models/Stepper.svelte.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import type { OperatorListModel } from './OperatorList';
|
||||
|
||||
export class DebugStepper {
|
||||
breakPoints: number[];
|
||||
nextBreakPoint: number | null = null;
|
||||
currentIdx: number = $state(0);
|
||||
pageNum: number = 0;
|
||||
onBreak: boolean = $state(false);
|
||||
opList: OperatorListModel | null = null;
|
||||
callback: (() => void) | null = null;
|
||||
|
||||
constructor(pageNum: number, breakPoints: Set<number>) {
|
||||
this.pageNum = pageNum;
|
||||
this.breakPoints = Array.from(breakPoints).sort(function (a, b) {
|
||||
return b - a;
|
||||
});
|
||||
}
|
||||
|
||||
updateBreakPoints(breakPoints: Set<number>) {
|
||||
this.breakPoints = Array.from(breakPoints).filter(a => a > this.currentIdx).sort(function (a, b) {
|
||||
return b - a;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public async breakIt(idx: number, callback: () => void) {
|
||||
this.onBreak = true;
|
||||
this.currentIdx = idx;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public continue() {
|
||||
this.nextBreakPoint = this.popNextBreakPoint();
|
||||
if (this.callback) {
|
||||
this.callback();
|
||||
}
|
||||
this.onBreak = false;
|
||||
this.callback = null;
|
||||
this._resume();
|
||||
}
|
||||
|
||||
public step() {
|
||||
this.nextBreakPoint = this.currentIdx + 1;
|
||||
this._resume();
|
||||
}
|
||||
|
||||
private _resume() {
|
||||
if (this.callback) {
|
||||
this.callback();
|
||||
}
|
||||
this.onBreak = false;
|
||||
this.callback = null;
|
||||
}
|
||||
|
||||
public getNextBreakPoint() {
|
||||
|
||||
return this.nextBreakPoint ?? this.breakPoints.pop();
|
||||
}
|
||||
|
||||
private popNextBreakPoint() {
|
||||
return this.breakPoints.pop() ?? null;
|
||||
}
|
||||
|
||||
init(operatorList: OperatorListModel) {
|
||||
this.opList = operatorList;
|
||||
}
|
||||
|
||||
updateOperatorList(operatorList: OperatorListModel) {
|
||||
this.opList = operatorList;
|
||||
}
|
||||
}
|
||||
|
||||
export class StepperManager {
|
||||
stepper: DebugStepper | null = null;
|
||||
enabled: boolean = false;
|
||||
|
||||
register(stepper: DebugStepper) {
|
||||
this.stepper = stepper;
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
unregister(stepper: DebugStepper) {
|
||||
this.stepper = null;
|
||||
this.enabled = false;
|
||||
}
|
||||
// must be called like this for api reasons
|
||||
create(pageNum: number) {
|
||||
return this.stepper;
|
||||
}
|
||||
}
|
||||
6
src/models/ViewState.ts
Normal file
6
src/models/ViewState.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export type ViewState = {
|
||||
x: number;
|
||||
y: number;
|
||||
scale: number;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user