From 2c643696942012b8dac8299cecfe8be9a627f32c Mon Sep 17 00:00:00 2001
From: Sejong Yoo
Date: Wed, 22 Jul 2026 11:48:43 +0900
Subject: [PATCH] =?UTF-8?q?ocr=20=EB=B0=8F=20=EC=8A=A4=EC=BA=90=EB=84=88?=
=?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.dockerignore | 2 +
.gitignore | 1 +
package-lock.json | 1436 ++++++++++++++++-
packages/backend/src/index.ts | 13 +-
packages/frontend/package.json | 7 +
.../frontend/scripts/copy-viewer-assets.mjs | 143 ++
packages/frontend/src/App.vue | 4 +-
.../src/components/scanner/CornerEditor.vue | 196 +++
.../src/composables/scannerSession.ts | 11 +
.../src/composables/useOpenDocument.ts | 13 +-
packages/frontend/src/config/app.ts | 28 +
packages/frontend/src/config/favicons.ts | 9 +
.../src/integrations/scanner/buildPdf.ts | 83 +
.../src/integrations/scanner/detect.ts | 194 +++
.../frontend/src/integrations/scanner/ocr.ts | 104 ++
.../src/integrations/scanner/pdfPages.ts | 90 ++
.../src/integrations/scanner/types.ts | 39 +
.../frontend/src/integrations/scanner/warp.ts | 74 +
packages/frontend/src/router/index.ts | 5 +
.../frontend/src/types/scanner-shims.d.ts | 10 +
packages/frontend/src/views/HomeView.vue | 35 +-
packages/frontend/src/views/LicensesView.vue | 71 +
.../frontend/src/views/ToolsScannerView.vue | 992 ++++++++++++
packages/frontend/vite.config.ts | 4 +
24 files changed, 3556 insertions(+), 8 deletions(-)
create mode 100644 packages/frontend/src/components/scanner/CornerEditor.vue
create mode 100644 packages/frontend/src/composables/scannerSession.ts
create mode 100644 packages/frontend/src/integrations/scanner/buildPdf.ts
create mode 100644 packages/frontend/src/integrations/scanner/detect.ts
create mode 100644 packages/frontend/src/integrations/scanner/ocr.ts
create mode 100644 packages/frontend/src/integrations/scanner/pdfPages.ts
create mode 100644 packages/frontend/src/integrations/scanner/types.ts
create mode 100644 packages/frontend/src/integrations/scanner/warp.ts
create mode 100644 packages/frontend/src/types/scanner-shims.d.ts
create mode 100644 packages/frontend/src/views/ToolsScannerView.vue
diff --git a/.dockerignore b/.dockerignore
index af5c326..cd034b4 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,6 +2,8 @@ node_modules
**/node_modules
**/dist
packages/backend/public
+packages/frontend/public/assets
+packages/frontend/public/viewer-assets
.git
.env
.env.*
diff --git a/.gitignore b/.gitignore
index 3a6b5d6..1e7decd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ packages/frontend/public/assets/dxf-parser-worker.js
packages/frontend/public/assets/libredwg-parser-worker.js
packages/frontend/public/assets/mtext-renderer-worker.js
packages/frontend/public/viewer-assets/
+packages/frontend/.cache/
.env
.env.local
*.log
diff --git a/package-lock.json b/package-lock.json
index 0ac2f64..252614a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -132,6 +132,53 @@
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
+ "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "license": "MIT"
+ },
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
@@ -193,6 +240,18 @@
"integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==",
"license": "MIT"
},
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
"node_modules/@chevrotain/cst-dts-gen": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
@@ -232,6 +291,140 @@
"integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==",
"license": "Apache-2.0"
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz",
+ "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+ "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz",
+ "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.1.0",
+ "@csstools/css-calc": "^3.2.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz",
+ "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/@ctrl/tinycolor": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
@@ -810,6 +1003,23 @@
"node": ">=10"
}
},
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@floating-ui/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
@@ -858,6 +1068,16 @@
"url": "https://github.com/sponsors/ayuhito"
}
},
+ "node_modules/@fontsource/noto-sans-kr": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/noto-sans-kr/-/noto-sans-kr-5.3.0.tgz",
+ "integrity": "sha512-/JnpTjaCOXW7xUoqOyCVYSr05VOkDy5Yla9O+WAEiS7u+yYLsmHoqj6v1W0bf91G8G3XA2+NnRS3CX2Tf/FVZg==",
+ "dev": true,
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@fxts/core": {
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/@fxts/core/-/core-1.26.0.tgz",
@@ -1645,6 +1865,270 @@
"three": "^0.172.0"
}
},
+ "node_modules/@napi-rs/canvas": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.2.tgz",
+ "integrity": "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ==",
+ "license": "MIT",
+ "workspaces": [
+ "e2e/*"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas-android-arm64": "1.0.2",
+ "@napi-rs/canvas-darwin-arm64": "1.0.2",
+ "@napi-rs/canvas-darwin-x64": "1.0.2",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2",
+ "@napi-rs/canvas-linux-arm64-gnu": "1.0.2",
+ "@napi-rs/canvas-linux-arm64-musl": "1.0.2",
+ "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2",
+ "@napi-rs/canvas-linux-x64-gnu": "1.0.2",
+ "@napi-rs/canvas-linux-x64-musl": "1.0.2",
+ "@napi-rs/canvas-win32-arm64-msvc": "1.0.2",
+ "@napi-rs/canvas-win32-x64-msvc": "1.0.2"
+ }
+ },
+ "node_modules/@napi-rs/canvas-android-arm64": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.2.tgz",
+ "integrity": "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g==",
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.2.tgz",
+ "integrity": "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ==",
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.2.tgz",
+ "integrity": "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A==",
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.2.tgz",
+ "integrity": "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ==",
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.2.tgz",
+ "integrity": "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.2.tgz",
+ "integrity": "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.2.tgz",
+ "integrity": "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.2.tgz",
+ "integrity": "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.2.tgz",
+ "integrity": "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.2.tgz",
+ "integrity": "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==",
+ "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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.2.tgz",
+ "integrity": "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
"node_modules/@noto-pdf-ts/core": {
"version": "0.0.1-alpha.0",
"resolved": "https://registry.npmjs.org/@noto-pdf-ts/core/-/core-0.0.1-alpha.0.tgz",
@@ -1684,6 +2168,51 @@
"node": ">=8.0.0"
}
},
+ "node_modules/@pdf-lib/fontkit": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz",
+ "integrity": "sha512-KjMd7grNapIWS/Dm0gvfHEilSyAmeLvrEGVcqLGi0VYebuqqzTbgF29efCx7tvx+IEbG3zQciRSWl3GkUSvjZg==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "^1.0.6"
+ }
+ },
+ "node_modules/@pdf-lib/fontkit/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/@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/standard-fonts/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/@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/@pdf-lib/upng/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/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.8",
@@ -1695,6 +2224,63 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
+ "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
+ "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
@@ -2882,6 +3468,12 @@
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
+ "node_modules/@techstark/opencv-js": {
+ "version": "5.0.0-release.1",
+ "resolved": "https://registry.npmjs.org/@techstark/opencv-js/-/opencv-js-5.0.0-release.1.tgz",
+ "integrity": "sha512-PIm+eB0MFtieXoNC2GRao0dv/02sehG+Nv2nSW5D6pQm6J/4WqvDHm0RyoqOmGYQm67jdGiaOdIeTShCY3PIUg==",
+ "license": "Apache-2.0"
+ },
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
@@ -3283,7 +3875,6 @@
"version": "22.20.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -3805,6 +4396,35 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -3817,6 +4437,31 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/body-parser": {
"version": "1.20.6",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
@@ -3869,6 +4514,30 @@
"integrity": "sha512-LSvVX5e21LRrXqVMhqtAwj5xPgDb+fXAIH80NsnCQ9xuZPs2xWsOREi24RKgZa1XOiQRbcmVrv87+ulOKsgjxw==",
"license": "Apache-2.0"
},
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -3930,6 +4599,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/canvas": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz",
+ "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^7.0.0",
+ "prebuild-install": "^7.1.3"
+ },
+ "engines": {
+ "node": "^18.12.0 || >= 20.9.0"
+ }
+ },
"node_modules/canvas-roundrect-polyfill": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/canvas-roundrect-polyfill/-/canvas-roundrect-polyfill-0.0.1.tgz",
@@ -3986,6 +4669,12 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
"node_modules/clsx": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
@@ -4201,6 +4890,19 @@
"node": ">= 8"
}
},
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -4718,6 +5420,19 @@
"lodash-es": "^4.17.21"
}
},
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/dayjs": {
"version": "1.11.21",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
@@ -4740,6 +5455,36 @@
"ms": "2.0.0"
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "license": "MIT"
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/delaunator": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
@@ -4772,9 +5517,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
"license": "Apache-2.0",
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -4926,6 +5669,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -5059,6 +5811,15 @@
"node": ">=18.0.0"
}
},
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/express": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
@@ -5160,6 +5921,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/flatbuffers": {
+ "version": "25.9.23",
+ "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz",
+ "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -5187,6 +5954,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5264,6 +6037,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
"node_modules/glm": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/glm/-/glm-1.0.0.tgz",
@@ -5302,6 +6081,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/guid-typescript": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
+ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
+ "license": "ISC"
+ },
"node_modules/hachure-fill": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
@@ -5342,6 +6127,18 @@
"he": "bin/he"
}
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -5381,6 +6178,26 @@
"license": "ISC",
"peer": true
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/image-blob-reduce": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/image-blob-reduce/-/image-blob-reduce-3.0.1.tgz",
@@ -5412,6 +6229,12 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@@ -5480,6 +6303,12 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "license": "MIT"
+ },
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -5529,6 +6358,56 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
+ "node_modules/jscanify": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/jscanify/-/jscanify-1.4.3.tgz",
+ "integrity": "sha512-BoGW0ZZvuK6BUA5o32R9W3QuguiZY/y0IApTD+cCDNRhBen2YC3IdSLQcLQdazGGKvdzGF6+Y0Z9l7tmjDfzrg==",
+ "license": "MIT",
+ "dependencies": {
+ "canvas": "^3.2.3",
+ "jsdom": "^29.1.1"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "29.1.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
+ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.1.11",
+ "@asamuzakjp/dom-selector": "^7.1.1",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.3.5",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.25.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
@@ -5732,6 +6611,12 @@
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -5744,6 +6629,15 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -5774,6 +6668,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "license": "CC0-1.0"
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -5915,6 +6815,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
@@ -5952,6 +6864,12 @@
"mkdirp": "bin/cmd.js"
}
},
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -6018,6 +6936,12 @@
"integrity": "sha512-fzN+T2K7/Ah25XU02MJkPZ5q4Tj5FpjmIYq4rvoHX4yb16HzFdCO6JxFFn5Y/oBhQ8no8fUZavnyIv9/+xkBBw==",
"license": "MIT"
},
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -6027,6 +6951,24 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-abi": {
+ "version": "3.94.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
+ "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT"
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -6075,6 +7017,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/online-3d-viewer": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/online-3d-viewer/-/online-3d-viewer-0.18.0.tgz",
@@ -6098,6 +7049,26 @@
"integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==",
"license": "MIT"
},
+ "node_modules/onnxruntime-common": {
+ "version": "1.27.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.27.0.tgz",
+ "integrity": "sha512-3KxL5wIVqa8Ex08jxSzncm9CMgw8CjOFyOQ7SxvG9o0cVLlhTNKXyIQuTbtX4tGPJEf73OER2xrjt4HJSBL4ow==",
+ "license": "MIT"
+ },
+ "node_modules/onnxruntime-web": {
+ "version": "1.27.0",
+ "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.27.0.tgz",
+ "integrity": "sha512-ogDLsqIozHZwifPuN37OproAo0byX6t43/bP8GzeZWBWD6MOGExswFAx3up4NS/vvWBOg2u2PXomDt3rMmdQSg==",
+ "license": "MIT",
+ "dependencies": {
+ "flatbuffers": "^25.1.24",
+ "guid-typescript": "^1.0.9",
+ "long": "^5.2.3",
+ "onnxruntime-common": "1.27.0",
+ "platform": "^1.3.6",
+ "protobufjs": "^7.2.4"
+ }
+ },
"node_modules/open-color": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/open-color/-/open-color-1.9.1.tgz",
@@ -6126,6 +7097,30 @@
"integrity": "sha512-WjR1hOeg+kki3ZIOjaf4b5WVcay1jaliKSYiEaB1XzwhMQZJxRdQRv0V31EKBYlxb4T7SK3hjfc/jxyU64BoSw==",
"license": "(MIT AND Zlib)"
},
+ "node_modules/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -6163,6 +7158,42 @@
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
"license": "MIT"
},
+ "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/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/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": "6.1.200",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.1.200.tgz",
+ "integrity": "sha512-o8MolyzirkkLrcdsae/HEOiIcXWI7DS5zGpvqW8xTC2YUsW30rltFw2bDGvw/fskUdEMrQm2br68jzDS5BH2vw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=22.13.0 || >=24"
+ },
+ "optionalDependencies": {
+ "@napi-rs/canvas": "^1.0.0"
+ }
+ },
"node_modules/perfect-freehand": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.0.tgz",
@@ -6201,6 +7232,12 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/platform": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
+ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
+ "license": "MIT"
+ },
"node_modules/png-chunk-text": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-chunk-text/-/png-chunk-text-1.0.0.tgz",
@@ -6276,12 +7313,112 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/ppu-paddle-ocr": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/ppu-paddle-ocr/-/ppu-paddle-ocr-6.1.2.tgz",
+ "integrity": "sha512-vlYLOAKAg8grkZV4FWqlz6bLxb4Nnig2RxnHVSnadmPJUFNN3eVKlbkA0ViPhIhhng/PjJdMKfMO2FlMZa8Tpw==",
+ "license": "MIT",
+ "dependencies": {
+ "ppu-ocv": "^4.0.0"
+ },
+ "bin": {
+ "ppu-paddle-ocr": "cli/index.js"
+ },
+ "peerDependencies": {
+ "@shopify/react-native-skia": ">=1.0.0",
+ "onnxruntime-node": "^1.23.2",
+ "onnxruntime-react-native": "^1.23.2",
+ "onnxruntime-web": "^1.23.2"
+ },
+ "peerDependenciesMeta": {
+ "@shopify/react-native-skia": {
+ "optional": true
+ },
+ "onnxruntime-node": {
+ "optional": true
+ },
+ "onnxruntime-react-native": {
+ "optional": true
+ },
+ "onnxruntime-web": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ppu-paddle-ocr/node_modules/ppu-ocv": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/ppu-ocv/-/ppu-ocv-4.0.0.tgz",
+ "integrity": "sha512-ol+S9/KLZ0aTV0xbC8+ZB0iM+FYux8ujoULeQoYY2IDBZj4LAuujJUkzHzUA2O3KzHs17KuuIzV/w4/htPdn3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@napi-rs/canvas": "^1.0.0",
+ "@techstark/opencv-js": "^5.0.0-release.1"
+ },
+ "peerDependencies": {
+ "@shopify/react-native-skia": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@shopify/react-native-skia": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
+ "node_modules/protobufjs": {
+ "version": "7.6.5",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
+ "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.1",
+ "@protobufjs/fetch": "^1.1.1",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
+ "@types/node": ">=13.7.0",
+ "long": "^5.3.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -6295,6 +7432,25 @@
"node": ">= 0.10"
}
},
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/pwacompat": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/pwacompat/-/pwacompat-2.0.17.tgz",
@@ -6341,6 +7497,21 @@
"node": ">= 0.8"
}
},
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -6480,6 +7651,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/rhino3dm": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/rhino3dm/-/rhino3dm-8.17.0.tgz",
@@ -6604,6 +7784,18 @@
"node": ">=12.0.0"
}
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -6810,6 +8002,51 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
@@ -6869,6 +8106,15 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/stylis": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
@@ -6884,6 +8130,54 @@
"vue": ">=3.2.26 < 4"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "license": "MIT"
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
+ "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar-stream/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/three": {
"version": "0.172.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.172.0.tgz",
@@ -6916,6 +8210,24 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tldts": {
+ "version": "7.4.9",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz",
+ "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==",
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.9"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.9",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz",
+ "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==",
+ "license": "MIT"
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6937,6 +8249,30 @@
"node": ">=0.6"
}
},
+ "node_modules/tough-cookie": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+ "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/ts-dedent": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz",
@@ -6971,6 +8307,18 @@
"fsevents": "~2.3.3"
}
},
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/tunnel-rat": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
@@ -7025,11 +8373,19 @@
"node": ">=8"
}
},
+ "node_modules/undici": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/unpipe": {
@@ -7819,12 +9175,56 @@
"typescript": ">=5.0.0"
}
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/webworkify": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.5.0.tgz",
"integrity": "sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==",
"license": "MIT"
},
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7840,6 +9240,27 @@
"node": ">= 8"
}
},
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "license": "MIT"
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -7925,15 +9346,21 @@
"@mlightcad/cad-viewer": "^1.5.8",
"@mlightcad/data-model": "^1.11.2",
"@mlightcad/dxf-json-converter": "^1.11.2",
+ "@pdf-lib/fontkit": "^1.1.1",
"@rhwp/editor": "^0.7.18",
"@vueuse/core": "^11.3.0",
"draco3d": "^1.5.7",
"element-plus": "^2.14.3",
"fflate": "^0.8.2",
+ "jscanify": "^1.4.3",
"lodash-es": "^4.17.21",
"mermaid": "^11.16.0",
"occt-import-js": "^0.0.22",
"online-3d-viewer": "^0.18.0",
+ "onnxruntime-web": "^1.27.0",
+ "pdf-lib": "^1.17.1",
+ "pdfjs-dist": "^6.1.200",
+ "ppu-paddle-ocr": "^6.1.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rhino3dm": "^8.17.0",
@@ -7945,6 +9372,7 @@
},
"devDependencies": {
"@fontsource-variable/noto-sans-kr": "^5.3.0",
+ "@fontsource/noto-sans-kr": "^5.3.0",
"@noto-pdf-ts/fonts-kr": "^0.0.1-alpha.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 4bc0bc0..192eab0 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -40,12 +40,23 @@ async function main() {
app.use('/api/documents', documentsRouter);
if (fs.existsSync(config.publicDir)) {
- app.use(express.static(config.publicDir));
+ app.use(express.static(config.publicDir, {
+ setHeaders(res, filePath) {
+ if (filePath.endsWith('.wasm')) {
+ res.setHeader('Content-Type', 'application/wasm');
+ }
+ },
+ }));
app.get('*', (req, res, next) => {
if (req.path.startsWith('/api')) {
next();
return;
}
+ // Do not SPA-fallback missing static assets (wasm, workers, traineddata).
+ if (/\.[a-z0-9]+$/i.test(req.path)) {
+ res.status(404).end();
+ return;
+ }
res.sendFile(path.join(config.publicDir, 'index.html'));
});
}
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 777b37c..83326b8 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -23,15 +23,21 @@
"@mlightcad/cad-viewer": "^1.5.8",
"@mlightcad/data-model": "^1.11.2",
"@mlightcad/dxf-json-converter": "^1.11.2",
+ "@pdf-lib/fontkit": "^1.1.1",
"@rhwp/editor": "^0.7.18",
"@vueuse/core": "^11.3.0",
"draco3d": "^1.5.7",
"element-plus": "^2.14.3",
"fflate": "^0.8.2",
+ "jscanify": "^1.4.3",
"lodash-es": "^4.17.21",
"mermaid": "^11.16.0",
"occt-import-js": "^0.0.22",
"online-3d-viewer": "^0.18.0",
+ "onnxruntime-web": "^1.27.0",
+ "pdf-lib": "^1.17.1",
+ "pdfjs-dist": "^6.1.200",
+ "ppu-paddle-ocr": "^6.1.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rhino3dm": "^8.17.0",
@@ -43,6 +49,7 @@
},
"devDependencies": {
"@fontsource-variable/noto-sans-kr": "^5.3.0",
+ "@fontsource/noto-sans-kr": "^5.3.0",
"@noto-pdf-ts/fonts-kr": "^0.0.1-alpha.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
diff --git a/packages/frontend/scripts/copy-viewer-assets.mjs b/packages/frontend/scripts/copy-viewer-assets.mjs
index b4fb42f..29949dc 100644
--- a/packages/frontend/scripts/copy-viewer-assets.mjs
+++ b/packages/frontend/scripts/copy-viewer-assets.mjs
@@ -193,6 +193,149 @@ await writeFile(
].join('\n'),
);
+const scannerAssets = [
+ [resolvePackage('jscanify/src/opencv.js'), 'viewer-assets/scanner/opencv.js'],
+ [resolvePackage('jscanify/src/jscanify.js'), 'viewer-assets/scanner/jscanify.js'],
+ [resolvePackage('jscanify/LICENSE'), 'viewer-assets/scanner/licenses/jscanify.LICENSE'],
+ [
+ resolvePackage('ppu-paddle-ocr/LICENSE'),
+ 'viewer-assets/scanner/licenses/ppu-paddle-ocr.LICENSE',
+ ],
+ [
+ resolvePackage('pdfjs-dist/build/pdf.worker.min.mjs'),
+ 'viewer-assets/scanner/pdfjs/pdf.worker.min.mjs',
+ ],
+ [
+ resolvePackage('pdfjs-dist/LICENSE'),
+ 'viewer-assets/scanner/licenses/pdfjs-dist.LICENSE',
+ ],
+];
+
+await Promise.all(
+ scannerAssets.map(([sources, destination]) =>
+ copyFirst(sources, destination),
+ ),
+);
+
+{
+ const { access, readdir, rm } = await import('node:fs/promises');
+ const legacyTesseract = path.join(publicDir, 'viewer-assets/scanner/tesseract');
+ try {
+ await rm(legacyTesseract, { recursive: true, force: true });
+ } catch {
+ // ignore
+ }
+
+ const ortDest = path.join(publicDir, 'viewer-assets/scanner/ort');
+ await mkdir(ortDest, { recursive: true });
+ const ortNames = [
+ 'ort-wasm-simd-threaded.wasm',
+ 'ort-wasm-simd-threaded.mjs',
+ 'ort-wasm-simd-threaded.jsep.wasm',
+ 'ort-wasm-simd-threaded.jsep.mjs',
+ ];
+ let ortCopied = false;
+ for (const candidate of resolvePackage('onnxruntime-web/dist')) {
+ try {
+ const entries = await readdir(candidate);
+ const wanted = ortNames.filter((name) => entries.includes(name));
+ if (wanted.length < ortNames.length) continue;
+ await Promise.all(
+ wanted.map((name) =>
+ cp(path.join(candidate, name), path.join(ortDest, name)),
+ ),
+ );
+ ortCopied = true;
+ break;
+ } catch {
+ // try next
+ }
+ }
+ if (!ortCopied) {
+ throw new Error('Missing onnxruntime-web WASM assets');
+ }
+
+ const paddleDest = path.join(publicDir, 'viewer-assets/scanner/paddle');
+ await mkdir(paddleDest, { recursive: true });
+ const modelCacheDir = path.join(frontendDir, '.cache', 'paddle-ocr-models');
+ await mkdir(modelCacheDir, { recursive: true });
+
+ const MODEL_BASE =
+ 'https://media.githubusercontent.com/media/PT-Perkasa-Pilar-Utama/ppu-paddle-ocr-models/main';
+ const DICT_BASE =
+ 'https://raw.githubusercontent.com/PT-Perkasa-Pilar-Utama/ppu-paddle-ocr-models/main';
+ const paddleFiles = [
+ {
+ url: `${MODEL_BASE}/detection/PP-OCRv5_mobile_det_infer.onnx`,
+ name: 'PP-OCRv5_mobile_det_infer.onnx',
+ },
+ {
+ url: `${MODEL_BASE}/recognition/multi/korean/v5/korean_PP-OCRv5_mobile_rec_infer.onnx`,
+ name: 'korean_PP-OCRv5_mobile_rec_infer.onnx',
+ },
+ {
+ url: `${DICT_BASE}/recognition/multi/korean/v5/ppocrv5_korean_dict.txt`,
+ name: 'ppocrv5_korean_dict.txt',
+ },
+ ];
+
+ async function downloadToCache(url, name) {
+ const cached = path.join(modelCacheDir, name);
+ try {
+ await access(cached);
+ return cached;
+ } catch {
+ // download
+ }
+ console.log(`downloading paddle model: ${name}`);
+ let lastError;
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
+ try {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status} for ${url}`);
+ }
+ const buffer = Buffer.from(await response.arrayBuffer());
+ if (buffer.byteLength < 64) {
+ throw new Error(`Unexpectedly small download for ${name}`);
+ }
+ await writeFile(cached, buffer);
+ return cached;
+ } catch (error) {
+ lastError = error;
+ console.warn(`retry ${attempt}/3 failed for ${name}:`, error);
+ }
+ }
+ throw lastError ?? new Error(`Failed to download ${name}`);
+ }
+
+ for (const file of paddleFiles) {
+ const cachedPath = await downloadToCache(file.url, file.name);
+ await cp(cachedPath, path.join(paddleDest, file.name));
+ }
+
+ await writeFile(
+ path.join(publicDir, 'viewer-assets/scanner/licenses/paddle-ocr-models.NOTICE.txt'),
+ [
+ 'PP-OCRv5 ONNX models are sourced from:',
+ 'https://github.com/PT-Perkasa-Pilar-Utama/ppu-paddle-ocr-models',
+ 'upstream: https://www.paddleocr.ai/ (Apache-2.0)',
+ '',
+ ].join('\n'),
+ );
+}
+
+await writeFile(
+ path.join(publicDir, 'viewer-assets/scanner/README.txt'),
+ [
+ 'Self-hosted document scanner libraries (OpenCV.js, jscanify, ppu-paddle-ocr, pdf.js).',
+ 'OCR: PP-OCRv5 Korean mobile (det + rec + dict) under paddle/.',
+ 'ONNX Runtime Web WASM under ort/.',
+ 'PDF.js worker under pdfjs/.',
+ '',
+ ].join('\n'),
+);
+
const O3DV_BASE = '/viewer-assets/o3dv/';
const replacements = [
[
diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue
index fd5431d..4f3ecb0 100644
--- a/packages/frontend/src/App.vue
+++ b/packages/frontend/src/App.vue
@@ -20,7 +20,8 @@ const viewKey = computed(() => {
route.name === 'mermaid-editor' ||
route.name === 'drawio-editor' ||
route.name === 'cad-viewer' ||
- route.name === 'model3d-viewer'
+ route.name === 'model3d-viewer' ||
+ route.name === 'tools-scanner'
) {
return String(route.name);
}
@@ -39,6 +40,7 @@ const viewKey = computed(() => {
'DrawioEditorView',
'CadViewerView',
'Model3DViewerView',
+ 'ToolsScannerView',
]"
:max="10"
>
diff --git a/packages/frontend/src/components/scanner/CornerEditor.vue b/packages/frontend/src/components/scanner/CornerEditor.vue
new file mode 100644
index 0000000..a297297
--- /dev/null
+++ b/packages/frontend/src/components/scanner/CornerEditor.vue
@@ -0,0 +1,196 @@
+
+
+
+
+
![스캔 원본]()
+
+
+
+
+
+
diff --git a/packages/frontend/src/composables/scannerSession.ts b/packages/frontend/src/composables/scannerSession.ts
new file mode 100644
index 0000000..2f30e4c
--- /dev/null
+++ b/packages/frontend/src/composables/scannerSession.ts
@@ -0,0 +1,11 @@
+let pending: File[] = [];
+
+export function stageScannerFiles(files: readonly File[]) {
+ pending = [...files];
+}
+
+export function consumeScannerFiles(): File[] {
+ const files = pending;
+ pending = [];
+ return files;
+}
diff --git a/packages/frontend/src/composables/useOpenDocument.ts b/packages/frontend/src/composables/useOpenDocument.ts
index a288add..e9d64a6 100644
--- a/packages/frontend/src/composables/useOpenDocument.ts
+++ b/packages/frontend/src/composables/useOpenDocument.ts
@@ -4,6 +4,7 @@ import {
diagramKindFor,
isHwp,
isOffice,
+ toolKindFor,
viewerKindFor,
} from '@/config/app';
import {
@@ -18,6 +19,7 @@ import {
storeHwpFile,
} from '@/composables/useSessionLifecycle';
import { stageDiagramFile } from '@/composables/diagramSession';
+import { stageScannerFiles } from '@/composables/scannerSession';
import { stageViewerFiles } from '@/composables/viewerSession';
export function useOpenDocument() {
@@ -37,6 +39,7 @@ export function useOpenDocument() {
files.find(
(candidate) =>
viewerKindFor(candidate.name) ||
+ toolKindFor(candidate.name) ||
diagramKindFor(candidate.name) ||
isHwp(candidate.name) ||
isOffice(candidate.name),
@@ -53,6 +56,14 @@ export function useOpenDocument() {
return true;
}
+ const toolKind = toolKindFor(file.name);
+ if (toolKind === 'scanner') {
+ const images = files.filter((candidate) => toolKindFor(candidate.name) === 'scanner');
+ stageScannerFiles(images.length ? images : [file]);
+ await router.push({ name: 'tools-scanner' });
+ return true;
+ }
+
const diagramKind = diagramKindFor(file.name);
if (diagramKind) {
stageDiagramFile(diagramKind, file);
@@ -69,7 +80,7 @@ export function useOpenDocument() {
if (!isOffice(file.name)) {
error.value =
- '지원하지 않는 형식입니다. 오피스, HWP 또는 다이어그램 파일을 선택해 주세요.';
+ '지원하지 않는 형식입니다. 오피스, HWP, 다이어그램, CAD/3D 또는 이미지를 선택해 주세요.';
return false;
}
diff --git a/packages/frontend/src/config/app.ts b/packages/frontend/src/config/app.ts
index 512476c..91175f4 100644
--- a/packages/frontend/src/config/app.ts
+++ b/packages/frontend/src/config/app.ts
@@ -7,6 +7,7 @@ export const APP_DISPLAY_NAME = "Sejong Office";
export type DiagramKind = "excalidraw" | "mermaid" | "drawio";
export type ViewerKind = "cad" | "model3d";
+export type ToolKind = "scanner";
const DIAGRAM_EXTENSION_KIND: Record = {
excalidraw: "excalidraw",
@@ -16,6 +17,25 @@ const DIAGRAM_EXTENSION_KIND: Record = {
xml: "drawio",
};
+/** Image files opened in the document scanner tool. */
+export const SCANNER_EXTENSIONS = new Set([
+ "png",
+ "jpg",
+ "jpeg",
+ "webp",
+ "gif",
+ "bmp",
+]);
+
+const TOOL_EXTENSION_KIND: Record = {
+ png: "scanner",
+ jpg: "scanner",
+ jpeg: "scanner",
+ webp: "scanner",
+ gif: "scanner",
+ bmp: "scanner",
+};
+
/** Online3DViewer import formats (+ zip container). */
export const MODEL3D_EXTENSIONS = [
"3dm",
@@ -95,3 +115,11 @@ export function diagramKindFor(filename: string): DiagramKind | null {
export function viewerKindFor(filename: string): ViewerKind | null {
return VIEWER_EXTENSION_KIND[extensionOf(filename)] ?? null;
}
+
+export function toolKindFor(filename: string): ToolKind | null {
+ return TOOL_EXTENSION_KIND[extensionOf(filename)] ?? null;
+}
+
+export function isScannerImage(filename: string): boolean {
+ return SCANNER_EXTENSIONS.has(extensionOf(filename));
+}
diff --git a/packages/frontend/src/config/favicons.ts b/packages/frontend/src/config/favicons.ts
index 7c70559..aa182e2 100644
--- a/packages/frontend/src/config/favicons.ts
+++ b/packages/frontend/src/config/favicons.ts
@@ -17,6 +17,7 @@ export type FaviconKind =
| 'diagram'
| 'cad'
| 'model3d'
+ | 'scanner'
| 'text'
| 'csv'
| 'file';
@@ -35,6 +36,7 @@ export const ICON_PATHS: Record = {
diagram: 'M4 3H10V9H8V11H16V9H14V3H20V9H18V13H13V15H16V21H8V15H11V13H6V9H4V3M6 5V7H8V5H6M16 5V7H18V5H16M10 17V19H14V17H10Z',
cad: 'M3 3H21V21H3V3M5 5V19H19V5H5M7 17V15H9V13H11V11H13V9H15V7H17V17H7Z',
model3d: 'M12 2L21 7V17L12 22L3 17V7L12 2M12 4.3L5.4 8L12 11.7L18.6 8L12 4.3M5 9.7V15.8L11 19.1V13.1L5 9.7M13 19.1L19 15.8V9.7L13 13.1V19.1Z',
+ scanner: 'M4 4H10V6H6V10H4V4M14 4H20V10H18V6H14V4M4 14H6V18H10V20H4V14M18 14H20V20H14V18H18V14M8 8H16V16H8V8Z',
text: 'M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z',
csv: 'M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2M18 20H6V4H13V9H18V20M10 19L12 15H9V10H15V15L13 19H10',
file: 'M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z',
@@ -52,6 +54,7 @@ export const ICON_COLORS: Record = {
diagram: '#2e7d8a',
cad: '#5c6bc0',
model3d: '#6d4c41',
+ scanner: '#00897b',
text: '#607d8b',
csv: '#00838f',
file: '#546e7a',
@@ -105,6 +108,12 @@ const EXT_KIND: Record = {
ply: 'model3d',
wrl: 'model3d',
zip: 'model3d',
+ png: 'scanner',
+ jpg: 'scanner',
+ jpeg: 'scanner',
+ webp: 'scanner',
+ gif: 'scanner',
+ bmp: 'scanner',
txt: 'text',
};
diff --git a/packages/frontend/src/integrations/scanner/buildPdf.ts b/packages/frontend/src/integrations/scanner/buildPdf.ts
new file mode 100644
index 0000000..f3c7df0
--- /dev/null
+++ b/packages/frontend/src/integrations/scanner/buildPdf.ts
@@ -0,0 +1,83 @@
+import fontkit from '@pdf-lib/fontkit';
+import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
+import type { OcrResult } from './types';
+
+export interface PdfPageInput {
+ imageBytes: ArrayBuffer;
+ width: number;
+ height: number;
+ ocr?: OcrResult | null;
+}
+
+async function loadCjkFont(pdf: PDFDocument) {
+ pdf.registerFontkit(fontkit);
+ const response = await fetch(
+ '/viewer-assets/cad-resources/fonts/NotoSansKR-VF.ttf',
+ );
+ if (!response.ok) {
+ throw new Error('PDF용 한글 글꼴을 불러오지 못했습니다.');
+ }
+ const bytes = await response.arrayBuffer();
+ return pdf.embedFont(bytes, { subset: true });
+}
+
+export async function buildSearchablePdf(pages: PdfPageInput[]) {
+ if (!pages.length) {
+ throw new Error('PDF로 내보낼 페이지가 없습니다.');
+ }
+
+ const pdf = await PDFDocument.create();
+ let cjkFont = null as Awaited> | null;
+ let latinFont = await pdf.embedFont(StandardFonts.Helvetica);
+
+ const needsCjk = pages.some((page) =>
+ page.ocr?.words.some((word) => /[^\u0000-\u007f]/.test(word.text)),
+ );
+ if (needsCjk || pages.some((page) => page.ocr?.text)) {
+ try {
+ cjkFont = await loadCjkFont(pdf);
+ } catch {
+ cjkFont = null;
+ }
+ }
+
+ for (const page of pages) {
+ const image = await pdf.embedPng(page.imageBytes);
+ const pdfPage = pdf.addPage([page.width, page.height]);
+ pdfPage.drawImage(image, {
+ x: 0,
+ y: 0,
+ width: page.width,
+ height: page.height,
+ });
+
+ const font = cjkFont ?? latinFont;
+ if (page.ocr?.words.length) {
+ for (const word of page.ocr.words) {
+ const text = word.text.trim();
+ if (!text) continue;
+ if (!cjkFont && /[^\u0000-\u007f]/.test(text)) continue;
+ const boxWidth = Math.max(1, word.bbox.x1 - word.bbox.x0);
+ const boxHeight = Math.max(1, word.bbox.y1 - word.bbox.y0);
+ const fontSize = Math.max(4, Math.min(boxHeight * 0.9, boxWidth));
+ const x = word.bbox.x0;
+ const y = page.height - word.bbox.y1;
+ try {
+ pdfPage.drawText(text, {
+ x,
+ y,
+ size: fontSize,
+ font,
+ color: rgb(0, 0, 0),
+ opacity: 0,
+ maxWidth: boxWidth,
+ });
+ } catch {
+ // Skip glyphs the embedded font cannot encode.
+ }
+ }
+ }
+ }
+
+ return pdf.save();
+}
diff --git a/packages/frontend/src/integrations/scanner/detect.ts b/packages/frontend/src/integrations/scanner/detect.ts
new file mode 100644
index 0000000..c273d54
--- /dev/null
+++ b/packages/frontend/src/integrations/scanner/detect.ts
@@ -0,0 +1,194 @@
+import type { CornerPoints, Point2D } from './types';
+
+declare global {
+ interface Window {
+ cv?: {
+ Mat: new () => unknown;
+ imread: (el: HTMLCanvasElement | HTMLImageElement) => {
+ delete: () => void;
+ cols: number;
+ rows: number;
+ };
+ onRuntimeInitialized?: () => void;
+ };
+ jscanify?: new () => JscanifyInstance;
+ }
+}
+
+export interface JscanifyInstance {
+ findPaperContour: (mat: unknown) => unknown | null;
+ getCornerPoints: (contour: unknown) => Partial;
+ extractPaper: (
+ image: HTMLCanvasElement | HTMLImageElement,
+ width: number,
+ height: number,
+ corners?: CornerPoints,
+ ) => HTMLCanvasElement | null;
+}
+
+const OPENCV_URL = '/viewer-assets/scanner/opencv.js';
+const JSCANIFY_URL = '/viewer-assets/scanner/jscanify.js';
+
+let opencvReady: Promise | null = null;
+let jscanifyReady: Promise | null = null;
+
+function loadScript(src: string) {
+ return new Promise((resolve, reject) => {
+ const existing = document.querySelector(
+ `script[data-scanner-src="${src}"]`,
+ );
+ if (existing) {
+ if (existing.dataset.loaded === '1') {
+ resolve();
+ return;
+ }
+ existing.addEventListener('load', () => resolve(), { once: true });
+ existing.addEventListener(
+ 'error',
+ () => reject(new Error(`스크립트를 불러오지 못했습니다: ${src}`)),
+ { once: true },
+ );
+ return;
+ }
+ const script = document.createElement('script');
+ script.src = src;
+ script.async = true;
+ script.dataset.scannerSrc = src;
+ script.onload = () => {
+ script.dataset.loaded = '1';
+ resolve();
+ };
+ script.onerror = () =>
+ reject(new Error(`스크립트를 불러오지 못했습니다: ${src}`));
+ document.head.appendChild(script);
+ });
+}
+
+export async function ensureOpenCv() {
+ if (typeof window.cv?.Mat === 'function') return;
+ if (!opencvReady) {
+ opencvReady = (async () => {
+ await loadScript(OPENCV_URL);
+ const cv = window.cv;
+ if (!cv) throw new Error('OpenCV.js를 초기화하지 못했습니다.');
+ if (typeof cv.Mat === 'function') return;
+ await new Promise((resolve, reject) => {
+ const timeout = window.setTimeout(() => {
+ reject(new Error('OpenCV.js 초기화 시간이 초과되었습니다.'));
+ }, 60_000);
+ const finish = () => {
+ window.clearTimeout(timeout);
+ window.clearInterval(poll);
+ resolve();
+ };
+ const previous = cv.onRuntimeInitialized;
+ cv.onRuntimeInitialized = () => {
+ previous?.();
+ finish();
+ };
+ const poll = window.setInterval(() => {
+ if (typeof cv.Mat === 'function') finish();
+ }, 50);
+ });
+ })().catch((error) => {
+ opencvReady = null;
+ throw error;
+ });
+ }
+ await opencvReady;
+}
+
+export async function ensureJscanify() {
+ await ensureOpenCv();
+ if (window.jscanify) return;
+ if (!jscanifyReady) {
+ jscanifyReady = loadScript(JSCANIFY_URL);
+ }
+ await jscanifyReady;
+ if (!window.jscanify) {
+ throw new Error('jscanify를 불러오지 못했습니다.');
+ }
+}
+
+export function createScanner() {
+ if (!window.jscanify) {
+ throw new Error('jscanify가 준비되지 않았습니다.');
+ }
+ return new window.jscanify();
+}
+
+export function fullFrameCorners(
+ width: number,
+ height: number,
+): CornerPoints {
+ return {
+ topLeftCorner: { x: 0, y: 0 },
+ topRightCorner: { x: width, y: 0 },
+ bottomRightCorner: { x: width, y: height },
+ bottomLeftCorner: { x: 0, y: height },
+ };
+}
+
+function isValidPoint(point: Point2D | undefined): point is Point2D {
+ return (
+ !!point &&
+ Number.isFinite(point.x) &&
+ Number.isFinite(point.y)
+ );
+}
+
+export function normalizeCorners(
+ corners: Partial | null | undefined,
+ width: number,
+ height: number,
+): CornerPoints {
+ const fallback = fullFrameCorners(width, height);
+ if (!corners) return fallback;
+ const next: CornerPoints = {
+ topLeftCorner: isValidPoint(corners.topLeftCorner)
+ ? corners.topLeftCorner
+ : fallback.topLeftCorner,
+ topRightCorner: isValidPoint(corners.topRightCorner)
+ ? corners.topRightCorner
+ : fallback.topRightCorner,
+ bottomRightCorner: isValidPoint(corners.bottomRightCorner)
+ ? corners.bottomRightCorner
+ : fallback.bottomRightCorner,
+ bottomLeftCorner: isValidPoint(corners.bottomLeftCorner)
+ ? corners.bottomLeftCorner
+ : fallback.bottomLeftCorner,
+ };
+ return next;
+}
+
+export function detectCornersFromImage(
+ image: HTMLImageElement | HTMLCanvasElement,
+): CornerPoints {
+ const scanner = createScanner();
+ const cv = window.cv!;
+ const mat = cv.imread(image);
+ try {
+ const contour = scanner.findPaperContour(mat);
+ if (!contour) {
+ return fullFrameCorners(image.width, image.height);
+ }
+ const corners = scanner.getCornerPoints(contour);
+ if (
+ typeof (contour as { delete?: () => void }).delete === 'function'
+ ) {
+ (contour as { delete: () => void }).delete();
+ }
+ return normalizeCorners(corners, image.width, image.height);
+ } finally {
+ mat.delete();
+ }
+}
+
+export function loadImageFromUrl(url: string) {
+ return new Promise((resolve, reject) => {
+ const image = new Image();
+ image.onload = () => resolve(image);
+ image.onerror = () => reject(new Error('이미지를 불러오지 못했습니다.'));
+ image.src = url;
+ });
+}
diff --git a/packages/frontend/src/integrations/scanner/ocr.ts b/packages/frontend/src/integrations/scanner/ocr.ts
new file mode 100644
index 0000000..16dc253
--- /dev/null
+++ b/packages/frontend/src/integrations/scanner/ocr.ts
@@ -0,0 +1,104 @@
+import * as ort from 'onnxruntime-web';
+import {
+ PaddleOcrService,
+ V5_KOREAN_MOBILE_MODEL,
+} from 'ppu-paddle-ocr/web';
+import type { OcrResult, OcrWord } from './types';
+
+const ORT_WASM_PATH = '/viewer-assets/scanner/ort/';
+const PADDLE_BASE = '/viewer-assets/scanner/paddle';
+
+let servicePromise: Promise | null = null;
+
+async function toCanvas(
+ source: string | Blob | HTMLCanvasElement,
+): Promise {
+ if (source instanceof HTMLCanvasElement) return source;
+
+ const objectUrl =
+ typeof source === 'string' ? null : URL.createObjectURL(source);
+ const src = typeof source === 'string' ? source : objectUrl!;
+ try {
+ const img = await new Promise((resolve, reject) => {
+ const image = new Image();
+ image.onload = () => resolve(image);
+ image.onerror = () =>
+ reject(new Error('OCR용 이미지를 불러오지 못했습니다.'));
+ image.src = src;
+ });
+ const canvas = document.createElement('canvas');
+ canvas.width = img.naturalWidth || img.width;
+ canvas.height = img.naturalHeight || img.height;
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
+ if (!ctx) throw new Error('Canvas를 초기화하지 못했습니다.');
+ ctx.drawImage(img, 0, 0);
+ return canvas;
+ } finally {
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
+ }
+}
+
+async function getService(
+ onProgress?: (value: number, label: string) => void,
+): Promise {
+ if (!servicePromise) {
+ servicePromise = (async () => {
+ onProgress?.(0.05, 'OCR 모델 준비 중');
+ ort.env.wasm.wasmPaths = ORT_WASM_PATH;
+ const service = new PaddleOcrService({
+ model: {
+ ...V5_KOREAN_MOBILE_MODEL,
+ detection: `${PADDLE_BASE}/PP-OCRv5_mobile_det_infer.onnx`,
+ recognition: `${PADDLE_BASE}/korean_PP-OCRv5_mobile_rec_infer.onnx`,
+ charactersDictionary: `${PADDLE_BASE}/ppocrv5_korean_dict.txt`,
+ },
+ processing: { engine: 'canvas-native' },
+ debugging: { verbose: false, debug: false },
+ });
+ await service.initialize();
+ onProgress?.(0.35, 'OCR 모델 준비 완료');
+ return service;
+ })().catch((error) => {
+ servicePromise = null;
+ throw error;
+ });
+ }
+ return servicePromise;
+}
+
+export async function recognizeImage(
+ source: string | Blob | HTMLCanvasElement,
+ onProgress?: (value: number, label: string) => void,
+): Promise {
+ const service = await getService(onProgress);
+ onProgress?.(0.4, '문자 인식 중');
+ const canvas = await toCanvas(source);
+ const result = await service.recognize(canvas, {
+ flatten: true,
+ strategy: 'per-box',
+ });
+ const words: OcrWord[] = result.results
+ .filter((item) => item.text?.trim())
+ .map((item) => ({
+ text: item.text,
+ confidence: Math.round((item.confidence ?? 0) * 100),
+ bbox: {
+ x0: item.box.x,
+ y0: item.box.y,
+ x1: item.box.x + item.box.width,
+ y1: item.box.y + item.box.height,
+ },
+ }));
+ onProgress?.(1, 'OCR 완료');
+ return {
+ text: result.text?.trim() ?? words.map((word) => word.text).join(' '),
+ words,
+ };
+}
+
+export async function terminateOcrWorker() {
+ if (!servicePromise) return;
+ const service = await servicePromise;
+ servicePromise = null;
+ await service.destroy();
+}
diff --git a/packages/frontend/src/integrations/scanner/pdfPages.ts b/packages/frontend/src/integrations/scanner/pdfPages.ts
new file mode 100644
index 0000000..3256647
--- /dev/null
+++ b/packages/frontend/src/integrations/scanner/pdfPages.ts
@@ -0,0 +1,90 @@
+import { GlobalWorkerOptions, getDocument } from 'pdfjs-dist';
+
+const WORKER_SRC = '/viewer-assets/scanner/pdfjs/pdf.worker.min.mjs';
+const MAX_SIDE = 1800;
+
+let workerConfigured = false;
+
+function ensureWorker() {
+ if (workerConfigured) return;
+ GlobalWorkerOptions.workerSrc = WORKER_SRC;
+ workerConfigured = true;
+}
+
+function canvasToPngBlob(canvas: HTMLCanvasElement): Promise {
+ return new Promise((resolve, reject) => {
+ canvas.toBlob(
+ (blob) => {
+ if (!blob) {
+ reject(new Error('PDF 페이지를 PNG로 변환하지 못했습니다.'));
+ return;
+ }
+ resolve(blob);
+ },
+ 'image/png',
+ );
+ });
+}
+
+export interface PdfRenderedPage {
+ name: string;
+ blob: Blob;
+ width: number;
+ height: number;
+}
+
+export async function renderPdfToImages(
+ file: File,
+ onProgress?: (done: number, total: number) => void,
+): Promise {
+ ensureWorker();
+ const data = await file.arrayBuffer();
+ const pdf = await getDocument({ data }).promise;
+ const base = file.name.replace(/\.pdf$/i, '') || 'document';
+ const pages: PdfRenderedPage[] = [];
+
+ try {
+ for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber += 1) {
+ onProgress?.(pageNumber - 1, pdf.numPages);
+ const page = await pdf.getPage(pageNumber);
+ const unscaled = page.getViewport({ scale: 1 });
+ const longest = Math.max(unscaled.width, unscaled.height);
+ const scale = longest > 0 ? Math.min(2.5, MAX_SIDE / longest) : 1;
+ const viewport = page.getViewport({ scale });
+ const canvas = document.createElement('canvas');
+ canvas.width = Math.max(1, Math.floor(viewport.width));
+ canvas.height = Math.max(1, Math.floor(viewport.height));
+ const context = canvas.getContext('2d', { willReadFrequently: true });
+ if (!context) {
+ throw new Error('PDF 렌더용 Canvas를 만들지 못했습니다.');
+ }
+ await page.render({ canvasContext: context, viewport, canvas }).promise;
+ const blob = await canvasToPngBlob(canvas);
+ pages.push({
+ name: `${base}-p${pageNumber}.png`,
+ blob,
+ width: canvas.width,
+ height: canvas.height,
+ });
+ page.cleanup();
+ }
+ onProgress?.(pdf.numPages, pdf.numPages);
+ } finally {
+ await pdf.cleanup();
+ }
+
+ return pages;
+}
+
+export function isPdfFile(file: File): boolean {
+ return (
+ file.type === 'application/pdf' || /\.pdf$/i.test(file.name)
+ );
+}
+
+export function isScannerImageFile(file: File): boolean {
+ return (
+ /^image\/(png|jpe?g|webp|gif|bmp)$/i.test(file.type) ||
+ /\.(png|jpe?g|webp|gif|bmp)$/i.test(file.name)
+ );
+}
diff --git a/packages/frontend/src/integrations/scanner/types.ts b/packages/frontend/src/integrations/scanner/types.ts
new file mode 100644
index 0000000..6fa9a26
--- /dev/null
+++ b/packages/frontend/src/integrations/scanner/types.ts
@@ -0,0 +1,39 @@
+export interface Point2D {
+ x: number;
+ y: number;
+}
+
+export interface CornerPoints {
+ topLeftCorner: Point2D;
+ topRightCorner: Point2D;
+ bottomLeftCorner: Point2D;
+ bottomRightCorner: Point2D;
+}
+
+export type PaperAspect = 'a4-portrait' | 'a4-landscape' | 'square' | 'auto';
+
+export interface OcrWord {
+ text: string;
+ /** Pixel bbox on the warped image: x0,y0,x1,y1 */
+ bbox: { x0: number; y0: number; x1: number; y1: number };
+ confidence: number;
+}
+
+export interface OcrResult {
+ text: string;
+ words: OcrWord[];
+}
+
+export interface ScannerPage {
+ id: string;
+ name: string;
+ sourceUrl: string;
+ sourceWidth: number;
+ sourceHeight: number;
+ corners: CornerPoints;
+ aspect: PaperAspect;
+ warpedUrl: string | null;
+ warpedWidth: number;
+ warpedHeight: number;
+ ocr: OcrResult | null;
+}
diff --git a/packages/frontend/src/integrations/scanner/warp.ts b/packages/frontend/src/integrations/scanner/warp.ts
new file mode 100644
index 0000000..e77b932
--- /dev/null
+++ b/packages/frontend/src/integrations/scanner/warp.ts
@@ -0,0 +1,74 @@
+import {
+ createScanner,
+ ensureJscanify,
+ loadImageFromUrl,
+} from './detect';
+import type { CornerPoints, PaperAspect } from './types';
+
+export function outputSizeForAspect(
+ aspect: PaperAspect,
+ corners: CornerPoints,
+) {
+ if (aspect === 'a4-portrait') return { width: 1240, height: 1754 };
+ if (aspect === 'a4-landscape') return { width: 1754, height: 1240 };
+ if (aspect === 'square') return { width: 1400, height: 1400 };
+
+ const widthTop = Math.hypot(
+ corners.topRightCorner.x - corners.topLeftCorner.x,
+ corners.topRightCorner.y - corners.topLeftCorner.y,
+ );
+ const widthBottom = Math.hypot(
+ corners.bottomRightCorner.x - corners.bottomLeftCorner.x,
+ corners.bottomRightCorner.y - corners.bottomLeftCorner.y,
+ );
+ const heightLeft = Math.hypot(
+ corners.bottomLeftCorner.x - corners.topLeftCorner.x,
+ corners.bottomLeftCorner.y - corners.topLeftCorner.y,
+ );
+ const heightRight = Math.hypot(
+ corners.bottomRightCorner.x - corners.topRightCorner.x,
+ corners.bottomRightCorner.y - corners.topRightCorner.y,
+ );
+ const width = Math.max(widthTop, widthBottom, 1);
+ const height = Math.max(heightLeft, heightRight, 1);
+ const scale = Math.min(1800 / width, 1800 / height, 1);
+ return {
+ width: Math.max(1, Math.round(width * scale)),
+ height: Math.max(1, Math.round(height * scale)),
+ };
+}
+
+export async function warpImage(
+ sourceUrl: string,
+ corners: CornerPoints,
+ aspect: PaperAspect,
+) {
+ await ensureJscanify();
+ const image = await loadImageFromUrl(sourceUrl);
+ const size = outputSizeForAspect(aspect, corners);
+ const scanner = createScanner();
+ const canvas = scanner.extractPaper(
+ image,
+ size.width,
+ size.height,
+ corners,
+ );
+ if (!canvas) {
+ throw new Error('문서 영역을 추출하지 못했습니다.');
+ }
+ const blob = await new Promise((resolve, reject) => {
+ canvas.toBlob(
+ (value) =>
+ value
+ ? resolve(value)
+ : reject(new Error('보정 이미지를 만들지 못했습니다.')),
+ 'image/png',
+ );
+ });
+ return {
+ blob,
+ width: canvas.width,
+ height: canvas.height,
+ url: URL.createObjectURL(blob),
+ };
+}
diff --git a/packages/frontend/src/router/index.ts b/packages/frontend/src/router/index.ts
index 774c316..9eb2a67 100644
--- a/packages/frontend/src/router/index.ts
+++ b/packages/frontend/src/router/index.ts
@@ -44,6 +44,11 @@ const router = createRouter({
name: 'model3d-viewer',
component: () => import('@/views/Model3DViewerView.vue'),
},
+ {
+ path: '/tools/scanner',
+ name: 'tools-scanner',
+ component: () => import('@/views/ToolsScannerView.vue'),
+ },
{
path: '/licenses',
name: 'licenses',
diff --git a/packages/frontend/src/types/scanner-shims.d.ts b/packages/frontend/src/types/scanner-shims.d.ts
new file mode 100644
index 0000000..ba52ed8
--- /dev/null
+++ b/packages/frontend/src/types/scanner-shims.d.ts
@@ -0,0 +1,10 @@
+declare module '@pdf-lib/fontkit' {
+ import type { Fontkit } from 'pdf-lib';
+ const fontkit: Fontkit;
+ export default fontkit;
+}
+
+declare module 'jscanify/client' {
+ const jscanify: unknown;
+ export default jscanify;
+}
diff --git a/packages/frontend/src/views/HomeView.vue b/packages/frontend/src/views/HomeView.vue
index 5539082..68f7c8f 100644
--- a/packages/frontend/src/views/HomeView.vue
+++ b/packages/frontend/src/views/HomeView.vue
@@ -62,6 +62,15 @@ const viewerTypes = [
},
] as const;
+const toolTypes = [
+ {
+ route: 'tools-scanner',
+ extension: 'png',
+ title: '문서 스캐너',
+ subtitle: '코너 보정 · OCR · PDF',
+ },
+] as const;
+
function onDrop(e: DragEvent) {
dragging.value = false;
void openDocument(e.dataTransfer?.files);
@@ -116,7 +125,7 @@ function onOpenTool(route: string) {
{{ busy ? '여는 중…' : '파일을 끌어다 놓거나 클릭하여 선택' }}
- 오피스·PDF · HWP/HWPX · 다이어그램 · CAD · 3D 모델(ZIP 포함)
+ 오피스·PDF · HWP/HWPX · 다이어그램 · CAD · 3D · 이미지(스캐너)
@@ -193,6 +202,30 @@ function onOpenTool(route: string) {
+
+
+ 도구
+
+ 브라우저에서 문서를 스캔·보정하고 OCR이 포함된 PDF로 저장합니다.
+
+
+
+
+
+
diff --git a/packages/frontend/src/views/LicensesView.vue b/packages/frontend/src/views/LicensesView.vue
index d06a77e..436614c 100644
--- a/packages/frontend/src/views/LicensesView.vue
+++ b/packages/frontend/src/views/LicensesView.vue
@@ -51,6 +51,41 @@ const thirdParty = [
license: 'MIT',
url: 'https://github.com/101arrowz/fflate',
},
+ {
+ name: 'jscanify',
+ license: 'MIT',
+ url: 'https://github.com/puffinsoft/jscanify',
+ },
+ {
+ name: 'OpenCV.js',
+ license: 'Apache-2.0',
+ url: 'https://opencv.org/',
+ },
+ {
+ name: 'ppu-paddle-ocr',
+ license: 'MIT',
+ url: 'https://github.com/PT-Perkasa-Pilar-Utama/ppu-paddle-ocr',
+ },
+ {
+ name: 'ONNX Runtime Web',
+ license: 'MIT',
+ url: 'https://github.com/microsoft/onnxruntime',
+ },
+ {
+ name: 'PaddleOCR models (PP-OCRv5)',
+ license: 'Apache-2.0',
+ url: 'https://github.com/PaddlePaddle/PaddleOCR',
+ },
+ {
+ name: 'pdf-lib',
+ license: 'MIT',
+ url: 'https://github.com/Hopding/pdf-lib',
+ },
+ {
+ name: 'PDF.js',
+ license: 'Apache-2.0',
+ url: 'https://github.com/mozilla/pdf.js',
+ },
{
name: 'Noto Sans KR',
license: 'SIL OFL 1.1',
@@ -149,6 +184,42 @@ const thirdParty = [
+
+ 문서 스캐너
+
+ 문서 코너 검출·퍼스펙티브 보정은
+ jscanify
+ 와 OpenCV.js(Apache-2.0)를 사용합니다. OCR은
+ ppu-paddle-ocr
+ (MIT)와 ONNX Runtime Web(MIT)으로 PP-OCRv5 한국어 모델을 브라우저에서
+ 실행하며, PDF 페이지 래스터화는
+ PDF.js
+ (Apache-2.0), PDF 생성은 pdf-lib(MIT)입니다. OpenCV·ORT WASM·ONNX
+ 모델·사전·PDF.js worker는
+ /viewer-assets/scanner/에 자체 호스팅됩니다.
+
+
+ 스캔 이미지와 OCR 결과는 브라우저에서만 처리하며 서버로 업로드하지
+ 않습니다. OCR을 켠 PDF에는 검색·복사용 투명 텍스트 레이어가
+ 포함됩니다.
+
+
+
Euro-Office Document Server
diff --git a/packages/frontend/src/views/ToolsScannerView.vue b/packages/frontend/src/views/ToolsScannerView.vue
new file mode 100644
index 0000000..87c159b
--- /dev/null
+++ b/packages/frontend/src/views/ToolsScannerView.vue
@@ -0,0 +1,992 @@
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
{{ notice }}
+
+
+
+
+
+
+
문서 스캐너
+
+ 이미지 또는 PDF를 올리면 페이지를 목록에 추가합니다. 드래그로 순서를
+ 바꾼 뒤 보정·OCR·검색 가능 PDF로 저장할 수 있습니다. 처리는 모두
+ 브라우저에서 수행됩니다.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 보정 미리보기
+
+ 보정 후 미리보기가 표시됩니다.
+
+
OCR 결과
+
{{ activePage.ocr.text || '(텍스트 없음)' }}
+
+
+
+
+
+
+
{{ progressLabel }}
+
+
{{ progressPercent }}% · {{ warpedCount }}/{{ pages.length }}
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index f2b6344..776a7b1 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -9,6 +9,9 @@ export default defineConfig({
'@': path.resolve(__dirname, 'src'),
},
},
+ optimizeDeps: {
+ exclude: ['ppu-paddle-ocr', 'onnxruntime-web'],
+ },
server: {
port: 5173,
proxy: {
@@ -22,4 +25,5 @@ export default defineConfig({
outDir: path.resolve(__dirname, '../backend/public'),
emptyOutDir: true,
},
+ assetsInclude: ['**/*.onnx', '**/*.ort', '**/*.wasm'],
});