first commit

This commit is contained in:
Kilian Schuettler 2025-03-16 19:10:13 +01:00
commit 4637c66185
79 changed files with 8243 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

63
.idea/codeStyles/Project.xml generated Normal file
View File

@ -0,0 +1,63 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="100" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pdf-forge.iml" filepath="$PROJECT_DIR$/.idea/pdf-forge.iml" />
</modules>
</component>
</project>

12
.idea/pdf-forge.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/prettier.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

18
.prettierrc Normal file
View File

@ -0,0 +1,18 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": [
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

39
eslint.config.js Normal file
View File

@ -0,0 +1,39 @@
import prettier from "eslint-config-prettier";
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
ignores: ["eslint.config.js", "svelte.config.js"],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);

Binary file not shown.

32
examples/api_examples.js Normal file
View File

@ -0,0 +1,32 @@
import fs from "fs";
import { getDocument } from "file://C:\\Users\\kj131\\pdf-forge\\pdf.js\\build\\dist\\build\\pdf.min.mjs";
// Some PDFs need external cmaps.
const CMAP_URL = "../node_modules/pdfjs-dist/cmaps/";
const CMAP_PACKED = true;
// Where the standard fonts are located.
const STANDARD_FONT_DATA_URL =
"../node_modules/pdfjs-dist/standard_fonts/";
// Loading file from file system into typed array.
const pdfPath =
process.argv[2] || "ISO_32000-2_2020(en).pdf";
const data = new Uint8Array(fs.readFileSync(pdfPath));
// Load the PDF file.
const loadingTask = getDocument({
data,
cMapUrl: CMAP_URL,
cMapPacked: CMAP_PACKED,
standardFontDataUrl: STANDARD_FONT_DATA_URL,
});
try {
const pdfDocument = await loadingTask.promise;
console.log("# PDF document loaded.");
const page = await pdfDocument.getPage(1);
const opList = await page.getOperatorList();
console.log(opList);
} catch (e) {
console.error(e);
}

912
examples/buildOpsLookup.js Normal file
View File

