mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-05 11:52:48 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89f422b2af | ||
|
|
12753349ec | ||
|
|
f3e7ef25fa | ||
|
|
ba20536436 | ||
|
|
e3468e3aaa | ||
|
|
e0d3075609 | ||
|
|
466646d15b | ||
|
|
be6c15fef2 | ||
|
|
d5a15bce78 | ||
|
|
3cbbc42434 | ||
|
|
06c36a590b | ||
|
|
de3c2fecc2 | ||
|
|
b7de4bf1ed | ||
|
|
e0e5d50240 | ||
|
|
11ba3fe81b | ||
|
|
05de28d791 | ||
|
|
0403a9665f | ||
|
|
0ddf54cba3 | ||
|
|
25c221f8b9 | ||
|
|
d4f1aa30f5 | ||
|
|
c9faee4fd9 | ||
|
|
929aa7e132 | ||
|
|
2b85a759af |
@@ -0,0 +1,2 @@
|
||||
github: alam00000
|
||||
ko_fi: alio01
|
||||
@@ -0,0 +1,15 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
groups:
|
||||
npm-dependencies:
|
||||
patterns:
|
||||
- '*'
|
||||
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
@@ -3,6 +3,7 @@
|
||||
**BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that allows you to manipulate, edit, merge, and process PDF files directly in your browser. No server-side processing is required, ensuring your files remain secure and private.
|
||||
|
||||
 [](https://ko-fi.com/alio0) 
|
||||
[](https://github.com/sponsors/alam00000)
|
||||
|
||||
## ⭐ Stargazers over time
|
||||
|
||||
@@ -25,16 +26,18 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
||||
|
||||
### Organize & Manage PDFs
|
||||
|
||||
| Tool Name | Description |
|
||||
| :----------------- | :------------------------------------------------------------------------- |
|
||||
| **Merge PDFs** | Combine multiple PDF files into one. |
|
||||
| **Split PDFs** | Extract specific pages or divide a document into smaller files. |
|
||||
| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. |
|
||||
| **Extract Pages** | Save a specific range of pages as a new PDF. |
|
||||
| **Delete Pages** | Remove unwanted pages from your document. |
|
||||
| **Rotate PDF** | Rotate individual or all pages in a document. |
|
||||
| **N-Up PDF** | Combine multiple pages onto a single page. |
|
||||
| **View PDF** | A powerful, integrated PDF viewer. |
|
||||
| Tool Name | Description |
|
||||
| :------------------------ | :------------------------------------------------------------------------- |
|
||||
| **Merge PDFs** | Combine multiple PDF files into one. |
|
||||
| **Split PDFs** | Extract specific pages or divide a document into smaller files. |
|
||||
| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. |
|
||||
| **Extract Pages** | Save a specific range of pages as a new PDF. |
|
||||
| **Delete Pages** | Remove unwanted pages from your document. |
|
||||
| **Rotate PDF** | Rotate individual or all pages in a document. |
|
||||
| **N-Up PDF** | Combine multiple pages onto a single page. |
|
||||
| **View PDF** | A powerful, integrated PDF viewer. |
|
||||
| **Alternate & Mix pages** | Merge pages by alternating pages from each PDF. |
|
||||
| **Posterize PDF** | Split a PDF into multiple smaller pages for print. |
|
||||
|
||||
### Edit & Modify PDFs
|
||||
|
||||
@@ -51,6 +54,7 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
||||
| **Fill Forms** | Fill out PDF forms directly in your browser. |
|
||||
| **Flatten PDF** | Flatten form fields and annotations into static content. |
|
||||
| **Remove Annotations** | Remove comments, highlights, and other annotations. |
|
||||
| **Remove Blank Pages** | Auto detect and remove blank pages in a PDF. |
|
||||
|
||||
### Convert to PDF
|
||||
|
||||
@@ -81,6 +85,8 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
||||
| **Redact Content** | Permanently remove sensitive content from your PDFs. |
|
||||
| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). |
|
||||
| **Remove Metadata** | Strip all metadata from your PDF for privacy. |
|
||||
| **Linearize PDF** | Optimize PDF for fast web view. |
|
||||
| **Sanitize PDF** | Remove potentially unwanted or malicous files from PDF. |
|
||||
|
||||
---
|
||||
|
||||
@@ -164,8 +170,8 @@ npm run release:minor
|
||||
npm run release:major
|
||||
```
|
||||
|
||||
````
|
||||
For detailed release instructions, see [RELEASE.md](RELEASE.md).
|
||||
|
||||
### 🚀 Development Setup
|
||||
|
||||
#### Option 1: Run with npm
|
||||
@@ -175,7 +181,7 @@ For detailed release instructions, see [RELEASE.md](RELEASE.md).
|
||||
```bash
|
||||
git clone https://github.com/alam00000/bentopdf.git
|
||||
cd bentopdf
|
||||
````
|
||||
```
|
||||
|
||||
2. **Install Dependencies**:
|
||||
|
||||
@@ -234,10 +240,8 @@ BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript*
|
||||
|
||||
- **HTML to PDF**: Convert HTML files or web pages into PDF documents.
|
||||
- **Markdown to PDF**: Enhanced support for converting `.md` files to PDF.
|
||||
- **Sanitize PDF**: Remove potentially malicious content like scripts from PDFs.
|
||||
- **Convert to PDF/A**: Convert PDFs to the PDF/A archival format.
|
||||
- **Edit PDF Content**: Directly edit text and other content within your PDF.
|
||||
- **Linearize PDF**: Optimize PDFs for fast web viewing.
|
||||
- **PDF to Office**: Converts PDF files into editable Word, Excel, and PowerPoint formats.
|
||||
- **Office to PDF**: Converts Word, Excel, and PowerPoint documents into optimized PDFs.
|
||||
|
||||
@@ -265,10 +269,11 @@ BentoPDF wouldn't be possible without the amazing open-source tools and librarie
|
||||
- **[PDFLib.js](https://pdf-lib.js.org/)** – For enabling powerful client-side PDF manipulation.
|
||||
- **[PDF.js](https://mozilla.github.io/pdf.js/)** – For the robust PDF rendering engine in the browser.
|
||||
- **[PDFKit](https://pdfkit.org/)** – For creating and editing PDFs with ease.
|
||||
- **[EmbedPDF](https://github.com/embedpdf/embed-pdf-viewer)** – For seamless PDF embedding in web pages.
|
||||
- **[EmbedPDF](https://github.com/embedpdf/embed-pdf-viewer)** – For seamless PDF editing in pure JS.
|
||||
- **[Cropper.js](https://fengyuanchen.github.io/cropperjs/)** – For intuitive image cropping functionality.
|
||||
- **[Vite](https://vitejs.dev/)** – For lightning-fast development and build tooling.
|
||||
- **[Tailwind CSS](https://tailwindcss.com/)** – For rapid, flexible, and beautiful UI styling.
|
||||
- **[qpdf](https://github.com/qpdf/qpdf)** and **[qpdf-wasm](https://github.com/neslinesli93/qpdf-wasm)**– A powerful command-line tool and library for inspecting, repairing, and transforming PDF file ported to wasm
|
||||
|
||||
Your work inspires and empowers developers everywhere. Thank you for making open-source amazing!
|
||||
|
||||
|
||||
-22
@@ -4,28 +4,6 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BentoPDF - The Privacy First PDF Toolkit</title>
|
||||
<script
|
||||
async
|
||||
src="https://scripts.simpleanalyticscdn.com/latest.js"
|
||||
></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.css"
|
||||
/>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Dancing+Script:[email protected]&family=Great+Vibes&family=Kalam:wght@300;400;700&family=Lato:ital,wght@0,400;0,700;1,400&family=Merriweather:ital,wght@0,400;0,700;1,400&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="icon" type="image/png" href="./images/favicon.svg" />
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
Generated
+288
-221
@@ -1,16 +1,23 @@
|
||||
{
|
||||
"name": "bento-pdf",
|
||||
"version": "0.0.0",
|
||||
"version": "1.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bento-pdf",
|
||||
"version": "0.0.0",
|
||||
"version": "1.1.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||
"@fontsource/dancing-script": "^5.2.8",
|
||||
"@fontsource/dm-sans": "^5.2.8",
|
||||
"@fontsource/great-vibes": "^5.2.8",
|
||||
"@fontsource/kalam": "^5.2.8",
|
||||
"@fontsource/lato": "^5.2.7",
|
||||
"@fontsource/merriweather": "^5.2.11",
|
||||
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tailwindcss/vite": "^4.1.15",
|
||||
"blob-stream": "^0.1.3",
|
||||
"cropperjs": "^1.6.1",
|
||||
"heic2any": "^0.0.4",
|
||||
@@ -18,7 +25,7 @@
|
||||
"javascript-obfuscator": "^4.1.1",
|
||||
"jspdf": "^3.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide": "^0.545.0",
|
||||
"lucide": "^0.546.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^5.4.296",
|
||||
"pdfkit": "^0.17.2",
|
||||
@@ -32,17 +39,17 @@
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@types/blob-stream": "^0.1.33",
|
||||
"@types/html2canvas": "^0.5.35",
|
||||
"@types/html2canvas": "^1.0.0",
|
||||
"@types/pdfkit": "^0.17.3",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@types/utif": "^3.0.6",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsdom": "^27.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-migrate": "^0.1.35",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
@@ -86,9 +93,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.6.2.tgz",
|
||||
"integrity": "sha512-+AG0jN9HTwfDLBhjhX1FKi6zlIAc/YGgEHlN/OMaHD1pOPFsC5CpYQpLkPX0aFjyaVmoq9330cQDCU4qnSL1qA==",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.2.tgz",
|
||||
"integrity": "sha512-ccKogJI+0aiDhOahdjANIc9SDixSud1gbwdVrhn7kMopAtLXqsz9MKmQQtIl6Y5aC2IYq+j4dz/oedL2AVMmVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2576,6 +2583,69 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/cedarville-cursive": {
|
||||
"version": "5.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/cedarville-cursive/-/cedarville-cursive-5.2.7.tgz",
|
||||
"integrity": "sha512-G1Wcc8U9MsFV7t4vmxSaWEDVcJj86qbkuS/V27S5UngxkX6CoTpEdvDeGUIKBlF8FoHCDOQxSNZCFuCafRmvUg==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/dancing-script": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/dancing-script/-/dancing-script-5.2.8.tgz",
|
||||
"integrity": "sha512-zHVVgIQ5/rAIM0VVp1QMsqvJm9INHO5/C82E9rHT6NlabjaPTFYn8wl9lT4vPWizfB0TublGQM1999kfdA+XFA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/dm-sans": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/dm-sans/-/dm-sans-5.2.8.tgz",
|
||||
"integrity": "sha512-tlovG42m9ESG28WiHpLq3F5umAlm64rv0RkqTbYowRn70e9OlRr5a3yTJhrhrY+k5lftR/OFJjPzOLQzk8EfCA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/great-vibes": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/great-vibes/-/great-vibes-5.2.8.tgz",
|
||||
"integrity": "sha512-/PATz436NGixjNaigierUpv50wfNv2UhAAijcCPkn4ynvl41x66TCLbXVSVfp4pth9gjWnGqgoj0lVIhHapAuw==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/kalam": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/kalam/-/kalam-5.2.8.tgz",
|
||||
"integrity": "sha512-9WDYPX1llS28xRjrqAoz3IOLeaBhCtL3HCtNanYnF1fthFPmSD5aESL6s7C1SJxAlxq/qzTlpIpw4S1W9+q3OA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/lato": {
|
||||
"version": "5.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/lato/-/lato-5.2.7.tgz",
|
||||
"integrity": "sha512-k5mum1ADbDW5cTw1Ett1eQVWeoZ6gq0ct6SFBibEhB4LRxhniChJZTBgd6ph5yBxLkN1fcnsnmicBNA4S/3nbw==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/merriweather": {
|
||||
"version": "5.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/merriweather/-/merriweather-5.2.11.tgz",
|
||||
"integrity": "sha512-ZiIMeUh5iT8d73o6xlSF8GKgjV5pgiFrufYc5jZTVAfExtWKqM2vQHnsqXSFMv4ELhAcjt6Vf+5T3oVGXhAizQ==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||
@@ -2703,18 +2773,6 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/schema": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||
@@ -3420,52 +3478,47 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz",
|
||||
"integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz",
|
||||
"integrity": "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.6.0",
|
||||
"lightningcss": "1.30.1",
|
||||
"lightningcss": "1.30.2",
|
||||
"magic-string": "^0.30.19",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.14"
|
||||
"tailwindcss": "4.1.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz",
|
||||
"integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==",
|
||||
"hasInstallScript": true,
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz",
|
||||
"integrity": "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.4",
|
||||
"tar": "^7.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.14",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.14",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.14",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.14",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.14",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.14",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.14",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.14"
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.15",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.15",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.15",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.15",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.15",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.15",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz",
|
||||
"integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.15.tgz",
|
||||
"integrity": "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3479,9 +3532,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz",
|
||||
"integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.15.tgz",
|
||||
"integrity": "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3495,9 +3548,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz",
|
||||
"integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.15.tgz",
|
||||
"integrity": "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3511,9 +3564,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz",
|
||||
"integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.15.tgz",
|
||||
"integrity": "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3527,9 +3580,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz",
|
||||
"integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.15.tgz",
|
||||
"integrity": "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -3543,9 +3596,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz",
|
||||
"integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.15.tgz",
|
||||
"integrity": "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3559,9 +3612,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz",
|
||||
"integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.15.tgz",
|
||||
"integrity": "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3575,9 +3628,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz",
|
||||
"integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.15.tgz",
|
||||
"integrity": "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3591,9 +3644,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz",
|
||||
"integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.15.tgz",
|
||||
"integrity": "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3607,9 +3660,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz",
|
||||
"integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.15.tgz",
|
||||
"integrity": "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
@@ -3627,7 +3680,7 @@
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@emnapi/wasi-threads": "^1.1.0",
|
||||
"@napi-rs/wasm-runtime": "^1.0.5",
|
||||
"@napi-rs/wasm-runtime": "^1.0.7",
|
||||
"@tybys/wasm-util": "^0.10.1",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
@@ -3635,10 +3688,64 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.5.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.1.0",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.5.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.1.0",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.7",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz",
|
||||
"integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz",
|
||||
"integrity": "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -3652,9 +3759,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz",
|
||||
"integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.15.tgz",
|
||||
"integrity": "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3668,14 +3775,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz",
|
||||
"integrity": "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.15.tgz",
|
||||
"integrity": "sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.1.14",
|
||||
"@tailwindcss/oxide": "4.1.14",
|
||||
"tailwindcss": "4.1.14"
|
||||
"@tailwindcss/node": "4.1.15",
|
||||
"@tailwindcss/oxide": "4.1.15",
|
||||
"tailwindcss": "4.1.15"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
@@ -3801,23 +3908,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/html2canvas": {
|
||||
"version": "0.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-0.5.35.tgz",
|
||||
"integrity": "sha512-1A2dtWZbOIZ+rUK8jmAx1We/EiNV+5vScpphU3AF14Vby6COIazi/9StosrvlVCqlQegRhsEgZf7QYOuWbwuuA==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz",
|
||||
"integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==",
|
||||
"deprecated": "This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jquery": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz",
|
||||
"integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/sizzle": "*"
|
||||
"html2canvas": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
@@ -3859,13 +3957,6 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz",
|
||||
"integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/sortablejs": {
|
||||
"version": "1.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
|
||||
@@ -5046,15 +5137,6 @@
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cipher-base": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
|
||||
@@ -8088,21 +8170,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "27.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz",
|
||||
"integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==",
|
||||
"version": "27.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz",
|
||||
"integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/dom-selector": "^6.5.4",
|
||||
"cssstyle": "^5.3.0",
|
||||
"@asamuzakjp/dom-selector": "^6.7.2",
|
||||
"cssstyle": "^5.3.1",
|
||||
"data-urls": "^6.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"parse5": "^7.3.0",
|
||||
"parse5": "^8.0.0",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
@@ -8111,8 +8193,8 @@
|
||||
"webidl-conversions": "^8.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^15.0.0",
|
||||
"ws": "^8.18.2",
|
||||
"whatwg-url": "^15.1.0",
|
||||
"ws": "^8.18.3",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8306,9 +8388,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
@@ -8321,22 +8403,43 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-darwin-arm64": "1.30.1",
|
||||
"lightningcss-darwin-x64": "1.30.1",
|
||||
"lightningcss-freebsd-x64": "1.30.1",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.1",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.1",
|
||||
"lightningcss-linux-arm64-musl": "1.30.1",
|
||||
"lightningcss-linux-x64-gnu": "1.30.1",
|
||||
"lightningcss-linux-x64-musl": "1.30.1",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.1",
|
||||
"lightningcss-win32-x64-msvc": "1.30.1"
|
||||
"lightningcss-android-arm64": "1.30.2",
|
||||
"lightningcss-darwin-arm64": "1.30.2",
|
||||
"lightningcss-darwin-x64": "1.30.2",
|
||||
"lightningcss-freebsd-x64": "1.30.2",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||
"lightningcss-linux-x64-musl": "1.30.2",
|
||||
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
||||
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8354,9 +8457,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
|
||||
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8374,9 +8477,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
|
||||
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8394,9 +8497,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
|
||||
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8414,9 +8517,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
|
||||
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8434,9 +8537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
|
||||
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8454,9 +8557,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
|
||||
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8474,9 +8577,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
|
||||
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8494,9 +8597,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
|
||||
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8514,9 +8617,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
|
||||
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8630,9 +8733,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lucide": {
|
||||
"version": "0.545.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide/-/lucide-0.545.0.tgz",
|
||||
"integrity": "sha512-mrBH0upkb1TH8ZLkf0XERQAKMdTJ1C+Offr7eSvanmdQ7JrV8jkrFw5jgRAS8fygiZgG4X9mfEg/BXCL+mNMRw==",
|
||||
"version": "0.546.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide/-/lucide-0.546.0.tgz",
|
||||
"integrity": "sha512-YJES3MM/naQS4wJ0JLzTY3anooqWw5iTsPCffHbSMncdxJT2C5tmkCDAwIHMHG8TMtaQcu40KREMPss2qH/+yA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
@@ -8855,23 +8958,12 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mixin-deep": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||
@@ -9470,9 +9562,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
||||
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -11263,9 +11355,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.14",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
|
||||
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
||||
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
@@ -11281,22 +11373,6 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
|
||||
"integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/temp": {
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz",
|
||||
@@ -12256,9 +12332,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
||||
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
|
||||
"version": "7.1.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
@@ -12771,15 +12847,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
|
||||
+13
-6
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bento-pdf",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.3",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -24,23 +24,30 @@
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@types/blob-stream": "^0.1.33",
|
||||
"@types/html2canvas": "^0.5.35",
|
||||
"@types/html2canvas": "^1.0.0",
|
||||
"@types/pdfkit": "^0.17.3",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@types/utif": "^3.0.6",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsdom": "^27.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-migrate": "^0.1.35",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||
"@fontsource/dancing-script": "^5.2.8",
|
||||
"@fontsource/dm-sans": "^5.2.8",
|
||||
"@fontsource/great-vibes": "^5.2.8",
|
||||
"@fontsource/kalam": "^5.2.8",
|
||||
"@fontsource/lato": "^5.2.7",
|
||||
"@fontsource/merriweather": "^5.2.11",
|
||||
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tailwindcss/vite": "^4.1.15",
|
||||
"blob-stream": "^0.1.3",
|
||||
"cropperjs": "^1.6.1",
|
||||
"heic2any": "^0.0.4",
|
||||
@@ -48,7 +55,7 @@
|
||||
"javascript-obfuscator": "^4.1.1",
|
||||
"jspdf": "^3.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide": "^0.545.0",
|
||||
"lucide": "^0.546.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^5.4.296",
|
||||
"pdfkit": "^0.17.2",
|
||||
|
||||
@@ -1,5 +1,49 @@
|
||||
@import '@fontsource/dm-sans/400.css';
|
||||
@import '@fontsource/dm-sans/500.css';
|
||||
@import '@fontsource/dm-sans/600.css';
|
||||
@import '@fontsource/dm-sans/700.css';
|
||||
@import '@fontsource/dancing-script/400.css';
|
||||
@import '@fontsource/great-vibes/400.css';
|
||||
@import '@fontsource/kalam/300.css';
|
||||
@import '@fontsource/kalam/400.css';
|
||||
@import '@fontsource/kalam/700.css';
|
||||
@import '@fontsource/lato/400.css';
|
||||
@import '@fontsource/lato/700.css';
|
||||
@import '@fontsource/lato/400-italic.css';
|
||||
@import '@fontsource/merriweather/400.css';
|
||||
@import '@fontsource/merriweather/700.css';
|
||||
@import '@fontsource/merriweather/400-italic.css';
|
||||
@import '@fontsource/cedarville-cursive/400.css';
|
||||
@import 'cropperjs/dist/cropper.css';
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-dancing: 'Dancing Script', cursive;
|
||||
--font-vibes: 'Great Vibes', cursive;
|
||||
--font-kalam: 'Kalam', cursive;
|
||||
--font-lato: 'Lato', sans-serif;
|
||||
--font-merriweather: 'Merriweather', serif;
|
||||
--font-cedarville: 'Cedarville Cursive', cursive;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply antialiased;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: 'DM Sans', ui-sans-serif, system-ui;
|
||||
--font-dancing: 'Dancing Script', cursive;
|
||||
--font-vibes: 'Great Vibes', cursive;
|
||||
--font-kalam: 'Kalam', cursive;
|
||||
--font-lato: 'Lato', sans-serif;
|
||||
--font-merriweather: 'Merriweather', serif;
|
||||
--font-cedarville: 'Cedarville Cursive', cursive;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ export const singlePdfLoadTools = [
|
||||
'posterize',
|
||||
'remove-blank-pages',
|
||||
'add-attachments',
|
||||
'sanitize-pdf',
|
||||
'remove-restrictions',
|
||||
];
|
||||
|
||||
export const simpleTools = [
|
||||
|
||||
+17
-4
@@ -369,7 +369,7 @@ export const categories = [
|
||||
{
|
||||
id: 'posterize',
|
||||
name: 'Posterize PDF',
|
||||
icon: 'layout-grid',
|
||||
icon: 'notepad-text-dashed',
|
||||
subtitle: 'Split a large page into multiple smaller pages.',
|
||||
},
|
||||
],
|
||||
@@ -392,7 +392,7 @@ export const categories = [
|
||||
{
|
||||
id: 'linearize',
|
||||
name: 'Linearize PDF',
|
||||
icon: 'zap',
|
||||
icon: 'gauge',
|
||||
subtitle: 'Optimize PDF for fast web viewing.',
|
||||
},
|
||||
{
|
||||
@@ -401,6 +401,13 @@ export const categories = [
|
||||
icon: 'ruler',
|
||||
subtitle: 'Analyze page size, orientation, and units.',
|
||||
},
|
||||
{
|
||||
id: 'remove-restrictions',
|
||||
name: 'Remove Restrictions',
|
||||
icon: 'unlink',
|
||||
subtitle:
|
||||
'Remove password protection and security restrictions associated with digitally signed PDF files.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -410,13 +417,19 @@ export const categories = [
|
||||
id: 'encrypt',
|
||||
name: 'Encrypt PDF',
|
||||
icon: 'lock',
|
||||
subtitle: 'Add a password to protect your PDF.',
|
||||
subtitle: 'Lock your PDF by adding a password.',
|
||||
},
|
||||
{
|
||||
id: 'sanitize-pdf',
|
||||
name: 'Sanitize PDF',
|
||||
icon: 'brush-cleaning',
|
||||
subtitle: 'Remove metadata, annotations, scripts, and more.',
|
||||
},
|
||||
{
|
||||
id: 'decrypt',
|
||||
name: 'Decrypt PDF',
|
||||
icon: 'unlock',
|
||||
subtitle: 'Remove password protection from a PDF.',
|
||||
subtitle: 'Unlock PDF by removing password protection.',
|
||||
},
|
||||
{
|
||||
id: 'flatten',
|
||||
|
||||
@@ -33,7 +33,8 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
if (
|
||||
state.pdfDoc.isEncrypted &&
|
||||
toolId !== 'decrypt' &&
|
||||
toolId !== 'change-permissions'
|
||||
toolId !== 'change-permissions' &&
|
||||
toolId !== 'remove-restrictions'
|
||||
) {
|
||||
showAlert(
|
||||
'Protected PDF',
|
||||
@@ -269,9 +270,9 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
if ((child as Element).nodeType !== 1) continue;
|
||||
|
||||
let key = (child as Element).tagName;
|
||||
const elementChildren = Array.from((child as Element).children).filter(
|
||||
(c) => c.nodeType === 1
|
||||
);
|
||||
const elementChildren = Array.from(
|
||||
(child as Element).children
|
||||
).filter((c) => c.nodeType === 1);
|
||||
|
||||
if (key === 'rdf:li') {
|
||||
appendXmpNodes(child, ulElement, indentLevel);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
readFileAsArrayBuffer,
|
||||
hexToRgb,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { state, resetState } from '../state.js';
|
||||
|
||||
import {
|
||||
PDFDocument as PDFLibDocument,
|
||||
@@ -175,6 +175,15 @@ export async function addWatermark() {
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'watermarked.pdf'
|
||||
);
|
||||
|
||||
const toolid = state.activeTool;
|
||||
resetState();
|
||||
if (toolid) {
|
||||
const element = document.querySelector(
|
||||
`[data-tool-id="${toolid}"]`
|
||||
) as HTMLElement;
|
||||
if (element) element.click();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
|
||||
+149
-126
@@ -1,146 +1,169 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||
import blobStream from 'blob-stream';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
export async function changePermissions() {
|
||||
const currentPassword = (
|
||||
document.getElementById('current-password') as HTMLInputElement
|
||||
).value;
|
||||
const newUserPassword = (
|
||||
document.getElementById('new-user-password') as HTMLInputElement
|
||||
).value;
|
||||
const newOwnerPassword = (
|
||||
document.getElementById('new-owner-password') as HTMLInputElement
|
||||
).value;
|
||||
const file = state.files[0];
|
||||
const currentPassword =
|
||||
(document.getElementById('current-password') as HTMLInputElement)?.value ||
|
||||
'';
|
||||
const newUserPassword =
|
||||
(document.getElementById('new-user-password') as HTMLInputElement)?.value ||
|
||||
'';
|
||||
const newOwnerPassword =
|
||||
(document.getElementById('new-owner-password') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
// An owner password is required to enforce any permissions.
|
||||
if (
|
||||
!newOwnerPassword &&
|
||||
(newUserPassword ||
|
||||
document.querySelectorAll('input[type="checkbox"]:not(:checked)').length >
|
||||
0)
|
||||
) {
|
||||
showAlert(
|
||||
'Input Required',
|
||||
'You must set a "New Owner Password" to enforce specific permissions or to set a user password.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Preparing to process...');
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
const file = state.files[0];
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
showLoader('Initializing...');
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
let pdf;
|
||||
try {
|
||||
pdf = await pdfjsLib.getDocument({
|
||||
data: pdfData as ArrayBuffer,
|
||||
password: currentPassword,
|
||||
}).promise;
|
||||
} catch (e) {
|
||||
// This catch is specific to password errors in pdf.js
|
||||
if (e.name === 'PasswordException') {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The current password you entered is incorrect.'
|
||||
);
|
||||
return;
|
||||
showLoader('Reading PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
showLoader('Processing PDF permissions...');
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
// Add password if provided
|
||||
if (currentPassword) {
|
||||
args.push('--password=' + currentPassword);
|
||||
}
|
||||
|
||||
const shouldEncrypt = newUserPassword || newOwnerPassword;
|
||||
|
||||
if (shouldEncrypt) {
|
||||
const finalUserPassword = newUserPassword;
|
||||
const finalOwnerPassword = newOwnerPassword;
|
||||
|
||||
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
|
||||
|
||||
const allowPrinting = (
|
||||
document.getElementById('allow-printing') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowCopying = (
|
||||
document.getElementById('allow-copying') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowModifying = (
|
||||
document.getElementById('allow-modifying') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowAnnotating = (
|
||||
document.getElementById('allow-annotating') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowFillingForms = (
|
||||
document.getElementById('allow-filling-forms') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowDocumentAssembly = (
|
||||
document.getElementById('allow-document-assembly') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowPageExtraction = (
|
||||
document.getElementById('allow-page-extraction') as HTMLInputElement
|
||||
)?.checked;
|
||||
|
||||
if (finalOwnerPassword) {
|
||||
if (!allowModifying) args.push('--modify=none');
|
||||
if (!allowCopying) args.push('--extract=n');
|
||||
if (!allowPrinting) args.push('--print=none');
|
||||
if (!allowAnnotating) args.push('--annotate=n');
|
||||
if (!allowDocumentAssembly) args.push('--assemble=n');
|
||||
if (!allowFillingForms) args.push('--form=n');
|
||||
if (!allowPageExtraction) args.push('--extract=n');
|
||||
// --modify-other is not directly mapped, apply if modifying is disabled
|
||||
if (!allowModifying) args.push('--modify-other=n');
|
||||
} else if (finalUserPassword) {
|
||||
args.push('--allow-insecure');
|
||||
}
|
||||
throw e;
|
||||
} else {
|
||||
args.push('--decrypt');
|
||||
}
|
||||
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
args.push('--', outputPath);
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent =
|
||||
`Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
const errorMsg = qpdfError.message || '';
|
||||
|
||||
if (
|
||||
errorMsg.includes('invalid password') ||
|
||||
errorMsg.includes('incorrect password') ||
|
||||
errorMsg.includes('password')
|
||||
) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
if (
|
||||
errorMsg.includes('encrypted') ||
|
||||
errorMsg.includes('password required')
|
||||
) {
|
||||
throw new Error('PASSWORD_REQUIRED');
|
||||
}
|
||||
|
||||
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent =
|
||||
'Applying new permissions...';
|
||||
showLoader('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
const allowPrinting = (
|
||||
document.getElementById('allow-printing') as HTMLInputElement
|
||||
).checked;
|
||||
const allowCopying = (
|
||||
document.getElementById('allow-copying') as HTMLInputElement
|
||||
).checked;
|
||||
const allowModifying = (
|
||||
document.getElementById('allow-modifying') as HTMLInputElement
|
||||
).checked;
|
||||
const allowAnnotating = (
|
||||
document.getElementById('allow-annotating') as HTMLInputElement
|
||||
).checked;
|
||||
const allowFillingForms = (
|
||||
document.getElementById('allow-filling-forms') as HTMLInputElement
|
||||
).checked;
|
||||
const allowContentAccessibility = (
|
||||
document.getElementById('allow-content-accessibility') as HTMLInputElement
|
||||
).checked;
|
||||
const allowDocumentAssembly = (
|
||||
document.getElementById('allow-document-assembly') as HTMLInputElement
|
||||
).checked;
|
||||
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
|
||||
|
||||
// Apply the new, separate user and owner passwords
|
||||
userPassword: newUserPassword,
|
||||
ownerPassword: newOwnerPassword,
|
||||
|
||||
// Apply all seven permissions from the checkboxes
|
||||
permissions: {
|
||||
printing: allowPrinting ? 'highResolution' : false,
|
||||
modifying: allowModifying,
|
||||
copying: allowCopying,
|
||||
annotating: allowAnnotating,
|
||||
fillingForms: allowFillingForms,
|
||||
contentAccessibility: allowContentAccessibility,
|
||||
documentAssembly: allowDocumentAssembly,
|
||||
},
|
||||
});
|
||||
|
||||
const stream = doc.pipe(blobStream());
|
||||
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0)
|
||||
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, {
|
||||
width: pageImages[i].width,
|
||||
height: pageImages[i].height,
|
||||
});
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Processing resulted in an empty file.');
|
||||
}
|
||||
|
||||
doc.end();
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Permissions changed successfully!');
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Error', `An unexpected error occurred: ${e.message}`);
|
||||
|
||||
let successMessage = 'PDF permissions changed successfully!';
|
||||
if (!shouldEncrypt) {
|
||||
successMessage =
|
||||
'PDF decrypted successfully! All encryption and restrictions removed.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage);
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF permission change:', error);
|
||||
hideLoader();
|
||||
|
||||
if (error.message === 'INVALID_PASSWORD') {
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The current password you entered is incorrect. Please try again.'
|
||||
);
|
||||
} else if (error.message === 'PASSWORD_REQUIRED') {
|
||||
showAlert(
|
||||
'Password Required',
|
||||
'This PDF is password-protected. Please enter the current password to proceed.'
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Processing Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password protected.'}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+85
-59
@@ -1,79 +1,105 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||
import blobStream from 'blob-stream';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
export async function decrypt() {
|
||||
const file = state.files[0];
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const password = document.getElementById('password-input').value;
|
||||
if (!password.trim()) {
|
||||
const password = (
|
||||
document.getElementById('password-input') as HTMLInputElement
|
||||
)?.value;
|
||||
|
||||
if (!password) {
|
||||
showAlert('Input Required', 'Please enter the PDF password.');
|
||||
return;
|
||||
}
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
showLoader('Preparing to process...');
|
||||
const pdfData = (await readFileAsArrayBuffer(file)) as ArrayBuffer;
|
||||
const pdf = await pdfjsLib.getDocument({
|
||||
data: pdfData,
|
||||
password: password,
|
||||
}).promise;
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
showLoader('Initializing decryption...');
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent =
|
||||
`Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
canvas: canvas,
|
||||
}).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
showLoader('Reading encrypted PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
showLoader('Decrypting PDF...');
|
||||
|
||||
const args = [inputPath, '--password=' + password, '--decrypt', outputPath];
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
|
||||
if (
|
||||
qpdfError.message?.includes('invalid password') ||
|
||||
qpdfError.message?.includes('password')
|
||||
) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
throw qpdfError;
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent =
|
||||
'Building unlocked PDF...';
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
});
|
||||
const stream = doc.pipe(blobStream());
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0)
|
||||
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, {
|
||||
width: pageImages[i].width,
|
||||
height: pageImages[i].height,
|
||||
});
|
||||
}
|
||||
doc.end();
|
||||
showLoader('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `unlocked-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Decryption complete! Your download has started.');
|
||||
});
|
||||
} catch (error) {
|
||||
if (outputFile.length === 0) {
|
||||
throw new Error('Decryption resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `unlocked-${file.name}`);
|
||||
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF decrypted successfully! Your download has started.'
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF decryption:', error);
|
||||
hideLoader();
|
||||
if (error.name === 'PasswordException') {
|
||||
showAlert('Incorrect Password', 'The password you entered is incorrect.');
|
||||
|
||||
if (error.message === 'INVALID_PASSWORD') {
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The password you entered is incorrect. Please try again.'
|
||||
);
|
||||
} else if (error.message?.includes('password')) {
|
||||
showAlert(
|
||||
'Password Error',
|
||||
'Unable to decrypt the PDF with the provided password.'
|
||||
);
|
||||
} else {
|
||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||
showAlert(
|
||||
'Decryption Failed',
|
||||
`An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+96
-69
@@ -1,86 +1,113 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||
import blobStream from 'blob-stream';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
export async function encrypt() {
|
||||
const file = state.files[0];
|
||||
const password = (
|
||||
document.getElementById('password-input') as HTMLInputElement
|
||||
).value;
|
||||
if (!password.trim()) {
|
||||
showAlert('Input Required', 'Please enter a password.');
|
||||
const userPassword =
|
||||
(document.getElementById('user-password-input') as HTMLInputElement)
|
||||
?.value || '';
|
||||
const ownerPasswordInput =
|
||||
(document.getElementById('owner-password-input') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
if (!userPassword) {
|
||||
showAlert('Input Required', 'Please enter a user password.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ownerPassword = ownerPasswordInput || userPassword;
|
||||
const hasDistinctOwnerPassword = ownerPasswordInput !== '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
showLoader('Preparing to process...');
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer })
|
||||
.promise;
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
showLoader('Initializing encryption...');
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent =
|
||||
`Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
canvas: canvas,
|
||||
}).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
showLoader('Reading PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
showLoader('Encrypting PDF with 256-bit AES...');
|
||||
|
||||
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
|
||||
|
||||
// Only add restrictions if a distinct owner password was provided
|
||||
if (hasDistinctOwnerPassword) {
|
||||
args.push(
|
||||
'--modify=none',
|
||||
'--extract=n',
|
||||
'--print=none',
|
||||
'--accessibility=n',
|
||||
'--annotate=n',
|
||||
'--assemble=n',
|
||||
'--form=n',
|
||||
'--modify-other=n'
|
||||
);
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent =
|
||||
'Encrypting and building PDF...';
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
|
||||
userPassword: password,
|
||||
ownerPassword: password,
|
||||
permissions: {
|
||||
printing: 'highResolution',
|
||||
modifying: false,
|
||||
copying: false,
|
||||
annotating: false,
|
||||
fillingForms: false,
|
||||
contentAccessibility: true,
|
||||
documentAssembly: false,
|
||||
},
|
||||
});
|
||||
const stream = doc.pipe(blobStream());
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0)
|
||||
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, {
|
||||
width: pageImages[i].width,
|
||||
height: pageImages[i].height,
|
||||
});
|
||||
}
|
||||
doc.end();
|
||||
args.push('--', outputPath);
|
||||
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `encrypted-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Encryption complete! Your download has started.');
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
throw new Error(
|
||||
'Encryption failed: ' + (qpdfError.message || 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
showLoader('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Encryption resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `encrypted-${file.name}`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
let successMessage = 'PDF encrypted successfully with 256-bit AES!';
|
||||
if (!hasDistinctOwnerPassword) {
|
||||
successMessage +=
|
||||
' Note: Without a separate owner password, the PDF has no usage restrictions.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage);
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF encryption:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||
showAlert(
|
||||
'Encryption Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export function flattenFormsInDoc(pdfDoc) {
|
||||
const form = pdfDoc.getForm();
|
||||
form.flatten();
|
||||
}
|
||||
|
||||
export async function flatten() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
@@ -9,8 +14,7 @@ export async function flatten() {
|
||||
}
|
||||
showLoader('Flattening PDF...');
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
form.flatten();
|
||||
flattenFormsInDoc(state.pdfDoc);
|
||||
|
||||
const flattenedBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
|
||||
@@ -63,12 +63,15 @@ import {
|
||||
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
||||
import { linearizePdf } from './linearize.js';
|
||||
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
|
||||
import { sanitizePdf } from './sanitize-pdf.js';
|
||||
import { removeRestrictions } from './remove-restrictions.js';
|
||||
|
||||
export const toolLogic = {
|
||||
merge: { process: merge, setup: setupMergeTool },
|
||||
split: { process: split, setup: setupSplitTool },
|
||||
encrypt,
|
||||
decrypt,
|
||||
'remove-restrictions': removeRestrictions,
|
||||
organize,
|
||||
rotate,
|
||||
'add-page-numbers': addPageNumbers,
|
||||
@@ -135,4 +138,5 @@ export const toolLogic = {
|
||||
process: addAttachments,
|
||||
setup: setupAddAttachmentsTool,
|
||||
},
|
||||
'sanitize-pdf': sanitizePdf,
|
||||
};
|
||||
|
||||
@@ -28,6 +28,48 @@ export function setupRemoveAnnotationsTool() {
|
||||
});
|
||||
}
|
||||
|
||||
export function removeAnnotationsFromDoc(
|
||||
pdfDoc,
|
||||
pageIndices = null,
|
||||
annotationTypes = null
|
||||
) {
|
||||
const pages = pdfDoc.getPages();
|
||||
const targetPages =
|
||||
pageIndices || Array.from({ length: pages.length }, (_, i) => i);
|
||||
|
||||
for (const pageIndex of targetPages) {
|
||||
const page = pages[pageIndex];
|
||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||
|
||||
if (!annotationTypes) {
|
||||
if (annotRefs.length > 0) {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
} else {
|
||||
const annotsToKeep = [];
|
||||
|
||||
for (const ref of annotRefs) {
|
||||
const annot = pdfDoc.context.lookup(ref);
|
||||
const subtype = annot
|
||||
.get(PDFName.of('Subtype'))
|
||||
?.toString()
|
||||
.substring(1);
|
||||
|
||||
if (!subtype || !annotationTypes.has(subtype)) {
|
||||
annotsToKeep.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (annotsToKeep.length > 0) {
|
||||
const newAnnotsArray = pdfDoc.context.obj(annotsToKeep);
|
||||
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||
} else {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeAnnotations() {
|
||||
showLoader('Removing annotations...');
|
||||
try {
|
||||
@@ -80,34 +122,7 @@ export async function removeAnnotations() {
|
||||
if (typesToRemove.size === 0)
|
||||
throw new Error('Please select at least one annotation type to remove.');
|
||||
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
for (const pageIndex of targetPageIndices) {
|
||||
const page = pages[pageIndex];
|
||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||
|
||||
const annotsToKeep = [];
|
||||
|
||||
for (const ref of annotRefs) {
|
||||
const annot = state.pdfDoc.context.lookup(ref);
|
||||
const subtype = annot
|
||||
.get(PDFName.of('Subtype'))
|
||||
?.toString()
|
||||
.substring(1);
|
||||
|
||||
// If the subtype is NOT in the list to remove, add it to our new array
|
||||
if (!subtype || !typesToRemove.has(subtype)) {
|
||||
annotsToKeep.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (annotsToKeep.length > 0) {
|
||||
const newAnnotsArray = state.pdfDoc.context.obj(annotsToKeep);
|
||||
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||
} else {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
}
|
||||
removeAnnotationsFromDoc(state.pdfDoc, targetPageIndices, typesToRemove);
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFName } from 'pdf-lib';
|
||||
|
||||
export function removeMetadataFromDoc(pdfDoc) {
|
||||
const infoDict = pdfDoc.getInfoDict();
|
||||
const allKeys = infoDict.keys();
|
||||
allKeys.forEach((key: any) => {
|
||||
infoDict.delete(key);
|
||||
});
|
||||
|
||||
pdfDoc.setTitle('');
|
||||
pdfDoc.setAuthor('');
|
||||
pdfDoc.setSubject('');
|
||||
pdfDoc.setKeywords([]);
|
||||
pdfDoc.setCreator('');
|
||||
pdfDoc.setProducer('');
|
||||
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
if (catalogDict.has(PDFName.of('Metadata'))) {
|
||||
catalogDict.delete(PDFName.of('Metadata'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove XMP metadata:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const context = pdfDoc.context;
|
||||
if (context.trailerInfo) {
|
||||
delete context.trailerInfo.ID;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove document IDs:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
if (catalogDict.has(PDFName.of('PieceInfo'))) {
|
||||
catalogDict.delete(PDFName.of('PieceInfo'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove PieceInfo:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeMetadata() {
|
||||
showLoader('Removing all metadata...');
|
||||
try {
|
||||
const infoDict = state.pdfDoc.getInfoDict();
|
||||
|
||||
const allKeys = infoDict.keys();
|
||||
allKeys.forEach((key: any) => {
|
||||
infoDict.delete(key);
|
||||
});
|
||||
|
||||
state.pdfDoc.setTitle('');
|
||||
state.pdfDoc.setAuthor('');
|
||||
state.pdfDoc.setSubject('');
|
||||
state.pdfDoc.setKeywords([]);
|
||||
state.pdfDoc.setCreator('');
|
||||
state.pdfDoc.setProducer('');
|
||||
removeMetadataFromDoc(state.pdfDoc);
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function removeRestrictions() {
|
||||
const file = state.files[0];
|
||||
const password =
|
||||
(document.getElementById('owner-password-remove') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
showLoader('Initializing...');
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
showLoader('Reading PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
showLoader('Removing restrictions...');
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
if (password) {
|
||||
args.push(`--password=${password}`);
|
||||
}
|
||||
|
||||
args.push('--decrypt', '--remove-restrictions', '--', outputPath);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
if (
|
||||
qpdfError.message?.includes('password') ||
|
||||
qpdfError.message?.includes('encrypt')
|
||||
) {
|
||||
throw new Error(
|
||||
'Failed to remove restrictions. The PDF may require the correct owner password.'
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Failed to remove restrictions: ' +
|
||||
(qpdfError.message || 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
showLoader('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Operation resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `unrestricted-${file.name}`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF restrictions removed successfully! The file is now fully editable and printable.'
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error during restriction removal:', error);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Operation Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password-protected.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { removeMetadataFromDoc } from './remove-metadata.js';
|
||||
import { removeAnnotationsFromDoc } from './remove-annotations.js';
|
||||
import { flattenFormsInDoc } from './flatten.js';
|
||||
import { PDFName } from 'pdf-lib';
|
||||
|
||||
export async function sanitizePdf() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'No PDF document loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Sanitizing PDF...');
|
||||
try {
|
||||
const pdfDoc = state.pdfDoc;
|
||||
|
||||
const shouldFlattenForms = (
|
||||
document.getElementById('flatten-forms') as HTMLInputElement
|
||||
).checked;
|
||||
const shouldRemoveMetadata = (
|
||||
document.getElementById('remove-metadata') as HTMLInputElement
|
||||
).checked;
|
||||
const shouldRemoveAnnotations = (
|
||||
document.getElementById('remove-annotations') as HTMLInputElement
|
||||
).checked;
|
||||
const shouldRemoveJavascript = (
|
||||
document.getElementById('remove-javascript') as HTMLInputElement
|
||||
).checked;
|
||||
const shouldRemoveEmbeddedFiles = (
|
||||
document.getElementById('remove-embedded-files') as HTMLInputElement
|
||||
).checked;
|
||||
const shouldRemoveLayers = (
|
||||
document.getElementById('remove-layers') as HTMLInputElement
|
||||
).checked;
|
||||
const shouldRemoveLinks = (
|
||||
document.getElementById('remove-links') as HTMLInputElement
|
||||
).checked;
|
||||
const shouldRemoveStructureTree = (
|
||||
document.getElementById('remove-structure-tree') as HTMLInputElement
|
||||
).checked;
|
||||
const shouldRemoveMarkInfo = (
|
||||
document.getElementById('remove-markinfo') as HTMLInputElement
|
||||
).checked;
|
||||
const shouldRemoveFonts = (
|
||||
document.getElementById('remove-fonts') as HTMLInputElement
|
||||
).checked;
|
||||
|
||||
let changesMade = false;
|
||||
|
||||
if (shouldFlattenForms) {
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
changesMade = true;
|
||||
} catch (e) {
|
||||
console.warn(`Could not flatten forms: ${e.message}`);
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
if (catalogDict.has(PDFName.of('AcroForm'))) {
|
||||
catalogDict.delete(PDFName.of('AcroForm'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (removeError) {
|
||||
console.warn('Could not remove AcroForm:', removeError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRemoveMetadata) {
|
||||
removeMetadataFromDoc(pdfDoc);
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
if (shouldRemoveAnnotations) {
|
||||
removeAnnotationsFromDoc(pdfDoc);
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
if (shouldRemoveJavascript) {
|
||||
try {
|
||||
if (pdfDoc.javaScripts && pdfDoc.javaScripts.length > 0) {
|
||||
pdfDoc.javaScripts = [];
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
|
||||
const namesRef = catalogDict.get(PDFName.of('Names'));
|
||||
if (namesRef) {
|
||||
try {
|
||||
const namesDict = pdfDoc.context.lookup(namesRef);
|
||||
if (namesDict.has(PDFName.of('JavaScript'))) {
|
||||
namesDict.delete(PDFName.of('JavaScript'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not access Names/JavaScript:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (catalogDict.has(PDFName.of('OpenAction'))) {
|
||||
catalogDict.delete(PDFName.of('OpenAction'));
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
if (catalogDict.has(PDFName.of('AA'))) {
|
||||
catalogDict.delete(PDFName.of('AA'));
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
for (const page of pages) {
|
||||
try {
|
||||
const pageDict = page.node;
|
||||
|
||||
if (pageDict.has(PDFName.of('AA'))) {
|
||||
pageDict.delete(PDFName.of('AA'));
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
const annotRefs = pageDict.Annots()?.asArray() || [];
|
||||
for (const annotRef of annotRefs) {
|
||||
try {
|
||||
const annot = pdfDoc.context.lookup(annotRef);
|
||||
|
||||
if (annot.has(PDFName.of('A'))) {
|
||||
const actionRef = annot.get(PDFName.of('A'));
|
||||
try {
|
||||
const actionDict = pdfDoc.context.lookup(actionRef);
|
||||
const actionType = actionDict
|
||||
.get(PDFName.of('S'))
|
||||
?.toString()
|
||||
.substring(1);
|
||||
|
||||
if (actionType === 'JavaScript') {
|
||||
annot.delete(PDFName.of('A'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not read action:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (annot.has(PDFName.of('AA'))) {
|
||||
annot.delete(PDFName.of('AA'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not process annotation for JS:', e.message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove page actions:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const acroFormRef = catalogDict.get(PDFName.of('AcroForm'));
|
||||
if (acroFormRef) {
|
||||
const acroFormDict = pdfDoc.context.lookup(acroFormRef);
|
||||
const fieldsRef = acroFormDict.get(PDFName.of('Fields'));
|
||||
|
||||
if (fieldsRef) {
|
||||
const fieldsArray = pdfDoc.context.lookup(fieldsRef);
|
||||
const fields = fieldsArray.asArray();
|
||||
|
||||
for (const fieldRef of fields) {
|
||||
try {
|
||||
const field = pdfDoc.context.lookup(fieldRef);
|
||||
|
||||
if (field.has(PDFName.of('A'))) {
|
||||
field.delete(PDFName.of('A'));
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
if (field.has(PDFName.of('AA'))) {
|
||||
field.delete(PDFName.of('AA'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not process field for JS:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not process form fields for JS:', e.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Could not remove JavaScript: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRemoveEmbeddedFiles) {
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
|
||||
const namesRef = catalogDict.get(PDFName.of('Names'));
|
||||
if (namesRef) {
|
||||
try {
|
||||
const namesDict = pdfDoc.context.lookup(namesRef);
|
||||
if (namesDict.has(PDFName.of('EmbeddedFiles'))) {
|
||||
namesDict.delete(PDFName.of('EmbeddedFiles'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not access Names/EmbeddedFiles:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (catalogDict.has(PDFName.of('EmbeddedFiles'))) {
|
||||
catalogDict.delete(PDFName.of('EmbeddedFiles'));
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
for (const page of pages) {
|
||||
try {
|
||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||
const annotsToKeep = [];
|
||||
|
||||
for (const ref of annotRefs) {
|
||||
try {
|
||||
const annot = pdfDoc.context.lookup(ref);
|
||||
const subtype = annot
|
||||
.get(PDFName.of('Subtype'))
|
||||
?.toString()
|
||||
.substring(1);
|
||||
|
||||
if (subtype !== 'FileAttachment') {
|
||||
annotsToKeep.push(ref);
|
||||
} else {
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
annotsToKeep.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (annotsToKeep.length !== annotRefs.length) {
|
||||
if (annotsToKeep.length > 0) {
|
||||
const newAnnotsArray = pdfDoc.context.obj(annotsToKeep);
|
||||
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||
} else {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
}
|
||||
} catch (pageError) {
|
||||
console.warn(
|
||||
`Could not process page for attachments: ${pageError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (pdfDoc.embeddedFiles && pdfDoc.embeddedFiles.length > 0) {
|
||||
pdfDoc.embeddedFiles = [];
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
if (catalogDict.has(PDFName.of('Collection'))) {
|
||||
catalogDict.delete(PDFName.of('Collection'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Could not remove embedded files: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRemoveLayers) {
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
|
||||
if (catalogDict.has(PDFName.of('OCProperties'))) {
|
||||
catalogDict.delete(PDFName.of('OCProperties'));
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
for (const page of pages) {
|
||||
try {
|
||||
const pageDict = page.node;
|
||||
|
||||
if (pageDict.has(PDFName.of('OCProperties'))) {
|
||||
pageDict.delete(PDFName.of('OCProperties'));
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
const resourcesRef = pageDict.get(PDFName.of('Resources'));
|
||||
if (resourcesRef) {
|
||||
try {
|
||||
const resourcesDict = pdfDoc.context.lookup(resourcesRef);
|
||||
if (resourcesDict.has(PDFName.of('Properties'))) {
|
||||
resourcesDict.delete(PDFName.of('Properties'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not access Resources:', e.message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove page layers:', e.message);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Could not remove layers: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:@ALAM
|
||||
// Currently if the links are embedded in a stream they can't be removed
|
||||
// Find a way to remove them from the stream
|
||||
if (shouldRemoveLinks) {
|
||||
try {
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
|
||||
try {
|
||||
const page = pages[pageIndex];
|
||||
const pageDict = page.node;
|
||||
|
||||
const annotsRef = pageDict.get(PDFName.of('Annots'));
|
||||
if (!annotsRef) continue;
|
||||
|
||||
const annotsArray = pdfDoc.context.lookup(annotsRef);
|
||||
const annotRefs = annotsArray.asArray();
|
||||
|
||||
if (annotRefs.length === 0) continue;
|
||||
|
||||
const annotsToKeep = [];
|
||||
let linksRemoved = 0;
|
||||
|
||||
for (const ref of annotRefs) {
|
||||
try {
|
||||
const annot = pdfDoc.context.lookup(ref);
|
||||
const subtype = annot
|
||||
.get(PDFName.of('Subtype'))
|
||||
?.toString()
|
||||
.substring(1);
|
||||
|
||||
let isLink = false;
|
||||
|
||||
if (subtype === 'Link') {
|
||||
isLink = true;
|
||||
linksRemoved++;
|
||||
} else {
|
||||
const actionRef = annot.get(PDFName.of('A'));
|
||||
if (actionRef) {
|
||||
try {
|
||||
const actionDict = pdfDoc.context.lookup(actionRef);
|
||||
const actionType = actionDict
|
||||
.get(PDFName.of('S'))
|
||||
?.toString()
|
||||
.substring(1);
|
||||
|
||||
if (
|
||||
actionType === 'URI' ||
|
||||
actionType === 'Launch' ||
|
||||
actionType === 'GoTo' ||
|
||||
actionType === 'GoToR'
|
||||
) {
|
||||
isLink = true;
|
||||
linksRemoved++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not read action:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const dest = annot.get(PDFName.of('Dest'));
|
||||
if (dest && !isLink) {
|
||||
isLink = true;
|
||||
linksRemoved++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLink) {
|
||||
annotsToKeep.push(ref);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not process annotation:', e.message);
|
||||
annotsToKeep.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (linksRemoved > 0) {
|
||||
if (annotsToKeep.length > 0) {
|
||||
const newAnnotsArray = pdfDoc.context.obj(annotsToKeep);
|
||||
pageDict.set(PDFName.of('Annots'), newAnnotsArray);
|
||||
} else {
|
||||
pageDict.delete(PDFName.of('Annots'));
|
||||
}
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (pageError) {
|
||||
console.warn(
|
||||
`Could not process page ${pageIndex + 1} for links: ${pageError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
const namesRef = catalogDict.get(PDFName.of('Names'));
|
||||
if (namesRef) {
|
||||
try {
|
||||
const namesDict = pdfDoc.context.lookup(namesRef);
|
||||
if (namesDict.has(PDFName.of('Dests'))) {
|
||||
namesDict.delete(PDFName.of('Dests'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not access Names/Dests:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (catalogDict.has(PDFName.of('Dests'))) {
|
||||
catalogDict.delete(PDFName.of('Dests'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove named destinations:', e.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Could not remove links: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRemoveStructureTree) {
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
|
||||
if (catalogDict.has(PDFName.of('StructTreeRoot'))) {
|
||||
catalogDict.delete(PDFName.of('StructTreeRoot'));
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
const pages = pdfDoc.getPages();
|
||||
for (const page of pages) {
|
||||
try {
|
||||
const pageDict = page.node;
|
||||
if (pageDict.has(PDFName.of('StructParents'))) {
|
||||
pageDict.delete(PDFName.of('StructParents'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove page StructParents:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (catalogDict.has(PDFName.of('ParentTree'))) {
|
||||
catalogDict.delete(PDFName.of('ParentTree'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Could not remove structure tree: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRemoveMarkInfo) {
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
|
||||
if (catalogDict.has(PDFName.of('MarkInfo'))) {
|
||||
catalogDict.delete(PDFName.of('MarkInfo'));
|
||||
changesMade = true;
|
||||
}
|
||||
|
||||
if (catalogDict.has(PDFName.of('Marked'))) {
|
||||
catalogDict.delete(PDFName.of('Marked'));
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Could not remove MarkInfo: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRemoveFonts) {
|
||||
try {
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
|
||||
try {
|
||||
const page = pages[pageIndex];
|
||||
const pageDict = page.node;
|
||||
const resourcesRef = pageDict.get(PDFName.of('Resources'));
|
||||
|
||||
if (resourcesRef) {
|
||||
try {
|
||||
const resourcesDict = pdfDoc.context.lookup(resourcesRef);
|
||||
|
||||
if (resourcesDict.has(PDFName.of('Font'))) {
|
||||
const fontRef = resourcesDict.get(PDFName.of('Font'));
|
||||
|
||||
try {
|
||||
const fontDict = pdfDoc.context.lookup(fontRef);
|
||||
const fontKeys = fontDict.keys();
|
||||
|
||||
for (const fontKey of fontKeys) {
|
||||
try {
|
||||
const specificFontRef = fontDict.get(fontKey);
|
||||
const specificFont =
|
||||
pdfDoc.context.lookup(specificFontRef);
|
||||
|
||||
if (specificFont.has(PDFName.of('FontDescriptor'))) {
|
||||
const descriptorRef = specificFont.get(
|
||||
PDFName.of('FontDescriptor')
|
||||
);
|
||||
const descriptor =
|
||||
pdfDoc.context.lookup(descriptorRef);
|
||||
|
||||
const fontFileKeys = [
|
||||
'FontFile',
|
||||
'FontFile2',
|
||||
'FontFile3',
|
||||
];
|
||||
for (const key of fontFileKeys) {
|
||||
if (descriptor.has(PDFName.of(key))) {
|
||||
descriptor.delete(PDFName.of(key));
|
||||
changesMade = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Users/Developers: Uncomment this if you can delete the entire font entry -- might break the rendering though
|
||||
// fontDict.delete(fontKey);
|
||||
// changesMade = true;
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Could not process font ${fontKey}:`,
|
||||
e.message
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Could not access font dictionary:',
|
||||
e.message
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'Could not access Resources for fonts:',
|
||||
e.message
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Could not remove fonts from page ${pageIndex + 1}:`,
|
||||
e.message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (pdfDoc.fonts && pdfDoc.fonts.length > 0) {
|
||||
pdfDoc.fonts = [];
|
||||
changesMade = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Could not remove fonts: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changesMade) {
|
||||
showAlert(
|
||||
'No Changes',
|
||||
'No items were selected for removal or none were found in the PDF.'
|
||||
);
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedPdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([sanitizedPdfBytes], { type: 'application/pdf' }),
|
||||
'sanitized.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF has been sanitized and downloaded.');
|
||||
} catch (e) {
|
||||
console.error('Sanitization Error:', e);
|
||||
showAlert('Error', `An error occurred during sanitization: ${e.message}`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
+219
-38
@@ -379,23 +379,73 @@ export const toolTemplates = {
|
||||
</div>
|
||||
`,
|
||||
encrypt: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Encrypt PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Upload a PDF to create a new, password-protected version.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<div id="encrypt-options" class="hidden space-y-4 mt-6">
|
||||
<div>
|
||||
<label for="password-input" class="block mb-2 text-sm font-medium text-gray-300">Set Encryption Password</label>
|
||||
<input type="password" id="password-input" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Enter a strong password">
|
||||
</div>
|
||||
<div class="p-4 bg-gray-900 border border-yellow-500/30 text-yellow-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2">Important Note</h3>
|
||||
<p class="text-sm text-gray-400">To create the secure file, each page is converted into an image. This means you won't be able to select text or click links in the final encrypted PDF.</p>
|
||||
</div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Encrypt & Download</button>
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Encrypt PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Add 256-bit AES password protection to your PDF.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<div id="encrypt-options" class="hidden space-y-4 mt-6">
|
||||
<div>
|
||||
<label for="user-password-input" class="block mb-2 text-sm font-medium text-gray-300">User Password</label>
|
||||
<input required type="password" id="user-password-input" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password to open the PDF">
|
||||
<p class="text-xs text-gray-500 mt-1">Required to open and view the PDF</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="owner-password-input" class="block mb-2 text-sm font-medium text-gray-300">Owner Password (Optional)</label>
|
||||
<input type="password" id="owner-password-input" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password for full permissions (recommended)">
|
||||
<p class="text-xs text-gray-500 mt-1">Allows changing permissions and removing encryption</p>
|
||||
</div>
|
||||
|
||||
<!-- Restriction checkboxes (shown when owner password is entered) -->
|
||||
<div id="restriction-options" class="hidden p-4 bg-gray-800 border border-gray-700 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2 text-white">🔒 Restrict PDF Permissions</h3>
|
||||
<p class="text-sm text-gray-400 mb-3">Select which actions to disable:</p>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="restrict-modify" checked>
|
||||
<span>Disable all modifications (--modify=none)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="restrict-extract" checked>
|
||||
<span>Disable text and image extraction (--extract=n)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="restrict-print" checked>
|
||||
<span>Disable all printing (--print=none)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="restrict-accessibility">
|
||||
<span>Disable accessibility text copying (--accessibility=n)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="restrict-annotate">
|
||||
<span>Disable annotations (--annotate=n)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="restrict-assemble">
|
||||
<span>Disable page assembly (--assemble=n)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="restrict-form">
|
||||
<span>Disable form filling (--form=n)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2">
|
||||
<input type="checkbox" id="restrict-modify-other">
|
||||
<span>Disable other modifications (--modify-other=n)</span>
|
||||
</label>
|
||||
</div>
|
||||
<canvas id="pdf-canvas" class="hidden"></canvas>
|
||||
`,
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-yellow-900/20 border border-yellow-500/30 text-yellow-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2">⚠️ Security Recommendation</h3>
|
||||
<p class="text-sm text-gray-300">For strong security, set both passwords. Without an owner password, the security restrictions (printing, copying, etc.) can be easily bypassed.</p>
|
||||
</div>
|
||||
<div class="p-4 bg-green-900/20 border border-green-500/30 text-green-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2">✓ High-Quality Encryption</h3>
|
||||
<p class="text-sm text-gray-300">256-bit AES encryption without quality loss. Text remains selectable and searchable.</p>
|
||||
</div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Encrypt & Download</button>
|
||||
</div>
|
||||
`,
|
||||
decrypt: () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Decrypt PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Upload an encrypted PDF and provide its password to create an unlocked version.</p>
|
||||
@@ -865,50 +915,71 @@ export const toolTemplates = {
|
||||
|
||||
'change-permissions': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Change PDF Permissions</h2>
|
||||
<p class="mb-6 text-gray-400">Unlock a PDF and re-save it with new passwords and permissions.</p>
|
||||
<p class="mb-6 text-gray-400">Modify passwords and permissions without losing quality.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<div id="permissions-options" class="hidden mt-6 space-y-4">
|
||||
<div>
|
||||
<label for="current-password" class="block mb-2 text-sm font-medium text-gray-300">Current Password (to unlock)</label>
|
||||
<input type="password" id="current-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Enter current password to open file">
|
||||
<label for="current-password" class="block mb-2 text-sm font-medium text-gray-300">Current Password (if encrypted)</label>
|
||||
<input type="password" id="current-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Leave blank if PDF is not password-protected">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="new-user-password" class="block mb-2 text-sm font-medium text-gray-300">New User Password (optional)</label>
|
||||
<input type="password" id="new-user-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password to open file">
|
||||
<input type="password" id="new-user-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password to open PDF">
|
||||
</div>
|
||||
<div>
|
||||
<label for="new-owner-password" class="block mb-2 text-sm font-medium text-gray-300">New Owner Password (optional)</label>
|
||||
<input type="password" id="new-owner-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Admin password for permissions">
|
||||
<input type="password" id="new-owner-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password for full permissions">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-900 border border-blue-500/30 text-blue-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2">How Passwords Work</h3>
|
||||
<ul class="list-disc list-inside text-sm text-gray-400 space-y-1">
|
||||
<li>The <strong>User Password</strong> is required to open and decrypt the PDF.</li>
|
||||
<li>The <strong>Owner Password</strong> is an admin key that bypasses all permissions.</li>
|
||||
<li>Leave both blank to create an unprotected PDF.</li>
|
||||
<li>Set an Owner Password to enforce the permissions you select below.</li>
|
||||
<div class="p-4 bg-blue-900/20 border border-blue-500/30 text-blue-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2">How It Works</h3>
|
||||
<ul class="list-disc list-inside text-sm text-gray-300 space-y-1">
|
||||
<li><strong>User Password:</strong> Required to open the PDF</li>
|
||||
<li><strong>Owner Password:</strong> Required to enforce the permissions below</li>
|
||||
<li>Leave both blank to remove all encryption and restrictions</li>
|
||||
<li>Check boxes below to ALLOW specific actions (unchecked = disabled)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<fieldset class="border border-gray-600 p-4 rounded-lg">
|
||||
<legend class="px-2 text-sm font-medium text-gray-300">Allow User to:</legend>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-printing" checked class="checkbox-style"> Print Document</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-copying" checked class="checkbox-style"> Copy Content</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-modifying" checked class="checkbox-style"> Modify Document</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-annotating" checked class="checkbox-style"> Annotate & Comment</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-filling-forms" checked class="checkbox-style"> Fill in Forms</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-content-accessibility" checked class="checkbox-style"> Enable Content Accessibility</label>
|
||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-document-assembly" checked class="checkbox-style"> Assemble Document</label>
|
||||
<legend class="px-2 text-sm font-medium text-gray-300">Permissions (only enforced with Owner Password):</legend>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||
<input type="checkbox" id="allow-printing" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||
Allow Printing
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||
<input type="checkbox" id="allow-copying" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||
Allow Text/Image Extraction
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||
<input type="checkbox" id="allow-modifying" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||
Allow Modifications
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||
<input type="checkbox" id="allow-annotating" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||
Allow Annotations
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||
<input type="checkbox" id="allow-filling-forms" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||
Allow Form Filling
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||
<input type="checkbox" id="allow-document-assembly" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||
Allow Page Assembly
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||
<input type="checkbox" id="allow-page-extraction" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||
Allow Page Extraction
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Save New Permissions</button>
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Apply Changes</button>
|
||||
`,
|
||||
|
||||
'pdf-to-markdown': () => `
|
||||
@@ -1910,4 +1981,114 @@ export const toolTemplates = {
|
||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
'sanitize-pdf': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Sanitize PDF</h2>
|
||||
<p class="mb-6 text-gray-400">Remove potentially sensitive or unnecessary information from your PDF before sharing. Select the items you want to remove.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
|
||||
<div id="sanitize-pdf-options" class="hidden mt-6 space-y-4 p-4 bg-gray-900 border border-gray-700 rounded-lg">
|
||||
<h3 class="text-lg font-semibold text-white mb-3">Sanitization Options</h3>
|
||||
<div>
|
||||
<strong class="font-semibold text-yellow-200">Note:</strong>
|
||||
Removing <code class="bg-gray-700 px-1 rounded text-white">Embedded Fonts</code> may break text rendering! Text may not display correctly or at all. Only use if you're sure the PDF viewer has substitute fonts.
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-gray-400 mb-2">Essential Security</h4>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="flatten-forms" name="sanitizeOption" value="flatten-forms" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white">Flatten Form Fields</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="remove-metadata" name="sanitizeOption" value="metadata" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white">Remove All Metadata</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="remove-annotations" name="sanitizeOption" value="annotations" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white">Remove Annotations</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="remove-javascript" name="sanitizeOption" value="javascript" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white">Remove JavaScript</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="remove-embedded-files" name="sanitizeOption" value="embeddedFiles" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white">Remove Embedded Files</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="remove-layers" name="sanitizeOption" value="layers" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white">Remove Layers (OCG)</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="remove-links" name="sanitizeOption" value="links" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white">Remove External Links</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-gray-400 mb-2">Additional Options</h4>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="remove-structure-tree" name="sanitizeOption" value="structure" class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white">Remove Structure Tree</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="remove-markinfo" name="sanitizeOption" value="markinfo" class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white">Remove Tagging Info</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||
<input type="checkbox" id="remove-fonts" name="sanitizeOption" value="fonts" class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||
<span class="text-white text-sm">Remove Embedded Fonts</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Sanitize PDF & Download</button>
|
||||
</div>
|
||||
`,
|
||||
|
||||
'remove-restrictions': () => `
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Remove PDF Restrictions</h2>
|
||||
<p class="mb-6 text-gray-400">Remove security restrictions and unlock PDF permissions for editing and printing.</p>
|
||||
${createFileInputHTML()}
|
||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||
<div id="remove-restrictions-options" class="hidden space-y-4 mt-6">
|
||||
<div class="p-4 bg-blue-900/20 border border-blue-500/30 text-blue-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2"> How it Works </h3>
|
||||
<p class="text-sm text-gray-300 mb-2">This operation will:</p>
|
||||
<ul class="text-sm text-gray-300 list-disc list-inside space-y-1 ml-2">
|
||||
<li>Remove all permission restrictions (printing, copying, editing)</li>
|
||||
<li>Remove encryption even if the file is encrypted</li>
|
||||
<li>Remove security restrictions associated with digitally signed PDF files (will make signature invalid)</li>
|
||||
<li>Create a fully editable, unrestricted PDF</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="owner-password-remove" class="block mb-2 text-sm font-medium text-gray-300">Owner Password (if required)</label>
|
||||
<input type="password" id="owner-password-remove" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Leave empty if PDF has no password">
|
||||
<p class="text-xs text-gray-500 mt-1">Enter the owner password if the PDF is password-protected</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-red-900/20 border border-red-500/30 text-red-200 rounded-lg">
|
||||
<h3 class="font-semibold text-base mb-2">Notice</h3>
|
||||
<p class="text-sm text-gray-300 mb-2">This tool is intended for legitimate purposes only, such as:</p>
|
||||
<ul class="text-sm text-gray-300 list-disc list-inside space-y-1 ml-2">
|
||||
<li>Removing restrictions from PDFs you own or have permission to modify</li>
|
||||
<li>Recovering access to a PDF when you legitimately forgot the password</li>
|
||||
<li>Accessing content you legally purchased or created</li>
|
||||
<li>Editing documents for authorized business purposes</li>
|
||||
<li>Opening documents for legitimate archival, compliance, or recovery workflows</li>
|
||||
<li class="font-semibold">Limitations: this tool can only remove restrictions from weakly protected PDFs or PDFs that do not have an owner password set. It cannot remove or bypass properly applied AES‑256 (256‑bit) encryption.</li>
|
||||
</ul>
|
||||
<p class="text-sm text-gray-300 mt-3 font-semibold">
|
||||
Using this tool to bypass copyright protections, violate intellectual property rights, or access documents without authorization may be illegal in your jurisdiction. We are not liable for any misuse of this tool — if you're unsure, consult legal counsel or the document owner before proceeding.
|
||||
</p>
|
||||
</div>
|
||||
<button id="process-btn" class="btn-gradient w-full mt-6">Remove Restrictions & Download</button>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import createModule from '@neslinesli93/qpdf-wasm';
|
||||
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||
|
||||
const STANDARD_SIZES = {
|
||||
A4: { width: 595.28, height: 841.89 },
|
||||
Letter: { width: 612, height: 792 },
|
||||
@@ -146,3 +149,31 @@ export function formatIsoDate(isoDateString) {
|
||||
return isoDateString; // Return original string on any error
|
||||
}
|
||||
}
|
||||
|
||||
let qpdfInstance: any = null;
|
||||
|
||||
/**
|
||||
* Initialize qpdf-wasm singleton.
|
||||
* Subsequent calls return the same instance.
|
||||
*/
|
||||
export async function initializeQpdf() {
|
||||
if (qpdfInstance) return qpdfInstance;
|
||||
|
||||
showLoader('Initializing PDF engine...');
|
||||
try {
|
||||
qpdfInstance = await createModule({
|
||||
locateFile: () => '/qpdf.wasm',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize qpdf-wasm:', error);
|
||||
showAlert(
|
||||
'Initialization Error',
|
||||
'Could not load the PDF engine. Please refresh the page and try again.'
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
return qpdfInstance;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('Tool Configuration Arrays', () => {
|
||||
|
||||
it('should have the correct number of tools', () => {
|
||||
// This acts as a snapshot test to catch unexpected additions/removals.
|
||||
expect(singlePdfLoadTools).toHaveLength(39);
|
||||
expect(singlePdfLoadTools).toHaveLength(41);
|
||||
});
|
||||
|
||||
it('should not contain any duplicate tools', () => {
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { state, resetState } from '../js/state';
|
||||
|
||||
describe('Watermark Feature', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<input type="text" id="watermark-text" value="Test Text" />
|
||||
<input type="file" id="image-watermark-input" />
|
||||
<input type="number" id="font-size" value="72" />
|
||||
<input type="color" id="text-color" value="#000000" />
|
||||
<input type="range" id="opacity-text" value="0.3" />
|
||||
<input type="range" id="angle-text" value="0" />
|
||||
<div id="text-watermark-options"></div>
|
||||
<div id="image-watermark-options" class="hidden"></div>
|
||||
<input type="radio" name="watermark-type" value="text" checked />
|
||||
<input type="radio" name="watermark-type" value="image" />
|
||||
<div id="file-display-area"></div>
|
||||
<div id="file-controls" class="hidden"></div>
|
||||
<button id="process-btn" disabled></button>
|
||||
<input type="file" id="file-input" />
|
||||
<div id="tool-content"></div>
|
||||
`;
|
||||
|
||||
state.activeTool = 'add-watermark';
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
state.pdfPages = [];
|
||||
state.currentPdfUrl = null;
|
||||
});
|
||||
|
||||
describe('resetState', () => {
|
||||
it('should clear watermark text input after reset', () => {
|
||||
const watermarkText = document.getElementById(
|
||||
'watermark-text'
|
||||
) as HTMLInputElement;
|
||||
watermarkText.value = 'CONFIDENTIAL';
|
||||
|
||||
resetState();
|
||||
|
||||
expect(state.activeTool).toBe(null);
|
||||
expect(state.files).toEqual([]);
|
||||
expect(state.pdfDoc).toBe(null);
|
||||
expect(state.pdfPages).toEqual([]);
|
||||
expect(state.currentPdfUrl).toBe(null);
|
||||
});
|
||||
|
||||
it('should reset state files array', () => {
|
||||
state.files = [new File(['content'], 'test.pdf')];
|
||||
|
||||
resetState();
|
||||
|
||||
expect(state.files).toEqual([]);
|
||||
});
|
||||
|
||||
it('should reset activeTool to null', () => {
|
||||
state.activeTool = 'add-watermark';
|
||||
|
||||
resetState();
|
||||
|
||||
expect(state.activeTool).toBe(null);
|
||||
});
|
||||
|
||||
it('should reset pdfDoc to null', () => {
|
||||
state.pdfDoc = {} as any;
|
||||
|
||||
resetState();
|
||||
|
||||
expect(state.pdfDoc).toBe(null);
|
||||
});
|
||||
|
||||
it('should reset pdfPages array', () => {
|
||||
state.pdfPages = [1, 2, 3] as any;
|
||||
|
||||
resetState();
|
||||
|
||||
expect(state.pdfPages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should reset currentPdfUrl to null', () => {
|
||||
state.currentPdfUrl = 'blob:http://example.com/123';
|
||||
|
||||
resetState();
|
||||
|
||||
expect(state.currentPdfUrl).toBe(null);
|
||||
});
|
||||
|
||||
it('should clear tool-content innerHTML', () => {
|
||||
const toolContent = document.getElementById('tool-content');
|
||||
if (toolContent) toolContent.innerHTML = '<div>Some content</div>';
|
||||
|
||||
resetState();
|
||||
|
||||
expect(toolContent?.innerHTML).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Watermark Type Toggle', () => {
|
||||
it('should show text options by default', () => {
|
||||
const textOptions = document.getElementById('text-watermark-options');
|
||||
const imageOptions = document.getElementById('image-watermark-options');
|
||||
|
||||
expect(imageOptions?.classList.contains('hidden')).toBe(true);
|
||||
expect(textOptions?.classList.contains('hidden')).toBe(false);
|
||||
});
|
||||
|
||||
it('should have text radio checked by default', () => {
|
||||
const textRadio = document.querySelector(
|
||||
'input[name="watermark-type"][value="text"]'
|
||||
) as HTMLInputElement;
|
||||
const imageRadio = document.querySelector(
|
||||
'input[name="watermark-type"][value="image"]'
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(textRadio.checked).toBe(true);
|
||||
expect(imageRadio.checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Watermark Input Validation', () => {
|
||||
it('should accept valid font size', () => {
|
||||
const fontSize = document.getElementById('font-size') as HTMLInputElement;
|
||||
fontSize.value = '48';
|
||||
|
||||
expect(parseInt(fontSize.value)).toBeGreaterThan(0);
|
||||
expect(parseInt(fontSize.value)).toBeLessThanOrEqual(200);
|
||||
});
|
||||
|
||||
it('should have default font size of 72', () => {
|
||||
const fontSize = document.getElementById('font-size') as HTMLInputElement;
|
||||
|
||||
expect(parseInt(fontSize.value)).toBe(72);
|
||||
});
|
||||
|
||||
it('should accept valid opacity range', () => {
|
||||
const opacity = document.getElementById(
|
||||
'opacity-text'
|
||||
) as HTMLInputElement;
|
||||
opacity.value = '0.5';
|
||||
|
||||
const opacityValue = parseFloat(opacity.value);
|
||||
expect(opacityValue).toBeGreaterThanOrEqual(0);
|
||||
expect(opacityValue).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should have default opacity of 0.3', () => {
|
||||
const opacity = document.getElementById(
|
||||
'opacity-text'
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(parseFloat(opacity.value)).toBe(0.3);
|
||||
});
|
||||
|
||||
it('should accept valid angle range', () => {
|
||||
const angle = document.getElementById('angle-text') as HTMLInputElement;
|
||||
angle.value = '45';
|
||||
|
||||
const angleValue = parseInt(angle.value);
|
||||
expect(angleValue).toBeGreaterThanOrEqual(-180);
|
||||
expect(angleValue).toBeLessThanOrEqual(180);
|
||||
});
|
||||
|
||||
it('should have default angle of 0', () => {
|
||||
const angle = document.getElementById('angle-text') as HTMLInputElement;
|
||||
|
||||
expect(parseInt(angle.value)).toBe(0);
|
||||
});
|
||||
|
||||
it('should accept valid hex color', () => {
|
||||
const textColor = document.getElementById(
|
||||
'text-color'
|
||||
) as HTMLInputElement;
|
||||
textColor.value = '#FF5733';
|
||||
|
||||
expect(textColor.value).toMatch(/^#[0-9A-F]{6}$/i);
|
||||
});
|
||||
|
||||
it('should have default color of black', () => {
|
||||
const textColor = document.getElementById(
|
||||
'text-color'
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(textColor.value).toBe('#000000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('State Management', () => {
|
||||
it('should maintain activeTool as add-watermark', () => {
|
||||
expect(state.activeTool).toBe('add-watermark');
|
||||
});
|
||||
|
||||
it('should start with empty files array', () => {
|
||||
expect(state.files).toEqual([]);
|
||||
});
|
||||
|
||||
it('should allow adding files to state', () => {
|
||||
const file = new File(['content'], 'test.pdf', {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
state.files.push(file);
|
||||
|
||||
expect(state.files.length).toBe(1);
|
||||
expect(state.files[0].name).toBe('test.pdf');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UI Elements State', () => {
|
||||
it('should have process button disabled by default', () => {
|
||||
const processBtn = document.getElementById(
|
||||
'process-btn'
|
||||
) as HTMLButtonElement;
|
||||
|
||||
expect(processBtn.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should have file controls hidden by default', () => {
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
|
||||
expect(fileControls?.classList.contains('hidden')).toBe(true);
|
||||
});
|
||||
|
||||
it('should have image watermark options hidden by default', () => {
|
||||
const imageOptions = document.getElementById('image-watermark-options');
|
||||
|
||||
expect(imageOptions?.classList.contains('hidden')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user