first commit
This commit is contained in:
commit
4637c66185
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal 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
8
.idea/.gitignore
generated
vendored
Normal 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
63
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
6
.idea/jsLibraryMappings.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
12
.idea/pdf-forge.iml
generated
Normal 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
6
.idea/prettier.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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>
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
# Package Managers
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
18
.prettierrc
Normal file
18
.prettierrc
Normal 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
38
README.md
Normal 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
39
eslint.config.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
BIN
examples/ISO_32000-2_2020(en).pdf
Normal file
BIN
examples/ISO_32000-2_2020(en).pdf
Normal file
Binary file not shown.
32
examples/api_examples.js
Normal file
32
examples/api_examples.js
Normal 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
912
examples/buildOpsLookup.js
Normal 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: { 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
52
package.json
Normal 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
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
72
src/app.css
Normal file
72
src/app.css
Normal 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
13
src/app.d.ts
vendored
Normal 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
13
src/app.html
Normal 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
297
src/components/App.svelte
Normal 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>
|
||||
49
src/components/ChangesModal.svelte
Normal file
49
src/components/ChangesModal.svelte
Normal 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>
|
||||
 
|
||||
<p>{change.ptype}</p> | 
|
||||
<p>{change.sub_type}</p> | 
|
||||
<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>
|
||||
173
src/components/ContentEditor.svelte
Normal file
173
src/components/ContentEditor.svelte
Normal 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>
|
||||
128
src/components/FileView.svelte
Normal file
128
src/components/FileView.svelte
Normal 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>
|
||||
79
src/components/Footer.svelte
Normal file
79
src/components/Footer.svelte
Normal 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>
|
||||
26
src/components/ImageViewer.svelte
Normal file
26
src/components/ImageViewer.svelte
Normal 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>
|
||||
49
src/components/NotificationModal.svelte
Normal file
49
src/components/NotificationModal.svelte
Normal 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>  <p class:error={notification.isError()} class:debug={notification.isDebug()}> {notification.level} </p>  
|
||||
<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>
|
||||
48
src/components/PageView.svelte
Normal file
48
src/components/PageView.svelte
Normal 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>
|
||||
35
src/components/PrimitiveIcon.svelte
Normal file
35
src/components/PrimitiveIcon.svelte
Normal 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>
|
||||
173
src/components/PrimitiveView.svelte
Normal file
173
src/components/PrimitiveView.svelte
Normal 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>
|
||||
33
src/components/StreamDataView.svelte
Normal file
33
src/components/StreamDataView.svelte
Normal 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>
|
||||
122
src/components/StreamEditor.svelte
Normal file
122
src/components/StreamEditor.svelte
Normal 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>
|
||||
77
src/components/TabBar.svelte
Normal file
77
src/components/TabBar.svelte
Normal 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>
|
||||
326
src/components/TitleBar.svelte
Normal file
326
src/components/TitleBar.svelte
Normal 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>
|
||||
61
src/components/ToolbarLeft.svelte
Normal file
61
src/components/ToolbarLeft.svelte
Normal 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>
|
||||
79
src/components/ToolbarRight.svelte
Normal file
79
src/components/ToolbarRight.svelte
Normal 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>
|
||||
112
src/components/TreeView.svelte
Normal file
112
src/components/TreeView.svelte
Normal 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>
|
||||
94
src/components/TreeViewEntry.svelte
Normal file
94
src/components/TreeViewEntry.svelte
Normal 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>
|
||||
159
src/components/WelcomeScreen.svelte
Normal file
159
src/components/WelcomeScreen.svelte
Normal 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>
|
||||
152
src/components/XRefTable.svelte
Normal file
152
src/components/XRefTable.svelte
Normal 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>
|
||||
55
src/components/ZoomControls.svelte
Normal file
55
src/components/ZoomControls.svelte
Normal 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>
|
||||
162
src/components/ZoomableContainer.svelte
Normal file
162
src/components/ZoomableContainer.svelte
Normal 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>
|
||||
10
src/events/PathSelectedEvent.ts
Normal file
10
src/events/PathSelectedEvent.ts
Normal 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
1
src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
24
src/models/ContentModel.svelte.ts
Normal file
24
src/models/ContentModel.svelte.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
136
src/models/Document.svelte.ts
Normal file
136
src/models/Document.svelte.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
289
src/models/FileViewState.svelte.ts
Normal file
289
src/models/FileViewState.svelte.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
27
src/models/ForgeNotification.svelte.ts
Normal file
27
src/models/ForgeNotification.svelte.ts
Normal 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
724
src/models/OperatorList.ts
Normal 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
6
src/models/PageModel.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export default interface PageModel {
|
||||
key: string,
|
||||
obj_num: number,
|
||||
page_num: number,
|
||||
}
|
||||
54
src/models/PathHistory.svelte.ts
Normal file
54
src/models/PathHistory.svelte.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
103
src/models/Primitive.svelte.ts
Normal file
103
src/models/Primitive.svelte.ts
Normal 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;
|
||||
}
|
||||
11
src/models/PrimitiveModel.ts
Normal file
11
src/models/PrimitiveModel.ts
Normal 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,
|
||||
}
|
||||
51
src/models/RawImageData.ts
Normal file
51
src/models/RawImageData.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/models/StreamData.svelte.ts
Normal file
20
src/models/StreamData.svelte.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
8
src/models/ToolBarState.ts
Normal file
8
src/models/ToolBarState.ts
Normal file
@ -0,0 +1,8 @@
|
||||
class ToolBarState {
|
||||
constructor(
|
||||
public xref: boolean = false,
|
||||
public tree: boolean = false,
|
||||
public pages: boolean = false
|
||||
) {
|
||||
}
|
||||
}
|
||||
14
src/models/TreeViewModel.ts
Normal file
14
src/models/TreeViewModel.ts
Normal 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[],
|
||||
) { }
|
||||
}
|
||||
55
src/models/TreeViewRequest.svelte.ts
Normal file
55
src/models/TreeViewRequest.svelte.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
198
src/models/TreeViewState.svelte.ts
Normal file
198
src/models/TreeViewState.svelte.ts
Normal 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
7
src/models/XRefEntry.ts
Normal 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
6
src/models/XRefTable.ts
Normal 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
8
src/monaco-worker.ts
Normal 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`;
|
||||
}
|
||||
};
|
||||
5
src/routes/+layout.svelte
Normal file
5
src/routes/+layout.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>import "../app.css";
|
||||
</script>
|
||||
<slot/>
|
||||
|
||||
|
||||
5
src/routes/+layout.ts
Normal file
5
src/routes/+layout.ts
Normal 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
5
src/routes/+page.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import App from "../components/App.svelte";
|
||||
</script>
|
||||
|
||||
<App/>
|
||||
43
src/utils.ts
Normal file
43
src/utils.ts
Normal 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));
|
||||
}
|
||||
BIN
static/pdf-forge-logo-25x25.png
Normal file
BIN
static/pdf-forge-logo-25x25.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/pdf-forge-logo-30x30.png
Normal file
BIN
static/pdf-forge-logo-30x30.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/pdf-forge-logo-bg.png
Normal file
BIN
static/pdf-forge-logo-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 327 KiB |
BIN
static/pdf-forge-logo.png
Normal file
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
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
17
svelte.config.js
Normal 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
37
tailwind.config.ts
Normal 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
19
tsconfig.json
Normal 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
17
vite.config.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}));
|
||||
Loading…
x
Reference in New Issue
Block a user