@ -0,0 +1,912 @@
// All the possible operations for an operator list.
const OPS = {
dependency: 1,
setLineWidth: 2,
setLineCap: 3,
setLineJoin: 4,
setMiterLimit: 5,
setDash: 6,
setRenderingIntent: 7,
setFlatness: 8,
setGState: 9,
save: 10,
restore: 11,
transform: 12,
moveTo: 13,
lineTo: 14,
curveTo: 15,
curveTo2: 16,
curveTo3: 17,
closePath: 18,
rectangle: 19,
stroke: 20,
closeStroke: 21,
fill: 22,
eoFill: 23,
fillStroke: 24,
eoFillStroke: 25,
closeFillStroke: 26,
closeEOFillStroke: 27,
endPath: 28,
clip: 29,
eoClip: 30,
beginText: 31,
endText: 32,
setCharSpacing: 33,
setWordSpacing: 34,
setHScale: 35,
setLeading: 36,
setFont: 37,
setTextRenderingMode: 38,
setTextRise: 39,
moveText: 40,
setLeadingMoveText: 41,
setTextMatrix: 42,
nextLine: 43,
showText: 44,
showSpacedText: 45,
nextLineShowText: 46,
nextLineSetSpacingShowText: 47,
setCharWidth: 48,
setCharWidthAndBounds: 49,
setStrokeColorSpace: 50,
setFillColorSpace: 51,
setStrokeColor: 52,
setStrokeColorN: 53,
setFillColor: 54,
setFillColorN: 55,
setStrokeGray: 56,
setFillGray: 57,
setStrokeRGBColor: 58,
setFillRGBColor: 59,
setStrokeCMYKColor: 60,
setFillCMYKColor: 61,
shadingFill: 62,
beginInlineImage: 63,
beginImageData: 64,
endInlineImage: 65,
paintXObject: 66,
markPoint: 67,
markPointProps: 68,
beginMarkedContent: 69,
beginMarkedContentProps: 70,
endMarkedContent: 71,
beginCompat: 72,
endCompat: 73,
paintFormXObjectBegin: 74,
paintFormXObjectEnd: 75,
beginGroup: 76,
endGroup: 77,
// beginAnnotations: 78,
// endAnnotations: 79,
beginAnnotation: 80,
endAnnotation: 81,
// paintJpegXObject: 82,
paintImageMaskXObject: 83,
paintImageMaskXObjectGroup: 84,
paintImageXObject: 85,
paintInlineImageXObject: 86,
paintInlineImageXObjectGroup: 87,
paintImageXObjectRepeat: 88,
paintImageMaskXObjectRepeat: 89,
paintSolidColorImageMask: 90,
constructPath: 91,
setStrokeTransparent: 92,
setFillTransparent: 93
};
const opMap = {
// Graphic state
w: { id: OPS.setLineWidth, numArgs: 1, variableArgs: false },
J: { id: OPS.setLineCap, numArgs: 1, variableArgs: false },
j: { id: OPS.setLineJoin, numArgs: 1, variableArgs: false },
M: { id: OPS.setMiterLimit, numArgs: 1, variableArgs: false },
d: { id: OPS.setDash, numArgs: 2, variableArgs: false },
ri: { id: OPS.setRenderingIntent, numArgs: 1, variableArgs: false },
i: { id: OPS.setFlatness, numArgs: 1, variableArgs: false },
gs: { id: OPS.setGState, numArgs: 1, variableArgs: false },
q: { id: OPS.save, numArgs: 0, variableArgs: false },
Q: { id: OPS.restore, numArgs: 0, variableArgs: false },
cm: { id: OPS.transform, numArgs: 6, variableArgs: false },
// Path
m: { id: OPS.moveTo, numArgs: 2, variableArgs: false },
l: { id: OPS.lineTo, numArgs: 2, variableArgs: false },
c: { id: OPS.curveTo, numArgs: 6, variableArgs: false },
v:dd-code nl-5 ol-5" data-line-typ id: OPS.curveTo2, numArgs: 4, variableArgs: false },
y: { id: OPS.curveTo3, numArgs: 4, variableArgs: false },
h: { id: OPS.closePath, numArgs: 0, variableArgs: false },
re: { id: OPS.rectangle, numArgs: 4, variableArgs: false },
S: { id: OPS.stroke, numArgs: 0, variableArgs: false },
s: { id: OPS.closeStroke, numArgs: 0, variableArgs: false },
f: { id: OPS.fill, numArgs: 0, variableArgs: false },
F: { id: OPS.fill, numArgs: 0, variableArgs: false },
'f*': { id: OPS.eoFill, numArgs: 0, variableArgs: false },
B: { id: OPS.fillStroke, numArgs: 0, variableArgs: false },
'B*': { id: OPS.eoFillStroke, numArgs: 0, variableArgs: false },
b: { id: OPS.closeFillStroke, numArgs: 0, variableArgs: false },
'b*': { id: OPS.closeEOFillStroke, numArgs: 0, variableArgs: false },
n: { id: OPS.endPath, numArgs: 0, variableArgs: false },
// Clipping
W: { id: OPS.clip, numArgs: 0, variableArgs: false },
'W*': { id: OPS.eoClip, numArgs: 0, variableArgs: false },
// Text
BT: { id: OPS.beginText, numArgs: 0, variableArgs: false },
ET: { id: OPS.endText, numArgs: 0, variableArgs: false },
Tc: { id: OPS.setCharSpacing, numArgs: 1, variableArgs: false },
Tw: { id: OPS.setWordSpacing, numArgs: 1, variableArgs: false },
Tz: { id: OPS.setHScale, numArgs: 1, variableArgs: false },
TL: { id: OPS.setLeading, numArgs: 1, variableArgs: false },
Tf: { id: OPS.setFont, numArgs: 2, variableArgs: false },
Tr: { id: OPS.setTextRenderingMode, numArgs: 1, variableArgs: false },
Ts: { id: OPS.setTextRise, numArgs: 1, variableArgs: false },
Td: { id: OPS.moveText, numArgs: 2, variableArgs: false },
TD: { id: OPS.setLeadingMoveText, numArgs: 2, variableArgs: false },
Tm: { id: OPS.setTextMatrix, numArgs: 6, variableArgs: false },
'T*': { id: OPS.nextLine, numArgs: 0, variableArgs: false },
Tj: { id: OPS.showText, numArgs: 1, variableArgs: false },
TJ: { id: OPS.showSpacedText, numArgs: 1, variableArgs: false },
'\'': { id: OPS.nextLineShowText, numArgs: 1, variableArgs: false },
'"': {
id: OPS.nextLineSetSpacingShowText,
numArgs: 3,
variableArgs: false
},
// Type3 fonts
d0: { id: OPS.setCharWidth, numArgs: 2, variableArgs: false },
d1: {
id: OPS.setCharWidthAndBounds,
numArgs: 6,
variableArgs: false
},
// Color
CS: { id: OPS.setStrokeColorSpace, numArgs: 1, variableArgs: false },
cs: { id: OPS.setFillColorSpace, numArgs: 1, variableArgs: false },
SC: { id: OPS.setStrokeColor, numArgs: 4, variableArgs: true },
SCN: { id: OPS.setStrokeColorN, numArgs: 33, variableArgs: true },
sc: { id: OPS.setFillColor, numArgs: 4, variableArgs: true },
scn: { id: OPS.setFillColorN, numArgs: 33, variableArgs: true },
G: { id: OPS.setStrokeGray, numArgs: 1, variableArgs: false },
g: { id: OPS.setFillGray, numArgs: 1, variableArgs: false },
RG: { id: OPS.setStrokeRGBColor, numArgs: 3, variableArgs: false },
rg: { id: OPS.setFillRGBColor, numArgs: 3, variableArgs: false },
K: { id: OPS.setStrokeCMYKColor, numArgs: 4, variableArgs: false },
k: { id: OPS.setFillCMYKColor, numArgs: 4, variableArgs: false },
// Shading
sh: { id: OPS.shadingFill, numArgs: 1, variableArgs: false },
// Images
BI: { id: OPS.beginInlineImage, numArgs: 0, variableArgs: false },
ID: { id: OPS.beginImageData, numArgs: 0, variableArgs: false },
EI: { id: OPS.endInlineImage, numArgs: 1, variableArgs: false },
// XObjects
Do: { id: OPS.paintXObject, numArgs: 1, variableArgs: false },
MP: { id: OPS.markPoint, numArgs: 1, variableArgs: false },
DP: { id: OPS.markPointProps, numArgs: 2, variableArgs: false },
BMC: { id: OPS.beginMarkedContent, numArgs: 1, variableArgs: false },
BDC: {
id: OPS.beginMarkedContentProps,
numArgs: 2,
variableArgs: false
},
EMC: { id: OPS.endMarkedContent, numArgs: 0, variableArgs: false },
// Compatibility
BX: { id: OPS.beginCompat, numArgs: 0, variableArgs: false },
EX: { id: OPS.endCompat, numArgs: 0, variableArgs: false }
};
const invertedOPS = Object.entries(OPS).reduce((acc, [key, value]) => {
acc[value] = {
name: key,
keyword: Object.entries(opMap).find(([_, op]) => op.id === value)?.[0] || null,
description: getOperatorDescription(key),
...(opMap[Object.entries(opMap).find(([_, op]) => op.id === value)?.[0]] || {})
};
return acc;
}, {});
function getOperatorDescription(opName) {
const descriptions = {
setLineWidth: 'Sets the line width in the graphics state',
setLineCap: 'Sets the line cap style in the graphics state (0=butt, 1=round, 2=square)',
setLineJoin: 'Sets the line join style in the graphics state (0=miter, 1=round, 2=bevel)',
setMiterLimit: 'Sets the miter limit in the graphics state',
setDash: 'Sets the line dash pattern in the graphics state',
setRenderingIntent: 'Sets the color rendering intent in the graphics state',
setFlatness: 'Sets the flatness tolerance in the graphics state',
setGState: 'Sets the specified parameters in the graphics state',
save: 'Pushes a copy of the current graphics state onto the graphics state stack',
restore: 'Restores the graphics state by popping it from the stack',
transform: 'Modifies the current transformation matrix (CTM)',
moveTo: 'Begins a new subpath by moving to coordinates (x, y)',
lineTo: 'Adds a straight line segment to the current path',
curveTo: 'Adds a Bézier curve segment to the current path using two control points',
curveTo2: 'Adds a Bézier curve segment to the current path using one control point',
curveTo3: 'Adds a Bézier curve segment to the current path using one control point',
closePath: 'Closes the current subpath',
rectangle: 'Adds a rectangle to the current path',
stroke: 'Strokes the current path',
closeStroke: 'Closes and strokes the current path',
fill: 'Fills the current path using nonzero winding number rule',
eoFill: 'Fills the current path using even-odd rule',
fillStroke: 'Fills and strokes the current path using nonzero winding number rule',
eoFillStroke: 'Fills and strokes the current path using even-odd rule',
closeFillStroke: 'Closes, fills, and strokes the current path using nonzero winding number rule',
closeEOFillStroke: 'Closes, fills, and strokes the current path using even-odd rule',
endPath: 'Ends the current path without filling or stroking',
clip: 'Sets the current path as the clipping path using nonzero winding number rule',
eoClip: 'Sets the current path as the clipping path using even-odd rule',
beginText: 'Begins a text object',
endText: 'Ends a text object',
setCharSpacing: 'Sets the character spacing',
setWordSpacing: 'Sets the word spacing',
setHScale: 'Sets the horizontal scaling',
setLeading: 'Sets the text leading',
setFont: 'Sets the text font and size',
setTextRenderingMode: 'Sets the text rendering mode',
setTextRise: 'Sets the text rise',
moveText: 'Moves to the start of the next line using offset coordinates',
setLeadingMoveText: 'Sets the text leading and moves to the next line',
setTextMatrix: 'Sets the text matrix and text line matrix',
nextLine: 'Moves to the start of the next line',
showText: 'Shows a text string',
showSpacedText: 'Shows a text string with individual glyph positioning',
nextLineShowText: 'Moves to the next line and shows a text string',
nextLineSetSpacingShowText: 'Sets word and character spacing, moves to next line, and shows text',
setCharWidth: 'Sets the character width information for Type 3 fonts',
setCharWidthAndBounds: 'Sets the character width and bounding box for Type 3 fonts',
setStrokeColorSpace: 'Sets the color space for stroking operations',
setFillColorSpace: 'Sets the color space for nonstroking operations',
setStrokeColor: 'Sets the color for stroking operations',
setStrokeColorN: 'Sets the color for stroking operations (ICCBased and special color spaces)',
setFillColor: 'Sets the color for nonstroking operations',
setFillColorN: 'Sets the color for nonstroking operations (ICCBased and special color spaces)',
setStrokeGray: 'Sets the gray level for stroking operations',
setFillGray: 'Sets the gray level for nonstroking operations',
setStrokeRGBColor: 'Sets the RGB color for stroking operations',
setFillRGBColor: 'Sets the RGB color for nonstroking operations',
setStrokeCMYKColor: 'Sets the CMYK color for stroking operations',
setFillCMYKColor: 'Sets the CMYK color for nonstroking operations',
shadingFill: 'Fills the current path using a shading pattern',
beginInlineImage: 'Begins an inline image object',
beginImageData: 'Begins the inline image data',
endInlineImage: 'Ends an inline image object',
paintXObject: 'Paints the specified XObject',
markPoint: 'Marks a point in the content stream',
markPointProps: 'Marks a point in the content stream with properties',
beginMarkedContent: 'Begins a marked-content sequence',
beginMarkedContentProps: 'Begins a marked-content sequence with properties',
endMarkedContent: 'Ends a marked-content sequence',
beginCompat: 'Begins a compatibility section',
endCompat: 'Ends a compatibility section',
paintFormXObjectBegin: 'Begins painting a form XObject',
paintFormXObjectEnd: 'Ends painting a form XObject',
beginGroup: 'Begins a transparency group',
endGroup: 'Ends a transparency group',
beginAnnotation: 'Begins an annotation',
endAnnotation: 'Ends an annotation',
paintImageMaskXObject: 'Paints an image mask XObject',
paintImageMaskXObjectGroup: 'Paints an image mask XObject group',
paintImageXObject: 'Paints an image XObject',
paintInlineImageXObject: 'Paints an inline image XObject',
paintInlineImageXObjectGroup: 'Paints an inline image XObject group',
paintImageXObjectRepeat: 'Paints a repeated image XObject',
paintImageMaskXObjectRepeat: 'Paints a repeated image mask XObject',
paintSolidColorImageMask: 'Paints a solid color image mask',
constructPath: 'Constructs a path from the specified segments',
setStrokeTransparent: 'Sets the stroke transparency',
setFillTransparent: 'Sets the fill transparency',
dependency: 'Marks a resource dependency'
};
return descriptions[opName] || 'No description available';
}
const opLookup = {
'1': {
name: 'dependency',
keyword: "Do",
description: 'Marks a resource dependency',
numArgs: 0,
variableArgs: false
},
'2': {
name: 'setLineWidth',
keyword: 'w',
description: 'Sets the line width in the graphics state',
numArgs: 1,
variableArgs: false
},
'3': {
name: 'setLineCap',
keyword: 'J',
description: 'Sets the line cap style in the graphics state (0=butt, 1=round, 2=square)',
numArgs: 1,
variableArgs: false
},
'4': {
name: 'setLineJoin',
keyword: 'j',
description: 'Sets the line join style in the graphics state (0=miter, 1=round, 2=bevel)',
numArgs: 1,
variableArgs: false
},
'5': {
name: 'setMiterLimit',
keyword: 'M',
description: 'Sets the miter limit in the graphics state',
numArgs: 1,
variableArgs: false
},
'6': {
name: 'setDash',
keyword: 'd',
description: 'Sets the line dash pattern in the graphics state',
numArgs: 2,
variableArgs: false
},
'7': {
name: 'setRenderingIntent',
keyword: 'ri',
description: 'Sets the color rendering intent in the graphics state',
numArgs: 1,
variableArgs: false
},
'8': {
name: 'setFlatness',
keyword: 'i',
description: 'Sets the flatness tolerance in the graphics state',
numArgs: 1,
variableArgs: false
},
'9': {
name: 'setGState',
keyword: 'gs',
description: 'Sets the specified parameters in the graphics state',
numArgs: 1,
variableArgs: false
},
'10': {
name: 'save',
keyword: 'q',
description: 'Pushes a copy of the current graphics state onto the graphics state stack',
numArgs: 0,
variableArgs: false
},
'11': {
name: 'restore',
keyword: 'Q',
description: 'Restores the graphics state by popping it from the stack',
numArgs: 0,
variableArgs: false
},
'12': {
name: 'transform',
keyword: 'cm',
description: 'Modifies the current transformation matrix (CTM)',
numArgs: 6,
variableArgs: false
},
'13': {
name: 'moveTo',
keyword: 'm',
description: 'Begins a new subpath by moving to coordinates (x, y)',
numArgs: 2,
variableArgs: false
},
'14': {
name: 'lineTo',
keyword: 'l',
description: 'Adds a straight line segment to the current path',
numArgs: 2,
variableArgs: false
},
'15': {
name: 'curveTo',
keyword: 'c',
description: 'Adds a Bézier curve segment to the current path using two control points',
numArgs: 6,
variableArgs: false
},
'16': {
name: 'curveTo2',
keyword: 'v',
description: 'Adds a Bézier curve segment to the current path using one control point',
numArgs: 4,
variableArgs: false
},
'17': {
name: 'curveTo3',
keyword: 'y',
description: 'Adds a Bézier curve segment to the current path using one control point',
numArgs: 4,
variableArgs: false
},
'18': {
name: 'closePath',
keyword: 'h',
description: 'Closes the current subpath',
numArgs: 0,
variableArgs: false
},
'19': {
name: 'rectangle',
keyword: 're',
description: 'Adds a rectangle to the current path',
numArgs: 4,
variableArgs: false
},
'20': {
name: 'stroke',
keyword: 'S',
description: 'Strokes the current path',
numArgs: 0,
variableArgs: false
},
'21': {
name: 'closeStroke',
keyword: 's',
description: 'Closes and strokes the current path',
numArgs: 0,
variableArgs: false
},
'22': {
name: 'fill',
keyword: 'f',
description: 'Fills the current path using nonzero winding number rule',
numArgs: 0,
variableArgs: false
},
'23': {
name: 'eoFill',
keyword: 'f*',
description: 'Fills the current path using even-odd rule',
numArgs: 0,
variableArgs: false
},
'24': {
name: 'fillStroke',
keyword: 'B',
description: 'Fills and strokes the current path using nonzero winding number rule',
numArgs: 0,
variableArgs: false
},
'25': {
name: 'eoFillStroke',
keyword: 'B*',
description: 'Fills and strokes the current path using even-odd rule',
numArgs: 0,
variableArgs: false
},
'26': {
name: 'closeFillStroke',
keyword: 'b',
description: 'Closes, fills, and strokes the current path using nonzero winding number rule',
numArgs: 0,
variableArgs: false
},
'27': {
name: 'closeEOFillStroke',
keyword: 'b*',
description: 'Closes, fills, and strokes the current path using even-odd rule',
numArgs: 0,
variableArgs: false
},
'28': {
name: 'endPath',
keyword: 'n',
description: 'Ends the current path without filling or stroking',
numArgs: 0,
variableArgs: false
},
'29': {
name: 'clip',
keyword: 'W',
description: 'Sets the current path as the clipping path using nonzero winding number rule',
numArgs: 0,
variableArgs: false
},
'30': {
name: 'eoClip',
keyword: 'W*',
description: 'Sets the current path as the clipping path using even-odd rule',
numArgs: 0,
variableArgs: false
},
'31': {
name: 'beginText',
keyword: 'BT',
description: 'Begins a text object',
numArgs: 0,
variableArgs: false
},
'32': {
name: 'endText',
keyword: 'ET',
description: 'Ends a text object',
numArgs: 0,
variableArgs: false
},
'33': {
name: 'setCharSpacing',
keyword: 'Tc',
description: 'Sets the character spacing',
numArgs: 1,
variableArgs: false
},
'34': {
name: 'setWordSpacing',
keyword: 'Tw',
description: 'Sets the word spacing',
numArgs: 1,
variableArgs: false
},
'35': {
name: 'setHScale',
keyword: 'Tz',
description: 'Sets the horizontal scaling',
numArgs: 1,
variableArgs: false
},
'36': {
name: 'setLeading',
keyword: 'TL',
description: 'Sets the text leading',
numArgs: 1,
variableArgs: false
},
'37': {
name: 'setFont',
keyword: 'Tf',
description: 'Sets the text font and size',
numArgs: 2,
variableArgs: false
},
'38': {
name: 'setTextRenderingMode',
keyword: 'Tr',
description: 'Sets the text rendering mode',
numArgs: 1,
variableArgs: false
},
'39': {
name: 'setTextRise',
keyword: 'Ts',
description: 'Sets the text rise',
numArgs: 1,
variableArgs: false
},
'40': {
name: 'moveText',
keyword: 'Td',
description: 'Moves to the start of the next line using offset coordinates',
numArgs: 2,
variableArgs: false
},
'41': {
name: 'setLeadingMoveText',
keyword: 'TD',
description: 'Sets the text leading and moves to the next line',
numArgs: 2,
variableArgs: false
},
'42': {
name: 'setTextMatrix',
keyword: 'Tm',
description: 'Sets the text matrix and text line matrix',
numArgs: 6,
variableArgs: false
},
'43': {
name: 'nextLine',
keyword: 'T*',
description: 'Moves to the start of the next line',
numArgs: 0,
variableArgs: false
},
'44': {
name: 'showText',
keyword: 'Tj',
description: 'Shows a text string',
numArgs: 1,
variableArgs: false
},
'45': {
name: 'showSpacedText',
keyword: 'TJ',
description: 'Shows a text string with individual glyph positioning',
numArgs: 1,
variableArgs: false
},
'46': {
name: 'nextLineShowText',
keyword: '\'',
description: 'Moves to the next line and shows a text string',
numArgs: 1,
variableArgs: false
},
'47': {
name: 'nextLineSetSpacingShowText',
keyword: '"',
description: 'Sets word and character spacing, moves to next line, and shows text',
numArgs: 3,
variableArgs: false
},
'48': {
name: 'setCharWidth',
keyword: 'd0',
description: 'Sets the character width information for Type 3 fonts',
numArgs: 2,
variableArgs: false
},
'49': {
name: 'setCharWidthAndBounds',
keyword: 'd1',
description: 'Sets the character width and bounding box for Type 3 fonts',
numArgs: 6,
variableArgs: false
},
'50': {
name: 'setStrokeColorSpace',
keyword: 'CS',
description: 'Sets the color space for stroking operations',
numArgs: 1,
variableArgs: false
},
'51': {
name: 'setFillColorSpace',
keyword: 'cs',
description: 'Sets the color space for nonstroking operations',
numArgs: 1,
variableArgs: false
},
'52': {
name: 'setStrokeColor',
keyword: 'SC',
description: 'Sets the color for stroking operations',
numArgs: 4,
variableArgs: true
},
'53': {
name: 'setStrokeColorN',
keyword: 'SCN',
description: 'Sets the color for stroking operations (ICCBased and special color spaces)',
numArgs: 33,
variableArgs: true
},
'54': {
name: 'setFillColor',
keyword: 'sc',
description: 'Sets the color for nonstroking operations',
numArgs: 4,
variableArgs: true
},
'55': {
name: 'setFillColorN',
keyword: 'scn',
description: 'Sets the color for nonstroking operations (ICCBased and special color spaces)',
numArgs: 33,
variableArgs: true
},
'56': {
name: 'setStrokeGray',
keyword: 'G',
description: 'Sets the gray level for stroking operations',
numArgs: 1,
variableArgs: false
},
'57': {
name: 'setFillGray',
keyword: 'g',
description: 'Sets the gray level for nonstroking operations',
numArgs: 1,
variableArgs: false
},
'58': {
name: 'setStrokeRGBColor',
keyword: 'RG',
description: 'Sets the RGB color for stroking operations',
numArgs: 3,
variableArgs: false
},
'59': {
name: 'setFillRGBColor',
keyword: 'rg',
description: 'Sets the RGB color for nonstroking operations',
numArgs: 3,
variableArgs: false
},
'60': {
name: 'setStrokeCMYKColor',
keyword: 'K',
description: 'Sets the CMYK color for stroking operations',
numArgs: 4,
variableArgs: false
},
'61': {
name: 'setFillCMYKColor',
keyword: 'k',
description: 'Sets the CMYK color for nonstroking operations',
numArgs: 4,
variableArgs: false
},
'62': {
name: 'shadingFill',
keyword: 'sh',
description: 'Fills the current path using a shading pattern',
numArgs: 1,
variableArgs: false
},
'63': {
name: 'beginInlineImage',
keyword: 'BI',
description: 'Begins an inline image object',
numArgs: 0,
variableArgs: false
},
'64': {
name: 'beginImageData',
keyword: 'ID',
description: 'Begins the inline image data',
numArgs: 0,
variableArgs: false
},
'65': {
name: 'endInlineImage',
keyword: 'EI',
description: 'Ends an inline image object',
numArgs: 1,
variableArgs: false
},
'66': {
name: 'paintXObject',
keyword: 'Do',
description: 'Paints the specified XObject',
numArgs: 1,
variableArgs: false
},
'67': {
name: 'markPoint',
keyword: 'MP',
description: 'Marks a point in the content stream',
numArgs: 1,
variableArgs: false
},
'68': {
name: 'markPointProps',
keyword: 'DP',
description: 'Marks a point in the content stream with properties',
numArgs: 2,
variableArgs: false
},
'69': {
name: 'beginMarkedContent',
keyword: 'BMC',
description: 'Begins a marked-content sequence',
numArgs: 1,
variableArgs: false
},
'70': {
name: 'beginMarkedContentProps',
keyword: 'BDC',
description: 'Begins a marked-content sequence with properties',
numArgs: 2,
variableArgs: false
},
'71': {
name: 'endMarkedContent',
keyword: 'EMC',
description: 'Ends a marked-content sequence',
numArgs: 0,
variableArgs: false
},
'72': {
name: 'beginCompat',
keyword: 'BX',
description: 'Begins a compatibility section',
numArgs: 0,
variableArgs: false
},
'73': {
name: 'endCompat',
keyword: 'EX',
description: 'Ends a compatibility section',
numArgs: 0,
variableArgs: false
},
'74': {
name: 'paintFormXObjectBegin',
keyword: null,
description: 'Begins painting a form XObject'
},
'75': {
name: 'paintFormXObjectEnd',
keyword: null,
description: 'Ends painting a form XObject'
},
'76': {
name: 'beginGroup',
keyword: null,
description: 'Begins a transparency group'
},
'77': {
name: 'endGroup',
keyword: null,
description: 'Ends a transparency group'
},
'80': {
name: 'beginAnnotation',
keyword: null,
description: 'Begins an annotation'
},
'81': {
name: 'endAnnotation',
keyword: null,
description: 'Ends an annotation'
},
'83': {
name: 'paintImageMaskXObject',
keyword: null,
description: 'Paints an image mask XObject'
},
'84': {
name: 'paintImageMaskXObjectGroup',
keyword: null,
description: 'Paints an image mask XObject group'
},
'85': {
name: 'paintImageXObject',
keyword: null,
description: 'Paints an image XObject'
},
'86': {
name: 'paintInlineImageXObject',
keyword: null,
description: 'Paints an inline image XObject'
},
'87': {
name: 'paintInlineImageXObjectGroup',
keyword: null,
description: 'Paints an inline image XObject group'
},
'88': {
name: 'paintImageXObjectRepeat',
keyword: null,
description: 'Paints a repeated image XObject'
},
'89': {
name: 'paintImageMaskXObjectRepeat',
keyword: null,
description: 'Paints a repeated image mask XObject'
},
'90': {
name: 'paintSolidColorImageMask',
keyword: null,
description: 'Paints a solid color image mask'
},
'91': {
name: 'constructPath',
keyword: null,
description: 'Constructs a path from the specified segments'
},
'92': {
name: 'setStrokeTransparent',
keyword: null,
description: 'Sets the stroke transparency'
},
'93': {
name: 'setFillTransparent',
keyword: null,
description: 'Sets the fill transparency'
}
};
console.log(opLookup[2]); // Example output for OpCode 2 (setLineWidth)

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "pdf-forge",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/postcss": "^4.0.9",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.9",
"autoprefixer": "^10.4.20",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-preprocess": "^6.0.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.2",
"typescript-eslint": "^8.20.0",
"vite": "^6.0.0"
},
"dependencies": {
"@monaco-editor/loader": "^1.4.0",
"async-mutex": "^0.5.0",
"flowbite-svelte": "^0.47.4",
"flowbite-svelte-icons": "^2.0.2",
"monaco-editor": "^0.52.2",
"paths": "^0.1.1",
"svelte-splitpanes": "^8.0.9",
"uuid": "^11.1.0",
"vite-plugin-monaco-editor": "^1.1.0"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

72
src/app.css Normal file
View File

@ -0,0 +1,72 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
:root {
/* Colors */
--background-color: rgb(43, 45, 48);
--boundary-color: rgba(0, 0, 0, 0.29);
--secondary-color: rgba(103, 101, 101, 0.6);
--accent-color: rgb(44, 97, 97);
--font-color: #c0cacd;
--secondary-font-color: #6c6c6c;
}
body {
margin: 0;
font-family: 'Arial', sans-serif;
background-color: var(--background-color);
color: var(--font-color);
border-color: var(--secondary-color)
}
::before,
::after {
border-color: var(--secondary-color);
}
.full-container {
position: relative;
height: 100%;
width: 100%;
}
/* Monaco-style dark theme scrollbars */
/* For Webkit browsers (like Chrome and Edge) */
::-webkit-scrollbar {
width: 12px;
height: 12px;
transition: opacity 0.5s ease-in-out;
opacity: 0;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--secondary-color);
border: 3px solid transparent;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-color);
}
/* Show scrollbar when scrolling */
:hover::-webkit-scrollbar {
opacity: 1;
}
/* For Firefox */
* {
scrollbar-color: var(--secondary-color) transparent;
scrollbar-width: thin;
}
/* Optional: Hide scrollbars but keep functionality */
/* ::-webkit-scrollbar { display: none; } */

