diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..de96078 --- /dev/null +++ b/.containerignore @@ -0,0 +1,40 @@ +# Container Ignore Datei (analog zu .dockerignore) +# Verhindert, dass unnötige Dateien in den Build-Kontext kopiert werden + +# Git +.git +.github +.gitignore + +# Node +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build Output +main.js +styles.css +*.map + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode +.idea +*.swp +*.swo + +# Dokumentation +README.md +LICENSE + +# Container files (nicht in sich selbst kopieren) +Containerfile +Dockerfile +compose.yaml +docker-compose.yml +.containerignore +.dockerignore diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..863fec0 --- /dev/null +++ b/Containerfile @@ -0,0 +1,37 @@ +# Podman/Docker Containerfile für tldraw-in-obsidian Entwicklung +# Verwendung: podman build -t tldraw-dev -f Containerfile . +# podman run -it --rm -v .:/app:Z -p 3000:3000 tldraw-dev + +FROM node:24-alpine + +# Labels für Dokumentation +LABEL maintainer="tldraw-in-obsidian" +LABEL description="Development environment for tldraw-in-obsidian Obsidian plugin" + +# Arbeitsverzeichnis setzen +WORKDIR /app + +# Abhängigkeiten für native Module (falls benötigt) +RUN apk add --no-cache git python3 make g++ + +# npm konfigurieren für schnellere Installationen +RUN npm config set progress=false && \ + npm config set loglevel=warn + +# package.json und package-lock.json kopieren (für besseres Caching) +COPY package*.json ./ + +# patches Ordner kopieren (für postinstall patch-package) +COPY patches ./patches/ + +# Abhängigkeiten installieren +RUN npm ci + +# Rest des Quellcodes kopieren +COPY . . + +# Port für mögliche Entwicklungsserver +EXPOSE 3000 + +# Standard-Befehl: dev-Server starten +CMD ["npm", "run", "dev"] diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..8f3faee --- /dev/null +++ b/compose.yaml @@ -0,0 +1,27 @@ +# Podman Compose / Docker Compose für tldraw-in-obsidian Entwicklung +# Verwendung: podman-compose up +# ODER: podman compose up + +services: + dev: + build: + context: . + dockerfile: Containerfile + container_name: tldraw-dev + volumes: + # Source code mounten für Hot-Reload + - .:/app:Z + # node_modules im Container belassen (Performance) + - node_modules:/app/node_modules + ports: + - "3000:3000" + # Interaktives Terminal für Entwicklung + stdin_open: true + tty: true + # Automatisch neu starten falls Container abstürzt + restart: unless-stopped + # Befehl: npm run dev + command: npm run dev + +volumes: + node_modules: diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 6bdacd2..ce28aa5 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -2,7 +2,6 @@ import esbuild from "esbuild"; import process from "process"; import builtins from "builtin-modules"; import { readFileSync } from "fs"; -// import svgr from "esbuild-plugin-svgr"; const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD @@ -19,6 +18,20 @@ const TLDRAW_VERSION = (() => { console.log({ TLDRAW_VERSION }) +// Plugin to load pdf.worker.min.mjs as text for blob URL creation +const pdfWorkerPlugin = { + name: 'pdf-worker-text', + setup(build) { + build.onLoad({ filter: /pdf\.worker\.min\.mjs$/ }, async (args) => { + const contents = readFileSync(args.path, 'utf8'); + return { + contents: `export default ${JSON.stringify(contents)};`, + loader: 'js', + }; + }); + }, +}; + const context = await esbuild.context({ banner: { js: banner, @@ -46,7 +59,6 @@ const context = await esbuild.context({ logLevel: "info", sourcemap: prod ? false : "inline", treeShaking: true, - // outfile: "main.js", loader: { ".js": "jsx", ".woff2": "dataurl", @@ -60,8 +72,8 @@ const context = await esbuild.context({ "TLDRAW_COMPONENT_LOGGING": `${!prod}`, "MARKDOWN_POST_PROCESSING_LOGGING": `${!prod}`, "TLDRAW_VERSION": `"${TLDRAW_VERSION}"`, - } - // plugins: [svgr({ typescript: true })], + }, + plugins: [pdfWorkerPlugin], }); if (prod) { diff --git a/package-lock.json b/package-lock.json index b63adb4..39b648f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "tldraw-in-obsidian", - "version": "1.26.1", + "version": "1.26.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tldraw-in-obsidian", - "version": "1.26.1", + "version": "1.26.2", "hasInstallScript": true, "dependencies": { "monkey-around": "^2.3.0", + "pdf-lib": "^1.17.1", + "pdfjs-dist": "^4.10.38", "react": "^18.3.1", "react-dom": "^18.3.1", "tldraw": "^3.15.3", @@ -28,7 +30,7 @@ "typescript": "^5.5.4" } }, - "../../../../../../../repos/tldraw": { + "../repos/tldraw": { "name": "@tldraw/monorepo", "version": "0.0.0", "extraneous": true, @@ -526,7 +528,6 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -550,7 +551,6 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, - "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -598,7 +598,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, - "peer": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -613,7 +612,6 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "peer": true, "engines": { "node": ">=12.22" }, @@ -626,16 +624,264 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.88.tgz", + "integrity": "sha512-/p08f93LEbsL5mDZFQ3DBxcPv/I4QG9EDYRRq1WNlCOXVfAHBTHMSVMwxlqG/AtnSfUr9+vgfN7MKiyDo0+Weg==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.88", + "@napi-rs/canvas-darwin-arm64": "0.1.88", + "@napi-rs/canvas-darwin-x64": "0.1.88", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.88", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.88", + "@napi-rs/canvas-linux-arm64-musl": "0.1.88", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.88", + "@napi-rs/canvas-linux-x64-gnu": "0.1.88", + "@napi-rs/canvas-linux-x64-musl": "0.1.88", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.88", + "@napi-rs/canvas-win32-x64-msvc": "0.1.88" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.88.tgz", + "integrity": "sha512-KEaClPnZuVxJ8smUWjV1wWFkByBO/D+vy4lN+Dm5DFH514oqwukxKGeck9xcKJhaWJGjfruGmYGiwRe//+/zQQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.88.tgz", + "integrity": "sha512-Xgywz0dDxOKSgx3eZnK85WgGMmGrQEW7ZLA/E7raZdlEE+xXCozobgqz2ZvYigpB6DJFYkqnwHjqCOTSDGlFdg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.88.tgz", + "integrity": "sha512-Yz4wSCIQOUgNucgk+8NFtQxQxZV5NO8VKRl9ePKE6XoNyNVC8JDqtvhh3b3TPqKK8W5p2EQpAr1rjjm0mfBxdg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.88.tgz", + "integrity": "sha512-9gQM2SlTo76hYhxHi2XxWTAqpTOb+JtxMPEIr+H5nAhHhyEtNmTSDRtz93SP7mGd2G3Ojf2oF5tP9OdgtgXyKg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.88.tgz", + "integrity": "sha512-7qgaOBMXuVRk9Fzztzr3BchQKXDxGbY+nwsovD3I/Sx81e+sX0ReEDYHTItNb0Je4NHbAl7D0MKyd4SvUc04sg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.88.tgz", + "integrity": "sha512-kYyNrUsHLkoGHBc77u4Unh067GrfiCUMbGHC2+OTxbeWfZkPt2o32UOQkhnSswKd9Fko/wSqqGkY956bIUzruA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.88.tgz", + "integrity": "sha512-HVuH7QgzB0yavYdNZDRyAsn/ejoXB0hn8twwFnOqUbCCdkV+REna7RXjSR7+PdfW0qMQ2YYWsLvVBT5iL/mGpw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.88.tgz", + "integrity": "sha512-hvcvKIcPEQrvvJtJnwD35B3qk6umFJ8dFIr8bSymfrSMem0EQsfn1ztys8ETIFndTwdNWJKWluvxztA41ivsEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.88.tgz", + "integrity": "sha512-eSMpGYY2xnZSQ6UxYJ6plDboxq4KeJ4zT5HaVkUnbObNN6DlbJe0Mclh3wifAmquXfrlgTZt6zhHsUgz++AK6g==", + "cpu": [ + "x64" + ], "license": "MIT", - "peer": true + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.88.tgz", + "integrity": "sha512-qcIFfEgHrchyYqRrxsCeTQgpJZ/GqHiqPcU/Fvw/ARVlQeDX1VyFH+X+0gCR2tca6UJrq96vnW+5o7buCq+erA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.88", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.88.tgz", + "integrity": "sha512-ROVqbfS4QyZxYkqmaIBBpbz/BQvAR+05FXM5PAtTYVc0uyY8Y4BHJSMdGAaMf6TdIVRsQsiq+FG/dH9XhvWCFQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -672,6 +918,24 @@ "node": ">= 8" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -2200,6 +2464,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.5.tgz", "integrity": "sha512-jb0KTdUJaJY53JaN7ooY3XAxHQNoMYti/H6ANo707PsLXVeEqJ9o8+eBup1JU5CuwzrgnDc2dECt2WIGX9f8Jw==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -2529,6 +2794,7 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.5.tgz", "integrity": "sha512-z9JFtqc5ZOsdQLd9vRnXfTCQ8v5ADAfRt9Nm7SqP6FUHII8E1hs38ACzf5xursmth/VonJYb5+73Pqxk1hGIPw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-changeset": "^2.2.1", "prosemirror-collab": "^1.3.1", @@ -2766,6 +3032,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", "devOptional": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2776,6 +3043,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "devOptional": true, + "peer": true, "dependencies": { "@types/react": "*" } @@ -2833,6 +3101,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", "integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.12.0", "@typescript-eslint/types": "7.12.0", @@ -3008,8 +3277,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@use-gesture/core": { "version": "10.3.1", @@ -3053,7 +3321,6 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -3063,7 +3330,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3080,7 +3346,6 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -3180,7 +3445,6 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "peer": true, "engines": { "node": ">=6" } @@ -3304,8 +3568,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/detect-node-es": { "version": "1.1.0", @@ -3330,7 +3593,6 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -3461,7 +3723,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -3490,7 +3751,6 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "peer": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -3508,7 +3768,6 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, - "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -3521,7 +3780,6 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "peer": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -3534,7 +3792,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "peer": true, "engines": { "node": ">=4.0" } @@ -3544,7 +3801,6 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3592,15 +3848,13 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/fastq": { "version": "1.17.1", @@ -3616,7 +3870,6 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, - "peer": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -3641,7 +3894,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3667,7 +3919,6 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, - "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -3681,8 +3932,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/fractional-indexing-jittered": { "version": "1.0.0", @@ -3745,7 +3995,6 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -3758,7 +4007,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "peer": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -3838,7 +4086,6 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, - "peer": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3855,7 +4102,6 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "peer": true, "engines": { "node": ">=0.8.19" } @@ -3926,7 +4172,6 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -3968,7 +4213,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -3980,15 +4224,13 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/json-stable-stringify": { "version": "1.0.2", @@ -4006,8 +4248,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/jsonfile": { "version": "6.1.0", @@ -4035,7 +4276,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -4054,7 +4294,6 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4083,7 +4322,6 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -4111,8 +4349,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/lodash.throttle": { "version": "4.1.1", @@ -4282,7 +4519,6 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -4315,7 +4551,6 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4331,7 +4566,6 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -4342,12 +4576,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "peer": true, "dependencies": { "callsites": "^3.0.0" }, @@ -4412,7 +4651,6 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -4444,6 +4682,36 @@ "node": ">=8" } }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/pdfjs-dist": { + "version": "4.10.38", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", + "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.65" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4461,7 +4729,6 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "peer": true, "engines": { "node": ">= 0.8.0" } @@ -4578,6 +4845,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.0.tgz", "integrity": "sha512-/8XUmxWf0pkj2BmtqZHYJipTBMHIdVjuvFzMvEoxrtyGNmfvdhBiRwYt/eFwy2wA9DtBW3RLqvZnjurEkHaFCw==", "license": "MIT", + "peer": true, "dependencies": { "orderedmap": "^2.0.0" } @@ -4607,6 +4875,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -4655,6 +4924,7 @@ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.38.1.tgz", "integrity": "sha512-4FH/uM1A4PNyrxXbD+RAbAsf0d/mM0D/wAKSVVWK7o0A9Q/oOXJBrw786mBf2Vnrs/Edly6dH6Z2gsb7zWwaUw==", "license": "MIT", + "peer": true, "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", @@ -4666,7 +4936,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "peer": true, "engines": { "node": ">=6" } @@ -4781,6 +5050,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4792,6 +5062,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4874,7 +5145,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "peer": true, "engines": { "node": ">=4" } @@ -4895,7 +5165,6 @@ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -4990,7 +5259,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -5003,7 +5271,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "peer": true, "engines": { "node": ">=8" }, @@ -5016,8 +5283,7 @@ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", @@ -5035,8 +5301,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/tippy.js": { "version": "6.3.7", @@ -5119,7 +5384,6 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -5132,7 +5396,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -5145,6 +5408,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5173,7 +5437,6 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "peer": true, "dependencies": { "punycode": "^2.1.0" } @@ -5254,7 +5517,6 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5279,7 +5541,6 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 9a71a73..8348eb5 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ }, "dependencies": { "monkey-around": "^2.3.0", + "pdf-lib": "^1.17.1", + "pdfjs-dist": "^4.10.38", "react": "^18.3.1", "react-dom": "^18.3.1", "tldraw": "^3.15.3", diff --git a/src/components/TldrawApp.tsx b/src/components/TldrawApp.tsx index 3d82cb5..377c9bc 100644 --- a/src/components/TldrawApp.tsx +++ b/src/components/TldrawApp.tsx @@ -1,12 +1,15 @@ import * as React from "react"; import { createRoot } from "react-dom/client"; import { + DefaultContextMenu, + DefaultContextMenuContent, DefaultMainMenu, DefaultMainMenuContent, Editor, TLComponents, Tldraw, TldrawEditorStoreProps, + TldrawUiMenuGroup, TldrawUiMenuItem, TldrawUiMenuSubmenu, TLStateNodeConstructor, @@ -15,11 +18,12 @@ import { TLUiEventHandler, TLUiOverrides, useActions, + useEditor, } from "tldraw"; import { OPEN_FILE_ACTION, SAVE_FILE_COPY_ACTION, SAVE_FILE_COPY_IN_VAULT_ACTION } from "src/utils/file"; import { PLUGIN_ACTION_TOGGLE_ZOOM_LOCK, uiOverrides } from "src/tldraw/ui-overrides"; import TldrawPlugin from "src/main"; -import { Platform } from "obsidian"; +import { Platform, FuzzySuggestModal, Notice, TFile } from "obsidian"; import { useTldrawAppEffects } from "src/hooks/useTldrawAppHook"; import { useClickAwayListener } from "src/hooks/useClickAwayListener"; import { TLDataDocumentStore } from "src/utils/document"; @@ -29,6 +33,8 @@ import { lockZoomIcon } from "src/assets/data-icons"; import { isObsidianThemeDark } from "src/utils/utils"; import { TldrawInObsidianPluginProvider } from "src/contexts/plugin"; import { PTLEditorBlockBlur } from "src/utils/dom-attributes"; +import { usePdfDynamicRendering } from "./pdf/usePdfDynamicRendering"; +import { upgradeLegacyPdfShapes } from "src/utils/migration"; type TldrawAppOptions = { iconAssetUrls?: TLUiAssetUrlOverrides['icons'], @@ -100,10 +106,239 @@ const components = (plugin: TldrawPlugin): TLComponents => ({ ), KeyboardShortcutsDialog: PluginKeyboardShortcutsDialog, QuickActions: PluginQuickActions, + ContextMenu: (props) => , }); +// Custom context menu with PDF DPI option +function CustomContextMenu(props: any) { + const editor = useEditor(); + const actions = useActions(); + + // Helper to recursively get all descendant shapes + function getAllDescendants(shapes: any[]): any[] { + const result: any[] = []; + for (const shape of shapes) { + result.push(shape); + if (shape.type === 'group') { + const children = editor.getSortedChildIdsForParent(shape.id) + .map((id: any) => editor.getShape(id)) + .filter(Boolean); + result.push(...getAllDescendants(children as any[])); + } + } + return result; + } + + // Check if any selected shape is a PDF + const selectedShapes = editor.getSelectedShapes(); + const allShapes = getAllDescendants(selectedShapes); + const hasPdfSelected = allShapes.some((shape: any) => { + if (shape.type !== 'image') return false; + const asset = editor.getAsset((shape.props as any).assetId); + return asset && (asset.meta as any)?.isPdfAsset; + }); + + const handleChangePdfDpi = React.useCallback(() => { + actions['change-pdf-dpi']?.onSelect('context-menu' as any); + }, [actions]); + + return ( + + {hasPdfSelected && ( + + + + )} + + + ); +} + + function LocalFileMenu(props: { plugin: TldrawPlugin }) { const actions = useActions(); + const editor = useEditor(); + + const handleImportPdf = React.useCallback(async () => { + if (!editor) return; + + console.log("[PDF Import] Starting import..."); + try { + // Import PDF loading utilities + const { loadPdfMetadata } = await import("./pdf/loadPdf"); + const { AssetRecordType } = await import("tldraw"); + + // Get PDF files from vault + // @ts-ignore + const pdfFiles = props.plugin.app.vault.getFiles().filter((file: any) => file.extension === "pdf"); + + if (pdfFiles.length === 0) { + new Notice("No PDF files found in vault"); + return; + } + + // Use Obsidian's suggest modal + // @ts-ignore + class PdfPickerModal extends FuzzySuggestModal { + private resolve: (file: any) => void; + + constructor(app: any, resolve: (file: any) => void) { + super(app); + this.resolve = resolve; + // @ts-ignore + this.setPlaceholder("Search for a PDF file..."); + } + + getItems() { + return pdfFiles; + } + + getItemText(item: any) { + return item.path; + } + + onChooseItem(item: any) { + this.resolve(item); + } + } + + const selectedFile = await new Promise((resolve) => { + // @ts-ignore + const modal = new PdfPickerModal(props.plugin.app, resolve); + // @ts-ignore + modal.open(); + }); + + if (!selectedFile) return; + + console.log("[PDF Import] File selected:", selectedFile.path); + + // Load pages metadata (no images!) + const pages = await loadPdfMetadata( + props.plugin.app, + selectedFile.path + ); + + console.log("[PDF Import] Loaded metadata for", pages.length, "pages"); + + // Show import options modal + const { default: PdfImportModal, PdfImportCanceled } = await import("src/obsidian/modal/PdfImportModal"); + let importOptions; + try { + importOptions = await PdfImportModal.show( + props.plugin, + selectedFile, + pages + ); + } catch (e) { + if (e instanceof PdfImportCanceled) { + console.log("[PDF Import] Canceled by user"); + return; + } + throw e; + } + + const { selectedPages, groupPages, spacing } = importOptions; + const pagesToImport = pages.filter(p => selectedPages.includes(p.pageNumber)); + + if (pagesToImport.length === 0) { + new Notice("No pages selected"); + return; + } + + console.log("[PDF Import] Importing", pagesToImport.length, "pages"); + + // Calculate start X position (to the right of existing shapes) + const existingBounds = editor.getCurrentPageBounds(); + const startX = existingBounds ? existingBounds.maxX + spacing : 0; + + const PAGE_SPACING = 32; + let top = existingBounds ? existingBounds.minY : 0; + let widest = 0; + + // Calculate layout + for (const page of pagesToImport) { + widest = Math.max(widest, page.width); + } + + const shapes: any[] = []; + const assets: any[] = []; + const shapeIds: any[] = []; + const now = Date.now(); + + // Create image shapes with PDF protocol assets + for (const page of pagesToImport) { + const assetId = `asset:pdf-${now}-${page.pageNumber}` as any; + const shapeId = `shape:pdf-${now}-${page.pageNumber}` as any; + shapeIds.push(shapeId); + + const x = startX + (widest - page.width) / 2; + const y = top; + + // Create asset with PDF protocol URL (no image data stored!) + assets.push({ + id: assetId, + type: 'image', + typeName: 'asset', + props: { + name: `${selectedFile.basename} page ${page.pageNumber}`, + src: `asset:pdf.[[${selectedFile.name}]]#${page.pageNumber}`, + w: page.width, + h: page.height, + mimeType: 'image/png', + isAnimated: false, + }, + meta: { isPdfAsset: true, pdfPath: selectedFile.path, pageNumber: page.pageNumber, dpi: importOptions.dpi }, + }); + + // Create standard image shape + shapes.push({ + id: shapeId, + type: 'image', + x: x, + y: y, + props: { + assetId: assetId, + w: page.width, + h: page.height, + }, + meta: { isPdfPage: true }, + }); + + top += page.height + PAGE_SPACING; + } + + // Batch create assets and shapes + editor.run(() => { + editor.store.put(assets); + editor.createShapes(shapes); + + // Group if requested + if (groupPages && shapeIds.length > 1) { + editor.select(...shapeIds); + editor.groupShapes(shapeIds); + } + }); + + console.log("[PDF Import] Created", shapes.length, "shapes", groupPages ? "(grouped)" : ""); + + // Set zoom to 100% and center on first page + setTimeout(() => { + editor.resetZoom(); + console.log("[PDF Import] Set zoom to 100%"); + }, 100); + + + } catch (error) { + console.error("[PDF Import] Error:", error); + new Notice("PDF Import failed: " + (error as Error).message); + } + }, [editor, props.plugin]); + return ( @@ -114,6 +349,17 @@ function LocalFileMenu(props: { plugin: TldrawPlugin }) { } + + actions['change-pdf-dpi']?.onSelect?.('menu' as any)} + /> ); } @@ -170,6 +416,10 @@ const TldrawApp = ({ plugin, store, _onInitialSnapshot(editor.store.getStoreSnapshot()); setOnInitialSnapshot(undefined); } + // Run auto-migration for legacy PDF shapes + setTimeout(() => { + upgradeLegacyPdfShapes(editor); + }, 1000); }, [_onInitialSnapshot]) const onUiEvent = React.useCallback((...args) => { @@ -204,11 +454,15 @@ const TldrawApp = ({ plugin, store, setFocusedEditor: (editor) => setFocusedEditor(true, editor), }); + // Enable dynamic PDF rendering + // Moved from LocalFileMenu to ensure it stays mounted + // usePdfDynamicRendering(editor ?? null, plugin.app); // REPLACED BY CUSTOM SHAPE + const editorContainerRef = useClickAwayListener({ enableClickAwayListener: isFocused, handler(ev) { // We allow event targets to specify if they should block the editor from being blurred. - if(PTLEditorBlockBlur.shouldEventBlockBlur(ev)) return; + if (PTLEditorBlockBlur.shouldEventBlockBlur(ev)) return; const blurEditor = onClickAwayBlur?.(ev); if (blurEditor !== undefined && !blurEditor) return; diff --git a/src/components/pdf/ExportPdfButton.tsx b/src/components/pdf/ExportPdfButton.tsx new file mode 100644 index 0000000..8292e6a --- /dev/null +++ b/src/components/pdf/ExportPdfButton.tsx @@ -0,0 +1,112 @@ +import * as React from "react"; +import { PDFDocument } from "pdf-lib"; +import { useState } from "react"; +import { Editor, useEditor } from "tldraw"; +import { Pdf } from "./pdf.types"; +import { App, Notice } from "obsidian"; + +interface ExportPdfButtonProps { + pdf: Pdf; + app: App; +} + +export function ExportPdfButton({ pdf, app }: ExportPdfButtonProps) { + const [exportProgress, setExportProgress] = useState(null); + const editor = useEditor(); + + return ( + + ); +} + +async function exportPdf( + editor: Editor, + { name, source, pages }: Pdf, + app: App, + onProgress: (progress: number) => void +) { + const totalThings = pages.length * 2 + 2; + let progressCount = 0; + const tickProgress = () => { + progressCount++; + onProgress(progressCount / totalThings); + }; + + const pdfDoc = await PDFDocument.load(source); + tickProgress(); + + const pdfPages = pdfDoc.getPages(); + if (pdfPages.length !== pages.length) { + throw new Error("PDF page count mismatch"); + } + + const pageShapeIds = new Set(pages.map((page) => page.shapeId)); + const allIds = Array.from(editor.getCurrentPageShapeIds()).filter( + (id) => !pageShapeIds.has(id) + ); + + for (let i = 0; i < pages.length; i++) { + const page = pages[i]; + const pdfPage = pdfPages[i]; + + const bounds = page.bounds; + const shapesInBounds = allIds.filter((id) => { + const shapePageBounds = editor.getShapePageBounds(id); + if (!shapePageBounds) return false; + return shapePageBounds.collides(bounds); + }); + + if (shapesInBounds.length === 0) { + tickProgress(); + tickProgress(); + continue; + } + + const exportedPng = await editor.toImage(allIds, { + format: "png", + background: false, + bounds: page.bounds, + padding: 0, + scale: 1, + }); + tickProgress(); + + const pngBytes = await exportedPng.blob.arrayBuffer(); + const pngImage = await pdfDoc.embedPng(pngBytes); + pdfPage.drawImage(pngImage, { + x: 0, + y: 0, + width: pdfPage.getWidth(), + height: pdfPage.getHeight(), + }); + tickProgress(); + } + + const pdfBytes = await pdfDoc.save(); + tickProgress(); + + // Save to Obsidian vault + const outputName = name.replace(/\.pdf$/i, "_annotated.pdf"); + const outputPath = outputName; + + await app.vault.createBinary(outputPath, pdfBytes); + new Notice(`Saved as ${outputPath}`); +} diff --git a/src/components/pdf/PdfEditor.tsx b/src/components/pdf/PdfEditor.tsx new file mode 100644 index 0000000..f15f689 --- /dev/null +++ b/src/components/pdf/PdfEditor.tsx @@ -0,0 +1,219 @@ +import * as React from "react"; +import { useMemo } from "react"; +import { + Box, + SVGContainer, + TLComponents, + TLImageShape, + TLShapePartial, + Tldraw, + getIndicesBetween, + react, + sortByIndex, + track, + useEditor, + Editor, + TLShape, + TLShapeId, +} from "tldraw"; +import { ExportPdfButton } from "./ExportPdfButton"; +import { Pdf } from "./pdf.types"; +import { App } from "obsidian"; +import { isObsidianThemeDark } from "src/utils/utils"; + +interface PdfEditorProps { + pdf: Pdf; + app: App; + isDarkMode?: boolean; +} + +/** + * PDF Editor component that renders PDF pages as locked image shapes + * and allows annotations on top of them + */ +export function PdfEditor({ pdf, app, isDarkMode }: PdfEditorProps) { + const darkMode = isDarkMode ?? isObsidianThemeDark(); + + const components = useMemo( + () => ({ + PageMenu: null, + Overlays: () => , + SharePanel: () => , + }), + [pdf, app, darkMode] + ); + + return ( + { + // Create assets for each PDF page + editor.createAssets( + pdf.pages.map((page) => ({ + id: page.assetId, + typeName: "asset", + type: "image", + meta: {}, + props: { + w: page.bounds.w, + h: page.bounds.h, + mimeType: "image/png", + src: page.src, + name: "page", + isAnimated: false, + }, + })) + ); + + // Create image shapes for each page (locked) + editor.createShapes( + pdf.pages.map( + (page): TLShapePartial => ({ + id: page.shapeId, + type: "image", + x: page.bounds.x, + y: page.bounds.y, + isLocked: true, + props: { + assetId: page.assetId, + w: page.bounds.w, + h: page.bounds.h, + }, + }) + ) + ); + + const shapeIds = pdf.pages.map((page) => page.shapeId); + const shapeIdSet = new Set(shapeIds); + + // Prevent unlocking PDF page shapes + editor.sideEffects.registerBeforeChangeHandler("shape", (prev, next) => { + if (!shapeIdSet.has(next.id)) return next; + if (next.isLocked) return next; + return { ...prev, isLocked: true }; + }); + + // Keep PDF pages at the bottom of the z-order + function makeSureShapesAreAtBottom() { + const shapes = shapeIds + .map((id) => editor.getShape(id)!) + .filter(Boolean) + .sort(sortByIndex); + if (shapes.length === 0) return; + + const pageId = editor.getCurrentPageId(); + const siblings = editor.getSortedChildIdsForParent(pageId); + const currentBottomShapes = siblings + .slice(0, shapes.length) + .map((id) => editor.getShape(id)!); + + if (currentBottomShapes.every((shape, i) => shape.id === shapes[i].id)) return; + + const otherSiblings = siblings.filter((id) => !shapeIdSet.has(id)); + if (otherSiblings.length === 0) return; + + const bottomSibling = otherSiblings[0]; + const bottomShape = editor.getShape(bottomSibling); + if (!bottomShape) return; + + const lowestIndex = bottomShape.index; + const indexes = getIndicesBetween(undefined, lowestIndex, shapes.length); + + editor.updateShapes( + shapes.map((shape, i) => ({ + ...shape, + id: shape.id, + isLocked: shape.isLocked, + index: indexes[i], + })) + ); + } + + makeSureShapesAreAtBottom(); + editor.sideEffects.registerAfterCreateHandler("shape", makeSureShapesAreAtBottom); + editor.sideEffects.registerAfterChangeHandler("shape", makeSureShapesAreAtBottom); + + // Configure camera constraints for PDF navigation + const targetBounds = pdf.pages.reduce( + (acc, page) => acc.union(page.bounds), + pdf.pages[0].bounds.clone() + ); + + function updateCameraBounds(isMobile: boolean) { + editor.setCameraOptions({ + constraints: { + bounds: targetBounds, + padding: { x: isMobile ? 16 : 164, y: 64 }, + origin: { x: 0.5, y: 0 }, + initialZoom: "fit-x-100", + baseZoom: "default", + behavior: "contain", + }, + }); + editor.setCamera(editor.getCamera(), { reset: true }); + } + + let isMobile = editor.getViewportScreenBounds().width < 840; + + react("update camera", () => { + const isMobileNow = editor.getViewportScreenBounds().width < 840; + if (isMobileNow === isMobile) return; + isMobile = isMobileNow; + updateCameraBounds(isMobile); + }); + + updateCameraBounds(isMobile); + }} + components={components} + /> + ); +} + +/** + * Overlay component that dims areas outside PDF pages + */ +const PageOverlayScreen = track(function PageOverlayScreen({ + pdf, + isDarkMode, +}: { + pdf: Pdf; + isDarkMode: boolean; +}) { + const editor = useEditor(); + const viewportPageBounds = editor.getViewportPageBounds(); + + const relevantPageBounds = pdf.pages + .map((page) => { + if (!viewportPageBounds.collides(page.bounds)) return null; + return page.bounds; + }) + .filter((bounds): bounds is Box => bounds !== null); + + function pathForPageBounds(bounds: Box) { + return `M ${bounds.x} ${bounds.y} L ${bounds.maxX} ${bounds.y} L ${bounds.maxX} ${bounds.maxY} L ${bounds.x} ${bounds.maxY} Z`; + } + + const viewportPath = `M ${viewportPageBounds.x} ${viewportPageBounds.y} L ${viewportPageBounds.maxX} ${viewportPageBounds.y} L ${viewportPageBounds.maxX} ${viewportPageBounds.maxY} L ${viewportPageBounds.x} ${viewportPageBounds.maxY} Z`; + + return ( + <> + + + + {relevantPageBounds.map((bounds, i) => ( +
+ ))} + + ); +}); diff --git a/src/components/pdf/PdfPageRenderer.tsx b/src/components/pdf/PdfPageRenderer.tsx new file mode 100644 index 0000000..fbcd07d --- /dev/null +++ b/src/components/pdf/PdfPageRenderer.tsx @@ -0,0 +1,164 @@ +import * as React from "react"; +import { useEditor } from "tldraw"; +import { useObsidian } from "src/contexts/plugin"; +import * as PdfJS from "pdfjs-dist"; + +// Debounce utility +function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = React.useState(value); + + React.useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => clearTimeout(handler); + }, [value, delay]); + + return debouncedValue; +} + +// Cache for loaded PDFs +const pdfCache = new Map>(); + +// Load PDF logic (moved here or shared) +async function loadPdfDocument(app: any, pdfPath: string): Promise { + if (!pdfCache.has(pdfPath)) { + const loadPromise = (async () => { + if (!app) throw new Error("Obsidian app not available"); + const file = app.vault.getAbstractFileByPath(pdfPath); + if (!file) throw new Error(`PDF not found: ${pdfPath}`); + const arrayBuffer = await app.vault.readBinary(file); + return await PdfJS.getDocument({ data: new Uint8Array(arrayBuffer) }).promise; + })(); + pdfCache.set(pdfPath, loadPromise); + } + return pdfCache.get(pdfPath)!; +} + +export function PdfPageRenderer({ + pdfPath, + pageNumber, + width, + height, +}: { + pdfPath: string; + pageNumber: number; + width: number; + height: number; +}) { + const canvasRef = React.useRef(null); + const [error, setError] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const renderIdRef = React.useRef(0); + + const editor = useEditor(); + const app = useObsidian(); + const zoomLevel = editor.getZoomLevel(); + // Debounce zoom changes to avoid too many re-renders + const debouncedZoom = useDebounce(zoomLevel, 100); + + const renderScale = React.useMemo(() => { + const dpr = window.devicePixelRatio || 1; + // Improve base quality: at least 3.0 for sharper text + const baseQuality = Math.max(dpr, 3.0); + + // Scale with zoom, but cap at 5x relative to base + const zoomFactor = Math.min(Math.max(debouncedZoom, 1), 5); + + return baseQuality * zoomFactor; + }, [debouncedZoom]); + + React.useEffect(() => { + renderIdRef.current += 1; + const currentRenderId = renderIdRef.current; + + async function renderPage() { + if (!canvasRef.current || !pdfPath) return; + + try { + // Determine if we need to show loading (only if switching pages/files, not just zoom) + // Actually, for zoom updates, we prefer keeping the old canvas visible until new one is ready + // but we explicitly want to "sharpen". For now, keeping simple loading state is safer for bugs. + // Optim: Don't set loading=true if we are just re-scaling? + // Let's keep it simple: set loading. + + // setLoading(true); // Maybe don't flash loading on zoom? + // Visual polish: only set loading if completely new pdfPath/pageNumber + + // Ensure we have the doc + const pdf = await loadPdfDocument(app, pdfPath); + if (currentRenderId !== renderIdRef.current) return; + + const page = await pdf.getPage(pageNumber); + if (currentRenderId !== renderIdRef.current) return; + + const canvas = canvasRef.current; + if (!canvas) return; // Guard against unmount + const context = canvas.getContext('2d', { alpha: false }); + if (!context) return; + + const baseViewport = page.getViewport({ scale: 1 }); + + // Calculate scale to fill the shape dimensions + // We use 'width' / 'height' from props which match the Shape's dimensions in Tldraw + const fitScaleX = width / baseViewport.width; + const fitScaleY = height / baseViewport.height; + const fitScale = Math.min(fitScaleX, fitScaleY); + + const finalScale = fitScale * renderScale; + const scaledViewport = page.getViewport({ scale: finalScale }); + + // Update canvas internal dimensions (high res) + canvas.width = scaledViewport.width; + canvas.height = scaledViewport.height; + + // Update canvas display dimensions (match shape) + canvas.style.width = '100%'; + canvas.style.height = '100%'; + + context.clearRect(0, 0, canvas.width, canvas.height); + await page.render({ + canvasContext: context, + viewport: scaledViewport, + }).promise; + + if (currentRenderId === renderIdRef.current) { + setLoading(false); + setError(null); + } + } catch (err) { + if (currentRenderId === renderIdRef.current) { + console.error("PDF Render Error:", err); + setError((err as Error).message); + setLoading(false); + } + } + } + + renderPage(); + }, [pdfPath, pageNumber, width, height, renderScale, app]); + + if (error) { + return ( +
+ Failed to load PDF +
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/src/components/pdf/PdfPicker.ts b/src/components/pdf/PdfPicker.ts new file mode 100644 index 0000000..4679830 --- /dev/null +++ b/src/components/pdf/PdfPicker.ts @@ -0,0 +1,68 @@ +import { App, FuzzySuggestModal, TFile } from "obsidian"; +import { loadPdf } from "./loadPdf"; +import { Pdf } from "./pdf.types"; + +/** + * Modal for selecting a PDF file from the Obsidian vault + */ +export class PdfPickerModal extends FuzzySuggestModal { + private onSelect: (pdf: Pdf) => void; + private onCancel?: () => void; + + constructor( + app: App, + onSelect: (pdf: Pdf) => void, + onCancel?: () => void + ) { + super(app); + this.onSelect = onSelect; + this.onCancel = onCancel; + this.setPlaceholder("Search for a PDF file..."); + } + + getItems(): TFile[] { + return this.app.vault.getFiles().filter((file) => file.extension === "pdf"); + } + + getItemText(item: TFile): string { + return item.path; + } + + async onChooseItem(item: TFile): Promise { + try { + const arrayBuffer = await this.app.vault.readBinary(item); + const pdf = await loadPdf(item.name, arrayBuffer); + this.onSelect(pdf); + } catch (error) { + console.error("Failed to load PDF:", error); + } + } + + onClose(): void { + super.onClose(); + // Only call onCancel if no item was selected + // This is a bit tricky - we'll handle this via the caller + } +} + +/** + * Opens the PDF picker modal and returns a promise that resolves with the selected PDF + */ +export function openPdfPicker(app: App): Promise { + return new Promise((resolve) => { + let resolved = false; + const modal = new PdfPickerModal( + app, + (pdf) => { + resolved = true; + resolve(pdf); + }, + () => { + if (!resolved) { + resolve(null); + } + } + ); + modal.open(); + }); +} diff --git a/src/components/pdf/index.ts b/src/components/pdf/index.ts new file mode 100644 index 0000000..8e2e8a6 --- /dev/null +++ b/src/components/pdf/index.ts @@ -0,0 +1,5 @@ +export { PdfEditor } from "./PdfEditor"; +export { ExportPdfButton } from "./ExportPdfButton"; +export { PdfPickerModal, openPdfPicker } from "./PdfPicker"; +export { loadPdf } from "./loadPdf"; +export type { Pdf, PdfPage, PdfLoadOptions } from "./pdf.types"; diff --git a/src/components/pdf/loadPdf.ts b/src/components/pdf/loadPdf.ts new file mode 100644 index 0000000..67dd853 --- /dev/null +++ b/src/components/pdf/loadPdf.ts @@ -0,0 +1,61 @@ +import * as PdfJS from "pdfjs-dist"; +import { AssetRecordType, Box, createShapeId } from "tldraw"; + +// Import the real PDF.js worker code as a string (via esbuild text loader) +// @ts-ignore - imported as text via esbuild loader config +import pdfWorkerCode from "pdfjs-dist/build/pdf.worker.min.mjs"; + +// Create a blob URL from the real worker code +const workerBlob = new Blob([pdfWorkerCode], { type: 'application/javascript' }); +PdfJS.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob); + +export interface PdfPageInfo { + pageNumber: number; + width: number; + height: number; + src?: string; // Optional thumbnail data URL +} + +/** + * Load PDF page dimensions only. + */ +export async function loadPdfMetadata( + app: any, // Obsidian App + pdfPath: string +): Promise { + console.log("[loadPdfMetadata] Loading PDF:", pdfPath); + + const file = app.vault.getAbstractFileByPath(pdfPath); + if (!file) { + throw new Error(`PDF not found: ${pdfPath}`); + } + + const arrayBuffer = await app.vault.readBinary(file); + + const loadingTask = PdfJS.getDocument({ + data: new Uint8Array(arrayBuffer), + }); + + const pdf = await loadingTask.promise; + console.log("[loadPdfMetadata] PDF loaded, pages:", pdf.numPages); + + const pages: PdfPageInfo[] = []; + + // Scale 1.0 for dimensions (72 DPI) + // tldraw uses 1 unit = 1 pixel at 100% zoom + const DIMENSION_SCALE = 1.0; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const viewport = page.getViewport({ scale: DIMENSION_SCALE }); + + pages.push({ + pageNumber: i, + width: viewport.width, + height: viewport.height, + }); + } + + return pages; +} + diff --git a/src/components/pdf/pdf.types.ts b/src/components/pdf/pdf.types.ts new file mode 100644 index 0000000..9d77e9d --- /dev/null +++ b/src/components/pdf/pdf.types.ts @@ -0,0 +1,37 @@ +import { Box, TLAssetId, TLShapeId } from "tldraw"; + +/** + * Represents a single page of a PDF document + */ +export interface PdfPage { + /** Base64 data URL of the rendered page image */ + src: string; + /** Bounds of the page in the tldraw canvas */ + bounds: Box; + /** Asset ID for the page image */ + assetId: TLAssetId; + /** Shape ID for the page image shape */ + shapeId: TLShapeId; +} + +/** + * Represents a loaded PDF document + */ +export interface Pdf { + /** Name of the PDF file */ + name: string; + /** Array of rendered pages */ + pages: PdfPage[]; + /** Original PDF source data */ + source: ArrayBuffer; +} + +/** + * Options for loading a PDF + */ +export interface PdfLoadOptions { + /** Visual scale for rendering (default: 1.5) */ + visualScale?: number; + /** Spacing between pages in pixels (default: 32) */ + pageSpacing?: number; +} diff --git a/src/contexts/plugin.tsx b/src/contexts/plugin.tsx index 3a02f0f..6942dc3 100644 --- a/src/contexts/plugin.tsx +++ b/src/contexts/plugin.tsx @@ -1,7 +1,7 @@ import React, { createContext, ReactNode, useContext, useMemo } from "react"; -import TldrawPlugin from "src/main"; +import type TldrawPlugin from "src/main"; import TldrawInObsidianPluginInstance from "src/obsidian/plugin/instance"; export const PluginContext = createContext(undefined); diff --git a/src/hooks/useTldrawAppHook.ts b/src/hooks/useTldrawAppHook.ts index 6cf3d44..713e8cc 100644 --- a/src/hooks/useTldrawAppHook.ts +++ b/src/hooks/useTldrawAppHook.ts @@ -94,4 +94,31 @@ export function useTldrawAppEffects({ isPasteAtCursorMode: settings.clipboard?.pasteAtCursor }); }, [editor, settings]); + + /** + * Effect for syncing with Obsidian theme changes + */ + React.useEffect(() => { + if (!editor) return; + + const { themeMode } = settingsManager.settings; + + // Sync with Obsidian unless user explicitly set a fixed theme + if (themeMode === 'light' || themeMode === 'dark') return; + + // Watch for theme changes on the body element + const observer = new MutationObserver(() => { + const isDark = isObsidianThemeDark(); + editor.user.updateUserPreferences({ + colorScheme: isDark ? 'dark' : 'light', + }); + }); + + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class'] + }); + + return () => observer.disconnect(); + }, [editor, settingsManager.settings.themeMode]); } diff --git a/src/obsidian/TldrawMixins.ts b/src/obsidian/TldrawMixins.ts index 43f1716..518ab2f 100644 --- a/src/obsidian/TldrawMixins.ts +++ b/src/obsidian/TldrawMixins.ts @@ -100,7 +100,7 @@ export function TldrawLoadableMixin F editor.navigateToDeepLink(this.#deepLink); return; } - return editor.zoomToFit(); + return editor.resetZoom(); } }; } diff --git a/src/obsidian/modal/PdfImportModal.ts b/src/obsidian/modal/PdfImportModal.ts new file mode 100644 index 0000000..738e08f --- /dev/null +++ b/src/obsidian/modal/PdfImportModal.ts @@ -0,0 +1,229 @@ +import { Modal, Setting, TFile } from "obsidian"; +import TldrawPlugin from "src/main"; + +export interface PdfPageInfo { + pageNumber: number; + width: number; + height: number; +} + +export interface PdfImportOptions { + selectedPages: number[]; + groupPages: boolean; + spacing: number; + dpi: number; +} + +export class PdfImportCanceled extends Error { + constructor() { super('PDF import canceled'); } +} + +export default class PdfImportModal extends Modal { + private selectedPages: Set; + private groupPages: boolean = false; + private spacing: number = 100; + private dpi: number = 150; + private pageListEl!: HTMLElement; + private rangeInputEl!: HTMLInputElement; + + constructor( + private readonly plugin: TldrawPlugin, + private readonly pdfFile: TFile, + private readonly pages: PdfPageInfo[], + private readonly res: (options: PdfImportOptions) => void, + private readonly rej: (err: unknown) => void, + ) { + super(plugin.app); + // All pages selected by default + this.selectedPages = new Set(pages.map(p => p.pageNumber)); + } + + static async show( + plugin: TldrawPlugin, + pdfFile: TFile, + pages: PdfPageInfo[] + ): Promise { + return new Promise((res, rej) => { + const modal = new PdfImportModal(plugin, pdfFile, pages, res, rej); + modal.open(); + }); + } + + onOpen(): void { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('ptl-pdf-import-modal'); + + // Title + contentEl.createEl('h2', { text: `Import PDF: ${this.pdfFile.basename}` }); + + // Page selection header + const pageHeader = contentEl.createDiv({ cls: 'ptl-pdf-import-header' }); + pageHeader.createEl('h4', { text: `Pages (${this.pages.length} total)` }); + + // Select all / none buttons + const selectBtns = pageHeader.createDiv({ cls: 'ptl-pdf-import-select-btns' }); + selectBtns.createEl('button', { text: 'All' }).onclick = () => { + this.selectedPages = new Set(this.pages.map(p => p.pageNumber)); + this.updatePageList(); + this.updateRangeInput(); + }; + selectBtns.createEl('button', { text: 'None' }).onclick = () => { + this.selectedPages.clear(); + this.updatePageList(); + this.updateRangeInput(); + }; + + // Page range input + const rangeContainer = contentEl.createDiv({ cls: 'ptl-pdf-import-range' }); + rangeContainer.createSpan({ text: 'Pages: ' }); + this.rangeInputEl = rangeContainer.createEl('input', { + type: 'text', + placeholder: `e.g. 1-3,5,7-10 (1-${this.pages.length})`, + cls: 'ptl-pdf-import-range-input' + }); + this.rangeInputEl.value = this.pagesToRangeString(); + this.rangeInputEl.oninput = () => { + this.parseRangeInput(this.rangeInputEl.value); + this.updatePageList(); + }; + + // Page list with checkboxes + this.pageListEl = contentEl.createDiv({ cls: 'ptl-pdf-import-pages' }); + this.updatePageList(); + + // Options + new Setting(contentEl) + .setName('Group pages') + .setDesc('Create a group containing all imported pages') + .addToggle(toggle => toggle + .setValue(this.groupPages) + .onChange(value => this.groupPages = value)); + + new Setting(contentEl) + .setName('Spacing (px)') + .setDesc('Horizontal space between this PDF and existing shapes') + .addText(text => text + .setValue(String(this.spacing)) + .onChange(value => { + const num = parseInt(value) || 100; + this.spacing = Math.max(0, num); + })); + + new Setting(contentEl) + .setName('DPI (render quality)') + .setDesc('Higher = sharper but slower (72-300, default: 150)') + .addText(text => text + .setValue(String(this.dpi)) + .onChange(value => { + const num = parseInt(value) || 150; + this.dpi = Math.min(300, Math.max(72, num)); + })); + + // Buttons + const buttonContainer = contentEl.createDiv({ cls: 'ptl-pdf-import-buttons' }); + + buttonContainer.createEl('button', { text: 'Cancel' }).onclick = () => { + this.close(); + }; + + const importBtn = buttonContainer.createEl('button', { + text: 'Import', + cls: 'mod-cta' + }); + importBtn.onclick = () => { + if (this.selectedPages.size === 0) { + return; // Nothing to import + } + this.res({ + selectedPages: Array.from(this.selectedPages).sort((a, b) => a - b), + groupPages: this.groupPages, + spacing: this.spacing, + dpi: this.dpi, + }); + this.close(); + }; + } + + private updatePageList(): void { + this.pageListEl.empty(); + + for (const page of this.pages) { + const isSelected = this.selectedPages.has(page.pageNumber); + const pageItem = this.pageListEl.createDiv({ cls: 'ptl-pdf-import-page-item' }); + + const checkbox = pageItem.createEl('input', { type: 'checkbox' }); + checkbox.checked = isSelected; + checkbox.onclick = () => { + if (checkbox.checked) { + this.selectedPages.add(page.pageNumber); + } else { + this.selectedPages.delete(page.pageNumber); + } + this.updateRangeInput(); + }; + + pageItem.createSpan({ text: `Page ${page.pageNumber}` }); + pageItem.createSpan({ + text: `(${Math.round(page.width)} × ${Math.round(page.height)})`, + cls: 'ptl-pdf-import-page-size' + }); + } + } + + /** Parse range string like "1-3,5,7-10" into page numbers */ + private parseRangeInput(value: string): void { + const maxPage = this.pages.length; + const result = new Set(); + + const parts = value.split(',').map(s => s.trim()).filter(s => s); + for (const part of parts) { + if (part.includes('-')) { + const [startStr, endStr] = part.split('-'); + const start = parseInt(startStr) || 1; + const end = parseInt(endStr) || maxPage; + for (let i = Math.max(1, start); i <= Math.min(maxPage, end); i++) { + result.add(i); + } + } else { + const num = parseInt(part); + if (num >= 1 && num <= maxPage) { + result.add(num); + } + } + } + + this.selectedPages = result; + } + + /** Convert selected pages to compact range string */ + private pagesToRangeString(): string { + const sorted = Array.from(this.selectedPages).sort((a, b) => a - b); + if (sorted.length === 0) return ''; + if (sorted.length === this.pages.length) return `1-${this.pages.length}`; + + const ranges: string[] = []; + let start = sorted[0]; + let end = start; + + for (let i = 1; i <= sorted.length; i++) { + if (sorted[i] === end + 1) { + end = sorted[i]; + } else { + ranges.push(start === end ? String(start) : `${start}-${end}`); + start = sorted[i]; + end = start; + } + } + + return ranges.join(','); + } + + private updateRangeInput(): void { + this.rangeInputEl.value = this.pagesToRangeString(); + } + + onClose(): void { + this.rej(new PdfImportCanceled()); + } +} diff --git a/src/obsidian/modal/TldrawStoreExistsIndexedDBModal.ts b/src/obsidian/modal/TldrawStoreExistsIndexedDBModal.ts index 412aa5d..9b1f886 100644 --- a/src/obsidian/modal/TldrawStoreExistsIndexedDBModal.ts +++ b/src/obsidian/modal/TldrawStoreExistsIndexedDBModal.ts @@ -160,7 +160,7 @@ export default class TldrawStoreExistsIndexedDBModal extends Modal { isReadonly: true, selectNone: true, initialTool: 'hand', - onEditorMount: (editor) => editor.zoomToFit(), + onEditorMount: (editor) => editor.resetZoom(), } satisfies TldrawAppProps['options']; this.markdownTldrawRoot = createRootAndRenderTldrawApp( diff --git a/src/styles.css b/src/styles.css index 9fd0f5b..b3d65f2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -72,7 +72,8 @@ } /* OVERRIDING Obsidian's CSS */ -.tldraw-view-root ol > li::marker, .tldraw-view-root ul > li::marker { +.tldraw-view-root ol>li::marker, +.tldraw-view-root ul>li::marker { color: inherit; } @@ -80,6 +81,7 @@ div[data-type="tldraw-view"] .view-content.tldraw-view-content, div[data-type="tldraw-read-only"] .view-content.tldraw-view-content { /* gets rid of the padding so the canvas can bleed against the edges */ padding: 0; + @container style(--status-bar-position: fixed) { /* creates a space at the bottom so that the status bar isn't covering the canvas */ padding-bottom: calc(var(--size-4-1) * 2 + var(--status-bar-font-size) + 8px); @@ -373,7 +375,7 @@ div[data-type="tldraw-read-only"] .view-content.tldraw-view-content { } .ptl-settings-tab-item[data-is-active=false]:hover { - background-color: var(--interactive-hover); + background-color: var(--interactive-hover); } .ptl-settings-tab-item[data-is-active=true] { @@ -404,28 +406,178 @@ div[data-type="tldraw-read-only"] .view-content.tldraw-view-content { /* Copied from tldraw.com */ .tl-shape[data-shape-type="arrow"] .tl-text { - height: max-content; + height: max-content; } /* Copied from tldraw.com */ .tl-shape[data-shape-type="arrow"] .tl-text-label { - position: absolute; - top: -1px; - left: -1px; - width: 2px; - height: 2px; - padding: 0; - display: flex; - justify-content: center; - align-items: center; - text-align: center; - color: var(--tl-color-text); - text-shadow: var(--tl-text-outline); + position: absolute; + top: -1px; + left: -1px; + width: 2px; + height: 2px; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + color: var(--tl-color-text); + text-shadow: var(--tl-text-outline); } /* Copied from tldraw.com */ .tl-shape[data-shape-type="arrow"] .tl-text-label__inner { - height: max-content; - box-sizing: content-box; - border-radius: var(--tl-radius-1); + height: max-content; + box-sizing: content-box; + border-radius: var(--tl-radius-1); +} + +/* ==================== PDF Editor Styles ==================== */ + +.ptl-pdf-editor { + position: absolute; + inset: 0; +} + +.ptl-pdf-picker { + position: absolute; + inset: 1rem; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + flex-direction: column; + gap: 1rem; +} + +.ptl-pdf-picker button { + padding: 0.5rem 1rem; + border: none; + background: var(--background-secondary); + cursor: pointer; + font: inherit; + border-radius: var(--radius-m); +} + +.ptl-pdf-picker button:hover { + opacity: 0.9; +} + +/* Page overlay screen - dims areas outside PDF pages */ +.PageOverlayScreen-screen { + pointer-events: none; + fill: var(--tl-color-background); + fill-opacity: 0.8; + stroke: none; +} + +.PageOverlayScreen-outline { + position: absolute; + pointer-events: none; + box-shadow: var(--tl-shadow-2); +} + +/* Dark mode support for PDF pages - targets image shapes with pdf in their ID */ +body.theme-dark .tl-shape[data-shape-type="image"][data-shape-id*="pdf"] { + filter: invert(1) hue-rotate(180deg) !important; +} + +/* Export PDF button */ +.ExportPdfButton { + font: inherit; + background: var(--tl-color-primary); + border: none; + color: var(--tl-color-selected-contrast); + font-size: 1rem; + padding: 0.5rem 1rem; + border-radius: 6px; + margin: 6px; + margin-bottom: 0; + pointer-events: all; + z-index: var(--tl-layer-panels); + border: 2px solid var(--tl-color-background); + cursor: pointer; +} + +.ExportPdfButton:hover { + filter: brightness(1.1); +} + +/* ==================== PDF Import Modal ==================== */ + +.ptl-pdf-import-modal { + padding: 1rem; +} + +.ptl-pdf-import-modal h2 { + margin: 0 0 1rem 0; +} + +.ptl-pdf-import-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.ptl-pdf-import-header h4 { + margin: 0; +} + +.ptl-pdf-import-select-btns { + display: flex; + gap: 0.5rem; +} + +.ptl-pdf-import-select-btns button { + padding: 0.25rem 0.75rem; + font-size: 0.8rem; +} + +.ptl-pdf-import-pages { + max-height: 200px; + overflow-y: auto; + border: 1px solid var(--background-modifier-border); + border-radius: var(--radius-m); + padding: 0.5rem; + margin-bottom: 1rem; +} + +.ptl-pdf-import-page-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0; +} + +.ptl-pdf-import-page-item input[type="checkbox"] { + margin: 0; +} + +.ptl-pdf-import-page-size { + color: var(--text-muted); + font-size: 0.85rem; +} + +.ptl-pdf-import-buttons { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + margin-top: 1rem; +} + +.ptl-pdf-import-buttons button { + padding: 0.5rem 1rem; +} + +.ptl-pdf-import-range { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.ptl-pdf-import-range-input { + flex: 1; + padding: 0.25rem 0.5rem; } \ No newline at end of file diff --git a/src/tldraw/TldrawStoresManager.ts b/src/tldraw/TldrawStoresManager.ts index 89d6a96..bcbb3f6 100644 --- a/src/tldraw/TldrawStoresManager.ts +++ b/src/tldraw/TldrawStoresManager.ts @@ -1,5 +1,5 @@ import { logFn, TLDRAW_STORES_MANAGER_LOGGING } from "src/utils/logging"; -import { createTLStore, HistoryEntry, TLRecord, TLStore } from "tldraw"; +import { createTLStore, HistoryEntry, TLRecord, TLStore, defaultShapeUtils } from "tldraw"; export type StoreInstanceInfo = { instanceId: string, @@ -188,11 +188,12 @@ export default class TldrawStoresManager { * @param storeGroup Contains the store to synchronize to */ function createSourceStore(storeGroup: Group): TLStore { - const snapshot = storeGroup.main.store.getStoreSnapshot(); const store = createTLStore({ - snapshot: snapshot, + shapeUtils: defaultShapeUtils, }); + store.put(storeGroup.main.store.allRecords()); + // NOTE: We want to preserve the assets object that is attached to props, otherwise the context will be lost if provided as a param in createTLStore store.props.assets = storeGroup.main.store.props.assets; diff --git a/src/tldraw/asset-store.ts b/src/tldraw/asset-store.ts index 51fe658..132c784 100644 --- a/src/tldraw/asset-store.ts +++ b/src/tldraw/asset-store.ts @@ -6,6 +6,90 @@ import { DEFAULT_SUPPORTED_IMAGE_TYPES, TLAsset, TLAssetContext, TLAssetStore, T import { TldrawStoreIndexedDB } from "./indexeddb-store"; import { vaultFileToBlob } from "src/obsidian/helpers/vault"; import { createImageAsset } from "./helpers/create-asset"; +import * as PdfJS from "pdfjs-dist"; + +// @ts-ignore - imported as text via esbuild loader config +import pdfWorkerCode from "pdfjs-dist/build/pdf.worker.min.mjs"; + +// Create a blob URL from the real worker code (only once) +const workerBlob = new Blob([pdfWorkerCode], { type: 'application/javascript' }); +PdfJS.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob); + +// PDF document cache +const pdfDocumentCache = new Map>(); + +// PDF rendered page cache (blob URLs) +const pdfRenderedCache = new Map(); + +/** + * Render a PDF page to a Blob URL + */ +async function renderPdfPageToBlob( + plugin: TldrawPlugin, + pdfPath: string, + pageNumber: number, + width: number, + height: number, + dpi: number = 150 +): Promise { + const cacheKey = `${pdfPath}#${pageNumber}@${dpi}`; + + // Check cache + if (pdfRenderedCache.has(cacheKey)) { + return pdfRenderedCache.get(cacheKey)!; + } + + // Load PDF document + let pdfPromise = pdfDocumentCache.get(pdfPath); + if (!pdfPromise) { + pdfPromise = (async () => { + const file = plugin.app.vault.getAbstractFileByPath(pdfPath); + if (!file || !(file instanceof TFile)) { + throw new Error(`PDF not found: ${pdfPath}`); + } + const arrayBuffer = await plugin.app.vault.readBinary(file); + return PdfJS.getDocument({ data: new Uint8Array(arrayBuffer) }).promise; + })(); + pdfDocumentCache.set(pdfPath, pdfPromise); + } + + const pdf = await pdfPromise; + const page = await pdf.getPage(pageNumber); + + // Calculate render scale using DPI (72 DPI = 1x scale in PDF) + const baseViewport = page.getViewport({ scale: 1 }); + const fitScale = Math.min(width / baseViewport.width, height / baseViewport.height); + // Convert DPI to scale factor (PDF default is 72 DPI) + const dpiScale = dpi / 72; + const renderScale = fitScale * dpiScale; + const viewport = page.getViewport({ scale: renderScale }); + + // Create offscreen canvas + const canvas = document.createElement('canvas'); + canvas.width = viewport.width; + canvas.height = viewport.height; + const context = canvas.getContext('2d', { alpha: false }); + if (!context) throw new Error('Failed to get canvas context'); + + // White background + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + + // Render + await page.render({ canvasContext: context, viewport }).promise; + + // Convert to blob URL - use JPEG for smaller size and faster rendering + const blob = await new Promise((resolve, reject) => { + canvas.toBlob((b) => b ? resolve(b) : reject(new Error('toBlob failed')), 'image/jpeg', 0.85); + }); + const blobUrl = URL.createObjectURL(blob); + + // Cache it + pdfRenderedCache.set(cacheKey, blobUrl); + + return blobUrl; +} + const blockRefAssetPrefix = 'obsidian.blockref.'; type BlockRefAssetId = `${typeof blockRefAssetPrefix}${string}`; @@ -181,7 +265,7 @@ export class ObsidianMarkdownFileTLAssetStoreProxy { immediatelyCache?: boolean, } = {}): Promise { const assetBlob = await vaultFileToBlob(assetFile); - + if (!(DEFAULT_SUPPORTED_IMAGE_TYPES as readonly string[]).includes(assetBlob.type)) { throw new Error(`Expected an image mime-type, got ${assetBlob.type}`, { cause: { @@ -221,7 +305,7 @@ export class ObsidianMarkdownFileTLAssetStoreProxy { }, }); } finally { - if(!immediatelyCache) { + if (!immediatelyCache) { // We only needed the object url for getting the width and height. URL.revokeObjectURL(assetUri); } @@ -279,6 +363,46 @@ export class ObsidianTLAssetStore implements TLAssetStore { const assetSrc = asset.props.src; if (!assetSrc) return null; + // Handle PDF asset: asset:pdf.[[wikilink]]#pageNumber or asset:pdf.path#pageNumber + if (assetSrc.startsWith('asset:pdf.')) { + try { + const rest = assetSrc.slice(10); // Remove "asset:pdf." + const hashIndex = rest.lastIndexOf('#'); + let pdfRef = hashIndex >= 0 ? rest.slice(0, hashIndex) : rest; + const pageNumber = hashIndex >= 0 ? parseInt(rest.slice(hashIndex + 1)) || 1 : 1; + + // Check if it's a WikiLink format [[name]] + let pdfPath: string; + const wikiLinkMatch = pdfRef.match(/^\[\[(.+?)\]\]$/); + if (wikiLinkMatch) { + // Resolve WikiLink using Obsidian's metadataCache + const linkName = wikiLinkMatch[1]; + const plugin = this.proxy['plugin']; + // Use the Tldraw file's path as source for relative resolution + const sourcePath = this.proxy['tFile']?.path || ''; + const resolvedFile = plugin.app.metadataCache.getFirstLinkpathDest(linkName, sourcePath); + if (!resolvedFile) { + console.error('[PDF Resolve] WikiLink not found:', linkName); + return null; + } + pdfPath = resolvedFile.path; + } else { + // Direct path format + pdfPath = pdfRef; + } + + // Get dimensions and DPI from asset + const w = (asset.props as any).w || 595; + const h = (asset.props as any).h || 842; + const dpi = (asset.meta as any)?.dpi || 150; + + return await renderPdfPageToBlob(this.proxy['plugin'], pdfPath, pageNumber, w, h, dpi); + } catch (err) { + console.error('[PDF Resolve] Error:', err); + return null; + } + } + if (!assetSrc.startsWith('asset:')) return assetSrc; const assetId = assetSrc.split(':').at(1); diff --git a/src/tldraw/helpers.ts b/src/tldraw/helpers.ts index f5aee57..4727fff 100644 --- a/src/tldraw/helpers.ts +++ b/src/tldraw/helpers.ts @@ -10,12 +10,14 @@ export function processInitialData(initialData: TLDataDocument): TLDataDocumentS return initialData; } + const store = createTLStore({ + shapeUtils: defaultShapeUtils, + initialData: initialData.raw, + }); + return { meta: initialData.meta, - store: createTLStore({ - shapeUtils: defaultShapeUtils, - initialData: initialData.raw, - }) + store } })(); diff --git a/src/tldraw/shapes/PdfPageShapeUtil.tsx b/src/tldraw/shapes/PdfPageShapeUtil.tsx new file mode 100644 index 0000000..90f9efe --- /dev/null +++ b/src/tldraw/shapes/PdfPageShapeUtil.tsx @@ -0,0 +1,88 @@ +import * as React from "react"; +import { + BaseBoxShapeUtil, + HTMLContainer, + RecordProps, + T, + TLBaseShape, + TLOnResizeHandler, + resizeBox, +} from "tldraw"; +import * as PdfJS from "pdfjs-dist"; +// Removed import: import { PdfPageRenderer } from "src/components/pdf/PdfPageRenderer"; + +// Lazy load the renderer to break circular dependency +const PdfPageRenderer = React.lazy(async () => { + const { PdfPageRenderer } = await import("src/components/pdf/PdfPageRenderer"); + return { default: PdfPageRenderer }; +}); + +// Import the real PDF.js worker code as a string (via esbuild text loader) +// @ts-ignore - imported as text via esbuild loader config +import pdfWorkerCode from "pdfjs-dist/build/pdf.worker.min.mjs"; + +// Create a blob URL from the real worker code (only once) +const workerBlob = new Blob([pdfWorkerCode], { type: 'application/javascript' }); +PdfJS.GlobalWorkerOptions.workerSrc = URL.createObjectURL(workerBlob); + +// Define the shape type +export type PdfPageShape = TLBaseShape< + 'pdf-page', + { + pdfPath: string; // Path to PDF in Obsidian vault + pageNumber: number; // 1-indexed page number + w: number; + h: number; + } +>; + +// PDF Page Shape Util +export class PdfPageShapeUtil extends BaseBoxShapeUtil { + static override type = 'pdf-page' as const; + + static override props: RecordProps = { + pdfPath: T.string, + pageNumber: T.number, + w: T.number, + h: T.number, + }; + + override getDefaultProps(): PdfPageShape['props'] { + return { + pdfPath: '', + pageNumber: 1, + w: 595, // A4 width at 72 DPI + h: 842, // A4 height at 72 DPI + }; + } + + override component(shape: PdfPageShape) { + return ( + + }> + + + + ); + } + + override indicator(shape: PdfPageShape) { + return ; + } + + override onResize: TLOnResizeHandler = (shape, info) => { + return resizeBox(shape, info); + }; +} diff --git a/src/tldraw/shapes/index.ts b/src/tldraw/shapes/index.ts new file mode 100644 index 0000000..8bbdd3a --- /dev/null +++ b/src/tldraw/shapes/index.ts @@ -0,0 +1,2 @@ +export { PdfPageShapeUtil, setObsidianApp } from "./PdfPageShapeUtil"; +export type { PdfPageShape } from "./PdfPageShapeUtil"; diff --git a/src/tldraw/ui-overrides.ts b/src/tldraw/ui-overrides.ts index 195206f..a1a9ffa 100644 --- a/src/tldraw/ui-overrides.ts +++ b/src/tldraw/ui-overrides.ts @@ -65,26 +65,217 @@ export function uiOverrides(plugin: TldrawPlugin): TLUiOverrides { }, } + // Change PDF DPI action + actions['change-pdf-dpi'] = { + id: 'change-pdf-dpi', + label: { default: 'Change PDF Quality (DPI)' }, + readonlyOk: false, + onSelect() { + const selectedShapes = editor.getSelectedShapes(); + if (selectedShapes.length === 0) return; + + // Helper to recursively get all descendant shapes + function getAllDescendants(shapes: any[]): any[] { + const result: any[] = []; + for (const shape of shapes) { + result.push(shape); + if (shape.type === 'group') { + const children = editor.getSortedChildIdsForParent(shape.id) + .map((id: any) => editor.getShape(id)) + .filter(Boolean); + result.push(...getAllDescendants(children)); + } + } + return result; + } + + const allShapes = getAllDescendants(selectedShapes); + + // Get all PDF asset IDs from all shapes (including group children) + const pdfAssetIds = new Set(); + const pdfImageShapes: any[] = []; + for (const shape of allShapes) { + if (shape.type === 'image' && (shape.props as any).assetId) { + const asset = editor.getAsset((shape.props as any).assetId); + if (asset && (asset.meta as any)?.isPdfAsset) { + pdfAssetIds.add(asset.id); + pdfImageShapes.push(shape); + } + } + } + + if (pdfAssetIds.size === 0) { + return; + } + + // Prompt for new DPI + const currentDpi = (() => { + const firstAssetId = Array.from(pdfAssetIds)[0]; + const asset = editor.getAsset(firstAssetId as any); + return (asset?.meta as any)?.dpi || 150; + })(); + + // Create a simple input dialog using DOM (Electron doesn't support prompt()) + const container = editor.getContainer(); + // Check Obsidian theme (body.theme-dark), not Tldraw theme + const isDark = container.ownerDocument.body.classList.contains('theme-dark'); + + const overlay = document.createElement('div'); + overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:10000'; + + // Theme-aware colors + const bgColor = isDark ? '#2d2d2d' : '#ffffff'; + const textColor = isDark ? '#fff' : '#1a1a1a'; + const mutedColor = isDark ? '#aaa' : '#666'; + const inputBg = isDark ? '#1a1a1a' : '#f5f5f5'; + const inputBorder = isDark ? '#555' : '#ccc'; + const btnBg = isDark ? '#444' : '#e0e0e0'; + const btnText = isDark ? '#fff' : '#333'; + + const dialog = document.createElement('div'); + dialog.style.cssText = `background:${bgColor};color:${textColor};padding:20px;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5);min-width:300px;font-family:system-ui,-apple-system,sans-serif`; + dialog.innerHTML = ` +
Change PDF Quality (DPI)
+
Higher = sharper but slower (72-300)
+ +
+ + +
+ `; + + overlay.appendChild(dialog); + container.ownerDocument.body.appendChild(overlay); + + const input = dialog.querySelector('input') as HTMLInputElement; + input.focus(); + input.select(); + + const cleanup = () => overlay.remove(); + + const apply = () => { + const newDpi = Math.min(300, Math.max(72, parseInt(input.value) || 150)); + cleanup(); + + // Update all selected PDF assets + editor.run(() => { + for (const assetId of pdfAssetIds) { + const asset = editor.getAsset(assetId as any); + if (asset) { + editor.updateAssets([{ + ...asset, + meta: { ...asset.meta, dpi: newDpi } + }]); + } + } + }); + + // Force re-render by nudging shapes slightly + editor.run(() => { + for (const shape of pdfImageShapes) { + editor.updateShape({ id: shape.id, type: shape.type, x: shape.x + 0.001 }); + editor.updateShape({ id: shape.id, type: shape.type, x: shape.x }); + } + }); + }; + + dialog.querySelector('.cancel-btn')?.addEventListener('click', cleanup); + dialog.querySelector('.ok-btn')?.addEventListener('click', apply); + overlay.addEventListener('click', (e) => { if (e.target === overlay) cleanup(); }); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') apply(); + if (e.key === 'Escape') cleanup(); + }); + }, + } + return actions; }, - // toolbar(editor, toolbar, { tools }) { - // // console.log(toolbar); - // // toolbar.splice(4, 0, toolbarItem(tools.card)) - // return toolbar; - // }, - // keyboardShortcutsMenu(editor, keyboardShortcutsMenu, { tools }) { - // // console.log(keyboardShortcutsMenu); - // // const toolsGroup = keyboardShortcutsMenu.find( - // // (group) => group.id === 'shortcuts-dialog.tools' - // // ) as TLUiMenuGroup - // // toolsGroup.children.push(menuItem(tools.card)) - // return keyboardShortcutsMenu; - // }, - // contextMenu(editor, schema, helpers) { - // // console.log({ schema }); - // // console.log(JSON.stringify(schema[0])); - // return schema; - // }, + contextMenu(editor, schema, helpers) { + // Helper to recursively get all descendant shapes + function getAllDescendants(shapes: any[]): any[] { + const result: any[] = []; + for (const shape of shapes) { + result.push(shape); + if (shape.type === 'group') { + const children = editor.getSortedChildIdsForParent(shape.id) + .map((id: any) => editor.getShape(id)) + .filter(Boolean); + result.push(...getAllDescendants(children)); + } + } + return result; + } + + // Add PDF DPI option if a PDF shape is selected (or inside a selected group) + const selectedShapes = editor.getSelectedShapes(); + const allShapes = getAllDescendants(selectedShapes); + + console.log('[PDF Menu Debug] Selected shapes:', selectedShapes.length, selectedShapes.map((s: any) => ({ id: s.id, type: s.type }))); + console.log('[PDF Menu Debug] All shapes (including children):', allShapes.length); + + const hasPdfSelected = allShapes.some((shape: any) => { + if (shape.type !== 'image') return false; + const assetId = (shape.props as any).assetId; + const asset = editor.getAsset(assetId); + console.log('[PDF Menu Debug] Image shape:', shape.id, 'assetId:', assetId, 'asset:', asset, 'meta:', asset?.meta); + return asset && (asset.meta as any)?.isPdfAsset; + }); + + console.log('[PDF Menu Debug] hasPdfSelected:', hasPdfSelected); + + if (hasPdfSelected) { + // Add to the beginning of context menu + schema.unshift({ + id: 'pdf-options', + type: 'group', + children: [ + { id: 'change-pdf-dpi', type: 'item' } + ] + }); + } + + return schema; + }, + actionsMenu(editor, schema, helpers) { + // Helper to recursively get all descendant shapes + function getAllDescendants(shapes: any[]): any[] { + const result: any[] = []; + for (const shape of shapes) { + result.push(shape); + if (shape.type === 'group') { + const children = editor.getSortedChildIdsForParent(shape.id) + .map((id: any) => editor.getShape(id)) + .filter(Boolean); + result.push(...getAllDescendants(children)); + } + } + return result; + } + + // Add PDF DPI option if a PDF shape is selected + const selectedShapes = editor.getSelectedShapes(); + const allShapes = getAllDescendants(selectedShapes); + + const hasPdfSelected = allShapes.some((shape: any) => { + if (shape.type !== 'image') return false; + const asset = editor.getAsset((shape.props as any).assetId); + return asset && (asset.meta as any)?.isPdfAsset; + }); + + if (hasPdfSelected) { + // Add to menu + schema.push({ + id: 'pdf-options', + type: 'group', + children: [ + { id: 'change-pdf-dpi', type: 'item' } + ] + }); + } + + return schema; + }, } } diff --git a/src/utils/migrate/tl-data-to-tlstore.ts b/src/utils/migrate/tl-data-to-tlstore.ts index b76d075..eca370c 100644 --- a/src/utils/migrate/tl-data-to-tlstore.ts +++ b/src/utils/migrate/tl-data-to-tlstore.ts @@ -1,5 +1,6 @@ -import { TldrawFile, TLStore, parseTldrawJsonFile, createTLSchema, JsonObject, UnknownRecord } from "tldraw"; +import { TldrawFile, TLStore, parseTldrawJsonFile, createTLSchema, JsonObject, UnknownRecord, defaultShapeUtils } from "tldraw"; import { TLData } from "../document"; +import { PdfPageShapeUtil } from "src/tldraw/shapes/PdfPageShapeUtil"; /** * Tldraw handles the migration here. @@ -9,7 +10,9 @@ import { TLData } from "../document"; export function migrateTldrawFileDataIfNecessary(tldrawFileData: string | TldrawFile): TLStore { const res = parseTldrawJsonFile( { - schema: createTLSchema(), + schema: createTLSchema({ + shapeUtils: [...defaultShapeUtils, PdfPageShapeUtil] + }), json: typeof tldrawFileData === 'string' ? tldrawFileData : JSON.stringify(tldrawFileData) @@ -25,7 +28,7 @@ export function migrateTldrawFileDataIfNecessary(tldrawFileData: string | Tldraw function isJsonUnknownRecord(json: JsonObject): json is JsonObject & UnknownRecord { const { id, typeName } = json as Partial; - if(id === undefined || typeName === undefined) return false; + if (id === undefined || typeName === undefined) return false; return true; } @@ -37,7 +40,7 @@ function isJsonUnknownRecord(json: JsonObject): json is JsonObject & UnknownReco export function tLDataToTLStore(tldata: TLData): TLStore { const { tldrawFileFormatVersion, schema, records } = tldata.raw as Partial; - if(tldrawFileFormatVersion && schema && records) { + if (tldrawFileFormatVersion && schema && records) { return migrateTldrawFileDataIfNecessary({ tldrawFileFormatVersion, schema, records }) @@ -48,15 +51,15 @@ export function tLDataToTLStore(tldata: TLData): TLStore { } const oldRecords = Object.values(tldata.raw ?? {}) - .filter((e) => e !== undefined && e !== null) - .filter((e) => typeof e === 'object') - .filter((e): e is JsonObject => !Array.isArray(e)) - .map((e) => { - if(!isJsonUnknownRecord(e)) { - throw new Error(`Invalid json object found while parsing: ${e}`) - } - return e; - }); + .filter((e) => e !== undefined && e !== null) + .filter((e) => typeof e === 'object') + .filter((e): e is JsonObject => !Array.isArray(e)) + .map((e) => { + if (!isJsonUnknownRecord(e)) { + throw new Error(`Invalid json object found while parsing: ${e}`) + } + return e; + }); /** * tldrawFileFormatVersion and schema were obtained by exporting a tldr file using tldraw version 2.1.4 and extracting the values. diff --git a/src/utils/migration.ts b/src/utils/migration.ts new file mode 100644 index 0000000..f9efdff --- /dev/null +++ b/src/utils/migration.ts @@ -0,0 +1,87 @@ +import { Editor, createShapeId } from "tldraw"; + +/** + * Upgrades legacy PDF 'image' shapes to new 'pdf-page' shapes. + * Keeps position, size, and rotation. + * Deletes the old image shape and its associated asset. + */ +export function upgradeLegacyPdfShapes(editor: Editor) { + // We only run this once per session ideally, or efficiently. + // Scan all shapes. + + const shapesToUpgrade: any[] = []; + const assetsToDelete: any[] = []; + const shapesToDelete: any[] = []; + + const shapes = editor.getCurrentPageShapes(); // Or all pages? Better just current page to be safe/fast initially. + // actually better to check all shapes in store if possible, but store.allRecords() is safer. + + const allShapes = editor.store.allRecords().filter(r => r.typeName === 'shape' && r.type === 'image'); + + for (const shape of allShapes as any[]) { + if (shape.meta?.isPdfPage && shape.meta?.pdfPath) { + // Found a candidate + shapesToUpgrade.push(shape); + } + } + + if (shapesToUpgrade.length === 0) return; + + console.log(`[Migration] Upgrading ${shapesToUpgrade.length} legacy PDF shapes...`); + + editor.run(() => { + const newShapes: any[] = []; + + for (const oldShape of shapesToUpgrade) { + // Create new shape + // We use a new ID to avoid type conflicts during swap, + // but we could try to reuse ID if we delete first. + // Reuse ID is better for bindings (arrows). + const id = oldShape.id; + const assetId = oldShape.props.assetId; + + // Prepare new shape record + const newShape = { + id, + type: 'pdf-page', + x: oldShape.x, + y: oldShape.y, + rotation: oldShape.rotation, + index: oldShape.index, + parentId: oldShape.parentId, + opacity: oldShape.opacity, + isLocked: oldShape.isLocked, + props: { + pdfPath: oldShape.meta.pdfPath, + pageNumber: oldShape.meta.pageNumber, + w: oldShape.props.w, + h: oldShape.props.h, + }, + meta: { + ...oldShape.meta, + isLegacyUpgraded: true, + } + }; + + newShapes.push(newShape); + + if (assetId) { + assetsToDelete.push(assetId); + } + shapesToDelete.push(id); + } + + // Delete old shapes first (to free up IDs) + editor.deleteShapes(shapesToDelete); + + // Create new shapes (reusing IDs) + editor.createShapes(newShapes); + + // Cleanup assets + if (assetsToDelete.length > 0) { + editor.deleteAssets(assetsToDelete); + } + }); + + console.log(`[Migration] Upgrade complete. Removed ${assetsToDelete.length} obsolete assets.`); +} diff --git a/src/utils/tldraw-file/index.ts b/src/utils/tldraw-file/index.ts index af23b57..9881267 100644 --- a/src/utils/tldraw-file/index.ts +++ b/src/utils/tldraw-file/index.ts @@ -1,4 +1,5 @@ -import { createTLStore, TldrawFile, TLStore } from "tldraw" +import { createTLStore, TldrawFile, TLStore, defaultShapeUtils } from "tldraw" +import { PdfPageShapeUtil } from "src/tldraw/shapes/PdfPageShapeUtil"; /** * @@ -6,7 +7,9 @@ import { createTLStore, TldrawFile, TLStore } from "tldraw" * @returns */ export function createRawTldrawFile(store?: TLStore): TldrawFile { - store ??= createTLStore(); + store ??= createTLStore({ + shapeUtils: [...defaultShapeUtils, PdfPageShapeUtil] + }); return { tldrawFileFormatVersion: 1, schema: store.schema.serialize(),