13
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

13
src/app.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/pdf-forge-logo-bg.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PDF Forge</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

297
src/components/App.svelte Normal file
View File

@ -0,0 +1,297 @@
<script lang="ts">
import WelcomeScreen from "./WelcomeScreen.svelte";
import { Pane, Splitpanes } from "svelte-splitpanes";
import ToolbarLeft from "./ToolbarLeft.svelte";
import FileView from "./FileView.svelte";
import FileViewState from "../models/FileViewState.svelte";
import TitleBar from "./TitleBar.svelte";
import ToolbarRight from "./ToolbarRight.svelte";
import Footer from "./Footer.svelte";
const footerHeight: number = 30;
const titleBarHeight: number = 30;
let innerHeight: number = $state(1060);
let fileViewHeight: number = $derived(
Math.max(innerHeight - footerHeight - titleBarHeight, 0),
);
let fStates: FileViewState[] = $state([]);
let fState: FileViewState | undefined = $state();
function upload() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf';
input.onchange = handleUpload;
input.click();
}
async function handleUpload(event: Event) {
const target = event.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
const file = target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
const data = e.target?.result;
if (!data) return;
console.log(file, data);
createFileViewState(file.name, data)
};
reader.onerror = () => {
console.error('Error reading file');
};
reader.readAsArrayBuffer(file);
}
}
async function createFileViewState(path: string | undefined, data: string | ArrayBuffer) {
fState = await FileViewState.load(data, path);
fStates.push(fState);
}
function closeFile(stateToClose: FileViewState) {
stateToClose.dispose()
fStates.filter(state => state.fileId !== stateToClose.fileId);
}
</script>
<svelte:window bind:innerHeight />
<main style="height: {innerHeight}px; overflow: hidden;">
<div style="height: {titleBarHeight}px; z-index: 100; position: relative;">
<TitleBar
{fStates}
{fState}
closeTab={closeFile}
openTab={upload}
selectTab={(state) => (fState = state)}
></TitleBar>
</div>
<div class="fileview_container" style="height: {fileViewHeight}px">
<Splitpanes theme="forge-movable" dblClickSplitter={false}>
<Pane size={2.5} minSize={1.5} maxSize={4}>
<ToolbarLeft {fState}></ToolbarLeft>
</Pane>
<Pane>
{#if fState}
<FileView {fState} height={fileViewHeight}></FileView>
{:else}
<WelcomeScreen {upload}></WelcomeScreen>
{/if}
</Pane>
<Pane size={2.5} minSize={1.5} maxSize={4}>
<ToolbarRight {fState}></ToolbarRight>
</Pane>
</Splitpanes>
</div>
<Footer {fState} {footerHeight}></Footer>
</main>
<style global>
/* Resiable layout */
:global(.splitpanes.forge-movable) :global(.splitpanes__pane) {
background-color: var(--background-color);
}
:global(.splitpanes.forge-movable) :global(.splitpanes__splitter) {
background-color: var(--boundary-color);
box-sizing: border-box;
position: relative;
flex-shrink: 0;
}
:global(.splitpanes.forge-movable) :global(.splitpanes__splitter:before),
:global(.splitpanes.forge-movable) :global(.splitpanes__splitter:after) {
content: "";
position: absolute;
top: 50%;
left: 50%;
display: none;
background-color: var(--accent-color);
transition: background-color 0.3s;
}
:global(.splitpanes.forge-movable)
:global(.splitpanes__splitter:hover:before),
:global(.splitpanes.forge-movable)
:global(.splitpanes__splitter:hover:after) {
background-color: var(--boundary-color);
}
:global(.splitpanes.forge-movable)
:global(.splitpanes__splitter:first-child) {
cursor: auto;
}
:global(.forge-movable.splitpanes)
:global(.splitpanes)
:global(.splitpanes__splitter) {
z-index: 1;
}
:global(.forge-movable.splitpanes--vertical)
> :global(.splitpanes__splitter),
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter) {
width: 2px;
border-left: 1px solid var(--boundary-color);
cursor: col-resize;
}
:global(.forge-movable.splitpanes--vertical)
> :global(.splitpanes__splitter:before),
:global(.forge-movable.splitpanes--vertical)
> :global(.splitpanes__splitter:after),
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter:before),
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter:after) {
transform: translateY(-50%);
width: 1px;
height: 40px;
}
:global(.forge-movable.splitpanes--vertical)
> :global(.splitpanes__splitter:before),
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter:before) {
margin-left: -2px;
}
:global(.forge-movable.splitpanes--vertical)
> :global(.splitpanes__splitter:after),
:global(.forge-movable)
:global(.splitpanes--vertical)
> :global(.splitpanes__splitter:after) {
margin-left: 1px;
}
:global(.forge-movable.splitpanes--horizontal)
> :global(.splitpanes__splitter),
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter) {
height: 2px;
border-top: 1px solid var(--boundary-color);
cursor: row-resize;
}
:global(.forge-movable.splitpanes--horizontal)
> :global(.splitpanes__splitter:before),
:global(.forge-movable.splitpanes--horizontal)
> :global(.splitpanes__splitter:after),
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter:before),
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter:after) {
transform: translateX(-50%);
width: 40px;
height: 3px;
}
:global(.forge-movable.splitpanes--horizontal)
> :global(.splitpanes__splitter:before),
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter:before) {
margin-top: -2px;
}
:global(.forge-movable.splitpanes--horizontal)
> :global(.splitpanes__splitter:after),
:global(.forge-movable)
:global(.splitpanes--horizontal)
> :global(.splitpanes__splitter:after) {
margin-top: 1px;
}
/* Fixed layout */
:global(.splitpanes.forge-fixed) :global(.splitpanes__pane) {
background-color: var(--background-color);
}
:global(.splitpanes.forge-fixed) :global(.splitpanes__splitter) {
background-color: var(--boundary-color);
position: relative;
}
:global(div.splitpanes--horizontal.splitpanes--dragging) {
cursor: row-resize;
}
:global(div.splitpanes--vertical.splitpanes--dragging) {
cursor: col-resize;
}
:global(.splitpanes) {
display: flex;
width: 100%;
height: 100%;
}
:global(.splitpanes--vertical) {
flex-direction: row;
}
:global(.splitpanes--horizontal) {
flex-direction: column;
}
:global(.splitpanes--dragging) :global(*) {
user-select: none;
}
:global(.splitpanes__pane) {
width: 100%;
height: 100%;
overflow: hidden;
/** Add also a direct child selector, for dealing with specifity of nested splitpanes transition.
This issue was happening in the examples on nested splitpanes, vertical inside horizontal.
I think it's better to keep also the previous CSS selector for (potential) old browser compatibility.
*/
}
:global(.splitpanes--vertical) :global(.splitpanes__pane) {
transition: width 0.2s ease-out;
}
:global(.splitpanes--horizontal) :global(.splitpanes__pane) {
transition: height 0.2s ease-out;
}
:global(.splitpanes--vertical) > :global(.splitpanes__pane) {
transition: width 0.2s ease-out;
}
:global(.splitpanes--horizontal) > :global(.splitpanes__pane) {
transition: height 0.2s ease-out;
}
:global(.splitpanes--dragging) :global(.splitpanes__pane) {
transition: none;
pointer-events: none;
}
:global(.splitpanes--freeze) :global(.splitpanes__pane) {
transition: none;
}
:global(.splitpanes__splitter) {
touch-action: none;
}
:global(.splitpanes--vertical) > :global(.splitpanes__splitter) {
min-width: 1px;
}
:global(.splitpanes--horizontal) > :global(.splitpanes__splitter) {
min-height: 1px;
}
</style>

View File

@ -0,0 +1,49 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import {
TrashBinSolid
} from "flowbite-svelte-icons";
let {fState}: { fState: FileViewState } = $props();
let changes = $derived(fState.changes);
</script>
<div class="border-l border-r border-forge-sec bg-forge-sec flex flex-row" style="width: 100%; height: 24px">
<div class="text-sm p-1">
Changes:
</div>
<button class="clear" onclick={ () => fState.clearChanges()}>
<div class="img">
<TrashBinSolid size="sm"/>
</div>
</button>
</div>
<div class="flex flex-col">
{#each changes as change, index (index)}
<div class="text-xs flex flex-row">
<p class="whitespace-nowrap">{change.key + ":"}</p>
&nbsp
<p>{change.ptype}</p>&nbsp|&nbsp
<p>{change.sub_type}</p>&nbsp|&nbsp
<p>{change.value}</p>
</div>
<div class="bg-forge-bound mt-1 mb-1" style="height: 1px; width: 100%"></div>
{/each}
</div>
<style lang="postcss">
.clear {
@apply hover:bg-forge-acc h-[24px] w-[24px] rounded-2xl;
position: fixed;
right: 0;
top: 0;
}
.img {
position: fixed;
right: 4px;
top: 4px;
}
</style>

View File

@ -0,0 +1,173 @@
<script lang="ts">
import { onMount } from 'svelte';
import { UploadOutline } from 'flowbite-svelte-icons';
import * as monaco from 'monaco-editor';
import type ContentModel from '../models/ContentModel.svelte';
import { opLookup } from '../models/OperatorList';
import { OperatorList } from '../models/OperatorList.js';
import type { PDFOperator } from '../models/OperatorList';
monaco.languages.register({ id: 'pdf-content-stream' });
// Define a custom theme for PDF content stream highlighting
monaco.editor.defineTheme('forge-dark', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'comment', foreground: '#6A9955' }, // Green for comments
{ token: 'operator', foreground: '#bd4fdb' }, // Purple for graphics state operators
{ token: 'color-operator', foreground: '#61AFEF' }, // Blue for color operators
{ token: 'text-positioning', foreground: '#2c9f9f' }, // Dark cyan for text positioning
{ token: 'text-render', foreground: '#4cdcdc' }, // Light cyan for text rendering
{ token: 'text-state', foreground: '#2c9f9f' }, // Dark cyan for text state
{ token: 'path-construct', foreground: '#ffa500' }, // Orange for path construction
{ token: 'path-paint', foreground: '#ff6b6b' }, // Reddish for path painting
{ token: 'named-element', foreground: '#e1e1e1' }, // Light gray for named elements
{ token: 'hex-string', foreground: '#E06C75' }, // Red for hex strings
{ token: 'number', foreground: '#e1e1e1' }, // Light gray for numbers
{ token: 'matrix', foreground: '#ffd700' }, // Gold for matrices
{ token: 'marked-content', foreground: '#98c379' } // Green for marked content
],
colors: {
'editor.background': '#1E1F22FF'
}
});
monaco.languages.setMonarchTokensProvider('pdf-content-stream', {
tokenizer: {
root: [
// Comments
[/%.*$/, 'comment'],
// Numbers
[/[+-]?\d*\.?\d+([eE][-+]?\d+)?/, 'number'],
// Hex strings
[/<[0-9A-Fa-f]+>/, 'hex-string'],
// Operators
[/[A-Za-z'"][A-Za-z'*]*/, {
cases: Object.fromEntries(
Object.values(opLookup)
.filter(op => op.keyword !== null)
.map(op => [
// Escape special regex characters in the keyword
op.keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
op.class
])
)
}]
]
}
});
let {
height,
save,
contents
}: {
height: number;
save: (updated_data: string) => void;
contents: ContentModel;
} = $props();
let editorContainer: HTMLElement;
let editor: monaco.editor.IStandaloneCodeEditor;
let isEdited = $state(false);
let lastContents: ContentModel | undefined = $state(undefined);
onMount(() => {
editor = monaco.editor.create(editorContainer, {
value: '',
language: 'pdf-content-stream',
theme: 'forge-dark',
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
automaticLayout: true
});
// Add change listener to detect edits
editor.onDidChangeModelContent(() => {
isEdited = true;
});
loadContents(contents);
return () => {
editor.dispose();
};
});
$effect(() => {
loadContents(contents);
});
function loadContents(contents: ContentModel | undefined) {
if (!contents || !editor) return;
if (lastContents === contents) {
return;
}
lastContents = contents;
editor.setValue(contents.toDisplay());
isEdited = false;
// Add decorations for operator ranges if available
if (contents.opList && contents.opList.fnArray.length > 0) {
const decorations = [];
for (let i = 0; i < contents.opList.fnArray.length; i++) {
const fnId = contents.opList.fnArray[i];
const range = contents.opList.rangeArray[i];
const operator = opLookup[fnId];
if (operator && range) {
const startPos = editor.getModel()!.getPositionAt(range[0]);
const endPos = editor.getModel()!.getPositionAt(range[1]);
const monacoRange = new monaco.Range(
startPos.lineNumber,
startPos.column,
endPos.lineNumber,
endPos.column);
if (fnId == 91) {
console.log(range, monacoRange)
}
decorations.push({
range: monacoRange,
options: {
className: operator.class,
hoverMessage: {value: OperatorList.formatOperatorToMarkdown(fnId)},
}
});
}
}
editor.createDecorationsCollection(decorations);
}
}
</script>
<div class="relative">
<div bind:this={editorContainer} style:height={height + "px"}></div>
{#if isEdited}
<button onclick={() => save(editor.getValue())} class="save-button">
<UploadOutline size="xl" />
</button>
{/if}
</div>
<style lang="postcss">
.save-button {
@apply p-2 bg-forge-sec text-forge-text border-t border-forge-bound cursor-pointer;
position: absolute;
right: 2em;
bottom: 0px;
width: 48px;
height: 48px;
}
.save-button:hover {
background-color: #3c3d41;
}
</style>

View File

@ -0,0 +1,128 @@
<script lang="ts">
import {Pane, Splitpanes} from "svelte-splitpanes";
import XRefTable from "./XRefTable.svelte";
import PrimitiveView from "./PrimitiveView.svelte";
import TreeView from "./TreeView.svelte";
import type FileViewState from "../models/FileViewState.svelte";
import PageView from "./PageView.svelte";
import {onMount} from "svelte";
import NotificationModal from "./NotificationModal.svelte";
import ChangesModal from "./ChangesModal.svelte";
let {
fState,
height,
}: {
fState: FileViewState;
height: number;
} = $props();
let width: number = $state(0);
let modalWidth = $derived(fState.xRefShowing || fState.notificationsShowing || fState.changesShowing ? 281 : 0);
let splitPanesWidth: number = $derived(width - modalWidth);
function handleKeydown(event: KeyboardEvent) {
// Check for "Alt + Left Arrow"
if (event.altKey && event.key === "ArrowLeft") {
event.preventDefault();
fState.back();
}
if (event.altKey && event.key === "ArrowRight") {
event.preventDefault();
fState.forward();
}
}
function handleMouseButton(event: MouseEvent) {
// Check for mouse back button (button 4)
if (event.button === 3) {
event.preventDefault();
fState.back();
}
if (event.button === 4) {
event.preventDefault();
fState.forward();
}
}
onMount(() => {
window.addEventListener("keydown", handleKeydown);
window.addEventListener("mousedown", handleMouseButton);
return () => {
window.removeEventListener("keydown", handleKeydown);
window.removeEventListener("mousedown", handleMouseButton);
};
});
</script>
<div bind:clientWidth={width} class="file-view-container">
<div
class="flex flex-row"
style="width: {splitPanesWidth}px; height: {height}px"
>
<Splitpanes theme="forge-movable" pushOtherPanes={false}>
<Pane
size={fState.pageMode || fState.treeMode ? 15 : 0}
minSize={fState.pageMode || fState.treeMode ? 2 : 0}
maxSize={fState.pageMode || fState.treeMode ? 100 : 0}
>
<TreeView {fState} {height}></TreeView>
</Pane>
<Pane minSize={1}>
<div class="bg-forge-dark w-full" style="height: {height}px">
{#if fState.treeMode}
<PrimitiveView {fState} {height}></PrimitiveView>
{/if}
{#if fState.pageMode}
<PageView {fState} {height}></PageView>
{/if}
</div>
</Pane>
</Splitpanes>
</div>
{#if fState.xRefShowing}
<div class="xref-modal" class:visible={fState.xRefShowing}>
<XRefTable {fState} {height}></XRefTable>
</div>
{/if}
{#if fState.notificationsShowing}
<div class="xref-modal" class:visible={fState.notificationsShowing}>
<NotificationModal {fState}></NotificationModal>
</div>
{/if}
{#if fState.changesShowing}
<div class="xref-modal" class:visible={fState.changesShowing}>
<ChangesModal {fState}></ChangesModal>
</div>
{/if}
</div>
<style lang="postcss">
.file-view-container {
position: relative;
height: 100%;
width: 100%;
}
.xref-modal {
position: absolute;
border-left: 1px solid var(--border-color);
top: 0;
right: 0;
bottom: 0;
width: 280px;
background: var(--background-color);
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
z-index: 1000;
overflow: hidden;
transform: translateX(100%);
transition: transform 0.2s ease-in-out;
}
.xref-modal.visible {
transform: translateX(0);
}
</style>

View File

@ -0,0 +1,79 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import type { Trace } from "../models/Primitive.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import { CaretRightOutline } from "flowbite-svelte-icons";
class Path {
public value: string;
public jump?: string;
public index: number;
constructor(value: string, index: number, jump?: string) {
this.value = value;
this.jump = jump;
this.index = index;
}
}
let {
fState,
footerHeight,
}: { fState: FileViewState | undefined; footerHeight: number } = $props();
let elements: Path[] | undefined = $derived(
fState && fState.container_prim ? toElements(fState.container_prim.trace) : undefined,
);
function toElements(path: Trace[]): Path[] {
if (path.length == 0) {
return [new Path("Trailer", 0)];
}
let display: Path[] = [];
let lastJump: string | undefined;
for (let i = 0; i < path.length; i++) {
const pathElement = path[i];
if (pathElement.last_jump !== lastJump) {
lastJump = pathElement.last_jump;
display.push(new Path(pathElement.key, i, lastJump));
} else {
display.push(new Path(pathElement.key, i, undefined));
}
}
return display;
}
function selectPath(path: Path) {
if (fState) {
let newPath = fState.copyPath().slice(0, path.index + 1);
fState.selectPath(newPath);
}
}
</script>
<div class="footer" style="height: {footerHeight}px">
{#if elements}
{#each elements as path, index (index)}
<button
class="flex flex-row items-center ml-2"
onclick={() => selectPath(path)}
>
{#if path.jump}
<PrimitiveIcon ptype="Reference"></PrimitiveIcon>
{:else}
<CaretRightOutline class="text-forge-sec" />
{/if}
<p class="text-xs ml-1">{path.value}</p>
</button>
{/each}
{/if}
</div>
<style lang="postcss">
.footer {
@apply bg-forge-prim border border-forge-bound flex flex-row items-center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
</style>

View File

@ -0,0 +1,26 @@
<script lang="ts">
import { RefreshOutline } from "flowbite-svelte-icons";
import ZoomableContainer from './ZoomableContainer.svelte';
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} />
{/if}
</div>
<style lang="postcss">
.page-container {
@apply w-full bg-forge-dark relative m-auto;
}
.loading-container {
@apply flex items-center justify-center h-full;
}
</style>

View File

@ -0,0 +1,49 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import {
TrashBinSolid
} from "flowbite-svelte-icons";
let {fState}: { fState: FileViewState } = $props();
let notifications = $derived(fState.notifications);
</script>
<div class="border-l border-r border-forge-sec bg-forge-sec flex flex-row" style="width: 100%; height: 24px">
<div class="text-sm p-1">
Notifications:
</div>
<button class="clear" onclick={ () => fState.clearNotifications()}>
<div class="img">
<TrashBinSolid size="sm"/>
</div>
</button>
</div>
<div class="flex flex-col">
{#each notifications as notification, index (index)}
<div class="text-xs flex flex-row">
<small class="whitespace-nowrap">{notification.getDate() + ":"}</small>&nbsp <p class:error={notification.isError()} class:debug={notification.isDebug()}> {notification.level} </p> &nbsp
<p> {" - " + notification.message}</p>
</div>
<div class="bg-forge-bound mt-1 mb-1" style="height: 1px; width: 100%"></div>
{/each}
</div>
<style lang="postcss">
.error {
@apply text-red-600;
}
.clear {
@apply hover:bg-forge-acc h-[24px] w-[24px] rounded-2xl;
position: fixed;
right: 0;
top: 0;
}
.img {
position: fixed;
right: 4px;
top: 4px;
}
</style>

View File

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

View File

@ -0,0 +1,35 @@
<script lang="ts">
import {
FileOutline,
FolderArrowRightOutline,
FolderOutline,
CodeOutline,
} from "flowbite-svelte-icons";
let { ptype }: { ptype: string } = $props();
</script>
{#if ptype === "Dictionary"}
<FolderOutline class="stroke-blue-300 text-blue-300 primitive-icon" />
{:else if ptype === "Array"}
<FolderOutline class=" text-orange-300 primitive-icon " />
{:else if ptype === "Reference"}
<FolderArrowRightOutline class=" text-purple-300 primitive-icon" />
{:else if ptype === "Integer"}
<FileOutline class="text-pink-300 primitive-icon" />
{:else if ptype === "Number"}
<FileOutline class="text-lime-300 primitive-icon" />
{:else if ptype === "Boolean"}
<FileOutline class="text-fuchsia-300 primitive-icon" />
{:else if ptype === "String"}
<FileOutline class="text-green-300 primitive-icon" />
{:else if ptype === "Name"}
<FileOutline class="text-red-400 primitive-icon" />
{:else if ptype === "Stream Data"}
<CodeOutline class="text-purple-400 primitive_icon" />
{:else}
<FileOutline />
{/if}
<style lang="postcss">
</style>

View File

@ -0,0 +1,173 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import type Primitive from "../models/Primitive.svelte";
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import { Pane, Splitpanes } from "svelte-splitpanes";
import StreamDataView from "./StreamDataView.svelte";
const cellH = 29;
const headerOffset = 24;
let { fState, height }: { fState: FileViewState; height: number } =
$props();
let fillerHeight: number = $state(0);
let firstEntry = $state(0);
let lastEntry = $state(100);
let scrollY = $state(0);
let prim = $derived(fState.container_prim);
let entriesToDisplay: Primitive[] = $derived(
prim ? prim.children.slice(firstEntry, lastEntry) : [],
);
let tableHeight = $derived(prim ? prim.children.length * cellH : 0);
let bodyHeight = $derived(height);
let locallySelected: Primitive | undefined = $state(undefined);
$effect(() => {
locallySelected = fState.selected_leaf_prim;
});
function handlePrimClick(prim: Primitive) {
locallySelected = prim;
if (!prim.isContainer()) {
handlePrimDbLClick(prim);
}
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
scrollY = event.currentTarget.scrollTop;
firstEntry = Math.floor(scrollY / cellH);
lastEntry = Math.ceil((scrollY + bodyHeight) / cellH);
fillerHeight = firstEntry * cellH;
}
function handlePrimDbLClick(prim: Primitive) {
const _path: string[] = fState.copyPath();
_path.push(prim.key);
fState.selectPath(_path);
}
</script>
<Splitpanes theme="forge-movable">
<Pane minSize={1}>
{#if prim && prim.children && prim.children.length > 0}
<div
class="overflow-auto"
onscroll={handleScroll}
style="height: {bodyHeight}px"
>
<div class="w-[851px]">
<table style="position: relative; top: {scrollY}px">
<thead>
<tr>
<td class="page-cell t-header border-forge-prim"
>Key
</td>
<td class="ref-cell t-header border-forge-prim"
>Type
</td>
<td class="cell t-header border-forge-sec"
>Value</td
>
</tr>
</thead>
</table>
<div class="container" style="height: {tableHeight}px">
<table>
<tbody>
<tr
class="filler"
style="height: {fillerHeight}px"
></tr>
{#each entriesToDisplay as entry, index (index)}
<tr
class:selected={entry.key ===
locallySelected?.key}
class="row"
onclick={() => handlePrimClick(entry)}
ondblclick={() =>
handlePrimDbLClick(entry)}
>
<td class="page-cell t-data">
<div class="key-field">
<PrimitiveIcon
ptype={entry.ptype}
/>
<p class="text-left">
{entry.key}
</p>
</div>
</td>
<td class="ref-cell t-data"
>{entry.ptype}</td
>
<td class="cell t-data"
>{entry.value}</td
>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
{/if}
</Pane>
<Pane
size={fState.container_prim?.stream_data ? 50 : 0}
minSize={fState.container_prim?.stream_data ? 1 : 0}
maxSize={fState.container_prim?.stream_data ? 100 : 0}
>
<div style:height class="bg-forge-dark">
{#if fState.container_prim?.stream_data}
<StreamDataView {fState} {height}></StreamDataView>
{/if}
</div>
</Pane>
</Splitpanes>
<style lang="postcss">
.key-field {
display: flex;
flex-direction: row;
align-items: center;
gap: 3px;
}
.row {
@apply hover:bg-forge-sec hover:border-forge-bound;
height: 29px;
min-height: 29px;
max-height: 29px;
}
.selected {
@apply bg-forge-acc;
}
.t-header {
@apply uppercase text-xs text-forge-text p-5 bg-forge-sec border-r;
cursor: default;
}
.t-data {
@apply border border-forge-sec text-sm text-forge-text text-left;
text-align: left;
cursor: default;
}
.cell {
@apply min-w-[600px] w-[600px] p-1 m-0;
user-select: none;
}
.ref-cell {
@apply min-w-[100px] w-[100px] p-1 m-0;
user-select: none;
}
.page-cell {
@apply min-w-[150px] w-[150px] p-1 m-0;
user-select: none;
}
</style>

View File

@ -0,0 +1,33 @@
<script lang="ts">
import type FileViewState from "../models/FileViewState.svelte";
import StreamEditor from "./StreamEditor.svelte";
import type { StreamData } from "../models/StreamData.svelte";
import ImageViewer from "./ImageViewer.svelte";
let {
fState,
height,
}: {
fState: FileViewState;
height: number;
} = $props();
let streamData: StreamData | undefined = $derived(
fState.container_prim?.stream_data,
);
</script>
{#if streamData}
{#if streamData.type === "Image"}
<ImageViewer img={streamData.imageData} {height}
></ImageViewer>
{:else if streamData.textData}
<StreamEditor
stream_data={streamData.textData}
{height}
save={(updated_data) => fState.saveStreamData(updated_data)}
></StreamEditor>
{/if}
{/if}
<style lang="postcss">
</style>

View File

@ -0,0 +1,122 @@
<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
monaco.editor.defineTheme("forge-dark", {
base: "vs-dark",
inherit: true,
rules: [
{ token: 'comment', foreground: '#6A9955' }, // Green for comments
{ token: 'operator', foreground: '#bd4fdb' }, // Purple for graphics state operators
{ token: 'color-operator', foreground: '#61AFEF' }, // Blue for color operators
{ token: 'text-positioning', foreground: '#2c9f9f' }, // Dark cyan for text positioning
{ token: 'text-render', foreground: '#4cdcdc' }, // Light cyan for text rendering
{ token: 'text-state', foreground: '#2c9f9f' }, // Dark cyan for text state
{ token: 'path-construct', foreground: '#ffa500' }, // Orange for path construction
{ token: 'path-paint', foreground: '#ff6b6b' }, // Reddish for path painting
{ token: 'named-element', foreground: '#e1e1e1' }, // Light gray for named elements
{ token: 'hex-string', foreground: '#E06C75' }, // Red for hex strings
{ token: 'number', foreground: '#e1e1e1' }, // Light gray for numbers
{ token: 'matrix', foreground: '#ffd700' }, // Gold for matrices
{ token: 'marked-content', foreground: '#98c379' }, // Green for marked content
],
colors: {
"editor.background": "#1E1F22FF",
},
});
let {
stream_data,
height,
save,
}: {
height: number;
save: (updated_data: string) => void;
stream_data: string;
} = $props();
let editorContainer: HTMLElement;
let editor: monaco.editor.IStandaloneCodeEditor;
let isEdited = $state(false);
let last_data: string | undefined = $state(undefined);
onMount(() => {
editor = monaco.editor.create(editorContainer, {
value: "",
language: 'pdf-content-stream',
theme: "forge-dark",
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
automaticLayout: true,
});
loadContents(stream_data);
editor.onDidChangeModelContent(() => {
isEdited = editor.getValue() !== stream_data;
});
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
(context, args) => {
if (isEdited) {
save(editor.getValue());
}
},
);
window.addEventListener("keydown", (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
event.preventDefault();
}
});
return () => {
editor.dispose();
};
});
$effect(() => {
loadContents(stream_data);
});
function loadContents(stream_data: string | undefined) {
if (!stream_data || !editor) return;
if (last_data === stream_data) {
return;
}
isEdited = false;
last_data = stream_data;
editor.setValue(stream_data);
}
</script>
<div class="relative">
<div bind:this={editorContainer} style:height={height + "px"}></div>
{#if isEdited}
<button onclick={() => save(editor.getValue())} class="save-button">
<UploadOutline size="xl" />
</button>
{/if}
</div>
<style lang="postcss">
.save-button {
@apply p-2 bg-forge-sec text-forge-text border-t border-forge-bound cursor-pointer;
position: absolute;
right: 2em;
bottom: 0px;
width: 48px;
height: 48px;
}
.save-button:hover {
background-color: #3c3d41;
}
</style>

View File

@ -0,0 +1,77 @@
<script lang="ts">
import {PlusOutline, CloseOutline} from "flowbite-svelte-icons";
import type FileViewState from "../models/FileViewState.svelte";
let {fStates, fState, selectTab, closeTab, openTab}:
{ fStates: FileViewState[],
fState: FileViewState | undefined,
selectTab: (arg0: FileViewState) => void,
closeTab: (arg0: FileViewState) => void,
openTab: () => void } = $props();
function formatFileName(name: string) {
if (name.length < 20) {
return name;
}
return name.substring(0, 17) + "..."
}
</script>
<div class="tab-bar">
{#each fStates as state, index (index)}
<div class={state.file.id === fState?.file.id ? "tab-outer bg-forge-prim border-b-2 border-forge-acc" : "tab-outer bg-forge-dark"}>
<div class="tab-text">
<button onclick={() => selectTab(state)} class="tab-click">
{formatFileName(state.file.name)}
</button>
</div>
<div class="justify-center flex">
<button onclick={() => closeTab(state)} class="rounded-md hover:bg-forge-active">
<CloseOutline size="xs" color="custom"/>
</button>
</div>
</div>
{/each}
<div class="justify-center flex">
<button onclick={openTab} class="rounded hover:bg-forge-active">
<PlusOutline size="sm"/>
</button>
</div>
</div>
<style lang="postcss">
.tab-bar {
@apply border-b border-forge-bound bg-forge-dark;
height: 30px;
width: 100%;
display: flex;
justify-content: flex-start;
}
.tab-text {
@apply text-forge-text text-xs whitespace-nowrap col-span-5 justify-center flex pr-1;
}
.tab-outer {
@apply grid grid-rows-1 grid-cols-6 hover:bg-forge-prim pl-2 pr-1;
text-align: center;
height: 30px;
max-width: 150px;
transition: background-color;
}
.tab-click {
@apply m-0 p-0;
height: 100%;
width: 100%;
}
.file-tab {
@apply bg-forge-dark
rounded-none
text-xs
}
.file-tab.active {
@apply bg-forge-active
}
</style>

View File

@ -0,0 +1,326 @@
<script lang="ts">
import {
CloseOutline,
CaretDownOutline,
PlusOutline,
FilePdfSolid,
ArrowsRepeatOutline,
DownloadOutline,
ArrowLeftOutline,
ArrowRightOutline,
AngleDownOutline
} from "flowbite-svelte-icons";
import type FileViewState from "../models/FileViewState.svelte";
let home = "";
getHome();
let {
fStates,
fState,
selectTab,
closeTab,
openTab: newFile,
}: {
fStates: FileViewState[];
fState: FileViewState | undefined;
selectTab: (arg0: FileViewState) => void;
closeTab: (arg0: FileViewState) => void;
openTab: () => void;
} = $props();
let dropdownVisible = $state(false);
let dropdownEl: HTMLElement;
let historyVisible = $state(false);
let historyEl: HTMLElement;
async function getHome() {
home = "/home";
}
function formatFileName(name: string) {
if (name.length < 100) {
return name;
}
return name.substring(0, 17) + "...";
}
function formatFilePath(path: string) {
// Replace home directory with ~
if (path.startsWith(home)) {
path = path.replace(home, "~");
}
if (path.length < 40) {
return path;
}
const parts = path.split(/[/\\]/);
const first = parts[0];
const last = parts[parts.length - 1];
const middle = parts
.slice(1, -1)
.map((part) => part.charAt(0) + "...")
.join("/");
return `${first}/${middle}/${last}`;
}
function toggleDropdown() {
dropdownVisible = !dropdownVisible;
}
function toggleHistoryDropdown() {
historyVisible = !historyVisible;
}
function handleFocusOutHistory(e: FocusEvent) {
const dropdown = historyEl;
if (!dropdown) return;
const relatedTarget = e.relatedTarget as HTMLElement;
if (!dropdown.contains(relatedTarget)) {
historyVisible = false;
}
}
function handleFocusOut(e: FocusEvent) {
const dropdown = dropdownEl;
if (!dropdown) return;
const relatedTarget = e.relatedTarget as HTMLElement;
if (!dropdown.contains(relatedTarget)) {
dropdownVisible = false;
}
}
function handleSelectTab(state: FileViewState) {
selectTab(state);
dropdownVisible = false;
}
function handleNewFile() {
newFile();
dropdownVisible = false;
}
function handleCloseTab(state: FileViewState) {
closeTab(state);
}
</script>
<div data-tauri-drag-region class="titlebar">
<div class="m-[2px]">
<img
src="/pdf-forge-logo-30x30.png"
class="logo forge"
alt="PDF Forge Logo"
/>
</div>
<div class="mx-4"></div>
<div class="flex flex-row">
<button
onclick={() => fState?.back()}
class="nav-button"
id="titlebar-back"
class:possible={fState?.canBack}
>
<ArrowLeftOutline/>
</button>
<button
onfocusout={handleFocusOutHistory}
onclick={() => toggleHistoryDropdown()}
class="nav-button possible"
id="titlebar-forward"
>
<AngleDownOutline/>
</button>
{#if historyVisible && fState}
<div class="dropdown-menu" bind:this={historyEl}>
{#each fState.pathHistory.history as path, index (index)}
<button class="dropdown-item new-tab active:bg-forge-sec" class:active={fState.pathHistory.currentIndex === index} onclick={() => fState.jump(index)}>
{path.join("/")}
</button>
{/each}
</div>
{/if}
<button
onclick={() => fState?.forward()}
class="nav-button"
id="titlebar-forward"
class:possible={fState?.canForward}
>
<ArrowRightOutline/>
</button>
</div>
<div class="file-selector" onfocusout={handleFocusOut}>
<button class="file-dropdown-button" onclick={toggleDropdown}>
<FilePdfSolid size="sm" />
<span class="px-2"
>{fState
? formatFileName(fState.getFileName())
: "Select File"}</span
>
<CaretDownOutline class="ml-1 text-forge-text_sec" size="sm" />
</button>
{#if dropdownVisible}
<div class="dropdown-menu" bind:this={dropdownEl}>
<button class="dropdown-item new-tab" onclick={handleNewFile}>
<PlusOutline size="sm" />
<p class="ml-1">New File...</p>
</button>
<div class="divider"></div>
<div class="w-full text-forge-text_sec text-xs ml-3 mt-1 mb-1">
<p>Open Files</p>
</div>
<div class="divider"></div>
{#each fStates as state, index (index)}
<div
class="dropdown-item justify-between"
class:active={state.fileId === fState?.fileId}
>
<button
class="select-button text-left"
onclick={() => handleSelectTab(state)}
>
<FilePdfSolid size="md" />
<div class="flex flex-col">
<p class="ml-1">
{formatFileName(state.getFileName())}
</p>
<p class="path-text">
{formatFilePath(state.getFilePath())}
</p>
</div>
</button>
<button
class="close-button"
onclick={() => handleCloseTab(state)}
>
<CloseOutline size="xs" />
</button>
</div>
{/each}
</div>
{/if}
</div>
<div class="flex flex-row">
<button
onclick={() => fState?.reload()}
class="titlebar-button"
id="titlebar-reload"
>
<ArrowsRepeatOutline/>
</button>
<button
onclick={() => fState?.save()}
class="titlebar-button"
id="titlebar-save"
>
<DownloadOutline/>
</button>
</div>
</div>
<style lang="postcss">
.nav-button.possible {
@apply hover:bg-forge-acc text-forge-text;
}
.nav-button {
@apply text-forge-text_sec rounded ;
display: inline-flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
user-select: none;
-webkit-user-select: none;
}
.titlebar {
@apply border border-forge-bound;
height: 30px;
user-select: none;
display: flex;
flex-direction: row;
position: fixed;
top: 0;
left: 0;
right: 0;
}
.titlebar-button-group {
position: fixed;
right: 0;
display: flex;
justify-content: flex-end;
}
.titlebar-button {
@apply text-forge-text hover:bg-forge-acc rounded;
display: inline-flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
user-select: none;
-webkit-user-select: none;
}
.file-selector {
@apply relative ml-4;
}
.file-dropdown-button {
@apply flex flex-row items-center px-2 py-1 text-xs text-forge-text rounded hover:bg-forge-acc;
height: 26px;
margin-top: 2px;
}
.dropdown-menu {
@apply absolute top-full left-0 mt-1 bg-forge-prim border border-forge-sec rounded-md border;
min-width: 200px;
}
.dropdown-item {
@apply flex flex-row items-center w-full text-xs text-forge-text hover:bg-forge-acc;
}
.dropdown-item.active {
@apply bg-forge-active hover:bg-forge-acc;
}
.close-button {
@apply ml-2 p-1 rounded hover:bg-forge-active;
opacity: 0;
}
.dropdown-item:hover .close-button {
opacity: 1;
}
.new-tab {
@apply border-t border-forge-bound px-3 py-3;
}
.select-button {
@apply m-0 px-2 py-1 flex flex-row;
}
.path-text {
@apply text-forge-text_sec ml-2 mt-1;
max-width: 100%;
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.divider {
@apply h-[1px] bg-forge-sec w-full;
}
</style>

View File

@ -0,0 +1,61 @@
<script lang="ts">
import { BookOpenSolid, CodeBranchSolid } from "flowbite-svelte-icons";
import type FileViewState from "../models/FileViewState.svelte";
let {
fState
}: { fState: FileViewState | undefined } = $props();
function toggleTree() {
if (!fState) return;
fState.treeMode = !fState.treeMode;
if (fState.treeMode) {
fState.pageMode = false;
}
}
function togglePages() {
if (!fState) return;
fState.pageMode = !fState.pageMode;
if (fState.pageMode) {
fState.treeMode = false;
}
}
</script>
<div class="grid grid-cols-1">
<button
class={fState?.treeMode ? "tool-button active" : "tool-button"}
onclick={toggleTree}
>
<div class="justify-center flex m-0">
<CodeBranchSolid class="rotate-180 scale-x-[-1]" />
</div>
<b class="button-title">Tree</b>
</button>
<button
id="#page"
class={fState?.pageMode ? "tool-button active" : "tool-button"}
onclick={togglePages}
>
<div class="justify-center flex">
<BookOpenSolid />
</div>
<b class="button-title">Page</b>
</button>
</div>
<style lang="postcss">
.button-title {
@apply text-forge-text_sec text-xs;
}
.tool-button {
@apply rounded mt-2 mb-2 bg-forge-prim hover:bg-forge-sec;
height: 60px;
}
.tool-button.active {
@apply bg-forge-active focus:bg-forge-acc;
}
</style>

View File

@ -0,0 +1,79 @@
<script lang="ts">
import {ListOutline, InfoCircleOutline, UndoOutline} from "flowbite-svelte-icons";
import type FileViewState from "../models/FileViewState.svelte";
let {fState}: { fState: FileViewState | undefined } = $props()
function toggleXref() {
if (!fState) return;
if (fState.notificationsShowing) {
fState.notificationsShowing = false;
}
if (fState.changesShowing) {
fState.changesShowing = false
}
fState.xRefShowing = !fState.xRefShowing;
}
function toggleNots() {
if (!fState) return;
if (fState.xRefShowing) {
fState.xRefShowing = false;
}
if (fState.changesShowing) {
fState.changesShowing = false
}
fState.notificationsShowing = !fState.notificationsShowing;
}
function toggleChanges() {
if (!fState) return;
if (fState.xRefShowing) {
fState.xRefShowing = false;
}
if (fState.notificationsShowing) {
fState.notificationsShowing = false;
}
fState.changesShowing = !fState.changesShowing;
}
</script>
<div class="grid grid-cols-1">
<button class={ fState?.xRefShowing ? "tool-button active" : "tool-button" } onclick={toggleXref}>
<div class="justify-center flex text-forge-text">
<ListOutline/>
</div>
<b class="button-title">XRef</b>
</button>
<button class={ fState?.notificationsShowing ? "tool-button active" : "tool-button" } onclick={toggleNots}>
<div class="justify-center flex text-forge-text">
<InfoCircleOutline/>
</div>
<b class="button-title">Notifications</b>
</button>
<button class={ fState?.changesShowing ? "tool-button active" : "tool-button" } onclick={toggleChanges}>
<div class="justify-center flex text-forge-text">
<UndoOutline/>
</div>
<b class="button-title">Changes</b>
</button>
</div>
<style lang="postcss">
.button-title {
@apply text-forge-text_sec text-xs;
}
.tool-button {
@apply rounded mt-2 mb-2 bg-forge-prim hover:bg-forge-sec;
height: 60px;
}
.tool-button.active {
@apply bg-forge-active focus:bg-forge-acc;
}
</style>

View File

@ -0,0 +1,112 @@
<script lang="ts">
import TreeViewState from '../models/TreeViewState.svelte';
import { PathSelectedEvent } from '../events/PathSelectedEvent';
import type FileViewState from '../models/FileViewState.svelte';
import type { TreeViewModel } from '../models/TreeViewModel';
import TreeViewEntry from './TreeViewEntry.svelte';
import { onMount } from 'svelte';
let {
fState,
height
}: {
fState: FileViewState;
height: number;
} = $props();
const file_id = $derived(fState.fileId);
let scrollY = $state(0);
let treeState: TreeViewState | undefined = $state(fState.treeState);
let fillerHeight: number = $derived(
treeState ? treeState.filler_height : 0
);
let totalHeight: number = $derived(treeState ? treeState.total_height : 0);
let entries: TreeViewModel[] = $derived(treeState ? treeState.entries : []);
let stickies: TreeViewModel[] = $derived(
treeState ? treeState.stickies : []
);
onMount(() => {
scrollY = Math.min(scrollY, totalHeight);
}
);
$effect(() => {
treeState = fState.treeState;
if (treeState) {
scrollY = Math.min(scrollY, totalHeight);
treeState.updateTreeView(scrollY, height);
}
}
);
function handleSelect(prim: TreeViewModel) {
if (prim.expanded && prim.container) {
treeState
?.collapseTree(prim.trace.map((path) => path.key))
.then(() => {
treeState?.updateTreeView(scrollY, height);
});
return;
} else if (prim.container) {
treeState
?.expandTree(prim.trace.map((path) => path.key))
.then(() => {
treeState?.updateTreeView(scrollY, height);
});
}
fState.selectPathHandler(
new PathSelectedEvent(
file_id,
prim.trace.map((path) => path.key)
)
);
treeState?.updateTreeView(scrollY, height);
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
scrollY = event.currentTarget.scrollTop;
treeState?.updateTreeView(scrollY, height);
}
</script>
<div onscroll={handleScroll} class="overflow-auto" style="height: {height}px; overflow-anchor: none;">
{#if entries}
<div style="height: {totalHeight}px; width: 100%">
<table
class="border-b-2 border-forge-bound bg-forge-prim"
style="position: relative; top: {scrollY}px"
>
<thead>
<tr>
{#each stickies as entry, index (index)}
<TreeViewEntry
{entry}
onclick={() => handleSelect(entry)}
></TreeViewEntry>
{/each}
</tr>
</thead>
</table>
<div
class="filler"
style="height: {fillerHeight}px; width: 100%"
></div>
<div>
{#each entries as entry, index (index)}
<TreeViewEntry
onclick={() => handleSelect(entry)}
{entry}
></TreeViewEntry>
{/each}
</div>
</div>
{/if}
</div>
<style lang="postcss">
</style>

View File

@ -0,0 +1,94 @@
<script lang="ts">
const rowHeight = 24;
import PrimitiveIcon from "./PrimitiveIcon.svelte";
import type {TreeViewModel} from "../models/TreeViewModel";
import {CaretDownOutline, CaretRightOutline} from "flowbite-svelte-icons";
let {entry, onclick}: { entry: TreeViewModel, onclick: () => void } = $props();
function formatDisplayKey(key: string) {
if (key.startsWith("Page") && !key.startsWith("Pages")) {
return key.replace("Page", "Page ");
}
return key;
}
</script>
{#if entry.depth === 0}
<div
style="height: 1px; width: 100%;"
class="bg-forge-bound"
></div>
{/if}
<button
class="row text-sm hover:bg-forge-sec w-full group whitespace-nowrap"
style="height: {rowHeight}px;"
onclick={onclick}
>
<div style="margin-left: {entry.depth * 1.25}em">
{#if entry.container}
<div class="caret group-hover:text-forge-text_hint">
{#if entry.expanded}
<CaretDownOutline />
{:else}
<CaretRightOutline />
{/if}
</div>
{:else}
<span class="no-caret"></span>
{/if}
</div>
<div>
<PrimitiveIcon ptype={entry.ptype}/>
</div>
<div class="pl-1 prim_name whitespace-nowrap">
<p>
{formatDisplayKey(entry.key)}
</p>
<div
class="details group-hover:text-forge-text_hint"
>
{" | " +
entry.value +
" | " +
entry.sub_type +
" | " +
entry.ptype}
</div>
</div>
</button>
<style lang="postcss">
.prim_name {
display: flex;
flex-direction: row;
bottom: 0;
width: 100%;
text-align: center;
}
.caret {
@apply text-forge-sec align-middle;
cursor: pointer;
user-select: none;
align-items: center;
}
.details {
@apply ml-1 text-forge-prim whitespace-nowrap font-extralight;
}
.row {
text-align: center;
display: flex;
flex-direction: row;
user-select: none;
}
.no-caret {
@apply pl-5;
user-select: none;
}
</style>

View File

@ -0,0 +1,159 @@
<script lang="ts">
import {Button} from "flowbite-svelte";
let {upload} = $props();
let result_message = $state("");
</script>
<h1 class="text-xl m-5">Welcome to PDF Forge!</h1>
<div class="row" on:click={upload}>
<img src="/pdf-forge-logo-bg.png" class="logo forge" alt="PDF Forge Logo"/>
</div>
<div class = "row">
<Button class="bg-forge-prim hover:bg-forge-sec border-forge-acc border focus:ring-1 focus:ring-forge-acc m-3" onclick={upload}>
Start forging!
</Button>
</div>
<style lang="postcss">
.logo.forge:hover {
filter: drop-shadow(0 0 2em #747bff);
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #ffffff;
background-color: #0f0f0f;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
height: 100%;
width: 100%;
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 30em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.row {
display: flex;
justify-content: center;
margin: auto;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input {
border: none;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
input[type="file"]::file-selector-button {
background-color: #01d4b7; /* Color of the button */
color: #000; /* Color of the text/icon */
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 700;
font-family: inherit;
border-radius: 8px;
cursor: pointer;
border: none;
transition: background-color 0.25s ease, border-color 0.25s;
}
button {
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #259769;
background-color: #000000;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #259769;
}
button:active {
border-color: #259769;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}
</style>

View File

@ -0,0 +1,152 @@
<script lang="ts">
import type FileViewState from '../models/FileViewState.svelte';
import type XRefEntry from '../models/XRefEntry';
const cellH = 25;
const headerOffset = 2 * cellH;
let { fState, height }: { fState: FileViewState; height: number } =
$props();
let fillerHeight: number = $state(0);
let firstEntry = $state(0);
let lastEntry = $state(100);
let entriesToDisplay: XRefEntry[] = $derived(
fState.xref_entries.slice(firstEntry, lastEntry)
);
let bodyViewHeight: number = $derived(Math.max(height - headerOffset, 0));
let selectedPath: number | string | undefined = $derived(
fState.getLastJump()
);
let totalBodyHeight: number = $derived(
Math.max(0, fState.xref_entries.length * cellH - headerOffset)
);
let scrollContainer: HTMLElement;
$effect(() => {
if (typeof selectedPath === 'number') {
smoothScrollTo(scrollContainer, selectedPath);
}
});
function smoothScrollTo(scrollContainer: HTMLElement, target: number) {
const targetY = Math.max(target, 0) * cellH;
const startY = scrollContainer.scrollTop;
if (
targetY - startY > 0 &&
targetY - startY < Math.max(height - headerOffset, 0)
) {
return;
}
const duration = 500;
const diff = targetY - startY;
const startTime = Date.now();
function animate() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1); // Normalize to [0,1]
scrollContainer.scrollTop = startY + diff * easeInOutQuad(progress);
if (progress < 1) {
requestAnimationFrame(animate);
}
}
function easeInOutQuad(t: number) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
requestAnimationFrame(animate);
}
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
let scrollY = event.currentTarget.scrollTop;
firstEntry = Math.floor(scrollY / cellH);
lastEntry = Math.ceil((scrollY + bodyViewHeight) / cellH);
fillerHeight = firstEntry * cellH;
}
</script>
<div class="fullContainer" style="height: {height}px; width: 281px;">
<table>
<thead>
<tr>
<td class="cell t-header border-forge-prim">Obj Nr</td>
<td class="cell t-header border-forge-prim">Gen Nr</td>
<td class="cell t-header border-forge-prim">Type</td>
<td class="cell t-header border-forge-sec">Offset</td>
</tr>
<tr
class={selectedPath === "Trailer"
? "bg-forge-acc"
: "hover:bg-forge-sec"}
onclick={() => fState.selectXref(undefined)}
>
<td class="cell t-data">Trailer</td>
<td class="cell t-data">65535</td>
<td class="cell t-data">Dictionary</td>
<td class="cell t-data">End of file</td>
</tr>
</thead>
</table>
<div
class="scrollContainer"
style="height: {bodyViewHeight}px"
onscroll={handleScroll}
bind:this={scrollContainer}
>
<div style="height: {totalBodyHeight}px">
<table>
<tbody>
<tr class="filler" style="height: {fillerHeight}px"
></tr>
{#each entriesToDisplay as entry, index (index)}
<tr
class={selectedPath === entry.obj_num
? "bg-forge-acc"
: "hover:bg-forge-sec"}
onclick={() => fState.selectXref(entry)}
>
<td class="cell t-data">{entry.obj_num}</td>
<td class="cell t-data">{entry.gen_num}</td>
<td class="cell t-data">{entry.obj_type}</td>
<td class="cell t-data">{entry.offset}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
</div>
<style lang="postcss">
.t-header {
@apply uppercase text-xs text-forge-text p-5 bg-forge-sec border-r;
cursor: default;
}
.t-data {
@apply border text-xs text-forge-text border-forge-sec;
}
.cell {
@apply min-w-[70px] w-[70px] p-1 m-0;
text-align: center;
}
.scrollContainer {
overflow-y: auto;
overflow-anchor: none;
overflow-x: hidden;
}
.fullContainer {
overflow-x: auto;
overflow-y: hidden;
}
</style>

View File

@ -0,0 +1,55 @@
<script lang="ts">
import {
ZoomInOutline,
ZoomOutOutline,
RefreshOutline,
} from "flowbite-svelte-icons";
let {
scale,
onZoomIn,
onZoomOut,
resetZoom,
}: {
scale: number;
onZoomIn: () => void;
onZoomOut: () => void;
resetZoom: () => void;
} = $props();
</script>
<div class="controls">
<span class="zoom-level">{(scale * 100).toFixed(0)}%</span>
<button class="control-button" onclick={onZoomIn}>
<ZoomInOutline />
</button>
<button class="control-button" onclick={onZoomOut}>
<ZoomOutOutline />
</button>
<button class="control-button" onclick={resetZoom}>
<RefreshOutline />
</button>
</div>
<style lang="postcss">
.controls {
@apply bg-forge-prim border-forge-bound border flex flex-row;
position: absolute;
top: 0;
right: 0;
justify-content: flex-end;
z-index: 1;
}
.control-button {
@apply bg-forge-prim p-1 rounded-sm hover:bg-forge-acc m-1;
cursor: pointer;
}
.zoom-level {
display: inline-flex;
align-items: center;
padding: 0 8px;
}
</style>

View File

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

View File

@ -0,0 +1,10 @@
export class PathSelectedEvent extends CustomEvent<{ file_id: string, path: string[] }> {
static readonly eventName = 'pathselected';
constructor(file_id: string, path: string[]) {
super(PathSelectedEvent.eventName, {
detail: { file_id, path },
bubbles: true
});
}
}

1
src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -0,0 +1,24 @@
import type { OperatorList } from './OperatorList';
export default class ContentModel {
parts: string[];
opList: OperatorList;
constructor(parts: string[], opList: OperatorList) {
this.parts = parts;
this.opList = opList;
}
public toDisplay(): string {
let text = "";
if (this.parts.length > 1) {
for (const part of this.parts) {
text += part;
}
} else if (this.parts.length == 1) {
text = this.parts[0];
}
return text;
}
}

View File

@ -0,0 +1,136 @@
import {getDocument, type PDFDocumentProxy, GlobalWorkerOptions} from "pdfjs-dist";
import type { PrimitiveModel } from './PrimitiveModel';
import type TreeViewRequest from './TreeViewRequest.svelte';
import type { TreeViewModel } from './TreeViewModel';
import type XRefTable from './XRefTable';
import ContentModel from './ContentModel.svelte';
import { OperatorList } from './OperatorList';
export default class DocumentWorker {
doc: PDFDocumentProxy;
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);
}
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()
title = metadata?.info?.Title;
return title === "" ? undefined : title;
}
public getNumberOfPages(): number {
return this.doc.numPages;
}
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 getXrefEntries(): Promise<XRefTable> {
return await this.doc.getXRefEntries();
}
public async getStreamAsString(path: string): Promise<string> {
return await this.doc.getStreamAsString(path);
}
public async getContents(pageNumber: number): Promise<ContentModel> {
try {
const pathStem = `/Page${pageNumber}/Contents`;
const contents = await this.doc.getPrimitiveByPath(pathStem);
const promises = [];
if (contents && contents.ptype === "Array") {
for (const child of contents.children) {
promises.push(this.doc.getStreamAsString(`${pathStem}/${child.key}/Data`));
}
} else {
promises.push(this.doc.getStreamAsString(`${pathStem}/Data`));
}
const parts = await Promise.all(promises);
return new ContentModel(parts, await this.getOperatorList(pageNumber));
} catch (error: unknown) {
console.error(error);
return new ContentModel([], new OperatorList([], [], []));
}
}
public dispose() {
this.doc.destroy();
}
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 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');
}
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});
const renderContext = {
canvasContext: context,
transform: transform,
viewport: viewport,
};
const renderTask = page.render(renderContext);
context.fillStyle = '#ffffff';
context.fillRect(viewport.offsetX, viewport.offsetY, viewport.width, viewport.height);
await renderTask.promise
}
}

View File

@ -0,0 +1,289 @@
import type XRefEntry from './XRefEntry';
import Primitive from './Primitive.svelte';
import type { PathSelectedEvent } from '../events/PathSelectedEvent';
import type { PrimitiveModel } from './PrimitiveModel';
import { StreamData } from './StreamData.svelte';
import { ForgeNotification } from './ForgeNotification.svelte';
import { Mutex } from 'async-mutex';
import TreeViewState from './TreeViewState.svelte';
import PathHistory from './PathHistory.svelte';
import DocumentWorker from './Document.svelte.js';
import { v4 as uuidv4 } from 'uuid';
export default class FileViewState {
public pathHistory: PathHistory = new PathHistory();
public fileId: string;
public fileName: string | undefined = $state();
public filePath: string | undefined = $state();
public currentPageNumber: number | undefined = $state();
public treeMode: boolean = $state(true);
public pageMode: boolean = $state(false);
public xRefShowing: boolean = $state(false);
public notificationsShowing: boolean = $state(false);
public changesShowing: boolean = $state(false);
public canForward: boolean = $derived(this.pathHistory.canForward);
public canBack: boolean = $derived(this.pathHistory.canBack);
public path: string[] = $state(['Trailer']);
public container_prim: Primitive | undefined = $state();
public selected_leaf_prim: Primitive | undefined = $state();
public xref_entries: XRefEntry[] = $state([]);
public treeState: TreeViewState | undefined = $state();
public notifications: ForgeNotification[] = $state([]);
notificationMutex = new Mutex();
public changes: PrimitiveModel[] = $state([]);
public document: DocumentWorker;
constructor(path: string | undefined, document: DocumentWorker) {
this.fileId = uuidv4();
this.notificationMutex = new Mutex();
this.document = document;
this.filePath = path;
this.loadNameFromMetadata();
this.loadTreeState();
this.loadXrefEntries();
this.selectPath(this.path);
}
static async load(data: string | ArrayBuffer, path: string | undefined) {
const document = await DocumentWorker.load(data);
return new FileViewState(path, document);
}
async loadTreeState() {
this.treeState = new TreeViewState(this.document);
}
getLastJump(): string | number | undefined {
return this.container_prim?.getLastJump();
}
getFirstJump(): string | number | undefined {
return this.container_prim?.getFirstJump();
}
public getFileName(): string {
return this.fileName ? this.fileName : this.getFilePath();
}
public getFilePath(): string {
return this.filePath ? this.filePath : 'Unknown';
}
public saveStreamData(updated_data: string) {
if (!this.selected_leaf_prim) return;
if (!this.container_prim?.stream_data) return;
console.log('Saving stream data', updated_data);
// invoke("update_stream_data_as_string", {
// id: this.file.id,
// path: this.formatPaths(this.path),
// newData: updated_data
// })
// .then(() => {
// this.reload();
// this.container_prim?.stream_data?.setData(updated_data);
// })
// .catch(err => this.logError(err));
}
public async reload() {
this.container_prim = undefined;
this.selected_leaf_prim = undefined;
this.loadXrefEntries();
this.treeState?.reload();
await this.selectPath(this.path);
}
public logError(message: string) {
this.notificationMutex.acquire().then((release) => {
try {
console.error(message);
this.notifications.push(new ForgeNotification(Date.now(), 'ERROR', message));
} finally {
release();
}
});
}
public async deleteNotification(timestamp: number) {
const release = await this.notificationMutex.acquire();
try {
this.notifications = this.notifications.filter((n) => n.timestamp != timestamp);
} finally {
release();
}
}
public async clearNotifications() {
const release = await this.notificationMutex.acquire();
try {
this.notifications = [];
} finally {
release();
}
}
public async clearChanges() {
// invoke<PrimitiveModel[]>("clear_changes", {id: this.file.id})
// .then(_ => {
// this.reload()
// })
// .catch(err => this.logError(err));
}
public loadXrefEntries() {
this.document.getXrefEntries().then((xrefEntries) => {
this.xref_entries = xrefEntries.entries;
});
}
public async selectPath(newPath: string[]) {
if (this.container_prim?.pathEquals(newPath)) {
return;
}
let loadStreamData = false;
if (newPath.at(-1) === 'Data') {
newPath = newPath.slice(0, newPath.length - 1);
loadStreamData = true;
}
try {
const result = await this.document.getPrimByPath(this.formatPaths(newPath));
if (!result) return;
const newPrim = new Primitive(result);
if (newPrim.isContainer()) {
this.container_prim = newPrim;
this.currentPageNumber = this.getCurrentPageNumber()
this.path = newPath;
this.pathHistory.newPath(newPath);
if (loadStreamData || this.container_prim.ptype === 'Stream') {
this.loadStreamData(this.container_prim).catch((err) => this.logError(err));
}
return;
}
this.selected_leaf_prim = newPrim;
await this.selectPath(newPath.slice(0, newPath.length - 1)).catch((err) =>
this.logError(err)
);
} catch (e) {
// @ts-expect-error idk why
this.logError(e);
}
}
public async loadStreamData(container: Primitive | undefined) {
if (!container) return;
const path = this.formatPaths(this.path) + '/Data';
container.stream_data = new StreamData(container.sub_type);
if (container.sub_type === 'Image') {
const data = await this.document.getImageAsUrl(path);
if (!data) return;
container.stream_data.setImageData(data);
} else {
const data = await this.document.getStreamAsString(path);
if (!data) return;
container.stream_data.setData(data);
}
}
public dispose() {
this.document.dispose();
}
public selectPathHandler(event: PathSelectedEvent) {
if (event.detail.file_id !== this.fileId) {
return;
}
this.selectPath(event.detail.path);
}
public getMergedPath() {
return this.formatPaths(this.path);
}
public back() {
if (!this.canBack) return;
const prevPath = this.pathHistory.goBack();
if (prevPath) {
this.selectPath(prevPath);
}
}
public forward() {
if (!this.canForward) return;
const prevPath = this.pathHistory.goForward();
if (prevPath) {
this.selectPath(prevPath);
}
}
public jump(i: number) {
const prevPath = this.pathHistory.jump(i);
if (prevPath) {
this.selectPath(prevPath);
}
}
public save() {
console.log("saving")
const savePromise = this.document.save();
savePromise.then((data: Uint8Array) => {
const blob = new Blob([data], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = this.filePath ? this.filePath : "document.pdf";
a.click();
URL.revokeObjectURL(url);
}).catch((error: unknown) => {
console.error('Save failed:', error);
});
}
public copyPath() {
const _path: string[] = [];
for (const item of this.path) {
_path.push(item);
}
return _path;
}
public selectXref(entry: XRefEntry | undefined) {
if (!entry) {
this.selectPath(['/']);
return;
}
this.selectPath([entry.obj_num.toString()]);
}
public formatPaths(paths: string[]) {
if (paths.length == 0) {
return '/';
}
if (paths[0] === '/') {
return 'Trailer/' + paths.slice(1, paths.length).join('/');
}
return paths.join('/');
}
public getCurrentPageNumber(): number | undefined {
if (!(this.container_prim && this.container_prim.isPage())) return undefined;
return this.container_prim.getPageNumber();
}
private async loadNameFromMetadata() {
this.fileName = await this.document.getTitle();
}
}

View File

@ -0,0 +1,27 @@
export class ForgeNotification {
public timestamp: number;
date: Date;
public level: string;
public message: string;
public read: boolean = false;
constructor(timestamp: number, level: string, message: string) {
this.timestamp = timestamp;
this.level = level;
this.message = message;
this.date = new Date(timestamp);
}
public getDate(): string {
const hours = this.date.getHours().toString().padStart(2, '0');
const minutes = this.date.getMinutes().toString().padStart(2, '0');
const seconds = this.date.getSeconds().toString().padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
public isError() {
return this.level === "ERROR";
}
public isDebug() {
return this.level === "DEBUG";
}
}

724
src/models/OperatorList.ts Normal file
View File

@ -0,0 +1,724 @@
export interface PDFOperator {
name: string;
keyword: string | null;
description: string;
numArgs?: number;
variableArgs?: boolean;
class: string;
}
export class OperatorList {
fnArray: [];
argsArray: [];
rangeArray: [];
constructor(fnArray: [], argsArray: [], rangeArray: []) {
this.fnArray = fnArray;
this.argsArray = argsArray;
this.rangeArray = rangeArray;
}
public static formatOperatorToMarkdown(operatorId: keyof typeof opLookup): string {
const operator = opLookup[operatorId] as PDFOperator;
if (!operator) {
return `Unknown operator: ${operatorId}`;
}
const keyword = operator.keyword ? `\`${operator.keyword}\`` : 'N/A';
const args = operator.numArgs !== undefined ?
`Arguments: ${operator.numArgs}${operator.variableArgs ? ' (variable)' : ''}` :
'No arguments';
return `**${operator.name}** (${keyword}) ` +
`${operator.description} ` +
`${args}`;
}
}
export const opLookup = {
'1': {
name: 'dependency',
keyword: "Do",
description: 'Marks a resource dependency',
numArgs: 0,
variableArgs: false,
class: 'operator'
},
'2': {
name: 'setLineWidth',
keyword: 'w',
description: 'Sets the line width in the graphics state',
numArgs: 1,
variableArgs: false,
class: 'operator'
},
'3': {
name: 'setLineCap',
keyword: 'J',
description: 'Sets the line cap style in the graphics state (0=butt, 1=round, 2=square)',
numArgs: 1,
variableArgs: false,
class: 'operator'
},
'4': {
name: 'setLineJoin',
keyword: 'j',
description: 'Sets the line join style in the graphics state (0=miter, 1=round, 2=bevel)',
numArgs: 1,
variableArgs: false,
class: 'operator'
},
'5': {
name: 'setMiterLimit',
keyword: 'M',
description: 'Sets the miter limit in the graphics state',
numArgs: 1,
variableArgs: false,
class: 'operator'
},
'6': {
name: 'setDash',
keyword: 'd',
description: 'Sets the line dash pattern in the graphics state',
numArgs: 2,
variableArgs: false,
class: 'operator'
},
'7': {
name: 'setRenderingIntent',
keyword: 'ri',
description: 'Sets the color rendering intent in the graphics state',
numArgs: 1,
variableArgs: false,
class: 'operator'
},
'8': {
name: 'setFlatness',
keyword: 'i',
description: 'Sets the flatness tolerance in the graphics state',
numArgs: 1,
variableArgs: false,
class: 'operator'
},
'9': {
name: 'setGState',
keyword: 'gs',
description: 'Sets the specified parameters in the graphics state',
numArgs: 1,
variableArgs: false,
class: 'operator'
},
'10': {
name: 'save',
keyword: 'q',
description: 'Pushes a copy of the current graphics state onto the graphics state stack',
numArgs: 0,
variableArgs: false,
class: 'operator'
},
'11': {
name: 'restore',
keyword: 'Q',
description: 'Restores the graphics state by popping it from the stack',
numArgs: 0,
variableArgs: false,
class: 'operator'
},
'12': {
name: 'transform',
keyword: 'cm',
description: 'Modifies the current transformation matrix (CTM)',
numArgs: 6,
variableArgs: false,
class: 'matrix'
},
'13': {
name: 'moveTo',
keyword: 'm',
description: 'Begins a new subpath by moving to coordinates (x, y)',
numArgs: 2,
variableArgs: false,
class: 'path-construct'
},
'14': {
name: 'lineTo',
keyword: 'l',
description: 'Adds a straight line segment to the current path',
numArgs: 2,
variableArgs: false,
class: 'path-construct'
},
'15': {
name: 'curveTo',
keyword: 'c',
description: 'Adds a Bézier curve segment to the current path using two control points',
numArgs: 6,
variableArgs: false,
class: 'path-construct'
},
'16': {
name: 'curveTo2',
keyword: 'v',
description: 'Adds a Bézier curve segment to the current path using one control point',
numArgs: 4,
variableArgs: false,
class: 'path-construct'
},
'17': {
name: 'curveTo3',
keyword: 'y',
description: 'Adds a Bézier curve segment to the current path using one control point',
numArgs: 4,
variableArgs: false,
class: 'path-construct'
},
'18': {
name: 'closePath',
keyword: 'h',
description: 'Closes the current subpath',
numArgs: 0,
variableArgs: false,
class: 'path-construct'
},
'19': {
name: 'rectangle',
keyword: 're',
description: 'Adds a rectangle to the current path',
numArgs: 4,
variableArgs: false,
class: 'path-construct'
},
'20': {
name: 'stroke',
keyword: 'S',
description: 'Strokes the current path',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'21': {
name: 'closeStroke',
keyword: 's',
description: 'Closes and strokes the current path',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'22': {
name: 'fill',
keyword: 'f',
description: 'Fills the current path using nonzero winding number rule',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'23': {
name: 'eoFill',
keyword: 'f*',
description: 'Fills the current path using even-odd rule',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'24': {
name: 'fillStroke',
keyword: 'B',
description: 'Fills and strokes the current path using nonzero winding number rule',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'25': {
name: 'eoFillStroke',
keyword: 'B*',
description: 'Fills and strokes the current path using even-odd rule',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'26': {
name: 'closeFillStroke',
keyword: 'b',
description: 'Closes, fills, and strokes the current path using nonzero winding number rule',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'27': {
name: 'closeEOFillStroke',
keyword: 'b*',
description: 'Closes, fills, and strokes the current path using even-odd rule',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'28': {
name: 'endPath',
keyword: 'n',
description: 'Ends the current path without filling or stroking',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'29': {
name: 'clip',
keyword: 'W',
description: 'Sets the current path as the clipping path using nonzero winding number rule',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'30': {
name: 'eoClip',
keyword: 'W*',
description: 'Sets the current path as the clipping path using even-odd rule',
numArgs: 0,
variableArgs: false,
class: 'path-paint'
},
'31': {
name: 'beginText',
keyword: 'BT',
description: 'Begins a text object',
numArgs: 0,
variableArgs: false,
class: 'text-render'
},
'32': {
name: 'endText',
keyword: 'ET',
description: 'Ends a text object',
numArgs: 0,
variableArgs: false,
class: 'text-render'
},
'33': {
name: 'setCharSpacing',
keyword: 'Tc',
description: 'Sets the character spacing',
numArgs: 1,
variableArgs: false,
class: 'text-state'
},
'34': {
name: 'setWordSpacing',
keyword: 'Tw',
description: 'Sets the word spacing',
numArgs: 1,
variableArgs: false,
class: 'text-state'
},
'35': {
name: 'setHScale',
keyword: 'Tz',
description: 'Sets the horizontal scaling',
numArgs: 1,
variableArgs: false,
class: 'text-state'
},
'36': {
name: 'setLeading',
keyword: 'TL',
description: 'Sets the text leading',
numArgs: 1,
variableArgs: false,
class: 'text-state'
},
'37': {
name: 'setFont',
keyword: 'Tf',
description: 'Sets the text font and size',
numArgs: 2,
variableArgs: false,
class: 'text-state'
},
'38': {
name: 'setTextRenderingMode',
keyword: 'Tr',
description: 'Sets the text rendering mode',
numArgs: 1,
variableArgs: false,
class: 'text-state'
},
'39': {
name: 'setTextRise',
keyword: 'Ts',
description: 'Sets the text rise',
numArgs: 1,
variableArgs: false,
class: 'text-state'
},
'40': {
name: 'moveText',
keyword: 'Td',
description: 'Moves to the start of the next line using offset coordinates',
numArgs: 2,
variableArgs: false,
class: 'text-positioning'
},
'41': {
name: 'setLeadingMoveText',
keyword: 'TD',
description: 'Sets the text leading and moves to the next line',
numArgs: 2,
variableArgs: false,
class: 'text-positioning'
},
'42': {
name: 'setTextMatrix',
keyword: 'Tm',
description: 'Sets the text matrix and text line matrix',
numArgs: 6,
variableArgs: false,
class: 'matrix'
},
'43': {
name: 'nextLine',
keyword: 'T*',
description: 'Moves to the start of the next line',
numArgs: 0,
variableArgs: false,
class: 'text-positioning'
},
'44': {
name: 'showText',
keyword: 'Tj',
description: 'Shows a text string',
numArgs: 1,
variableArgs: false,
class: 'text-render'
},
'45': {
name: 'showSpacedText',
keyword: 'TJ',
description: 'Shows a text string with individual glyph positioning',
numArgs: 1,
variableArgs: false,
class: 'text-render'
},
'46': {
name: 'nextLineShowText',
keyword: '\'',
description: 'Moves to the next line and shows a text string',
numArgs: 1,
variableArgs: false,
class: 'text-render'
},
'47': {
name: 'nextLineSetSpacingShowText',
keyword: '"',
description: 'Sets word and character spacing, moves to next line, and shows text',
numArgs: 3,
variableArgs: false,
class: 'text-render'
},
'48': {
name: 'setCharWidth',
keyword: 'd0',
description: 'Sets the character width information for Type 3 fonts',
numArgs: 2,
variableArgs: false,
class: 'text-state'
},
'49': {
name: 'setCharWidthAndBounds',
keyword: 'd1',
description: 'Sets the character width and bounding box for Type 3 fonts',
numArgs: 6,
variableArgs: false,
class: 'text-state'
},
'50': {
name: 'setStrokeColorSpace',
keyword: 'CS',
description: 'Sets the color space for stroking operations',
numArgs: 1,
variableArgs: false,
class: 'color-operator'
},
'51': {
name: 'setFillColorSpace',
keyword: 'cs',
description: 'Sets the color space for nonstroking operations',
numArgs: 1,
variableArgs: false,
class: 'color-operator'
},
'52': {
name: 'setStrokeColor',
keyword: 'SC',
description: 'Sets the color for stroking operations',
numArgs: 4,
variableArgs: true,
class: 'color-operator'
},
'53': {
name: 'setStrokeColorN',
keyword: 'SCN',
description: 'Sets the color for stroking operations (ICCBased and special color spaces)',
numArgs: 33,
variableArgs: true,
class: 'color-operator'
},
'54': {
name: 'setFillColor',
keyword: 'sc',
description: 'Sets the color for nonstroking operations',
numArgs: 4,
variableArgs: true,
class: 'color-operator'
},
'55': {
name: 'setFillColorN',
keyword: 'scn',
description: 'Sets the color for nonstroking operations (ICCBased and special color spaces)',
numArgs: 33,
variableArgs: true,
class: 'color-operator'
},
'56': {
name: 'setStrokeGray',
keyword: 'G',
description: 'Sets the gray level for stroking operations',
numArgs: 1,
variableArgs: false,
class: 'color-operator'
},
'57': {
name: 'setFillGray',
keyword: 'g',
description: 'Sets the gray level for nonstroking operations',
numArgs: 1,
variableArgs: false,
class: 'color-operator'
},
'58': {
name: 'setStrokeRGBColor',
keyword: 'RG',
description: 'Sets the RGB color for stroking operations',
numArgs: 3,
variableArgs: false,
class: 'color-operator'
},
'59': {
name: 'setFillRGBColor',
keyword: 'rg',
description: 'Sets the RGB color for nonstroking operations',
numArgs: 3,
variableArgs: false,
class: 'color-operator'
},
'60': {
name: 'setStrokeCMYKColor',
keyword: 'K',
description: 'Sets the CMYK color for stroking operations',
numArgs: 4,
variableArgs: false,
class: 'color-operator'
},
'61': {
name: 'setFillCMYKColor',
keyword: 'k',
description: 'Sets the CMYK color for nonstroking operations',
numArgs: 4,
variableArgs: false,
class: 'color-operator'
},
'62': {
name: 'shadingFill',
keyword: 'sh',
description: 'Fills the current path using a shading pattern',
numArgs: 1,
variableArgs: false,
class: 'path-paint'
},
'63': {
name: 'beginInlineImage',
keyword: 'BI',
description: 'Begins an inline image object',
numArgs: 0,
variableArgs: false,
class: 'named-element'
},
'64': {
name: 'beginImageData',
keyword: 'ID',
description: 'Begins the inline image data',
numArgs: 0,
variableArgs: false,
class: 'named-element'
},
'65': {
name: 'endInlineImage',
keyword: 'EI',
description: 'Ends an inline image object',
numArgs: 1,
variableArgs: false,
class: 'named-element'
},
'66': {
name: 'paintXObject',
keyword: 'Do',
description: 'Paints the specified XObject',
numArgs: 1,
variableArgs: false,
class: 'named-element'
},
'67': {
name: 'markPoint',
keyword: 'MP',
description: 'Marks a point in the content stream',
numArgs: 1,
variableArgs: false,
class: 'marked-content'
},
'68': {
name: 'markPointProps',
keyword: 'DP',
description: 'Marks a point in the content stream with properties',
numArgs: 2,
variableArgs: false,
class: 'marked-content'
},
'69': {
name: 'beginMarkedContent',
keyword: 'BMC',
description: 'Begins a marked-content sequence',
numArgs: 1,
variableArgs: false,
class: 'marked-content'
},
'70': {
name: 'beginMarkedContentProps',
keyword: 'BDC',
description: 'Begins a marked-content sequence with properties',
numArgs: 2,
variableArgs: false,
class: 'marked-content'
},
'71': {
name: 'endMarkedContent',
keyword: 'EMC',
description: 'Ends a marked-content sequence',
numArgs: 0,
variableArgs: false,
class: 'marked-content'
},
'72': {
name: 'beginCompat',
keyword: 'BX',
description: 'Begins a compatibility section',
numArgs: 0,
variableArgs: false,
class: 'operator'
},
'73': {
name: 'endCompat',
keyword: 'EX',
description: 'Ends a compatibility section',
numArgs: 0,
variableArgs: false,
class: 'operator'
},
'74': {
name: 'paintFormXObjectBegin',
keyword: null,
description: 'Begins painting a form XObject',
class: 'named-element'
},
'75': {
name: 'paintFormXObjectEnd',
keyword: null,
description: 'Ends painting a form XObject',
class: 'named-element'
},
'76': {
name: 'beginGroup',
keyword: null,
description: 'Begins a transparency group',
class: 'operator'
},
'77': {
name: 'endGroup',
keyword: null,
description: 'Ends a transparency group',
class: 'operator'
},
'80': {
name: 'beginAnnotation',
keyword: null,
description: 'Begins an annotation',
class: 'named-element'
},
'81': {
name: 'endAnnotation',
keyword: null,
description: 'Ends an annotation',
class: 'named-element'
},
'83': {
name: 'paintImageMaskXObject',
keyword: null,
description: 'Paints an image mask XObject',
class: 'named-element'
},
'84': {
name: 'paintImageMaskXObjectGroup',
keyword: null,
description: 'Paints an image mask XObject group',
class: 'named-element'
},
'85': {
name: 'paintImageXObject',
keyword: null,
description: 'Paints an image XObject',
class: 'named-element'
},
'86': {
name: 'paintInlineImageXObject',
keyword: null,
description: 'Paints an inline image XObject',
class: 'named-element'
},
'87': {
name: 'paintInlineImageXObjectGroup',
keyword: null,
description: 'Paints an inline image XObject group',
class: 'named-element'
},
'88': {
name: 'paintImageXObjectRepeat',
keyword: null,
description: 'Paints a repeated image XObject',
class: 'named-element'
},
'89': {
name: 'paintImageMaskXObjectRepeat',
keyword: null,
description: 'Paints a repeated image mask XObject',
class: 'named-element'
},
'90': {
name: 'paintSolidColorImageMask',
keyword: null,
description: 'Paints a solid color image mask',
class: 'named-element'
},
'91': {
name: 'constructPath',
keyword: null,
description: 'Constructs a path from the specified segments',
class: 'path-construct'
},
'92': {
name: 'setStrokeTransparent',
keyword: null,
description: 'Sets the stroke transparency',
class: 'operator'
},
'93': {
name: 'setFillTransparent',
keyword: null,
description: 'Sets the fill transparency',
class: 'operator'
}
};

6
src/models/PageModel.ts Normal file
View File

@ -0,0 +1,6 @@
export default interface PageModel {
key: string,
obj_num: number,
page_num: number,
}

View File

@ -0,0 +1,54 @@
import {arraysAreEqual} from "../utils";
export default class PathHistory {
public currentIndex: number = $state(-1);
public history: string[][] = $state([]);
public canForward: boolean = $derived(this.currentIndex < this.history.length - 1);
public canBack: boolean = $derived(this.currentIndex > 0);
public newPath(path: string[]) {
let current = this.getCurrent();
if (current && arraysAreEqual(path, current)) {
return;
}
this.currentIndex ++;
this.history = this.history.slice(0, this.currentIndex);
this.history.push(this.copy(path));
}
public goBack(): string[] | undefined {
if (!this.canBack) return undefined;
this.currentIndex--;
return this.getCurrent();
}
public goForward(): string[] | undefined {
if (!this.canForward) return undefined;
this.currentIndex++;
return this.getCurrent();
}
public jump(i: number): string[] | undefined {
if (i >= 0 && i < this.history.length) {
this.currentIndex = i;
return this.getCurrent();
}
return undefined;
}
public getCurrent(): string[] | undefined {
if (!this.history || this.currentIndex < 0 || this.currentIndex >= this.history.length) return undefined;
return this.copy(this.history[this.currentIndex])
}
private copy(path: string[]) {
const _path: string[] = [];
for (let item of path) {
_path.push(item);
}
return _path;
}
}

View File

@ -0,0 +1,103 @@
import {tracesAreEqual, tracesAreEqual2} from "../utils";
import type {PrimitiveModel} from "./PrimitiveModel";
import type {StreamData} from "./StreamData.svelte";
export default class Primitive {
// input from api
public key: string;
public ptype: string;
public sub_type: string;
public value: string;
public children: Primitive[];
public trace: Trace[] = $state([]);
public expanded: boolean = $state(false);
//local state
public stream_data: StreamData | undefined = $state();
constructor(
p: PrimitiveModel
) {
this.key = p.key;
this.ptype = p.ptype;
this.sub_type = p.sub_type;
this.value = p.value;
this.children = [];
for (let child of p.children) {
this.children.push(new Primitive(child));
}
this.trace = [];
this.expanded = p.expanded;
for (let path of p.trace) {
this.trace.push(path);
}
}
public isContainer() {
return this.ptype === "Dictionary" || this.ptype === "Array" || this.ptype === "Reference" || this.ptype === "Stream";
}
public getPath(): string[] {
return this.trace.map(path => path.key);
}
public traceEquals(trace: Trace[]): boolean {
return tracesAreEqual(trace, this.trace)
}
public pathEquals(path: string[]): boolean {
return tracesAreEqual2(this.trace, path)
}
public getLastJump(): string | number {
let path = this.trace[this.trace.length - 1].last_jump;
if (path === "/") {
return path
}
;
return +path;
}
public isPage(): boolean {
return this.trace[0].key.startsWith("Page") && !isNaN(+this.trace[0].key.replace("Page", ""));
}
public getPageNumber(): number {
if (!this.isPage()) {
throw new Error("Primitive is not a page");
}
return +this.trace[0].key.replace("Page", "");
}
public getFirstJump(): string | number | undefined {
let path = this.trace[0].last_jump;
if (path === "Trailer") {
return path
}
;
if (path.startsWith("Page")) {
return path
}
;
return +path;
}
public withoutChildren(): Primitive {
return new Primitive({
...this,
children: [],
expanded: false,
});
}
public isChildOf(prim: Primitive | undefined): boolean {
if (!prim) {
return false;
}
return tracesAreEqual(this.trace.slice(0, -1), prim.trace);
}
}
export interface Trace {
readonly key: string;
readonly last_jump: string;
}

View File

@ -0,0 +1,11 @@
import type {Trace} from "./Primitive.svelte"
export interface PrimitiveModel {
readonly key: string,
readonly ptype: string,
readonly sub_type: string,
readonly value: string,
readonly children: PrimitiveModel[],
readonly trace: Trace[],
readonly expanded: boolean,
}

View File

@ -0,0 +1,51 @@
export class RawImageData {
height: number;
width: number;
pixels: ArrayBuffer;
private _objectUrl: string | null = null;
constructor(buff: ArrayBuffer) {
const view = new DataView(buff);
this.width = view.getUint32(0, true); // true for little-endian
this.height = view.getUint32(4, true);
this.pixels = buff.slice(8);
}
private createImageData(): ImageData {
const uint8Array = new Uint8Array(this.pixels);
return new ImageData(
new Uint8ClampedArray(uint8Array),
this.width,
this.height
,
);
}
toObjectUrl(): string {
if (this._objectUrl) {
return this._objectUrl;
}
const canvas = document.createElement('canvas');
canvas.width = this.width;
canvas.height = this.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Could not get canvas context');
}
const imageData = this.createImageData();
ctx.putImageData(imageData, 0, 0);
this._objectUrl = canvas.toDataURL('image/png');
return this._objectUrl;
}
dispose() {
if (this._objectUrl) {
URL.revokeObjectURL(this._objectUrl);
this._objectUrl = null;
}
}
}

View File

@ -0,0 +1,20 @@
import type {RawImageData} from "./RawImageData";
export class StreamData {
// Image, Text
public type: string;
public textData: string | undefined = $state();
public imageData: string | undefined = $state();
constructor(type: string) {
this.type = type;
}
public setData(data: string) {
this.textData = data;
}
public setImageData(data: string) {
this.imageData = data;
}
}

View File

@ -0,0 +1,8 @@
class ToolBarState {
constructor(
public xref: boolean = false,
public tree: boolean = false,
public pages: boolean = false
) {
}
}

View File

@ -0,0 +1,14 @@
import type { Trace } from "./Primitive.svelte";
export class TreeViewModel {
constructor(
public depth: number,
public key: string,
public ptype: string,
public sub_type: string,
public value: string,
public container: boolean,
public expanded: boolean,
public trace: Trace[],
) { }
}

View File

@ -0,0 +1,55 @@
export default class TreeViewRequest {
public key: string;
public children: TreeViewRequest[];
public displayName: string;
expand: boolean;
constructor(
key: string,
children: TreeViewRequest[],
expand: boolean = false,
) {
if (key.startsWith("Page")) {
this.displayName = "Page " + key.slice(4);
} else {
this.displayName = key;
}
this.key = key;
this.children = children;
this.expand = expand;
}
static fromPageCount(pageCount: number) {
let roots = [];
for (let i = 0; i < pageCount; i++) {
roots.push(new TreeViewRequest("Page" + (i + 1), []));
}
return roots;
}
static TRAILER = new TreeViewRequest("Trailer", [new TreeViewRequest("Root", [], true)], true);
public clone(): TreeViewRequest {
return new TreeViewRequest(this.key, this.children.map(child => child.clone()), this.expand);
}
public getChild(key: string) {
return this.children.find(child => child.key === key);
}
public addChild(key: string) {
this.expand = true;
let child = new TreeViewRequest(key, [], true)
this.children.push(child);
return child;
}
public removeChild(key: string) {
this.children = this.children.filter(child => child.key !== key);
}
public setExpand(expand: boolean) {
this.expand = expand;
}
}

View File

@ -0,0 +1,198 @@
import TreeViewRequest from './TreeViewRequest.svelte';
import { TreeViewModel } from './TreeViewModel';
import type { Trace } from './Primitive.svelte';
import type DocumentWorker from './Document.svelte';
const ROW_HEIGHT = 24;
const MAX_STICKIES: number = 5;
export default class TreeViewState {
private doc: DocumentWorker;
private initialRequest: TreeViewRequest[];
private activeRequest: Map<string, TreeViewRequest> = new Map();
private activeEntries: Map<string, TreeViewModel[]> = new Map();
public total_height: number = $state(100);
public filler_height: number = $state(0);
public entries: TreeViewModel[] = $state([]);
public stickies: TreeViewModel[] = $state([]);
private scrollY = 0;
private height = 100;
constructor(doc: DocumentWorker) {
this.initialRequest = [TreeViewRequest.TRAILER];
this.initialRequest = this.initialRequest.concat(
TreeViewRequest.fromPageCount(+doc.getNumberOfPages())
);
this.doc = doc;
this.updateTreeViewRequest([this.initialRequest[0]]).then(() => this.updateTreeView(0, 1080));
}
public getEntryCount() {
let count = 0;
this.activeEntries.forEach((value) => {
count += value.length - 1;
});
return this.initialRequest.length + count;
}
public getEntries(start: number, end: number): [TreeViewModel[], TreeViewModel[]] {
let stickies: TreeViewModel[] = [];
let totalIndex = 0;
const result: TreeViewModel[] = [];
for (const request of this.initialRequest) {
const entries = this.activeEntries.get(request.key);
if (entries) {
for (let i = 0; i < entries.length; i++) {
if (totalIndex > 0 && totalIndex == start) {
stickies = this.findParents(entries, i);
}
if (totalIndex >= start) {
const entry = entries[i];
result.push(entry);
}
totalIndex += 1;
if (totalIndex >= end) {
return [result, stickies];
}
}
} else {
if (totalIndex >= start) {
result.push(
new TreeViewModel(
0,
request.key,
'Dictionary',
'-',
'-',
true,
false,
[{ key: request.key, last_jump: request.key } as Trace],
)
);
}
totalIndex += 1;
if (totalIndex >= end) {
return [result, stickies];
}
}
}
return [result, stickies];
}
private findParents(entries: TreeViewModel[], start: number): TreeViewModel[] {
const parents = [];
let lastDepth = entries[start].depth;
for (let i = start; i >= 0; i--) {
const entry = entries[i];
if (entry.depth < lastDepth) {
parents.push(entry);
if (entry.depth == 0) {
return parents.reverse().slice(0, MAX_STICKIES);
}
lastDepth = entry.depth;
}
}
return parents.reverse().slice(0, MAX_STICKIES);
}
public async reload() {
await this.loadTreeView(this.scrollY, this.height);
}
public async loadTreeView(scrollY: number, height: number) {
const activeRequests = Array.from(this.activeRequest.values());
await this.updateTreeViewRequest(activeRequests);
await this.updateTreeView(scrollY, height);
}
public async updateTreeView(scrollY: number, height: number) {
const firstEntry = Math.floor(scrollY / ROW_HEIGHT);
const lastEntry = Math.ceil((scrollY + height) / ROW_HEIGHT);
this.total_height = this.getEntryCount() * ROW_HEIGHT;
this.filler_height = firstEntry * ROW_HEIGHT;
const entriesAndStickies = this.getEntries(firstEntry, lastEntry);
this.entries = entriesAndStickies[0];
this.stickies = entriesAndStickies[1];
this.scrollY = scrollY;
this.height = height;
}
public getRoot(): TreeViewRequest[] {
const _roots: TreeViewRequest[] = [];
this.initialRequest.forEach((root) => {
_roots.push(root.clone());
});
return _roots;
}
public async updateTreeViewRequest(treeViewRequests: TreeViewRequest[]) {
const result = await this.doc.getPrimTreeByPath(treeViewRequests);
for (let i = 0; i < treeViewRequests.length; i++) {
const request = treeViewRequests[i];
if (request.expand) {
this.activeRequest.set(request.key, request);
} else {
this.activeRequest.delete(request.key);
}
const prim = result.filter((r) => r.trace[0].key == request.key);
if (prim) {
this.activeEntries.set(request.key, prim);
}
}
}
public async expandTree(path: string[]) {
if (path.length == 0) {
console.error('Empty path');
return;
}
let root = this.activeRequest.get(path[0]);
if (!root) {
root = this.getRoot().find((root) => root.key === path[0]);
if (!root) {
console.error('Root not found for path: ' + path);
return;
}
}
root.setExpand(true);
let node = root;
for (const key of path.slice(1, path.length)) {
const _node: TreeViewRequest | undefined = node.getChild(key);
if (_node) {
_node.setExpand(true);
node = _node;
} else {
node = node.addChild(key);
}
}
await this.updateTreeViewRequest([root]);
}
public async collapseTree(path: string[]) {
if (path.length == 0) {
console.error('Empty path');
return;
}
if (path.length == 1) {
this.activeEntries.delete(path[0]);
return;
}
const root: TreeViewRequest | undefined = this.activeRequest.get(path[0]);
if (!root) {
console.error('Root not found for path: ' + path);
return;
}
let node: TreeViewRequest | undefined = root;
let parent: TreeViewRequest | undefined;
for (const key of path.slice(1, path.length)) {
parent = node;
node = parent?.getChild(key);
}
if (node && parent) {
parent.children = parent.children.filter(child => child.key !== node?.key);
await this.updateTreeViewRequest([root]);
}
}
}

7
src/models/XRefEntry.ts Normal file
View File

@ -0,0 +1,7 @@
export default interface XRefEntry {
readonly obj_type: string;
readonly obj_num: number | string;
readonly gen_num: number;
readonly offset: number | string;
readonly size: number;
}

6
src/models/XRefTable.ts Normal file
View File

@ -0,0 +1,6 @@
import type XRefEntry from "./XRefEntry";
export default interface XRefTable {
size: number
entries: XRefEntry[];
}

8
src/monaco-worker.ts Normal file
View File

@ -0,0 +1,8 @@
import * as monaco from 'monaco-editor';
// Configure Monaco Editor worker
self.MonacoEnvironment = {
getWorkerUrl: function (moduleId: string, label: string) {
return `/monaco-editor/${label}.worker.js`;
}
};

View File

@ -0,0 +1,5 @@
<script>import "../app.css";
</script>
<slot/>

5
src/routes/+layout.ts Normal file
View File

@ -0,0 +1,5 @@
// Tauri doesn't have a Node.js server to do proper SSR
// so we will use adapter-static to prerender the app (SSG)
// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info
export const prerender = true;
export const ssr = false;

5
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,5 @@
<script>
import App from "../components/App.svelte";
</script>
<App/>

43
src/utils.ts Normal file
View File

@ -0,0 +1,43 @@
import type { Action } from "@sveltejs/kit";
import type { Trace } from "./models/Primitive.svelte";
export function arraysAreEqual(arr1: string[], arr2: string[]) {
if (arr1.length !== arr2.length) {
return false; // Arrays of different lengths are not equal
}
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i]) {
return false; // Mismatched element found
}
}
return true; // All elements match
}
export function tracesAreEqual(arr1: Trace[], arr2: Trace[]) {
if (arr1.length !== arr2.length) {
return false; // Arrays of different lengths are not equal
}
for (let i = 0; i < arr1.length; i++) {
if (arr1[i].key !== arr2[i].key) {
return false; // Mismatched element found
}
}
return true; // All elements match
}
export function tracesAreEqual2(arr1: Trace[], arr2: string[]) {
if (arr1.length !== arr2.length) {
return false; // Arrays of different lengths are not equal
}
for (let i = 0; i < arr1.length; i++) {
if (arr1[i].key !== arr2[i]) {
return false; // Mismatched element found
}
}
return true; // All elements match
}
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

BIN
static/pdf-forge-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

21
static/pdf.worker.min.mjs Normal file

File diff suppressed because one or more lines are too long

17
svelte.config.js Normal file
View File

@ -0,0 +1,17 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

37
tailwind.config.ts Normal file
View File

@ -0,0 +1,37 @@
import type { Config } from "tailwindcss";
import flowbitePlugin from 'flowbite/plugin'
export default {
content: ['./src/**/*.{html,js,svelte,ts}', './node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}'],
darkMode: 'selector',
theme: {
extend: {
colors: {
// flowbite-svelte
primary: {
50: '#FFF5F2',
100: '#FFF1EE',
200: '#FFE4DE',
300: '#FFD5CC',
400: '#FFBCAD',
500: '#FE795D',
600: '#EF562F',
700: '#EB4F27',
800: '#CC4522',
900: '#A5371B'
},
forge: {
dark: 'rgb(30,31,34)',
prim: 'rgb(43,45,48)',
sec: 'rgb(76,77,80)',
acc: 'rgb(44, 97, 97)',
active: 'rgb(44, 71, 73)',
bound: 'rgba(0, 0, 0, 0.29)',
text: '#dadada',
text_sec: '#838686',
text_hint: '#5cc4c4c2',
}
}
}
},
plugins: [flowbitePlugin]
} as Config;

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

17
vite.config.ts Normal file
View File

@ -0,0 +1,17 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig(async () => ({
plugins: [
sveltekit(),
],
css: {
postcss: './postcss.config.cjs', // Path to PostCSS config
},
clearScreen: false,
resolve: {
alias: {
'pdfjs-dist': 'C:\\Users\\kj131\\pdf-forge\\pdf.js\\build\\dist',
},
},
}));

2490
yarn.lock Normal file

File diff suppressed because it is too large Load Diff