feat: separate AGPL libraries and add dynamic WASM loading
- Add WASM settings page for configuring external AGPL modules - Implement dynamic loading for PyMuPDF, Ghostscript, and CoherentPDF - Add Cloudflare Worker proxy for serving WASM files with CORS - Update all affected tool pages to check WASM availability - Add showWasmRequiredDialog for missing module configuration Documentation: - Update README, licensing.html, and docs to clarify AGPL components are not bundled and must be configured separately - Add WASM-PROXY.md deployment guide with recommended source URLs - Rename "CPDF" to "CoherentPDF" for consistency
This commit is contained in:
@@ -27,9 +27,13 @@ ARG BASE_URL
|
|||||||
ENV BASE_URL=$BASE_URL
|
ENV BASE_URL=$BASE_URL
|
||||||
|
|
||||||
RUN if [ -z "$BASE_URL" ]; then \
|
RUN if [ -z "$BASE_URL" ]; then \
|
||||||
npm run build -- --mode production; \
|
npm run build -- --mode production && \
|
||||||
|
npm run docs:build && \
|
||||||
|
node scripts/include-docs-in-dist.js; \
|
||||||
else \
|
else \
|
||||||
npm run build -- --base=${BASE_URL} --mode production; \
|
npm run build -- --base=${BASE_URL} --mode production && \
|
||||||
|
npm run docs:build && \
|
||||||
|
node scripts/include-docs-in-dist.js; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -1,5 +1,10 @@
|
|||||||
<p align="center"><img src="public/images/favicon-no-bg.svg" width="80"></p>
|
<p align="center"><img src="public/images/favicon-no-bg.svg" width="80"></p>
|
||||||
<h1 align="center">BentoPDF</h1>
|
<h1 align="center">BentoPDF</h1>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.digitalocean.com/?refcode=d93c189ef6d0&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge">
|
||||||
|
<img src="https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%203.svg" alt="DigitalOcean Referral Badge">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
**BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that is self hostable and 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.
|
**BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that is self hostable and 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.
|
||||||
|
|
||||||
@@ -85,6 +90,24 @@ BentoPDF is **dual-licensed** to fit your needs:
|
|||||||
|
|
||||||
📖 For more details, see our [Licensing Page](https://bentopdf.com/licensing.html)
|
📖 For more details, see our [Licensing Page](https://bentopdf.com/licensing.html)
|
||||||
|
|
||||||
|
### AGPL Components (Not Bundled)
|
||||||
|
|
||||||
|
BentoPDF does **not** bundle AGPL-licensed processing libraries. The following components must be configured separately via **Advanced Settings** if you wish to use their features:
|
||||||
|
|
||||||
|
| Component | License | Features Enabled |
|
||||||
|
| ---------------------- | -------- | --------------------------------------------------------------------------------------------------- |
|
||||||
|
| **PyMuPDF** | AGPL-3.0 | PDF to Text/Markdown/SVG/DOCX, Extract Images/Tables, EPUB/MOBI/XPS conversion, Compression, Deskew |
|
||||||
|
| **Ghostscript** | AGPL-3.0 | PDF/A Conversion, Font to Outline |
|
||||||
|
| **CoherentPDF (CPDF)** | AGPL-3.0 | Merge, Split by Bookmarks, Table of Contents, PDF to/from JSON, Attachments |
|
||||||
|
|
||||||
|
> **Why?** This separation ensures clear legal boundaries. Users who need these features can configure their own WASM sources or use our optional [WASM Proxy](cloudflare/WASM-PROXY.md) to load them from external URLs.
|
||||||
|
|
||||||
|
**To enable these features:**
|
||||||
|
|
||||||
|
1. Navigate to **Advanced Settings** in BentoPDF
|
||||||
|
2. Configure the URL for each WASM module you need
|
||||||
|
3. The modules will be loaded dynamically when required
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
## ⭐ Stargazers over time
|
## ⭐ Stargazers over time
|
||||||
@@ -806,6 +829,8 @@ Documentation files are in the `docs/` folder:
|
|||||||
|
|
||||||
BentoPDF wouldn't be possible without the amazing open-source tools and libraries that power it. We'd like to extend our heartfelt thanks to the creators and maintainers of:
|
BentoPDF wouldn't be possible without the amazing open-source tools and libraries that power it. We'd like to extend our heartfelt thanks to the creators and maintainers of:
|
||||||
|
|
||||||
|
**Bundled Libraries:**
|
||||||
|
|
||||||
- **[PDFLib.js](https://pdf-lib.js.org/)** – For enabling powerful client-side PDF manipulation.
|
- **[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.
|
- **[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.
|
- **[PDFKit](https://pdfkit.org/)** – For creating and editing PDFs with ease.
|
||||||
@@ -813,10 +838,15 @@ BentoPDF wouldn't be possible without the amazing open-source tools and librarie
|
|||||||
- **[Cropper.js](https://fengyuanchen.github.io/cropperjs/)** – For intuitive image cropping functionality.
|
- **[Cropper.js](https://fengyuanchen.github.io/cropperjs/)** – For intuitive image cropping functionality.
|
||||||
- **[Vite](https://vitejs.dev/)** – For lightning-fast development and build tooling.
|
- **[Vite](https://vitejs.dev/)** – For lightning-fast development and build tooling.
|
||||||
- **[Tailwind CSS](https://tailwindcss.com/)** – For rapid, flexible, and beautiful UI styling.
|
- **[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
|
- **[qpdf](https://github.com/qpdf/qpdf)** and **[qpdf-wasm](https://github.com/neslinesli93/qpdf-wasm)** – For inspecting, repairing, and transforming PDF files.
|
||||||
- **[cpdf](https://www.coherentpdf.com/)** – For content preserving pdf operations.
|
|
||||||
- **[LibreOffice](https://www.libreoffice.org/)** – For powerful document conversion capabilities.
|
- **[LibreOffice](https://www.libreoffice.org/)** – For powerful document conversion capabilities.
|
||||||
- **[PyMuPDF](https://github.com/pymupdf/PyMuPDF)** – For high-performance PDF manipulation and data extraction.
|
|
||||||
- **[Ghostscript(GhostPDL)](https://github.com/ArtifexSoftware/ghostpdl)** – Needs no Introduction.
|
**AGPL Libraries (Not Bundled - User Configured):**
|
||||||
|
|
||||||
|
- **[CoherentPDF (cpdf)](https://www.coherentpdf.com/)** – For content-preserving PDF operations. _(AGPL-3.0)_
|
||||||
|
- **[PyMuPDF](https://github.com/pymupdf/PyMuPDF)** – For high-performance PDF manipulation and data extraction. _(AGPL-3.0)_
|
||||||
|
- **[Ghostscript (GhostPDL)](https://github.com/ArtifexSoftware/ghostpdl)** – For PDF/A conversion and font outlining. _(AGPL-3.0)_
|
||||||
|
|
||||||
|
> **Note:** AGPL-licensed libraries are not bundled with BentoPDF. Users can optionally configure these via Advanced Settings to enable additional features.
|
||||||
|
|
||||||
Your work inspires and empowers developers everywhere. Thank you for making open-source amazing!
|
Your work inspires and empowers developers everywhere. Thank you for making open-source amazing!
|
||||||
|
|||||||
92
cloudflare/WASM-PROXY.md
Normal file
92
cloudflare/WASM-PROXY.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# WASM Proxy Setup Guide
|
||||||
|
|
||||||
|
BentoPDF uses a Cloudflare Worker to proxy WASM library requests, bypassing CORS restrictions when loading AGPL-licensed components (PyMuPDF, Ghostscript, CoherentPDF) from external sources.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Deploy the Worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cloudflare
|
||||||
|
npx wrangler login
|
||||||
|
npx wrangler deploy -c wasm-wrangler.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Source URLs
|
||||||
|
|
||||||
|
Set environment secrets with the base URLs for your WASM files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Option A: Interactive prompts
|
||||||
|
npx wrangler secret put PYMUPDF_SOURCE -c wasm-wrangler.toml
|
||||||
|
npx wrangler secret put GS_SOURCE -c wasm-wrangler.toml
|
||||||
|
npx wrangler secret put CPDF_SOURCE -c wasm-wrangler.toml
|
||||||
|
|
||||||
|
# Option B: Set via Cloudflare Dashboard
|
||||||
|
# Go to Workers & Pages > bentopdf-wasm-proxy > Settings > Variables
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended Source URLs:**
|
||||||
|
|
||||||
|
- PYMUPDF_SOURCE: `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.1.9/`
|
||||||
|
- GS_SOURCE: `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/`
|
||||||
|
- CPDF_SOURCE: `https://cdn.jsdelivr.net/npm/coherentpdf/dist/`
|
||||||
|
|
||||||
|
> **Note:** You can use your own hosted WASM files instead of the recommended URLs. Just ensure your files match the expected directory structure and file names that BentoPDF expects for each module.
|
||||||
|
|
||||||
|
### 3. Configure BentoPDF
|
||||||
|
|
||||||
|
In BentoPDF's Advanced Settings (wasm-settings.html), enter:
|
||||||
|
|
||||||
|
| Module | URL |
|
||||||
|
| ----------- | ------------------------------------------------------------------- |
|
||||||
|
| PyMuPDF | `https://bentopdf-wasm-proxy.<your-subdomain>.workers.dev/pymupdf/` |
|
||||||
|
| Ghostscript | `https://bentopdf-wasm-proxy.<your-subdomain>.workers.dev/gs/` |
|
||||||
|
| CoherentPDF | `https://bentopdf-wasm-proxy.<your-subdomain>.workers.dev/cpdf/` |
|
||||||
|
|
||||||
|
## Custom Domain (Optional)
|
||||||
|
|
||||||
|
To use a custom domain like `wasm.bentopdf.com`:
|
||||||
|
|
||||||
|
1. Add route in `wasm-wrangler.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
routes = [
|
||||||
|
{ pattern = "wasm.bentopdf.com/*", zone_name = "bentopdf.com" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add DNS record in Cloudflare:
|
||||||
|
- Type: AAAA
|
||||||
|
- Name: wasm
|
||||||
|
- Content: 100::
|
||||||
|
- Proxied: Yes
|
||||||
|
|
||||||
|
3. Redeploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx wrangler deploy -c wasm-wrangler.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
- **Origin validation**: Only allows requests from configured origins
|
||||||
|
- **Rate limiting**: 100 requests/minute per IP (requires KV namespace)
|
||||||
|
- **File type restrictions**: Only WASM-related files (.js, .wasm, .data, etc.)
|
||||||
|
- **Size limits**: Max 100MB per file
|
||||||
|
- **Caching**: Reduces origin requests and improves performance
|
||||||
|
|
||||||
|
## Self-Hosting Notes
|
||||||
|
|
||||||
|
1. Update `ALLOWED_ORIGINS` in `wasm-proxy-worker.js` to include your domain
|
||||||
|
2. Host your WASM files on any origin (R2, S3, or any CDN)
|
||||||
|
3. Set source URLs as secrets in your worker
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Description |
|
||||||
|
| ------------ | -------------------------------------- |
|
||||||
|
| `/` | Health check, shows configured modules |
|
||||||
|
| `/pymupdf/*` | PyMuPDF WASM files |
|
||||||
|
| `/gs/*` | Ghostscript WASM files |
|
||||||
|
| `/cpdf/*` | CoherentPDF files |
|
||||||
356
cloudflare/wasm-proxy-worker.js
Normal file
356
cloudflare/wasm-proxy-worker.js
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
/**
|
||||||
|
* BentoPDF WASM Proxy Worker
|
||||||
|
*
|
||||||
|
* This Cloudflare Worker proxies WASM module requests to bypass CORS restrictions.
|
||||||
|
* It fetches WASM libraries (PyMuPDF, Ghostscript, CoherentPDF) from configured sources
|
||||||
|
* and serves them with proper CORS headers.
|
||||||
|
*
|
||||||
|
* Endpoints:
|
||||||
|
* - /pymupdf/* - Proxies to PyMuPDF WASM source
|
||||||
|
* - /gs/* - Proxies to Ghostscript WASM source
|
||||||
|
* - /cpdf/* - Proxies to CoherentPDF WASM source
|
||||||
|
*
|
||||||
|
* Deploy: cd cloudflare && npx wrangler deploy -c wasm-wrangler.toml
|
||||||
|
*
|
||||||
|
* Required Environment Variables (set in Cloudflare dashboard):
|
||||||
|
* - PYMUPDF_SOURCE: Base URL for PyMuPDF WASM files (e.g., https://cdn.example.com/pymupdf)
|
||||||
|
* - GS_SOURCE: Base URL for Ghostscript WASM files (e.g., https://cdn.example.com/gs)
|
||||||
|
* - CPDF_SOURCE: Base URL for CoherentPDF files (e.g., https://cdn.example.com/cpdf)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ALLOWED_ORIGINS = ['https://www.bentopdf.com', 'https://bentopdf.com'];
|
||||||
|
|
||||||
|
const MAX_FILE_SIZE_BYTES = 100 * 1024 * 1024;
|
||||||
|
|
||||||
|
const RATE_LIMIT_MAX_REQUESTS = 100;
|
||||||
|
const RATE_LIMIT_WINDOW_MS = 60 * 1000;
|
||||||
|
|
||||||
|
const CACHE_TTL_SECONDS = 604800;
|
||||||
|
|
||||||
|
const ALLOWED_EXTENSIONS = [
|
||||||
|
'.js',
|
||||||
|
'.mjs',
|
||||||
|
'.wasm',
|
||||||
|
'.data',
|
||||||
|
'.py',
|
||||||
|
'.so',
|
||||||
|
'.zip',
|
||||||
|
'.json',
|
||||||
|
'.mem',
|
||||||
|
'.asm.js',
|
||||||
|
'.worker.js',
|
||||||
|
'.html',
|
||||||
|
];
|
||||||
|
|
||||||
|
function isAllowedOrigin(origin) {
|
||||||
|
if (!origin) return true; // Allow no-origin requests (e.g., direct browser navigation)
|
||||||
|
return ALLOWED_ORIGINS.some((allowed) =>
|
||||||
|
origin.startsWith(allowed.replace(/\/$/, ''))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedFile(pathname) {
|
||||||
|
const ext = pathname.substring(pathname.lastIndexOf('.')).toLowerCase();
|
||||||
|
if (ALLOWED_EXTENSIONS.includes(ext)) return true;
|
||||||
|
|
||||||
|
if (!pathname.includes('.') || pathname.endsWith('/')) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function corsHeaders(origin) {
|
||||||
|
return {
|
||||||
|
'Access-Control-Allow-Origin': origin || '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Range, Cache-Control',
|
||||||
|
'Access-Control-Expose-Headers':
|
||||||
|
'Content-Length, Content-Range, Content-Type',
|
||||||
|
'Access-Control-Max-Age': '86400',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOptions(request) {
|
||||||
|
const origin = request.headers.get('Origin');
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: corsHeaders(origin),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContentType(pathname) {
|
||||||
|
const ext = pathname.substring(pathname.lastIndexOf('.')).toLowerCase();
|
||||||
|
const contentTypes = {
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.mjs': 'application/javascript',
|
||||||
|
'.wasm': 'application/wasm',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.data': 'application/octet-stream',
|
||||||
|
'.py': 'text/x-python',
|
||||||
|
'.so': 'application/octet-stream',
|
||||||
|
'.zip': 'application/zip',
|
||||||
|
'.mem': 'application/octet-stream',
|
||||||
|
'.html': 'text/html',
|
||||||
|
};
|
||||||
|
return contentTypes[ext] || 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyRequest(request, env, sourceBaseUrl, subpath, origin) {
|
||||||
|
if (!sourceBaseUrl) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Source not configured',
|
||||||
|
message: 'This WASM module source URL has not been configured.',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 503,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders(origin),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedBase = sourceBaseUrl.endsWith('/')
|
||||||
|
? sourceBaseUrl.slice(0, -1)
|
||||||
|
: sourceBaseUrl;
|
||||||
|
const normalizedPath = subpath.startsWith('/') ? subpath : `/${subpath}`;
|
||||||
|
const targetUrl = `${normalizedBase}${normalizedPath}`;
|
||||||
|
|
||||||
|
if (!isAllowedFile(normalizedPath)) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Forbidden file type',
|
||||||
|
message: 'Only WASM-related file types are allowed.',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders(origin),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheKey = new Request(targetUrl, request);
|
||||||
|
const cache = caches.default;
|
||||||
|
let response = await cache.match(cacheKey);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
response = await fetch(targetUrl, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'BentoPDF-WASM-Proxy/1.0',
|
||||||
|
Accept: '*/*',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Failed to fetch resource',
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
targetUrl: targetUrl,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: response.status,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders(origin),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentLength = parseInt(
|
||||||
|
response.headers.get('Content-Length') || '0',
|
||||||
|
10
|
||||||
|
);
|
||||||
|
if (contentLength > MAX_FILE_SIZE_BYTES) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'File too large',
|
||||||
|
message: `File exceeds maximum size of ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB`,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 413,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders(origin),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
response = new Response(response.body, response);
|
||||||
|
response.headers.set(
|
||||||
|
'Cache-Control',
|
||||||
|
`public, max-age=${CACHE_TTL_SECONDS}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
await cache.put(cacheKey, response.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyData = await response.arrayBuffer();
|
||||||
|
|
||||||
|
return new Response(bodyData, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders(origin),
|
||||||
|
'Content-Type': getContentType(normalizedPath),
|
||||||
|
'Content-Length': bodyData.byteLength.toString(),
|
||||||
|
'Cache-Control': `public, max-age=${CACHE_TTL_SECONDS}`,
|
||||||
|
'X-Proxied-From': new URL(targetUrl).hostname,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Proxy error',
|
||||||
|
message: error.message,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders(origin),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request, env, ctx) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const pathname = url.pathname;
|
||||||
|
const origin = request.headers.get('Origin');
|
||||||
|
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
return handleOptions(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAllowedOrigin(origin)) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Forbidden',
|
||||||
|
message:
|
||||||
|
'Origin not allowed. Add your domain to ALLOWED_ORIGINS if self-hosting.',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders(origin),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||||
|
return new Response('Method not allowed', {
|
||||||
|
status: 405,
|
||||||
|
headers: corsHeaders(origin),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.RATE_LIMIT_KV) {
|
||||||
|
const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
|
||||||
|
const rateLimitKey = `wasm-ratelimit:${clientIP}`;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const rateLimitData = await env.RATE_LIMIT_KV.get(rateLimitKey, {
|
||||||
|
type: 'json',
|
||||||
|
});
|
||||||
|
const requests = rateLimitData?.requests || [];
|
||||||
|
const recentRequests = requests.filter(
|
||||||
|
(t) => now - t < RATE_LIMIT_WINDOW_MS
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recentRequests.length >= RATE_LIMIT_MAX_REQUESTS) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Rate limit exceeded',
|
||||||
|
message: `Maximum ${RATE_LIMIT_MAX_REQUESTS} requests per minute.`,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders(origin),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Retry-After': '60',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
recentRequests.push(now);
|
||||||
|
await env.RATE_LIMIT_KV.put(
|
||||||
|
rateLimitKey,
|
||||||
|
JSON.stringify({ requests: recentRequests }),
|
||||||
|
{
|
||||||
|
expirationTtl: 120,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith('/pymupdf/')) {
|
||||||
|
const subpath = pathname.replace('/pymupdf', '');
|
||||||
|
return proxyRequest(request, env, env.PYMUPDF_SOURCE, subpath, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith('/gs/')) {
|
||||||
|
const subpath = pathname.replace('/gs', '');
|
||||||
|
return proxyRequest(request, env, env.GS_SOURCE, subpath, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname.startsWith('/cpdf/')) {
|
||||||
|
const subpath = pathname.replace('/cpdf', '');
|
||||||
|
return proxyRequest(request, env, env.CPDF_SOURCE, subpath, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === '/' || pathname === '/health') {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
service: 'BentoPDF WASM Proxy',
|
||||||
|
version: '1.0.0',
|
||||||
|
endpoints: {
|
||||||
|
pymupdf: '/pymupdf/*',
|
||||||
|
gs: '/gs/*',
|
||||||
|
cpdf: '/cpdf/*',
|
||||||
|
},
|
||||||
|
configured: {
|
||||||
|
pymupdf: !!env.PYMUPDF_SOURCE,
|
||||||
|
gs: !!env.GS_SOURCE,
|
||||||
|
cpdf: !!env.CPDF_SOURCE,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders(origin),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Not Found',
|
||||||
|
message: 'Use /pymupdf/*, /gs/*, or /cpdf/* endpoints',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 404,
|
||||||
|
headers: {
|
||||||
|
...corsHeaders(origin),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
69
cloudflare/wasm-wrangler.toml
Normal file
69
cloudflare/wasm-wrangler.toml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
name = "bentopdf-wasm-proxy"
|
||||||
|
main = "wasm-proxy-worker.js"
|
||||||
|
compatibility_date = "2024-01-01"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DEPLOYMENT
|
||||||
|
# =============================================================================
|
||||||
|
# Deploy this worker:
|
||||||
|
# cd cloudflare
|
||||||
|
# npx wrangler deploy -c wasm-wrangler.toml
|
||||||
|
#
|
||||||
|
# Set environment secrets (one of the following methods):
|
||||||
|
# Option A: Cloudflare Dashboard
|
||||||
|
# Go to Workers & Pages > bentopdf-wasm-proxy > Settings > Variables
|
||||||
|
# Add: PYMUPDF_SOURCE, GS_SOURCE, CPDF_SOURCE
|
||||||
|
#
|
||||||
|
# Option B: Wrangler CLI
|
||||||
|
# npx wrangler secret put PYMUPDF_SOURCE -c wasm-wrangler.toml
|
||||||
|
# npx wrangler secret put GS_SOURCE -c wasm-wrangler.toml
|
||||||
|
# npx wrangler secret put CPDF_SOURCE -c wasm-wrangler.toml
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# WASM SOURCE URLS
|
||||||
|
# =============================================================================
|
||||||
|
# Set these as secrets in the Cloudflare dashboard or via wrangler:
|
||||||
|
#
|
||||||
|
# PYMUPDF_SOURCE: Base URL to PyMuPDF WASM files
|
||||||
|
# Example: https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm/assets
|
||||||
|
# https://your-bucket.r2.cloudflarestorage.com/pymupdf
|
||||||
|
#
|
||||||
|
# GS_SOURCE: Base URL to Ghostscript WASM files
|
||||||
|
# Example: https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets
|
||||||
|
# https://your-bucket.r2.cloudflarestorage.com/gs
|
||||||
|
#
|
||||||
|
# CPDF_SOURCE: Base URL to CoherentPDF files
|
||||||
|
# Example: https://cdn.jsdelivr.net/npm/coherentpdf/cpdf
|
||||||
|
# https://your-bucket.r2.cloudflarestorage.com/cpdf
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# USAGE FROM BENTOPDF
|
||||||
|
# =============================================================================
|
||||||
|
# In BentoPDF's WASM Settings page, configure URLs like:
|
||||||
|
# PyMuPDF: https://wasm.bentopdf.com/pymupdf/
|
||||||
|
# Ghostscript: https://wasm.bentopdf.com/gs/
|
||||||
|
# CoherentPDF: https://wasm.bentopdf.com/cpdf/
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# RATE LIMITING (Optional but recommended)
|
||||||
|
# =============================================================================
|
||||||
|
# Create KV namespace:
|
||||||
|
# npx wrangler kv namespace create "RATE_LIMIT_KV"
|
||||||
|
#
|
||||||
|
# Then uncomment and update the ID below:
|
||||||
|
# [[kv_namespaces]]
|
||||||
|
# binding = "RATE_LIMIT_KV"
|
||||||
|
# id = "<YOUR_KV_NAMESPACE_ID>"
|
||||||
|
|
||||||
|
# Use the same KV namespace as the CORS proxy if you want shared rate limiting
|
||||||
|
[[kv_namespaces]]
|
||||||
|
binding = "RATE_LIMIT_KV"
|
||||||
|
id = "b88e030b308941118cd484e3fcb3ae49"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CUSTOM DOMAIN (Optional)
|
||||||
|
# =============================================================================
|
||||||
|
# If you want a custom domain like wasm.bentopdf.com:
|
||||||
|
# routes = [
|
||||||
|
# { pattern = "wasm.bentopdf.com/*", zone_name = "bentopdf.com" }
|
||||||
|
# ]
|
||||||
@@ -12,13 +12,13 @@ For complete licensing information, delivery details, AGPL component notices, an
|
|||||||
|
|
||||||
## When Do You Need a Commercial License?
|
## When Do You Need a Commercial License?
|
||||||
|
|
||||||
| Use Case | License Required |
|
| Use Case | License Required |
|
||||||
|----------|------------------|
|
| ------------------------------------------- | ---------------------- |
|
||||||
| Open-source project with public source code | AGPL-3.0 (Free) |
|
| Open-source project with public source code | AGPL-3.0 (Free) |
|
||||||
| Internal company tool (not distributed) | AGPL-3.0 (Free) |
|
| Internal company tool (not distributed) | AGPL-3.0 (Free) |
|
||||||
| Proprietary/closed-source application | **Commercial License** |
|
| Proprietary/closed-source application | **Commercial License** |
|
||||||
| SaaS product without source code disclosure | **Commercial License** |
|
| SaaS product without source code disclosure | **Commercial License** |
|
||||||
| Redistributing without AGPL compliance | **Commercial License** |
|
| Redistributing without AGPL compliance | **Commercial License** |
|
||||||
|
|
||||||
## Delivery & Licensing Model
|
## Delivery & Licensing Model
|
||||||
|
|
||||||
@@ -30,12 +30,32 @@ For complete licensing information, delivery details, AGPL component notices, an
|
|||||||
|
|
||||||
## Important Notice on Third-Party Components
|
## Important Notice on Third-Party Components
|
||||||
|
|
||||||
::: warning AGPL Components
|
::: warning AGPL Components - Not Bundled
|
||||||
This software includes components licensed under the **GNU AGPL v3**, such as CPDF.
|
BentoPDF **does not bundle** AGPL-licensed processing libraries. The following components are loaded separately by users who configure them via **Advanced Settings**:
|
||||||
|
|
||||||
- This commercial license **does not** grant rights to use AGPL components in a closed-source manner.
|
| Component | License | Status |
|
||||||
- Users must comply with the AGPL v3 terms for these components.
|
| --------------- | -------- | ----------------------------- |
|
||||||
- Source code for all AGPL components is included in the distribution.
|
| **PyMuPDF** | AGPL-3.0 | Not bundled - user configured |
|
||||||
|
| **Ghostscript** | AGPL-3.0 | Not bundled - user configured |
|
||||||
|
| **CoherentPDF** | AGPL-3.0 | Not bundled - user configured |
|
||||||
|
|
||||||
|
**Why are AGPL binaries not included?**
|
||||||
|
|
||||||
|
To maintain clear legal separation, BentoPDF does not distribute AGPL-licensed binaries. Users who need features powered by these libraries can:
|
||||||
|
|
||||||
|
1. Configure their own WASM sources in Advanced Settings
|
||||||
|
2. Host their own WASM proxy to serve these files
|
||||||
|
3. Use any compatible CDN that hosts these packages
|
||||||
|
|
||||||
|
This approach ensures:
|
||||||
|
|
||||||
|
- BentoPDF's core code remains under its dual-license (AGPL-3.0 / Commercial)
|
||||||
|
- Users make an informed choice when enabling AGPL features
|
||||||
|
- Clear compliance boundaries for commercial users
|
||||||
|
:::
|
||||||
|
|
||||||
|
::: tip Commercial License & AGPL Features
|
||||||
|
The commercial license covers BentoPDF's own code. If you configure and use AGPL components (PyMuPDF, Ghostscript, CoherentPDF), you must still comply with their respective AGPL-3.0 license terms, which may require source code disclosure if you distribute modified versions.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Invoicing
|
## Invoicing
|
||||||
@@ -47,15 +67,15 @@ This software includes components licensed under the **GNU AGPL v3**, such as CP
|
|||||||
|
|
||||||
## What's Included
|
## What's Included
|
||||||
|
|
||||||
| Feature | Included |
|
| Feature | Included |
|
||||||
|---------|----------|
|
| ----------------------------- | -------------- |
|
||||||
| Full source code | ✅ |
|
| Full source code | ✅ |
|
||||||
| All 50+ PDF tools | ✅ |
|
| All 50+ PDF tools | ✅ |
|
||||||
| Self-hosting rights | ✅ |
|
| Self-hosting rights | ✅ |
|
||||||
| Lifetime updates | ✅ |
|
| Lifetime updates | ✅ |
|
||||||
| Remove branding (Simple Mode) | ✅ |
|
| Remove branding (Simple Mode) | ✅ |
|
||||||
| Commercial support | ✅ (via email) |
|
| Commercial support | ✅ (via email) |
|
||||||
| Priority feature requests | ✅ |
|
| Priority feature requests | ✅ |
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
@@ -69,7 +89,7 @@ Yes, with a commercial license. Without it, you must comply with AGPL-3.0, which
|
|||||||
|
|
||||||
### What about the AGPL components?
|
### What about the AGPL components?
|
||||||
|
|
||||||
Components like CPDF are licensed under AGPL v3 and remain under that license. The commercial license covers BentoPDF's own code but does not override third-party AGPL obligations.
|
Components like CoherentPDF are licensed under AGPL v3 and remain under that license. The commercial license covers BentoPDF's own code but does not override third-party AGPL obligations.
|
||||||
|
|
||||||
### How do I get an invoice?
|
### How do I get an invoice?
|
||||||
|
|
||||||
|
|||||||
@@ -118,13 +118,49 @@ Choose your platform:
|
|||||||
- [Kubernetes](/self-hosting/kubernetes)
|
- [Kubernetes](/self-hosting/kubernetes)
|
||||||
- [CORS Proxy](/self-hosting/cors-proxy) - Required for digital signatures
|
- [CORS Proxy](/self-hosting/cors-proxy) - Required for digital signatures
|
||||||
|
|
||||||
|
## Configuring AGPL WASM Components
|
||||||
|
|
||||||
|
BentoPDF **does not bundle** AGPL-licensed processing libraries. Some advanced features require you to configure WASM modules separately.
|
||||||
|
|
||||||
|
::: warning AGPL Components Not Included
|
||||||
|
The following WASM modules are **not bundled** with BentoPDF and must be configured by users who want to use features powered by these libraries:
|
||||||
|
|
||||||
|
| Component | License | Features |
|
||||||
|
| --------------- | -------- | ---------------------------------------------------------------- |
|
||||||
|
| **PyMuPDF** | AGPL-3.0 | EPUB/MOBI/FB2/XPS conversion, image extraction, table extraction |
|
||||||
|
| **Ghostscript** | AGPL-3.0 | PDF/A conversion, compression, deskewing, rasterization |
|
||||||
|
| **CoherentPDF** | AGPL-3.0 | Table of contents, attachments, PDF merge with bookmarks |
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
### How to Configure WASM Sources
|
||||||
|
|
||||||
|
1. Navigate to **Advanced Settings** in the BentoPDF interface
|
||||||
|
2. Enter the URLs for the WASM modules you want to use
|
||||||
|
3. You can use:
|
||||||
|
- Your own hosted WASM files
|
||||||
|
- A [WASM proxy](/self-hosting/cors-proxy) you deploy (handles CORS)
|
||||||
|
- Any compatible CDN hosting these packages
|
||||||
|
|
||||||
|
### Hosting Your Own WASM Proxy
|
||||||
|
|
||||||
|
If you need to serve AGPL WASM files with proper CORS headers, you can deploy a simple proxy. See the [Cloudflare WASM Proxy guide](https://github.com/alam00000/bentopdf/blob/main/cloudflare/WASM-PROXY.md) for an example implementation.
|
||||||
|
|
||||||
|
::: tip Why Separate?
|
||||||
|
This separation ensures:
|
||||||
|
|
||||||
|
- Clear legal compliance for commercial users
|
||||||
|
- Users make informed choices when enabling AGPL features
|
||||||
|
- BentoPDF's core remains under its dual-license (AGPL-3.0 / Commercial)
|
||||||
|
:::
|
||||||
|
|
||||||
## System Requirements
|
## System Requirements
|
||||||
|
|
||||||
| Requirement | Minimum |
|
| Requirement | Minimum |
|
||||||
| ----------- | ------------------------------- |
|
| ----------- | ----------------------------------- |
|
||||||
| Storage | ~500 MB (with all WASM modules) |
|
| Storage | ~100 MB (core without AGPL modules) |
|
||||||
| RAM | 512 MB |
|
| RAM | 512 MB |
|
||||||
| CPU | Any modern processor |
|
| CPU | Any modern processor |
|
||||||
|
|
||||||
::: tip
|
::: tip
|
||||||
BentoPDF is a static site—there's no database or backend server required!
|
BentoPDF is a static site—there's no database or backend server required!
|
||||||
|
|||||||
37
index.html
37
index.html
@@ -209,6 +209,21 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
<!-- DigitalOcean -->
|
||||||
|
<a
|
||||||
|
href="https://www.digitalocean.com/?refcode=d93c189ef6d0&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="mt-8 opacity-50 hover:opacity-100 transition-opacity duration-300 hide-section"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/images/badge.svg"
|
||||||
|
alt="DigitalOcean Referral Badge"
|
||||||
|
width="150"
|
||||||
|
height="32"
|
||||||
|
class="h-8"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -701,6 +716,28 @@
|
|||||||
></div>
|
></div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Settings Link -->
|
||||||
|
<a
|
||||||
|
href="wasm-settings.html"
|
||||||
|
class="flex items-center justify-between p-4 bg-gray-900 rounded-lg border border-gray-700 hover:bg-gray-800 hover:border-indigo-500/50 transition-all group"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<span
|
||||||
|
class="text-sm font-medium text-gray-200 group-hover:text-white"
|
||||||
|
>
|
||||||
|
Advanced Settings
|
||||||
|
</span>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">
|
||||||
|
Configure external processing modules (PyMuPDF, Ghostscript,
|
||||||
|
CoherentPDF)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
data-lucide="chevron-right"
|
||||||
|
class="w-5 h-5 text-gray-500 group-hover:text-indigo-400 transition-colors"
|
||||||
|
></i>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
143
licensing.html
143
licensing.html
@@ -989,72 +989,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
|
|
||||||
<h3
|
|
||||||
class="text-xl font-semibold text-white mb-4 flex items-center gap-3"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
data-lucide="alert-triangle"
|
|
||||||
class="w-6 h-6 text-yellow-400"
|
|
||||||
></i>
|
|
||||||
Important Notice on Third-Party Components
|
|
||||||
</h3>
|
|
||||||
<p class="text-gray-300 mb-4">
|
|
||||||
This software includes components licensed under the
|
|
||||||
<strong class="text-white">GNU AGPL v3</strong>, including:
|
|
||||||
</p>
|
|
||||||
<ul class="flex flex-wrap gap-2 mb-4">
|
|
||||||
<li
|
|
||||||
class="px-3 py-1 bg-gray-700 rounded-full text-sm text-gray-300"
|
|
||||||
>
|
|
||||||
CPDF
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="px-3 py-1 bg-gray-700 rounded-full text-sm text-gray-300"
|
|
||||||
>
|
|
||||||
PyMuPDF
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="px-3 py-1 bg-gray-700 rounded-full text-sm text-gray-300"
|
|
||||||
>
|
|
||||||
Ghostscript
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul class="space-y-3 text-gray-400">
|
|
||||||
<li class="flex items-start gap-3">
|
|
||||||
<i
|
|
||||||
data-lucide="alert-circle"
|
|
||||||
class="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5"
|
|
||||||
></i>
|
|
||||||
<span
|
|
||||||
>This commercial license
|
|
||||||
<strong class="text-white">does not</strong> grant rights to
|
|
||||||
use AGPL components in a closed-source manner.</span
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-start gap-3">
|
|
||||||
<i
|
|
||||||
data-lucide="alert-circle"
|
|
||||||
class="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5"
|
|
||||||
></i>
|
|
||||||
<span
|
|
||||||
>Users must comply with the AGPL v3 terms for these
|
|
||||||
components.</span
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li class="flex items-start gap-3">
|
|
||||||
<i
|
|
||||||
data-lucide="check-circle"
|
|
||||||
class="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5"
|
|
||||||
></i>
|
|
||||||
<span
|
|
||||||
>Source code for all AGPL components is included in the
|
|
||||||
distribution.</span
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
|
<div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
|
||||||
<h3
|
<h3
|
||||||
class="text-xl font-semibold text-white mb-4 flex items-center gap-3"
|
class="text-xl font-semibold text-white mb-4 flex items-center gap-3"
|
||||||
@@ -1104,6 +1038,83 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-800 rounded-xl p-6 border border-gray-700">
|
||||||
|
<h3
|
||||||
|
class="text-xl font-semibold text-white mb-4 flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
data-lucide="alert-triangle"
|
||||||
|
class="w-6 h-6 text-indigo-400"
|
||||||
|
></i>
|
||||||
|
AGPL Components - Not Bundled
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-300 mb-4">
|
||||||
|
BentoPDF
|
||||||
|
<strong class="text-white">does not bundle</strong> AGPL-licensed
|
||||||
|
processing libraries. The following components must be configured
|
||||||
|
separately via
|
||||||
|
<strong class="text-white">Advanced Settings</strong> if you wish
|
||||||
|
to use their features:
|
||||||
|
</p>
|
||||||
|
<ul class="flex flex-wrap gap-2 mb-4">
|
||||||
|
<li
|
||||||
|
class="px-3 py-1 bg-gray-700 rounded-full text-sm text-gray-300"
|
||||||
|
>
|
||||||
|
PyMuPDF (AGPL-3.0)
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="px-3 py-1 bg-gray-700 rounded-full text-sm text-gray-300"
|
||||||
|
>
|
||||||
|
Ghostscript (AGPL-3.0)
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="px-3 py-1 bg-gray-700 rounded-full text-sm text-gray-300"
|
||||||
|
>
|
||||||
|
CoherentPDF / CPDF (AGPL-3.0)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-gray-300 mb-4">
|
||||||
|
<strong class="text-white"
|
||||||
|
>To enable features powered by these libraries:</strong
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-2 text-gray-400 mb-4">
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<span class="text-indigo-400 font-bold">1.</span>
|
||||||
|
<span
|
||||||
|
>Navigate to
|
||||||
|
<strong class="text-white">Advanced Settings</strong> in
|
||||||
|
BentoPDF</span
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<span class="text-indigo-400 font-bold">2.</span>
|
||||||
|
<span>Configure the URL for each WASM module you need</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
<span class="text-indigo-400 font-bold">3.</span>
|
||||||
|
<span
|
||||||
|
>You can host your own files, use a
|
||||||
|
<a
|
||||||
|
href="https://github.com/alam00000/bentopdf/blob/main/cloudflare/WASM-PROXY.md"
|
||||||
|
class="text-indigo-400 hover:underline"
|
||||||
|
>WASM proxy</a
|
||||||
|
>, or use any compatible CDN</span
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-gray-400 text-sm mt-4">
|
||||||
|
<i
|
||||||
|
data-lucide="alert-circle"
|
||||||
|
class="w-4 h-4 inline-block mr-1 text-indigo-400"
|
||||||
|
></i>
|
||||||
|
The commercial license covers
|
||||||
|
<strong class="text-white">BentoPDF's own code only</strong>. It
|
||||||
|
does not bypass the AGPL licensing of these components. Users must
|
||||||
|
comply with the AGPL v3 terms for these components.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -9,8 +9,6 @@
|
|||||||
"version": "1.16.1",
|
"version": "1.16.1",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bentopdf/gs-wasm": "^0.1.0",
|
|
||||||
"@bentopdf/pymupdf-wasm": "^0.11.12",
|
|
||||||
"@fontsource/cedarville-cursive": "^5.2.7",
|
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||||
"@fontsource/dancing-script": "^5.2.8",
|
"@fontsource/dancing-script": "^5.2.8",
|
||||||
"@fontsource/dm-sans": "^5.2.8",
|
"@fontsource/dm-sans": "^5.2.8",
|
||||||
@@ -505,24 +503,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@bentopdf/gs-wasm": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bentopdf/gs-wasm/-/gs-wasm-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-C71zxZW4R7Oa6fdya5leTh2VOZOxqH8IQlveh13OeuwZ2ulrovSi9629xTzAiIeeVKvDZma1Klxy4MuK65xe9w==",
|
|
||||||
"license": "AGPL-3.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/emscripten": "^1.39.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@bentopdf/pymupdf-wasm": {
|
|
||||||
"version": "0.11.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bentopdf/pymupdf-wasm/-/pymupdf-wasm-0.11.12.tgz",
|
|
||||||
"integrity": "sha512-AcSg7v7pVhYcH23qLDEj3yTABlGIkZULPmrvWHRtEyD5QMS0TWOLUq/c0ATO371PKVlI4jEUpCBUj+iBsFJwVQ==",
|
|
||||||
"license": "AGPL-3.0",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@bentopdf/gs-wasm": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@braintree/sanitize-url": {
|
"node_modules/@braintree/sanitize-url": {
|
||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz",
|
||||||
@@ -3529,12 +3509,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/emscripten": {
|
|
||||||
"version": "1.41.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.41.5.tgz",
|
|
||||||
"integrity": "sha512-cMQm7pxu6BxtHyqJ7mQZ2kXWV5SLmugybFdHCBbJ5eHzOo6VhBckEgAT3//rP5FwPHNPeEiq4SmQ5ucBwsOo4Q==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
|||||||
@@ -64,8 +64,6 @@
|
|||||||
"vue": "^3.5.26"
|
"vue": "^3.5.26"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bentopdf/gs-wasm": "^0.1.0",
|
|
||||||
"@bentopdf/pymupdf-wasm": "^0.11.12",
|
|
||||||
"@fontsource/cedarville-cursive": "^5.2.7",
|
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||||
"@fontsource/dancing-script": "^5.2.8",
|
"@fontsource/dancing-script": "^5.2.8",
|
||||||
"@fontsource/dm-sans": "^5.2.8",
|
"@fontsource/dm-sans": "^5.2.8",
|
||||||
|
|||||||
101
public/images/badge.svg
Normal file
101
public/images/badge.svg
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 604 129" style="enable-background:new 0 0 604 129;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M174.3,3c4.9,0,8.7,2.9,8.7,8.6c0,5.6-3.8,8.5-8.7,8.5h-7.6v11.1h-3.5V3H174.3z M166.7,17.1h7.2
|
||||||
|
c3,0,5.6-1.8,5.6-5.5c0-3.8-2.5-5.5-5.6-5.5h-7.2V17.1z"/>
|
||||||
|
<path class="st0" d="M208.8,21.7c0,6.1-4.3,10-9.9,10c-5.6,0-9.9-3.9-9.9-10c0-6.1,4.3-10,9.9-10
|
||||||
|
C204.5,11.7,208.8,15.6,208.8,21.7z M192.3,21.7c0,4.5,2.9,7.2,6.6,7.2c3.7,0,6.6-2.7,6.6-7.2c0-4.5-2.9-7.1-6.6-7.1
|
||||||
|
C195.2,14.5,192.3,17.2,192.3,21.7z"/>
|
||||||
|
<path class="st0" d="M234.4,31.3l-5.2-13.8L224,31.3h-2.6L214.1,12h3.6l5.2,14l5.2-14h2.3l5.3,14l5.2-14h3.5L237,31.3H234.4z"/>
|
||||||
|
<path class="st0" d="M253,22.9c0.2,3.7,2.6,5.9,6,5.9c2.8,0,4.8-1.3,5.4-3.4l3.2,0.2c-0.8,3.5-4.1,6.1-8.6,6.1
|
||||||
|
c-5.5,0-9.6-3.7-9.6-10c0-6.3,4-10,9.5-10c5.5,0,8.8,3.7,8.8,9.4v1.8H253z M253,20.3h11.6c-0.1-3.4-2-5.7-5.6-5.7
|
||||||
|
C255.6,14.5,253.2,16.5,253,20.3z"/>
|
||||||
|
<path class="st0" d="M285.4,14.9c-3.4,0-5.6,2.3-5.6,5.3v11.1h-3.2V12h3.2v2.9c0.7-1.6,2.5-3.1,5.7-3.1V14.9z"/>
|
||||||
|
<path class="st0" d="M294.7,22.9c0.2,3.7,2.6,5.9,6,5.9c2.8,0,4.8-1.3,5.4-3.4l3.2,0.2c-0.8,3.5-4.1,6.1-8.6,6.1
|
||||||
|
c-5.5,0-9.6-3.7-9.6-10c0-6.3,4-10,9.5-10c5.5,0,8.8,3.7,8.8,9.4v1.8H294.7z M294.7,20.3h11.6c-0.1-3.4-2-5.7-5.6-5.7
|
||||||
|
C297.4,14.5,294.9,16.5,294.7,20.3z"/>
|
||||||
|
<path class="st0" d="M333.1,31.3v-3.1c-1.1,2-3.6,3.5-6.8,3.5c-5.3,0-9.3-3.8-9.3-10c0-6.2,4-10,9.3-10c3.2,0,5.6,1.4,6.6,3.2V2
|
||||||
|
h3.2v29.4H333.1z M320.3,21.7c0,4.6,2.8,7.2,6.5,7.2c3.6,0,6.2-2.2,6.2-6.6v-1.1c0-4.3-2.6-6.6-6.2-6.6
|
||||||
|
C323.1,14.5,320.3,17.1,320.3,21.7z"/>
|
||||||
|
<path class="st0" d="M361.8,14.9c1.1-1.9,3.4-3.2,6.7-3.2c5.3,0,9.3,3.8,9.3,10c0,6.2-4,10-9.3,10c-3.3,0-5.7-1.5-6.8-3.5v3.1
|
||||||
|
h-3.1V2h3.2V14.9z M361.9,21.1v1.1c0,4.4,2.6,6.6,6.2,6.6c3.7,0,6.5-2.5,6.5-7.2c0-4.6-2.8-7.1-6.5-7.1
|
||||||
|
C364.5,14.5,361.9,16.8,361.9,21.1z"/>
|
||||||
|
<path class="st0" d="M386.3,40.9l4.6-10.7L383.2,12h3.6l5.8,14.5l5.8-14.5h3.6l-12.2,28.9H386.3z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="XMLID_2369_">
|
||||||
|
<g>
|
||||||
|
<g id="XMLID_281_">
|
||||||
|
<g id="XMLID_282_">
|
||||||
|
<g>
|
||||||
|
<g id="XMLID_283_">
|
||||||
|
<g id="XMLID_287_">
|
||||||
|
<path id="XMLID_288_" class="st0" d="M64.4,127l0-24.2c25.6,0,45.5-25.4,35.7-52.3c-3.6-10-11.6-17.9-21.6-21.6
|
||||||
|
c-27-9.8-52.3,10-52.3,35.7c0,0,0,0,0,0L2,64.7C2,23.8,41.5-8,84.3,5.4c18.7,5.8,33.6,20.7,39.4,39.4
|
||||||
|
C137,87.6,105.2,127,64.4,127z"/>
|
||||||
|
</g>
|
||||||
|
<polygon id="XMLID_286_" class="st1" points="64.4,102.9 40.4,102.9 40.4,78.9 40.4,78.9 64.4,78.9 64.4,78.9 "/>
|
||||||
|
<polygon id="XMLID_285_" class="st1" points="40.3,121.5 21.8,121.5 21.8,121.5 21.8,102.9 40.4,102.9 40.4,121.5 "/>
|
||||||
|
<path id="XMLID_284_" class="st1" d="M21.9,102.9H6.3c0,0,0,0,0,0V87.4c0,0,0,0,0,0h15.5c0,0,0,0,0,0V102.9z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="XMLID_254_">
|
||||||
|
<path id="XMLID_278_" class="st0" d="M200.9,52.4c-5.5-3.8-12.4-5.8-20.5-5.8h-17.5v55.5h17.5c8,0,14.9-2.1,20.5-6.1
|
||||||
|
c3-2.1,5.4-5.1,7.1-8.9c1.7-3.7,2.5-8.2,2.5-13.1c0-4.9-0.8-9.3-2.5-13C206.3,57.4,203.9,54.4,200.9,52.4z M173.1,56h5.5
|
||||||
|
c6.1,0,11.1,1.2,15,3.6c4.2,2.6,6.4,7.4,6.4,14.4c0,7.2-2.2,12.3-6.4,15.1h0c-3.7,2.4-8.7,3.6-14.9,3.6h-5.6V56z"/>
|
||||||
|
<path id="XMLID_277_" class="st0" d="M222.6,45.9c-1.7,0-3.1,0.6-4.3,1.8c-1.2,1.1-1.8,2.6-1.8,4.2c0,1.7,0.6,3.1,1.8,4.3
|
||||||
|
c1.2,1.2,2.6,1.8,4.3,1.8c1.7,0,3.1-0.6,4.3-1.8c1.2-1.2,1.8-2.6,1.8-4.3c0-1.7-0.6-3.1-1.8-4.2
|
||||||
|
C225.7,46.5,224.3,45.9,222.6,45.9z"/>
|
||||||
|
<rect id="XMLID_276_" x="217.6" y="63" class="st0" width="9.8" height="39.1"/>
|
||||||
|
<path id="XMLID_273_" class="st0" d="M263.2,66.3c-3-2.6-6.3-4.2-9.9-4.2c-5.4,0-9.9,1.9-13.4,5.6c-3.5,3.7-5.3,8.4-5.3,14.1
|
||||||
|
c0,5.5,1.8,10.2,5.2,14c3.5,3.7,8,5.5,13.5,5.5c3.8,0,7.1-1.1,9.7-3.1V99c0,3.2-0.9,5.8-2.6,7.5c-1.7,1.7-4.1,2.6-7.1,2.6
|
||||||
|
c-4.5,0-7.4-1.8-10.9-6.5l-6.7,6.4l0.2,0.3c1.4,2,3.7,4,6.6,5.9c2.9,1.9,6.6,2.8,10.9,2.8c5.8,0,10.6-1.8,14.1-5.4
|
||||||
|
c3.5-3.6,5.3-8.4,5.3-14.2V63h-9.7V66.3z M260.6,89.4c-1.7,2-3.9,2.9-6.8,2.9c-2.8,0-5-0.9-6.7-2.9c-1.7-1.9-2.5-4.5-2.5-7.7
|
||||||
|
c0-3.2,0.9-5.8,2.5-7.7c1.7-1.9,3.9-2.9,6.7-2.9c2.8,0,5,1,6.8,2.9c1.7,2,2.6,4.6,2.6,7.7C263.2,84.9,262.3,87.5,260.6,89.4z"/>
|
||||||
|
<rect id="XMLID_272_" x="281.3" y="63" class="st0" width="9.8" height="39.1"/>
|
||||||
|
<path id="XMLID_271_" class="st0" d="M286.3,45.9c-1.7,0-3.1,0.6-4.3,1.8c-1.2,1.1-1.8,2.6-1.8,4.2c0,1.7,0.6,3.1,1.8,4.3
|
||||||
|
c1.2,1.2,2.6,1.8,4.3,1.8c1.7,0,3.1-0.6,4.3-1.8c1.2-1.2,1.8-2.6,1.8-4.3c0-1.7-0.6-3.1-1.8-4.2C289.4,46.5,288,45.9,286.3,45.9
|
||||||
|
z"/>
|
||||||
|
<path id="XMLID_270_" class="st0" d="M312.7,52.5H303V63h-5.6v9h5.6v16.2c0,5.1,1,8.7,3,10.8c2,2.1,5.6,3.2,10.6,3.2
|
||||||
|
c1.6,0,3.2-0.1,4.8-0.2l0.4,0v-9l-3.4,0.2c-2.3,0-3.9-0.4-4.7-1.2c-0.8-0.8-1.1-2.6-1.1-5.2V72h9.2v-9h-9.2V52.5z"/>
|
||||||
|
<rect id="XMLID_269_" x="368" y="46.6" class="st0" width="9.8" height="55.5"/>
|
||||||
|
<path id="XMLID_268_" class="st0" d="M477.3,88.2c-1.8,2-3.6,3.7-4.9,4.6v0c-1.4,0.9-3.1,1.3-5.1,1.3c-2.9,0-5.2-1.1-7.1-3.2
|
||||||
|
c-1.9-2.2-2.8-4.9-2.8-8.3s0.9-6.1,2.8-8.2c1.9-2.2,4.2-3.2,7.1-3.2c3.2,0,6.5,2,9.4,5.4l6.5-6.2l0,0c-4.2-5.5-9.7-8.1-16.1-8.1
|
||||||
|
c-5.4,0-10.1,2-13.9,5.8c-3.8,3.9-5.7,8.8-5.7,14.6s1.9,10.7,5.7,14.6c3.8,3.9,8.5,5.9,13.9,5.9c7.1,0,12.9-3.1,16.8-8.7
|
||||||
|
L477.3,88.2z"/>
|
||||||
|
<path id="XMLID_265_" class="st0" d="M517.7,68.5c-1.4-1.9-3.3-3.5-5.7-4.7c-2.3-1.1-5.1-1.7-8.1-1.7c-5.5,0-10,2-13.4,6
|
||||||
|
c-3.3,4-4.9,8.9-4.9,14.7c0,5.9,1.8,10.8,5.4,14.6c3.6,3.7,8.4,5.6,14.2,5.6c6.6,0,12.1-2.7,16.2-8l0.2-0.3l-6.4-6.2l0,0
|
||||||
|
c-0.6,0.7-1.4,1.5-2.2,2.3c-1,0.9-1.9,1.6-2.9,2.1c-1.5,0.7-3.1,1.1-5,1.1c-2.7,0-5-0.8-6.7-2.4c-1.6-1.5-2.6-3.5-2.8-5.9h26.1
|
||||||
|
l0.1-3.6c0-2.5-0.3-5-1-7.3C520.1,72.6,519.1,70.4,517.7,68.5z M496.2,77.7c0.5-1.9,1.3-3.4,2.6-4.6c1.3-1.3,3.1-2,5.2-2
|
||||||
|
c2.4,0,4.2,0.7,5.5,2c1.2,1.2,1.8,2.8,2,4.6H496.2z"/>
|
||||||
|
<path id="XMLID_262_" class="st0" d="M555.5,66L555.5,66c-3-2.5-7.1-3.8-12.3-3.8c-3.3,0-6.3,0.7-9.1,2.1
|
||||||
|
c-2.6,1.3-5.1,3.5-6.7,6.3l0.1,0.1l6.3,6c2.6-4.1,5.5-5.6,9.3-5.6c2.1,0,3.8,0.6,5.1,1.6c1.3,1.1,1.9,2.5,1.9,4.2v1.9
|
||||||
|
c-2.4-0.7-4.9-1.1-7.2-1.1c-4.9,0-8.9,1.2-11.8,3.4c-3,2.3-4.5,5.6-4.5,9.8c0,3.7,1.3,6.7,3.8,8.9c2.6,2.1,5.8,3.2,9.5,3.2
|
||||||
|
c3.7,0,7.3-1.5,10.4-4.1v3.2h9.7V77C560,72.2,558.5,68.5,555.5,66z M538,87.2c1.1-0.8,2.7-1.2,4.7-1.2c2.4,0,4.9,0.5,7.5,1.4
|
||||||
|
v3.8c-2.1,2-5,3-8.5,3c-1.7,0-3-0.4-3.9-1.1c-0.9-0.7-1.3-1.7-1.3-2.8C536.4,89,536.9,88,538,87.2z"/>
|
||||||
|
<path id="XMLID_261_" class="st0" d="M597.9,66.7c-2.7-3.1-6.6-4.6-11.5-4.6c-3.9,0-7.1,1.1-9.4,3.3V63h-9.7v39.1h9.8V80.6
|
||||||
|
c0-3,0.7-5.3,2.1-7c1.4-1.7,3.3-2.5,5.8-2.5c2.2,0,3.9,0.7,5.2,2.2c1.3,1.5,1.9,3.6,1.9,6.2v22.7h9.8V79.5
|
||||||
|
C602,74.1,600.6,69.8,597.9,66.7z"/>
|
||||||
|
<path id="XMLID_258_" class="st0" d="M355.6,66L355.6,66c-3-2.5-7.1-3.8-12.3-3.8c-3.3,0-6.3,0.7-9.1,2.1
|
||||||
|
c-2.6,1.3-5.1,3.5-6.7,6.3l0.1,0.1l6.3,6c2.6-4.1,5.5-5.6,9.3-5.6c2.1,0,3.8,0.6,5.1,1.6c1.3,1.1,1.9,2.5,1.9,4.2v1.9
|
||||||
|
c-2.4-0.7-4.9-1.1-7.2-1.1c-4.9,0-8.9,1.2-11.8,3.4c-3,2.3-4.5,5.6-4.5,9.8c0,3.7,1.3,6.7,3.8,8.9c2.6,2.1,5.8,3.2,9.5,3.2
|
||||||
|
c3.7,0,7.3-1.5,10.4-4.1v3.2h9.7V77C360.2,72.2,358.7,68.5,355.6,66z M338.2,87.2c1.1-0.8,2.7-1.2,4.7-1.2
|
||||||
|
c2.4,0,4.9,0.5,7.5,1.4v3.8c-2.1,2-5,3-8.5,3c-1.7,0-3-0.4-3.9-1.1c-0.9-0.7-1.3-1.7-1.3-2.8C336.6,89,337.1,88,338.2,87.2z"/>
|
||||||
|
<path id="XMLID_255_" class="st0" d="M413.6,103c-15.8,0-28.6-12.8-28.6-28.6s12.8-28.6,28.6-28.6s28.6,12.8,28.6,28.6
|
||||||
|
S429.4,103,413.6,103z M413.6,55.8c-10.2,0-18.5,8.3-18.5,18.5s8.3,18.5,18.5,18.5s18.5-8.3,18.5-18.5S423.8,55.8,413.6,55.8z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
5
public/workers/add-attachments.worker.d.ts
vendored
5
public/workers/add-attachments.worker.d.ts
vendored
@@ -5,6 +5,7 @@ interface AddAttachmentsMessage {
|
|||||||
pdfBuffer: ArrayBuffer;
|
pdfBuffer: ArrayBuffer;
|
||||||
attachmentBuffers: ArrayBuffer[];
|
attachmentBuffers: ArrayBuffer[];
|
||||||
attachmentNames: string[];
|
attachmentNames: string[];
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddAttachmentsSuccessResponse {
|
interface AddAttachmentsSuccessResponse {
|
||||||
@@ -17,4 +18,6 @@ interface AddAttachmentsErrorResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddAttachmentsResponse = AddAttachmentsSuccessResponse | AddAttachmentsErrorResponse;
|
type AddAttachmentsResponse =
|
||||||
|
| AddAttachmentsSuccessResponse
|
||||||
|
| AddAttachmentsErrorResponse;
|
||||||
|
|||||||
@@ -1,13 +1,32 @@
|
|||||||
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
|
let cpdfLoaded = false;
|
||||||
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
|
|
||||||
|
function loadCpdf(cpdfUrl) {
|
||||||
|
if (cpdfLoaded) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof coherentpdf !== 'undefined') {
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
self.importScripts(cpdfUrl);
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Failed to load CoherentPDF: ' + error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function parsePageRange(rangeString, totalPages) {
|
function parsePageRange(rangeString, totalPages) {
|
||||||
const pages = new Set();
|
const pages = new Set();
|
||||||
const parts = rangeString.split(',').map(s => s.trim());
|
const parts = rangeString.split(',').map((s) => s.trim());
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (part.includes('-')) {
|
if (part.includes('-')) {
|
||||||
const [start, end] = part.split('-').map(s => parseInt(s.trim(), 10));
|
const [start, end] = part.split('-').map((s) => parseInt(s.trim(), 10));
|
||||||
if (isNaN(start) || isNaN(end)) continue;
|
if (isNaN(start) || isNaN(end)) continue;
|
||||||
for (let i = Math.max(1, start); i <= Math.min(totalPages, end); i++) {
|
for (let i = Math.max(1, start); i <= Math.min(totalPages, end); i++) {
|
||||||
pages.add(i);
|
pages.add(i);
|
||||||
@@ -23,7 +42,13 @@ function parsePageRange(rangeString, totalPages) {
|
|||||||
return Array.from(pages).sort((a, b) => a - b);
|
return Array.from(pages).sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNames, attachmentLevel, pageRange) {
|
function addAttachmentsToPDFInWorker(
|
||||||
|
pdfBuffer,
|
||||||
|
attachmentBuffers,
|
||||||
|
attachmentNames,
|
||||||
|
attachmentLevel,
|
||||||
|
pageRange
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const uint8Array = new Uint8Array(pdfBuffer);
|
const uint8Array = new Uint8Array(pdfBuffer);
|
||||||
|
|
||||||
@@ -33,18 +58,21 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error.message || error.toString();
|
const errorMsg = error.message || error.toString();
|
||||||
|
|
||||||
if (errorMsg.includes('Failed to read PDF') ||
|
if (
|
||||||
|
errorMsg.includes('Failed to read PDF') ||
|
||||||
errorMsg.includes('Could not read object') ||
|
errorMsg.includes('Could not read object') ||
|
||||||
errorMsg.includes('No /Root entry') ||
|
errorMsg.includes('No /Root entry') ||
|
||||||
errorMsg.includes('PDFError')) {
|
errorMsg.includes('PDFError')
|
||||||
|
) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'The PDF file has structural issues and cannot be processed. The file may be corrupted, incomplete, or created with non-standard tools. Please try:\n\n• Opening and re-saving the PDF in another PDF viewer\n• Using a different PDF file\n• Repairing the PDF with a PDF repair tool'
|
message:
|
||||||
|
'The PDF file has structural issues and cannot be processed. The file may be corrupted, incomplete, or created with non-standard tools. Please try:\n\n• Opening and re-saving the PDF in another PDF viewer\n• Using a different PDF file\n• Repairing the PDF with a PDF repair tool',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: `Failed to load PDF: ${errorMsg}`
|
message: `Failed to load PDF: ${errorMsg}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -57,7 +85,7 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
|
|||||||
if (!pageRange) {
|
if (!pageRange) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Page range is required for page-level attachments.'
|
message: 'Page range is required for page-level attachments.',
|
||||||
});
|
});
|
||||||
coherentpdf.deletePdf(pdf);
|
coherentpdf.deletePdf(pdf);
|
||||||
return;
|
return;
|
||||||
@@ -66,7 +94,7 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
|
|||||||
if (targetPages.length === 0) {
|
if (targetPages.length === 0) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'Invalid page range specified.'
|
message: 'Invalid page range specified.',
|
||||||
});
|
});
|
||||||
coherentpdf.deletePdf(pdf);
|
coherentpdf.deletePdf(pdf);
|
||||||
return;
|
return;
|
||||||
@@ -82,21 +110,25 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
|
|||||||
coherentpdf.attachFileFromMemory(attachmentData, attachmentName, pdf);
|
coherentpdf.attachFileFromMemory(attachmentData, attachmentName, pdf);
|
||||||
} else {
|
} else {
|
||||||
for (const pageNum of targetPages) {
|
for (const pageNum of targetPages) {
|
||||||
coherentpdf.attachFileToPageFromMemory(attachmentData, attachmentName, pdf, pageNum);
|
coherentpdf.attachFileToPageFromMemory(
|
||||||
|
attachmentData,
|
||||||
|
attachmentName,
|
||||||
|
pdf,
|
||||||
|
pageNum
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to attach file ${attachmentNames[i]}:`, error);
|
console.warn(`Failed to attach file ${attachmentNames[i]}:`, error);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: `Failed to attach file ${attachmentNames[i]}: ${error.message || error}`
|
message: `Failed to attach file ${attachmentNames[i]}: ${error.message || error}`,
|
||||||
});
|
});
|
||||||
coherentpdf.deletePdf(pdf);
|
coherentpdf.deletePdf(pdf);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the modified PDF
|
|
||||||
const modifiedBytes = coherentpdf.toMemory(pdf, false, false);
|
const modifiedBytes = coherentpdf.toMemory(pdf, false, false);
|
||||||
coherentpdf.deletePdf(pdf);
|
coherentpdf.deletePdf(pdf);
|
||||||
|
|
||||||
@@ -105,22 +137,46 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
|
|||||||
modifiedBytes.byteOffset + modifiedBytes.byteLength
|
modifiedBytes.byteOffset + modifiedBytes.byteLength
|
||||||
);
|
);
|
||||||
|
|
||||||
self.postMessage({
|
self.postMessage(
|
||||||
status: 'success',
|
{
|
||||||
modifiedPDF: buffer
|
status: 'success',
|
||||||
}, [buffer]);
|
modifiedPDF: buffer,
|
||||||
|
},
|
||||||
|
[buffer]
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: error instanceof Error
|
message:
|
||||||
? error.message
|
error instanceof Error
|
||||||
: 'Unknown error occurred while adding attachments.'
|
? error.message
|
||||||
|
: 'Unknown error occurred while adding attachments.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.onmessage = (e) => {
|
self.onmessage = async function (e) {
|
||||||
|
const { cpdfUrl } = e.data;
|
||||||
|
|
||||||
|
if (!cpdfUrl) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadCpdf(cpdfUrl);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.data.command === 'add-attachments') {
|
if (e.data.command === 'add-attachments') {
|
||||||
addAttachmentsToPDFInWorker(
|
addAttachmentsToPDFInWorker(
|
||||||
e.data.pdfBuffer,
|
e.data.pdfBuffer,
|
||||||
|
|||||||
17
public/workers/alternate-merge.worker.d.ts
vendored
17
public/workers/alternate-merge.worker.d.ts
vendored
@@ -1,23 +1,24 @@
|
|||||||
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
||||||
|
|
||||||
interface InterleaveFile {
|
interface InterleaveFile {
|
||||||
name: string;
|
name: string;
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InterleaveMessage {
|
interface InterleaveMessage {
|
||||||
command: 'interleave';
|
command: 'interleave';
|
||||||
files: InterleaveFile[];
|
files: InterleaveFile[];
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InterleaveSuccessResponse {
|
interface InterleaveSuccessResponse {
|
||||||
status: 'success';
|
status: 'success';
|
||||||
pdfBytes: ArrayBuffer;
|
pdfBytes: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InterleaveErrorResponse {
|
interface InterleaveErrorResponse {
|
||||||
status: 'error';
|
status: 'error';
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterleaveResponse = InterleaveSuccessResponse | InterleaveErrorResponse;
|
type InterleaveResponse = InterleaveSuccessResponse | InterleaveErrorResponse;
|
||||||
|
|||||||
@@ -1,64 +1,109 @@
|
|||||||
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
|
let cpdfLoaded = false;
|
||||||
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
|
|
||||||
|
|
||||||
self.onmessage = function (e) {
|
function loadCpdf(cpdfUrl) {
|
||||||
const { command, files } = e.data;
|
if (cpdfLoaded) return Promise.resolve();
|
||||||
|
|
||||||
if (command === 'interleave') {
|
return new Promise((resolve, reject) => {
|
||||||
interleavePDFs(files);
|
if (typeof coherentpdf !== 'undefined') {
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
self.importScripts(cpdfUrl);
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Failed to load CoherentPDF: ' + error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = async function (e) {
|
||||||
|
const { command, files, cpdfUrl } = e.data;
|
||||||
|
|
||||||
|
if (!cpdfUrl) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadCpdf(cpdfUrl);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === 'interleave') {
|
||||||
|
interleavePDFs(files);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function interleavePDFs(files) {
|
function interleavePDFs(files) {
|
||||||
try {
|
try {
|
||||||
const loadedPdfs = [];
|
const loadedPdfs = [];
|
||||||
const pageCounts = [];
|
const pageCounts = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const uint8Array = new Uint8Array(file.data);
|
const uint8Array = new Uint8Array(file.data);
|
||||||
const pdfDoc = coherentpdf.fromMemory(uint8Array, "");
|
const pdfDoc = coherentpdf.fromMemory(uint8Array, '');
|
||||||
loadedPdfs.push(pdfDoc);
|
loadedPdfs.push(pdfDoc);
|
||||||
pageCounts.push(coherentpdf.pages(pdfDoc));
|
pageCounts.push(coherentpdf.pages(pdfDoc));
|
||||||
}
|
|
||||||
|
|
||||||
if (loadedPdfs.length < 2) {
|
|
||||||
throw new Error('At least two PDF files are required for interleaving.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxPages = Math.max(...pageCounts);
|
|
||||||
|
|
||||||
const pdfsToMerge = [];
|
|
||||||
const rangesToMerge = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= maxPages; i++) {
|
|
||||||
for (let j = 0; j < loadedPdfs.length; j++) {
|
|
||||||
if (i <= pageCounts[j]) {
|
|
||||||
pdfsToMerge.push(loadedPdfs[j]);
|
|
||||||
rangesToMerge.push(coherentpdf.range(i, i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pdfsToMerge.length === 0) {
|
|
||||||
throw new Error('No valid pages to merge.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedPdf = coherentpdf.mergeSame(pdfsToMerge, true, true, rangesToMerge);
|
|
||||||
|
|
||||||
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
|
||||||
const buffer = mergedPdfBytes.buffer;
|
|
||||||
coherentpdf.deletePdf(mergedPdf);
|
|
||||||
loadedPdfs.forEach(pdf => coherentpdf.deletePdf(pdf));
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
status: 'success',
|
|
||||||
pdfBytes: buffer
|
|
||||||
}, [buffer]);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
self.postMessage({
|
|
||||||
status: 'error',
|
|
||||||
message: error.message || 'Unknown error during interleave merge'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loadedPdfs.length < 2) {
|
||||||
|
throw new Error('At least two PDF files are required for interleaving.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxPages = Math.max(...pageCounts);
|
||||||
|
|
||||||
|
const pdfsToMerge = [];
|
||||||
|
const rangesToMerge = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= maxPages; i++) {
|
||||||
|
for (let j = 0; j < loadedPdfs.length; j++) {
|
||||||
|
if (i <= pageCounts[j]) {
|
||||||
|
pdfsToMerge.push(loadedPdfs[j]);
|
||||||
|
rangesToMerge.push(coherentpdf.range(i, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdfsToMerge.length === 0) {
|
||||||
|
throw new Error('No valid pages to merge.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedPdf = coherentpdf.mergeSame(
|
||||||
|
pdfsToMerge,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
rangesToMerge
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
||||||
|
const buffer = mergedPdfBytes.buffer;
|
||||||
|
coherentpdf.deletePdf(mergedPdf);
|
||||||
|
loadedPdfs.forEach((pdf) => coherentpdf.deletePdf(pdf));
|
||||||
|
|
||||||
|
self.postMessage(
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
pdfBytes: buffer,
|
||||||
|
},
|
||||||
|
[buffer]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message || 'Unknown error during interleave merge',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
public/workers/edit-attachments.worker.d.ts
vendored
25
public/workers/edit-attachments.worker.d.ts
vendored
@@ -4,6 +4,7 @@ interface GetAttachmentsMessage {
|
|||||||
command: 'get-attachments';
|
command: 'get-attachments';
|
||||||
fileBuffer: ArrayBuffer;
|
fileBuffer: ArrayBuffer;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditAttachmentsMessage {
|
interface EditAttachmentsMessage {
|
||||||
@@ -11,13 +12,21 @@ interface EditAttachmentsMessage {
|
|||||||
fileBuffer: ArrayBuffer;
|
fileBuffer: ArrayBuffer;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
attachmentsToRemove: number[];
|
attachmentsToRemove: number[];
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditAttachmentsWorkerMessage = GetAttachmentsMessage | EditAttachmentsMessage;
|
type EditAttachmentsWorkerMessage =
|
||||||
|
| GetAttachmentsMessage
|
||||||
|
| EditAttachmentsMessage;
|
||||||
|
|
||||||
interface GetAttachmentsSuccessResponse {
|
interface GetAttachmentsSuccessResponse {
|
||||||
status: 'success';
|
status: 'success';
|
||||||
attachments: Array<{ index: number; name: string; page: number; data: ArrayBuffer }>;
|
attachments: Array<{
|
||||||
|
index: number;
|
||||||
|
name: string;
|
||||||
|
page: number;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}>;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +46,12 @@ interface EditAttachmentsErrorResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAttachmentsResponse = GetAttachmentsSuccessResponse | GetAttachmentsErrorResponse;
|
type GetAttachmentsResponse =
|
||||||
type EditAttachmentsResponse = EditAttachmentsSuccessResponse | EditAttachmentsErrorResponse;
|
| GetAttachmentsSuccessResponse
|
||||||
type EditAttachmentsWorkerResponse = GetAttachmentsResponse | EditAttachmentsResponse;
|
| GetAttachmentsErrorResponse;
|
||||||
|
type EditAttachmentsResponse =
|
||||||
|
| EditAttachmentsSuccessResponse
|
||||||
|
| EditAttachmentsErrorResponse;
|
||||||
|
type EditAttachmentsWorkerResponse =
|
||||||
|
| GetAttachmentsResponse
|
||||||
|
| EditAttachmentsResponse;
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
|
let cpdfLoaded = false;
|
||||||
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
|
|
||||||
|
function loadCpdf(cpdfUrl) {
|
||||||
|
if (cpdfLoaded) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof coherentpdf !== 'undefined') {
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
self.importScripts(cpdfUrl);
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Failed to load CoherentPDF: ' + error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
|
function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
|
||||||
try {
|
try {
|
||||||
@@ -11,7 +30,7 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`
|
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -23,7 +42,7 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
|
|||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
attachments: [],
|
attachments: [],
|
||||||
fileName: fileName
|
fileName: fileName,
|
||||||
});
|
});
|
||||||
coherentpdf.deletePdf(pdf);
|
coherentpdf.deletePdf(pdf);
|
||||||
return;
|
return;
|
||||||
@@ -37,13 +56,16 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
|
|||||||
const attachmentData = coherentpdf.getAttachmentData(i);
|
const attachmentData = coherentpdf.getAttachmentData(i);
|
||||||
|
|
||||||
const dataArray = new Uint8Array(attachmentData);
|
const dataArray = new Uint8Array(attachmentData);
|
||||||
const buffer = dataArray.buffer.slice(dataArray.byteOffset, dataArray.byteOffset + dataArray.byteLength);
|
const buffer = dataArray.buffer.slice(
|
||||||
|
dataArray.byteOffset,
|
||||||
|
dataArray.byteOffset + dataArray.byteLength
|
||||||
|
);
|
||||||
|
|
||||||
attachments.push({
|
attachments.push({
|
||||||
index: i,
|
index: i,
|
||||||
name: String(name),
|
name: String(name),
|
||||||
page: Number(page),
|
page: Number(page),
|
||||||
data: buffer
|
data: buffer,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to get attachment ${i} from ${fileName}:`, error);
|
console.warn(`Failed to get attachment ${i} from ${fileName}:`, error);
|
||||||
@@ -56,22 +78,27 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
|
|||||||
const response = {
|
const response = {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
attachments: attachments,
|
attachments: attachments,
|
||||||
fileName: fileName
|
fileName: fileName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const transferBuffers = attachments.map(att => att.data);
|
const transferBuffers = attachments.map((att) => att.data);
|
||||||
self.postMessage(response, transferBuffers);
|
self.postMessage(response, transferBuffers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: error instanceof Error
|
message:
|
||||||
? error.message
|
error instanceof Error
|
||||||
: 'Unknown error occurred during attachment listing.'
|
? error.message
|
||||||
|
: 'Unknown error occurred during attachment listing.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove) {
|
function editAttachmentsInPDFInWorker(
|
||||||
|
fileBuffer,
|
||||||
|
fileName,
|
||||||
|
attachmentsToRemove
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const uint8Array = new Uint8Array(fileBuffer);
|
const uint8Array = new Uint8Array(fileBuffer);
|
||||||
|
|
||||||
@@ -81,7 +108,7 @@ function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove)
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`
|
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -103,7 +130,7 @@ function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove)
|
|||||||
attachmentsToKeep.push({
|
attachmentsToKeep.push({
|
||||||
name: String(name),
|
name: String(name),
|
||||||
page: Number(page),
|
page: Number(page),
|
||||||
data: dataCopy
|
data: dataCopy,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,9 +141,18 @@ function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove)
|
|||||||
|
|
||||||
for (const attachment of attachmentsToKeep) {
|
for (const attachment of attachmentsToKeep) {
|
||||||
if (attachment.page === 0) {
|
if (attachment.page === 0) {
|
||||||
coherentpdf.attachFileFromMemory(attachment.data, attachment.name, pdf);
|
coherentpdf.attachFileFromMemory(
|
||||||
|
attachment.data,
|
||||||
|
attachment.name,
|
||||||
|
pdf
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
coherentpdf.attachFileToPageFromMemory(attachment.data, attachment.name, pdf, attachment.page);
|
coherentpdf.attachFileToPageFromMemory(
|
||||||
|
attachment.data,
|
||||||
|
attachment.name,
|
||||||
|
pdf,
|
||||||
|
attachment.page
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,29 +160,58 @@ function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove)
|
|||||||
const modifiedBytes = coherentpdf.toMemory(pdf, false, true);
|
const modifiedBytes = coherentpdf.toMemory(pdf, false, true);
|
||||||
coherentpdf.deletePdf(pdf);
|
coherentpdf.deletePdf(pdf);
|
||||||
|
|
||||||
const buffer = modifiedBytes.buffer.slice(modifiedBytes.byteOffset, modifiedBytes.byteOffset + modifiedBytes.byteLength);
|
const buffer = modifiedBytes.buffer.slice(
|
||||||
|
modifiedBytes.byteOffset,
|
||||||
|
modifiedBytes.byteOffset + modifiedBytes.byteLength
|
||||||
|
);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
modifiedPDF: buffer,
|
modifiedPDF: buffer,
|
||||||
fileName: fileName
|
fileName: fileName,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.postMessage(response, [response.modifiedPDF]);
|
self.postMessage(response, [response.modifiedPDF]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: error instanceof Error
|
message:
|
||||||
? error.message
|
error instanceof Error
|
||||||
: 'Unknown error occurred during attachment editing.'
|
? error.message
|
||||||
|
: 'Unknown error occurred during attachment editing.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.onmessage = (e) => {
|
self.onmessage = async function (e) {
|
||||||
|
const { cpdfUrl } = e.data;
|
||||||
|
|
||||||
|
if (!cpdfUrl) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadCpdf(cpdfUrl);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.data.command === 'get-attachments') {
|
if (e.data.command === 'get-attachments') {
|
||||||
getAttachmentsFromPDFInWorker(e.data.fileBuffer, e.data.fileName);
|
getAttachmentsFromPDFInWorker(e.data.fileBuffer, e.data.fileName);
|
||||||
} else if (e.data.command === 'edit-attachments') {
|
} else if (e.data.command === 'edit-attachments') {
|
||||||
editAttachmentsInPDFInWorker(e.data.fileBuffer, e.data.fileName, e.data.attachmentsToRemove);
|
editAttachmentsInPDFInWorker(
|
||||||
|
e.data.fileBuffer,
|
||||||
|
e.data.fileName,
|
||||||
|
e.data.attachmentsToRemove
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -4,6 +4,7 @@ interface ExtractAttachmentsMessage {
|
|||||||
command: 'extract-attachments';
|
command: 'extract-attachments';
|
||||||
fileBuffers: ArrayBuffer[];
|
fileBuffers: ArrayBuffer[];
|
||||||
fileNames: string[];
|
fileNames: string[];
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExtractAttachmentSuccessResponse {
|
interface ExtractAttachmentSuccessResponse {
|
||||||
@@ -16,4 +17,6 @@ interface ExtractAttachmentErrorResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtractAttachmentResponse = ExtractAttachmentSuccessResponse | ExtractAttachmentErrorResponse;
|
type ExtractAttachmentResponse =
|
||||||
|
| ExtractAttachmentSuccessResponse
|
||||||
|
| ExtractAttachmentErrorResponse;
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
|
let cpdfLoaded = false;
|
||||||
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
|
|
||||||
|
function loadCpdf(cpdfUrl) {
|
||||||
|
if (cpdfLoaded) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof coherentpdf !== 'undefined') {
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
self.importScripts(cpdfUrl);
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Failed to load CoherentPDF: ' + error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
|
function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
|
||||||
try {
|
try {
|
||||||
@@ -37,7 +56,7 @@ function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
|
|||||||
|
|
||||||
let uniqueName = attachmentName;
|
let uniqueName = attachmentName;
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
while (allAttachments.some(att => att.name === uniqueName)) {
|
while (allAttachments.some((att) => att.name === uniqueName)) {
|
||||||
const nameParts = attachmentName.split('.');
|
const nameParts = attachmentName.split('.');
|
||||||
if (nameParts.length > 1) {
|
if (nameParts.length > 1) {
|
||||||
const extension = nameParts.pop();
|
const extension = nameParts.pop();
|
||||||
@@ -56,10 +75,13 @@ function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
|
|||||||
|
|
||||||
allAttachments.push({
|
allAttachments.push({
|
||||||
name: uniqueName,
|
name: uniqueName,
|
||||||
data: attachmentData.buffer.slice(0)
|
data: attachmentData.buffer.slice(0),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Failed to extract attachment ${j} from ${fileName}:`, error);
|
console.warn(
|
||||||
|
`Failed to extract attachment ${j} from ${fileName}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,21 +92,21 @@ function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
|
|||||||
if (allAttachments.length === 0) {
|
if (allAttachments.length === 0) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: 'No attachments were found in the selected PDF(s).'
|
message: 'No attachments were found in the selected PDF(s).',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
attachments: []
|
attachments: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const transferBuffers = [];
|
const transferBuffers = [];
|
||||||
for (const attachment of allAttachments) {
|
for (const attachment of allAttachments) {
|
||||||
response.attachments.push({
|
response.attachments.push({
|
||||||
name: attachment.name,
|
name: attachment.name,
|
||||||
data: attachment.data
|
data: attachment.data,
|
||||||
});
|
});
|
||||||
transferBuffers.push(attachment.data);
|
transferBuffers.push(attachment.data);
|
||||||
}
|
}
|
||||||
@@ -93,14 +115,36 @@ function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: error instanceof Error
|
message:
|
||||||
? error.message
|
error instanceof Error
|
||||||
: 'Unknown error occurred during attachment extraction.'
|
? error.message
|
||||||
|
: 'Unknown error occurred during attachment extraction.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.onmessage = (e) => {
|
self.onmessage = async function (e) {
|
||||||
|
const { cpdfUrl } = e.data;
|
||||||
|
|
||||||
|
if (!cpdfUrl) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadCpdf(cpdfUrl);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.data.command === 'extract-attachments') {
|
if (e.data.command === 'extract-attachments') {
|
||||||
extractAttachmentsFromPDFsInWorker(e.data.fileBuffers, e.data.fileNames);
|
extractAttachmentsFromPDFsInWorker(e.data.fileBuffers, e.data.fileNames);
|
||||||
}
|
}
|
||||||
|
|||||||
2
public/workers/json-to-pdf.worker.d.ts
vendored
2
public/workers/json-to-pdf.worker.d.ts
vendored
@@ -4,6 +4,7 @@ interface ConvertJSONToPDFMessage {
|
|||||||
command: 'convert';
|
command: 'convert';
|
||||||
fileBuffers: ArrayBuffer[];
|
fileBuffers: ArrayBuffer[];
|
||||||
fileNames: string[];
|
fileNames: string[];
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JSONToPDFSuccessResponse {
|
interface JSONToPDFSuccessResponse {
|
||||||
@@ -17,4 +18,3 @@ interface JSONToPDFErrorResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JSONToPDFResponse = JSONToPDFSuccessResponse | JSONToPDFErrorResponse;
|
type JSONToPDFResponse = JSONToPDFSuccessResponse | JSONToPDFErrorResponse;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
|
let cpdfLoaded = false;
|
||||||
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
|
|
||||||
|
function loadCpdf(cpdfUrl) {
|
||||||
|
if (cpdfLoaded) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof coherentpdf !== 'undefined') {
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
self.importScripts(cpdfUrl);
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Failed to load CoherentPDF: ' + error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function convertJSONsToPDFInWorker(fileBuffers, fileNames) {
|
function convertJSONsToPDFInWorker(fileBuffers, fileNames) {
|
||||||
try {
|
try {
|
||||||
@@ -15,13 +34,12 @@ function convertJSONsToPDFInWorker(fileBuffers, fileNames) {
|
|||||||
try {
|
try {
|
||||||
pdf = coherentpdf.fromJSONMemory(uint8Array);
|
pdf = coherentpdf.fromJSONMemory(uint8Array);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error && error.message
|
const errorMsg =
|
||||||
? error.message
|
error && error.message ? error.message : 'Unknown error';
|
||||||
: 'Unknown error';
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to convert "${fileName}" to PDF. ` +
|
`Failed to convert "${fileName}" to PDF. ` +
|
||||||
`The JSON file must be in the format produced by cpdf's outputJSONMemory. ` +
|
`The JSON file must be in the format produced by cpdf's outputJSONMemory. ` +
|
||||||
`Error: ${errorMsg}`
|
`Error: ${errorMsg}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,9 +74,29 @@ function convertJSONsToPDFInWorker(fileBuffers, fileNames) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.onmessage = (e) => {
|
self.onmessage = async function (e) {
|
||||||
|
const { cpdfUrl } = e.data;
|
||||||
|
|
||||||
|
if (!cpdfUrl) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadCpdf(cpdfUrl);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.data.command === 'convert') {
|
if (e.data.command === 'convert') {
|
||||||
convertJSONsToPDFInWorker(e.data.fileBuffers, e.data.fileNames);
|
convertJSONsToPDFInWorker(e.data.fileBuffers, e.data.fileNames);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
31
public/workers/merge.worker.d.ts
vendored
31
public/workers/merge.worker.d.ts
vendored
@@ -1,33 +1,34 @@
|
|||||||
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
||||||
|
|
||||||
interface MergeJob {
|
interface MergeJob {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
rangeType: 'all' | 'specific' | 'single' | 'range';
|
rangeType: 'all' | 'specific' | 'single' | 'range';
|
||||||
rangeString?: string;
|
rangeString?: string;
|
||||||
pageIndex?: number;
|
pageIndex?: number;
|
||||||
startPage?: number;
|
startPage?: number;
|
||||||
endPage?: number;
|
endPage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MergeFile {
|
interface MergeFile {
|
||||||
name: string;
|
name: string;
|
||||||
data: ArrayBuffer;
|
data: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MergeMessage {
|
interface MergeMessage {
|
||||||
command: 'merge';
|
command: 'merge';
|
||||||
files: MergeFile[];
|
files: MergeFile[];
|
||||||
jobs: MergeJob[];
|
jobs: MergeJob[];
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MergeSuccessResponse {
|
interface MergeSuccessResponse {
|
||||||
status: 'success';
|
status: 'success';
|
||||||
pdfBytes: ArrayBuffer;
|
pdfBytes: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MergeErrorResponse {
|
interface MergeErrorResponse {
|
||||||
status: 'error';
|
status: 'error';
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MergeResponse = MergeSuccessResponse | MergeErrorResponse;
|
type MergeResponse = MergeSuccessResponse | MergeErrorResponse;
|
||||||
|
|||||||
@@ -1,71 +1,116 @@
|
|||||||
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
|
let cpdfLoaded = false;
|
||||||
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
|
|
||||||
|
|
||||||
self.onmessage = function (e) {
|
function loadCpdf(cpdfUrl) {
|
||||||
const { command, files, jobs } = e.data;
|
if (cpdfLoaded) return Promise.resolve();
|
||||||
|
|
||||||
if (command === 'merge') {
|
return new Promise((resolve, reject) => {
|
||||||
mergePDFs(files, jobs);
|
if (typeof coherentpdf !== 'undefined') {
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
self.importScripts(cpdfUrl);
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Failed to load CoherentPDF: ' + error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = async function (e) {
|
||||||
|
const { command, files, jobs, cpdfUrl } = e.data;
|
||||||
|
|
||||||
|
if (!cpdfUrl) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadCpdf(cpdfUrl);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === 'merge') {
|
||||||
|
mergePDFs(files, jobs);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function mergePDFs(files, jobs) {
|
function mergePDFs(files, jobs) {
|
||||||
try {
|
try {
|
||||||
const loadedPdfs = {};
|
const loadedPdfs = {};
|
||||||
const pdfsToMerge = [];
|
const pdfsToMerge = [];
|
||||||
const rangesToMerge = [];
|
const rangesToMerge = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const uint8Array = new Uint8Array(file.data);
|
const uint8Array = new Uint8Array(file.data);
|
||||||
const pdfDoc = coherentpdf.fromMemory(uint8Array, "");
|
const pdfDoc = coherentpdf.fromMemory(uint8Array, '');
|
||||||
loadedPdfs[file.name] = pdfDoc;
|
loadedPdfs[file.name] = pdfDoc;
|
||||||
}
|
|
||||||
|
|
||||||
for (const job of jobs) {
|
|
||||||
const sourcePdf = loadedPdfs[job.fileName];
|
|
||||||
if (!sourcePdf) continue;
|
|
||||||
|
|
||||||
let range;
|
|
||||||
if (job.rangeType === 'all') {
|
|
||||||
range = coherentpdf.all(sourcePdf);
|
|
||||||
} else if (job.rangeType === 'specific') {
|
|
||||||
if (coherentpdf.validatePagespec(job.rangeString)) {
|
|
||||||
range = coherentpdf.parsePagespec(sourcePdf, job.rangeString);
|
|
||||||
} else {
|
|
||||||
range = coherentpdf.all(sourcePdf);
|
|
||||||
}
|
|
||||||
} else if (job.rangeType === 'single') {
|
|
||||||
const pageNum = job.pageIndex + 1;
|
|
||||||
range = coherentpdf.range(pageNum, pageNum);
|
|
||||||
} else if (job.rangeType === 'range') {
|
|
||||||
range = coherentpdf.range(job.startPage, job.endPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
pdfsToMerge.push(sourcePdf);
|
|
||||||
rangesToMerge.push(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pdfsToMerge.length === 0) {
|
|
||||||
throw new Error('No valid files or pages to merge.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedPdf = coherentpdf.mergeSame(pdfsToMerge, true, true, rangesToMerge);
|
|
||||||
|
|
||||||
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
|
||||||
const buffer = mergedPdfBytes.buffer;
|
|
||||||
|
|
||||||
coherentpdf.deletePdf(mergedPdf);
|
|
||||||
Object.values(loadedPdfs).forEach(pdf => coherentpdf.deletePdf(pdf));
|
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
status: 'success',
|
|
||||||
pdfBytes: buffer
|
|
||||||
}, [buffer]);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
self.postMessage({
|
|
||||||
status: 'error',
|
|
||||||
message: error.message || 'Unknown error during merge'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
const sourcePdf = loadedPdfs[job.fileName];
|
||||||
|
if (!sourcePdf) continue;
|
||||||
|
|
||||||
|
let range;
|
||||||
|
if (job.rangeType === 'all') {
|
||||||
|
range = coherentpdf.all(sourcePdf);
|
||||||
|
} else if (job.rangeType === 'specific') {
|
||||||
|
if (coherentpdf.validatePagespec(job.rangeString)) {
|
||||||
|
range = coherentpdf.parsePagespec(sourcePdf, job.rangeString);
|
||||||
|
} else {
|
||||||
|
range = coherentpdf.all(sourcePdf);
|
||||||
|
}
|
||||||
|
} else if (job.rangeType === 'single') {
|
||||||
|
const pageNum = job.pageIndex + 1;
|
||||||
|
range = coherentpdf.range(pageNum, pageNum);
|
||||||
|
} else if (job.rangeType === 'range') {
|
||||||
|
range = coherentpdf.range(job.startPage, job.endPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfsToMerge.push(sourcePdf);
|
||||||
|
rangesToMerge.push(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdfsToMerge.length === 0) {
|
||||||
|
throw new Error('No valid files or pages to merge.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedPdf = coherentpdf.mergeSame(
|
||||||
|
pdfsToMerge,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
rangesToMerge
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
||||||
|
const buffer = mergedPdfBytes.buffer;
|
||||||
|
|
||||||
|
coherentpdf.deletePdf(mergedPdf);
|
||||||
|
Object.values(loadedPdfs).forEach((pdf) => coherentpdf.deletePdf(pdf));
|
||||||
|
|
||||||
|
self.postMessage(
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
pdfBytes: buffer,
|
||||||
|
},
|
||||||
|
[buffer]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message || 'Unknown error during merge',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
public/workers/pdf-to-json.worker.d.ts
vendored
2
public/workers/pdf-to-json.worker.d.ts
vendored
@@ -4,6 +4,7 @@ interface ConvertPDFToJSONMessage {
|
|||||||
command: 'convert';
|
command: 'convert';
|
||||||
fileBuffers: ArrayBuffer[];
|
fileBuffers: ArrayBuffer[];
|
||||||
fileNames: string[];
|
fileNames: string[];
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PDFToJSONSuccessResponse {
|
interface PDFToJSONSuccessResponse {
|
||||||
@@ -17,4 +18,3 @@ interface PDFToJSONErrorResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PDFToJSONResponse = PDFToJSONSuccessResponse | PDFToJSONErrorResponse;
|
type PDFToJSONResponse = PDFToJSONSuccessResponse | PDFToJSONErrorResponse;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
|
let cpdfLoaded = false;
|
||||||
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
|
|
||||||
|
function loadCpdf(cpdfUrl) {
|
||||||
|
if (cpdfLoaded) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof coherentpdf !== 'undefined') {
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
self.importScripts(cpdfUrl);
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Failed to load CoherentPDF: ' + error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function convertPDFsToJSONInWorker(fileBuffers, fileNames) {
|
function convertPDFsToJSONInWorker(fileBuffers, fileNames) {
|
||||||
try {
|
try {
|
||||||
@@ -12,8 +31,6 @@ function convertPDFsToJSONInWorker(fileBuffers, fileNames) {
|
|||||||
const uint8Array = new Uint8Array(buffer);
|
const uint8Array = new Uint8Array(buffer);
|
||||||
const pdf = coherentpdf.fromMemory(uint8Array, '');
|
const pdf = coherentpdf.fromMemory(uint8Array, '');
|
||||||
|
|
||||||
//TODO:@ALAM -> add options for users to select these settings
|
|
||||||
// parse_content: true, no_stream_data: false, decompress_streams: false
|
|
||||||
const jsonData = coherentpdf.outputJSONMemory(true, false, false, pdf);
|
const jsonData = coherentpdf.outputJSONMemory(true, false, false, pdf);
|
||||||
|
|
||||||
const jsonBuffer = jsonData.buffer.slice(0);
|
const jsonBuffer = jsonData.buffer.slice(0);
|
||||||
@@ -44,9 +61,29 @@ function convertPDFsToJSONInWorker(fileBuffers, fileNames) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.onmessage = (e) => {
|
self.onmessage = async function (e) {
|
||||||
|
const { cpdfUrl } = e.data;
|
||||||
|
|
||||||
|
if (!cpdfUrl) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadCpdf(cpdfUrl);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.data.command === 'convert') {
|
if (e.data.command === 'convert') {
|
||||||
convertPDFsToJSONInWorker(e.data.fileBuffers, e.data.fileNames);
|
convertPDFsToJSONInWorker(e.data.fileBuffers, e.data.fileNames);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
1
public/workers/table-of-contents.worker.d.ts
vendored
1
public/workers/table-of-contents.worker.d.ts
vendored
@@ -7,6 +7,7 @@ interface GenerateTOCMessage {
|
|||||||
fontSize: number;
|
fontSize: number;
|
||||||
fontFamily: number;
|
fontFamily: number;
|
||||||
addBookmark: boolean;
|
addBookmark: boolean;
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TOCSuccessResponse {
|
interface TOCSuccessResponse {
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
|
let cpdfLoaded = false;
|
||||||
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
|
|
||||||
|
function loadCpdf(cpdfUrl) {
|
||||||
|
if (cpdfLoaded) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof coherentpdf !== 'undefined') {
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
self.importScripts(cpdfUrl);
|
||||||
|
cpdfLoaded = true;
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Failed to load CoherentPDF: ' + error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function generateTableOfContentsInWorker(
|
function generateTableOfContentsInWorker(
|
||||||
pdfData,
|
pdfData,
|
||||||
@@ -49,7 +68,28 @@ function generateTableOfContentsInWorker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.onmessage = (e) => {
|
self.onmessage = async function (e) {
|
||||||
|
const { cpdfUrl } = e.data;
|
||||||
|
|
||||||
|
if (!cpdfUrl) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadCpdf(cpdfUrl);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.data.command === 'generate-toc') {
|
if (e.data.command === 'generate-toc') {
|
||||||
generateTableOfContentsInWorker(
|
generateTableOfContentsInWorker(
|
||||||
e.data.pdfData,
|
e.data.pdfData,
|
||||||
|
|||||||
@@ -1,75 +1,52 @@
|
|||||||
/**
|
import { PACKAGE_VERSIONS } from '../const/cdn-version';
|
||||||
* WASM CDN Configuration
|
import { WasmProvider } from '../utils/wasm-provider';
|
||||||
*
|
|
||||||
* Centralized configuration for loading WASM files from jsDelivr CDN or local paths.
|
|
||||||
* Supports environment-based toggling and automatic fallback mechanism.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const USE_CDN = import.meta.env.VITE_USE_CDN === 'true';
|
export type WasmPackage = 'ghostscript' | 'pymupdf' | 'cpdf';
|
||||||
import { CDN_URLS, PACKAGE_VERSIONS } from '../const/cdn-version';
|
|
||||||
|
|
||||||
const LOCAL_PATHS = {
|
export function getWasmBaseUrl(packageName: WasmPackage): string | undefined {
|
||||||
ghostscript: import.meta.env.BASE_URL + 'ghostscript-wasm/',
|
const userUrl = WasmProvider.getUrl(packageName);
|
||||||
pymupdf: import.meta.env.BASE_URL + 'pymupdf-wasm/',
|
if (userUrl) {
|
||||||
} as const;
|
console.log(
|
||||||
|
`[WASM Config] Using configured URL for ${packageName}: ${userUrl}`
|
||||||
|
);
|
||||||
|
return userUrl;
|
||||||
|
}
|
||||||
|
|
||||||
export type WasmPackage = 'ghostscript' | 'pymupdf';
|
console.warn(
|
||||||
|
`[WASM Config] No URL configured for ${packageName}. Feature unavailable.`
|
||||||
export function getWasmBaseUrl(packageName: WasmPackage): string {
|
);
|
||||||
if (USE_CDN) {
|
return undefined;
|
||||||
return CDN_URLS[packageName];
|
|
||||||
}
|
|
||||||
return LOCAL_PATHS[packageName];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWasmFallbackUrl(packageName: WasmPackage): string {
|
export function isWasmAvailable(packageName: WasmPackage): boolean {
|
||||||
return LOCAL_PATHS[packageName];
|
return WasmProvider.isConfigured(packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function isCdnEnabled(): boolean {
|
|
||||||
return USE_CDN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a file with automatic CDN → local fallback
|
|
||||||
* @param packageName - WASM package name
|
|
||||||
* @param fileName - File name relative to package base
|
|
||||||
* @returns Response object
|
|
||||||
*/
|
|
||||||
export async function fetchWasmFile(
|
export async function fetchWasmFile(
|
||||||
packageName: WasmPackage,
|
packageName: WasmPackage,
|
||||||
fileName: string
|
fileName: string
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const cdnUrl = CDN_URLS[packageName] + fileName;
|
const baseUrl = getWasmBaseUrl(packageName);
|
||||||
const localUrl = LOCAL_PATHS[packageName] + fileName;
|
|
||||||
|
|
||||||
if (USE_CDN) {
|
if (!baseUrl) {
|
||||||
try {
|
throw new Error(
|
||||||
console.log(`[WASM CDN] Fetching from CDN: ${cdnUrl}`);
|
`No URL configured for ${packageName}. Please configure it in WASM Settings.`
|
||||||
const response = await fetch(cdnUrl);
|
);
|
||||||
if (response.ok) {
|
}
|
||||||
return response;
|
|
||||||
}
|
|
||||||
console.warn(`[WASM CDN] CDN fetch failed with status ${response.status}, trying local fallback...`);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[WASM CDN] CDN fetch error:`, error, `- trying local fallback...`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(localUrl);
|
const url = baseUrl + fileName;
|
||||||
if (!response.ok) {
|
console.log(`[WASM] Fetching: ${url}`);
|
||||||
throw new Error(`Failed to fetch ${fileName}: HTTP ${response.status}`);
|
|
||||||
}
|
const response = await fetch(url);
|
||||||
return response;
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch ${fileName}: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// use this to debug
|
|
||||||
export function getWasmConfigInfo() {
|
export function getWasmConfigInfo() {
|
||||||
return {
|
return {
|
||||||
cdnEnabled: USE_CDN,
|
packageVersions: PACKAGE_VERSIONS,
|
||||||
packageVersions: PACKAGE_VERSIONS,
|
configuredProviders: WasmProvider.getAllProviders(),
|
||||||
cdnUrls: CDN_URLS,
|
};
|
||||||
localPaths: LOCAL_PATHS,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
export const PACKAGE_VERSIONS = {
|
export const PACKAGE_VERSIONS = {
|
||||||
ghostscript: '0.1.0',
|
ghostscript: '0.1.0',
|
||||||
pymupdf: '0.1.9',
|
pymupdf: '0.1.9',
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const CDN_URLS = {
|
|
||||||
ghostscript: `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@${PACKAGE_VERSIONS.ghostscript}/assets/`,
|
|
||||||
pymupdf: `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@${PACKAGE_VERSIONS.pymupdf}/assets/`,
|
|
||||||
} as const;
|
} as const;
|
||||||
@@ -3,349 +3,399 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||||
|
import {
|
||||||
|
showWasmRequiredDialog,
|
||||||
|
WasmProvider,
|
||||||
|
} from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/add-attachments.worker.js');
|
const worker = new Worker(
|
||||||
|
import.meta.env.BASE_URL + 'workers/add-attachments.worker.js'
|
||||||
|
);
|
||||||
|
|
||||||
const pageState: AddAttachmentState = {
|
const pageState: AddAttachmentState = {
|
||||||
file: null,
|
file: null,
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.pdfDoc = null;
|
pageState.pdfDoc = null;
|
||||||
pageState.attachments = [];
|
pageState.attachments = [];
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const attachmentFileList = document.getElementById('attachment-file-list');
|
const attachmentFileList = document.getElementById('attachment-file-list');
|
||||||
if (attachmentFileList) attachmentFileList.innerHTML = '';
|
if (attachmentFileList) attachmentFileList.innerHTML = '';
|
||||||
|
|
||||||
const attachmentInput = document.getElementById('attachment-files-input') as HTMLInputElement;
|
const attachmentInput = document.getElementById(
|
||||||
if (attachmentInput) attachmentInput.value = '';
|
'attachment-files-input'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
if (attachmentInput) attachmentInput.value = '';
|
||||||
|
|
||||||
const attachmentLevelOptions = document.getElementById('attachment-level-options');
|
const attachmentLevelOptions = document.getElementById(
|
||||||
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
'attachment-level-options'
|
||||||
|
);
|
||||||
|
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
||||||
|
|
||||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||||
if (pageRangeWrapper) pageRangeWrapper.classList.add('hidden');
|
if (pageRangeWrapper) pageRangeWrapper.classList.add('hidden');
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) processBtn.classList.add('hidden');
|
if (processBtn) processBtn.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
||||||
const documentRadio = document.querySelector('input[name="attachment-level"][value="document"]') as HTMLInputElement;
|
const documentRadio = document.querySelector(
|
||||||
if (documentRadio) documentRadio.checked = true;
|
'input[name="attachment-level"][value="document"]'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
if (documentRadio) documentRadio.checked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
worker.onmessage = function (e) {
|
worker.onmessage = function (e) {
|
||||||
const data = e.data;
|
const data = e.data;
|
||||||
|
|
||||||
if (data.status === 'success' && data.modifiedPDF !== undefined) {
|
if (data.status === 'success' && data.modifiedPDF !== undefined) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
|
const originalName =
|
||||||
downloadFile(
|
pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
|
||||||
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
downloadFile(
|
||||||
`${originalName}_with_attachments.pdf`
|
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
||||||
);
|
`${originalName}_with_attachments.pdf`
|
||||||
|
);
|
||||||
|
|
||||||
showAlert('Success', `${pageState.attachments.length} file(s) attached successfully.`, 'success', function () {
|
showAlert(
|
||||||
resetState();
|
'Success',
|
||||||
});
|
`${pageState.attachments.length} file(s) attached successfully.`,
|
||||||
} else if (data.status === 'error') {
|
'success',
|
||||||
hideLoader();
|
function () {
|
||||||
showAlert('Error', data.message || 'Unknown error occurred.');
|
resetState();
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', data.message || 'Unknown error occurred.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.onerror = function (error) {
|
worker.onerror = function (error) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
console.error('Worker error:', error);
|
console.error('Worker error:', error);
|
||||||
showAlert('Error', 'Worker error occurred. Check console for details.');
|
showAlert('Error', 'Worker error occurred. Check console for details.');
|
||||||
};
|
};
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||||
|
|
||||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||||
ignoreEncryption: true,
|
ignoreEncryption: true,
|
||||||
throwOnInvalidObject: false
|
throwOnInvalidObject: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageCount = pageState.pdfDoc.getPageCount();
|
const pageCount = pageState.pdfDoc.getPageCount();
|
||||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||||
|
|
||||||
const totalPagesSpan = document.getElementById('attachment-total-pages');
|
const totalPagesSpan = document.getElementById('attachment-total-pages');
|
||||||
if (totalPagesSpan) totalPagesSpan.textContent = pageCount.toString();
|
if (totalPagesSpan) totalPagesSpan.textContent = pageCount.toString();
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF:', error);
|
console.error('Error loading PDF:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Failed to load PDF file.');
|
showAlert('Error', 'Failed to load PDF file.');
|
||||||
resetState();
|
resetState();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAttachmentList() {
|
function updateAttachmentList() {
|
||||||
const attachmentFileList = document.getElementById('attachment-file-list');
|
const attachmentFileList = document.getElementById('attachment-file-list');
|
||||||
const attachmentLevelOptions = document.getElementById('attachment-level-options');
|
const attachmentLevelOptions = document.getElementById(
|
||||||
const processBtn = document.getElementById('process-btn');
|
'attachment-level-options'
|
||||||
|
);
|
||||||
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (!attachmentFileList) return;
|
if (!attachmentFileList) return;
|
||||||
|
|
||||||
attachmentFileList.innerHTML = '';
|
attachmentFileList.innerHTML = '';
|
||||||
|
|
||||||
pageState.attachments.forEach(function (file) {
|
pageState.attachments.forEach(function (file) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'flex justify-between items-center p-2 bg-gray-800 rounded-md text-white';
|
div.className =
|
||||||
|
'flex justify-between items-center p-2 bg-gray-800 rounded-md text-white';
|
||||||
|
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate text-sm';
|
nameSpan.className = 'truncate text-sm';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('span');
|
const sizeSpan = document.createElement('span');
|
||||||
sizeSpan.className = 'text-xs text-gray-400';
|
sizeSpan.className = 'text-xs text-gray-400';
|
||||||
sizeSpan.textContent = formatBytes(file.size);
|
sizeSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
div.append(nameSpan, sizeSpan);
|
div.append(nameSpan, sizeSpan);
|
||||||
attachmentFileList.appendChild(div);
|
attachmentFileList.appendChild(div);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pageState.attachments.length > 0) {
|
if (pageState.attachments.length > 0) {
|
||||||
if (attachmentLevelOptions) attachmentLevelOptions.classList.remove('hidden');
|
if (attachmentLevelOptions)
|
||||||
if (processBtn) processBtn.classList.remove('hidden');
|
attachmentLevelOptions.classList.remove('hidden');
|
||||||
} else {
|
if (processBtn) processBtn.classList.remove('hidden');
|
||||||
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
} else {
|
||||||
if (processBtn) processBtn.classList.add('hidden');
|
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
||||||
}
|
if (processBtn) processBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addAttachments() {
|
async function addAttachments() {
|
||||||
if (!pageState.file || !pageState.pdfDoc) {
|
if (!pageState.file || !pageState.pdfDoc) {
|
||||||
showAlert('Error', 'Please upload a PDF first.');
|
showAlert('Error', 'Please upload a PDF first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageState.attachments.length === 0) {
|
if (pageState.attachments.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one file to attach.');
|
showAlert('No Files', 'Please select at least one file to attach.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentLevel = (
|
// Check if CPDF is configured
|
||||||
document.querySelector('input[name="attachment-level"]:checked') as HTMLInputElement
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachmentLevel =
|
||||||
|
(
|
||||||
|
document.querySelector(
|
||||||
|
'input[name="attachment-level"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
)?.value || 'document';
|
)?.value || 'document';
|
||||||
|
|
||||||
let pageRange: string = '';
|
let pageRange: string = '';
|
||||||
|
|
||||||
if (attachmentLevel === 'page') {
|
if (attachmentLevel === 'page') {
|
||||||
const pageRangeInput = document.getElementById('attachment-page-range') as HTMLInputElement;
|
const pageRangeInput = document.getElementById(
|
||||||
pageRange = pageRangeInput?.value?.trim() || '';
|
'attachment-page-range'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
pageRange = pageRangeInput?.value?.trim() || '';
|
||||||
|
|
||||||
if (!pageRange) {
|
if (!pageRange) {
|
||||||
showAlert('Error', 'Please specify a page range for page-level attachments.');
|
showAlert(
|
||||||
return;
|
'Error',
|
||||||
}
|
'Please specify a page range for page-level attachments.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Embedding files into PDF...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pdfBuffer = await pageState.file.arrayBuffer();
|
||||||
|
|
||||||
|
const attachmentBuffers: ArrayBuffer[] = [];
|
||||||
|
const attachmentNames: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < pageState.attachments.length; i++) {
|
||||||
|
const file = pageState.attachments[i];
|
||||||
|
showLoader(
|
||||||
|
`Reading ${file.name} (${i + 1}/${pageState.attachments.length})...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileBuffer = await file.arrayBuffer();
|
||||||
|
attachmentBuffers.push(fileBuffer);
|
||||||
|
attachmentNames.push(file.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Embedding files into PDF...');
|
showLoader('Attaching files to PDF...');
|
||||||
|
|
||||||
try {
|
const message = {
|
||||||
const pdfBuffer = await pageState.file.arrayBuffer();
|
command: 'add-attachments',
|
||||||
|
pdfBuffer: pdfBuffer,
|
||||||
|
attachmentBuffers: attachmentBuffers,
|
||||||
|
attachmentNames: attachmentNames,
|
||||||
|
attachmentLevel: attachmentLevel,
|
||||||
|
pageRange: pageRange,
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
|
};
|
||||||
|
|
||||||
const attachmentBuffers: ArrayBuffer[] = [];
|
const transferables = [pdfBuffer, ...attachmentBuffers];
|
||||||
const attachmentNames: string[] = [];
|
worker.postMessage(message, transferables);
|
||||||
|
} catch (error: any) {
|
||||||
for (let i = 0; i < pageState.attachments.length; i++) {
|
console.error('Error attaching files:', error);
|
||||||
const file = pageState.attachments[i];
|
hideLoader();
|
||||||
showLoader(`Reading ${file.name} (${i + 1}/${pageState.attachments.length})...`);
|
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||||
|
}
|
||||||
const fileBuffer = await file.arrayBuffer();
|
|
||||||
attachmentBuffers.push(fileBuffer);
|
|
||||||
attachmentNames.push(file.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Attaching files to PDF...');
|
|
||||||
|
|
||||||
const message = {
|
|
||||||
command: 'add-attachments',
|
|
||||||
pdfBuffer: pdfBuffer,
|
|
||||||
attachmentBuffers: attachmentBuffers,
|
|
||||||
attachmentNames: attachmentNames,
|
|
||||||
attachmentLevel: attachmentLevel,
|
|
||||||
pageRange: pageRange
|
|
||||||
};
|
|
||||||
|
|
||||||
const transferables = [pdfBuffer, ...attachmentBuffers];
|
|
||||||
worker.postMessage(message, transferables);
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error attaching files:', error);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `Failed to attach files: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAttachmentSelect(files: FileList | null) {
|
function handleAttachmentSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
pageState.attachments = Array.from(files);
|
pageState.attachments = Array.from(files);
|
||||||
updateAttachmentList();
|
updateAttachmentList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const attachmentInput = document.getElementById('attachment-files-input') as HTMLInputElement;
|
const attachmentInput = document.getElementById(
|
||||||
const attachmentDropZone = document.getElementById('attachment-drop-zone');
|
'attachment-files-input'
|
||||||
const processBtn = document.getElementById('process-btn');
|
) as HTMLInputElement;
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const attachmentDropZone = document.getElementById('attachment-drop-zone');
|
||||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (fileInput && dropZone) {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.addEventListener('change', function (e) {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const pdfFiles = Array.from(files).filter(function (f) {
|
|
||||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
|
||||||
});
|
|
||||||
if (pdfFiles.length > 0) {
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
dataTransfer.items.add(pdfFiles[0]);
|
|
||||||
handleFileSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', function () {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attachmentInput && attachmentDropZone) {
|
|
||||||
attachmentInput.addEventListener('change', function (e) {
|
|
||||||
handleAttachmentSelect((e.target as HTMLInputElement).files);
|
|
||||||
});
|
|
||||||
|
|
||||||
attachmentDropZone.addEventListener('dragover', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
attachmentDropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
attachmentDropZone.addEventListener('dragleave', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
attachmentDropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
attachmentDropZone.addEventListener('drop', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
attachmentDropZone.classList.remove('bg-gray-700');
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files) {
|
|
||||||
handleAttachmentSelect(files);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
attachmentInput.addEventListener('click', function () {
|
|
||||||
attachmentInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
|
|
||||||
attachmentLevelRadios.forEach(function (radio) {
|
|
||||||
radio.addEventListener('change', function (e) {
|
|
||||||
const value = (e.target as HTMLInputElement).value;
|
|
||||||
if (value === 'page' && pageRangeWrapper) {
|
|
||||||
pageRangeWrapper.classList.remove('hidden');
|
|
||||||
} else if (pageRangeWrapper) {
|
|
||||||
pageRangeWrapper.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (processBtn) {
|
dropZone.addEventListener('dragover', function (e) {
|
||||||
processBtn.addEventListener('click', addAttachments);
|
e.preventDefault();
|
||||||
}
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const pdfFiles = Array.from(files).filter(function (f) {
|
||||||
|
return (
|
||||||
|
f.type === 'application/pdf' ||
|
||||||
|
f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (pdfFiles.length > 0) {
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.items.add(pdfFiles[0]);
|
||||||
|
handleFileSelect(dataTransfer.files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('click', function () {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachmentInput && attachmentDropZone) {
|
||||||
|
attachmentInput.addEventListener('change', function (e) {
|
||||||
|
handleAttachmentSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
|
attachmentDropZone.addEventListener('dragover', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
attachmentDropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
attachmentDropZone.addEventListener('dragleave', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
attachmentDropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
attachmentDropZone.addEventListener('drop', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
attachmentDropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files) {
|
||||||
|
handleAttachmentSelect(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attachmentInput.addEventListener('click', function () {
|
||||||
|
attachmentInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachmentLevelRadios = document.querySelectorAll(
|
||||||
|
'input[name="attachment-level"]'
|
||||||
|
);
|
||||||
|
attachmentLevelRadios.forEach(function (radio) {
|
||||||
|
radio.addEventListener('change', function (e) {
|
||||||
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
if (value === 'page' && pageRangeWrapper) {
|
||||||
|
pageRangeWrapper.classList.remove('hidden');
|
||||||
|
} else if (pageRangeWrapper) {
|
||||||
|
pageRangeWrapper.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', addAttachments);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,245 +3,278 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||||
|
import {
|
||||||
|
showWasmRequiredDialog,
|
||||||
|
WasmProvider,
|
||||||
|
} from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
const pageState: AlternateMergeState = {
|
const pageState: AlternateMergeState = {
|
||||||
files: [],
|
files: [],
|
||||||
pdfBytes: new Map(),
|
pdfBytes: new Map(),
|
||||||
pdfDocs: new Map(),
|
pdfDocs: new Map(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const alternateMergeWorker = new Worker(import.meta.env.BASE_URL + 'workers/alternate-merge.worker.js');
|
const alternateMergeWorker = new Worker(
|
||||||
|
import.meta.env.BASE_URL + 'workers/alternate-merge.worker.js'
|
||||||
|
);
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.files = [];
|
pageState.files = [];
|
||||||
pageState.pdfBytes.clear();
|
pageState.pdfBytes.clear();
|
||||||
pageState.pdfDocs.clear();
|
pageState.pdfDocs.clear();
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
if (fileList) fileList.innerHTML = '';
|
if (fileList) fileList.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
|
|
||||||
if (!fileDisplayArea || !fileList) return;
|
if (!fileDisplayArea || !fileList) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.files.length > 0) {
|
if (pageState.files.length > 0) {
|
||||||
// Show file count summary
|
// Show file count summary
|
||||||
const summaryDiv = document.createElement('div');
|
const summaryDiv = document.createElement('div');
|
||||||
summaryDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
summaryDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoSpan = document.createElement('span');
|
const infoSpan = document.createElement('span');
|
||||||
infoSpan.className = 'text-gray-200';
|
infoSpan.className = 'text-gray-200';
|
||||||
infoSpan.textContent = `${pageState.files.length} PDF files selected`;
|
infoSpan.textContent = `${pageState.files.length} PDF files selected`;
|
||||||
|
|
||||||
const clearBtn = document.createElement('button');
|
const clearBtn = document.createElement('button');
|
||||||
clearBtn.className = 'text-red-400 hover:text-red-300';
|
clearBtn.className = 'text-red-400 hover:text-red-300';
|
||||||
clearBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
clearBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
clearBtn.onclick = function () {
|
clearBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
summaryDiv.append(infoSpan, clearBtn);
|
summaryDiv.append(infoSpan, clearBtn);
|
||||||
fileDisplayArea.appendChild(summaryDiv);
|
fileDisplayArea.appendChild(summaryDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
// Load PDFs and populate list
|
// Load PDFs and populate list
|
||||||
showLoader('Loading PDF files...');
|
showLoader('Loading PDF files...');
|
||||||
fileList.innerHTML = '';
|
fileList.innerHTML = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const file of pageState.files) {
|
for (const file of pageState.files) {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
pageState.pdfBytes.set(file.name, arrayBuffer);
|
pageState.pdfBytes.set(file.name, arrayBuffer);
|
||||||
|
|
||||||
const bytesForPdfJs = arrayBuffer.slice(0);
|
const bytesForPdfJs = arrayBuffer.slice(0);
|
||||||
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
||||||
pageState.pdfDocs.set(file.name, pdfjsDoc);
|
pageState.pdfDocs.set(file.name, pdfjsDoc);
|
||||||
const pageCount = pdfjsDoc.numPages;
|
const pageCount = pdfjsDoc.numPages;
|
||||||
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
|
li.className =
|
||||||
li.dataset.fileName = file.name;
|
'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
|
||||||
|
li.dataset.fileName = file.name;
|
||||||
|
|
||||||
const infoDiv = document.createElement('div');
|
const infoDiv = document.createElement('div');
|
||||||
infoDiv.className = 'flex items-center gap-2 truncate flex-1';
|
infoDiv.className = 'flex items-center gap-2 truncate flex-1';
|
||||||
|
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate font-medium text-white';
|
nameSpan.className = 'truncate font-medium text-white';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('span');
|
const metaSpan = document.createElement('span');
|
||||||
metaSpan.className = 'text-sm text-gray-400 flex-shrink-0';
|
metaSpan.className = 'text-sm text-gray-400 flex-shrink-0';
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pageCount} pages`;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pageCount} pages`;
|
||||||
|
|
||||||
infoDiv.append(nameSpan, metaSpan);
|
infoDiv.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const dragHandle = document.createElement('div');
|
const dragHandle = document.createElement('div');
|
||||||
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded ml-2';
|
dragHandle.className =
|
||||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded ml-2';
|
||||||
|
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
||||||
|
|
||||||
li.append(infoDiv, dragHandle);
|
li.append(infoDiv, dragHandle);
|
||||||
fileList.appendChild(li);
|
fileList.appendChild(li);
|
||||||
}
|
}
|
||||||
|
|
||||||
Sortable.create(fileList, {
|
Sortable.create(fileList, {
|
||||||
handle: '.drag-handle',
|
handle: '.drag-handle',
|
||||||
animation: 150,
|
animation: 150,
|
||||||
});
|
});
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
if (toolOptions && pageState.files.length >= 2) {
|
if (toolOptions && pageState.files.length >= 2) {
|
||||||
toolOptions.classList.remove('hidden');
|
toolOptions.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDFs:', error);
|
console.error('Error loading PDFs:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Failed to load one or more PDF files.');
|
showAlert('Error', 'Failed to load one or more PDF files.');
|
||||||
resetState();
|
resetState();
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mixPages() {
|
async function mixPages() {
|
||||||
if (pageState.pdfBytes.size < 2) {
|
if (pageState.pdfBytes.size < 2) {
|
||||||
showAlert('Not Enough Files', 'Please upload at least two PDF files to alternate and mix.');
|
showAlert(
|
||||||
return;
|
'Not Enough Files',
|
||||||
|
'Please upload at least two PDF files to alternate and mix.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if CPDF is configured
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Alternating and mixing pages...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileList = document.getElementById('file-list');
|
||||||
|
if (!fileList) throw new Error('File list not found');
|
||||||
|
|
||||||
|
const sortedFileNames = Array.from(fileList.children)
|
||||||
|
.map(function (li) {
|
||||||
|
return (li as HTMLElement).dataset.fileName;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
interface InterleaveFile {
|
||||||
|
name: string;
|
||||||
|
data: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Alternating and mixing pages...');
|
const filesToMerge: InterleaveFile[] = [];
|
||||||
|
for (const name of sortedFileNames) {
|
||||||
try {
|
const bytes = pageState.pdfBytes.get(name);
|
||||||
const fileList = document.getElementById('file-list');
|
if (bytes) {
|
||||||
if (!fileList) throw new Error('File list not found');
|
filesToMerge.push({ name, data: bytes });
|
||||||
|
}
|
||||||
const sortedFileNames = Array.from(fileList.children).map(function (li) {
|
|
||||||
return (li as HTMLElement).dataset.fileName;
|
|
||||||
}).filter(Boolean) as string[];
|
|
||||||
|
|
||||||
interface InterleaveFile {
|
|
||||||
name: string;
|
|
||||||
data: ArrayBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filesToMerge: InterleaveFile[] = [];
|
|
||||||
for (const name of sortedFileNames) {
|
|
||||||
const bytes = pageState.pdfBytes.get(name);
|
|
||||||
if (bytes) {
|
|
||||||
filesToMerge.push({ name, data: bytes });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filesToMerge.length < 2) {
|
|
||||||
showAlert('Error', 'At least two valid PDFs are required.');
|
|
||||||
hideLoader();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = {
|
|
||||||
command: 'interleave',
|
|
||||||
files: filesToMerge
|
|
||||||
};
|
|
||||||
|
|
||||||
alternateMergeWorker.postMessage(message, filesToMerge.map(function (f) { return f.data; }));
|
|
||||||
|
|
||||||
alternateMergeWorker.onmessage = function (e: MessageEvent) {
|
|
||||||
hideLoader();
|
|
||||||
if (e.data.status === 'success') {
|
|
||||||
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
|
||||||
downloadFile(blob, 'alternated-mixed.pdf');
|
|
||||||
showAlert('Success', 'PDFs have been mixed successfully!', 'success', function () {
|
|
||||||
resetState();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('Worker interleave error:', e.data.message);
|
|
||||||
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
alternateMergeWorker.onerror = function (e) {
|
|
||||||
hideLoader();
|
|
||||||
console.error('Worker error:', e);
|
|
||||||
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Alternate Merge error:', e);
|
|
||||||
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filesToMerge.length < 2) {
|
||||||
|
showAlert('Error', 'At least two valid PDFs are required.');
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
command: 'interleave',
|
||||||
|
files: filesToMerge,
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
|
};
|
||||||
|
|
||||||
|
alternateMergeWorker.postMessage(
|
||||||
|
message,
|
||||||
|
filesToMerge.map(function (f) {
|
||||||
|
return f.data;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
alternateMergeWorker.onmessage = function (e: MessageEvent) {
|
||||||
|
hideLoader();
|
||||||
|
if (e.data.status === 'success') {
|
||||||
|
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
||||||
|
downloadFile(blob, 'alternated-mixed.pdf');
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'PDFs have been mixed successfully!',
|
||||||
|
'success',
|
||||||
|
function () {
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error('Worker interleave error:', e.data.message);
|
||||||
|
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
alternateMergeWorker.onerror = function (e) {
|
||||||
|
hideLoader();
|
||||||
|
console.error('Worker error:', e);
|
||||||
|
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Alternate Merge error:', e);
|
||||||
|
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const pdfFiles = Array.from(files).filter(function (f) {
|
const pdfFiles = Array.from(files).filter(function (f) {
|
||||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
return (
|
||||||
});
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
if (pdfFiles.length > 0) {
|
);
|
||||||
pageState.files = pdfFiles;
|
});
|
||||||
updateUI();
|
if (pdfFiles.length > 0) {
|
||||||
}
|
pageState.files = pdfFiles;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (fileInput && dropZone) {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.addEventListener('change', function (e) {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
dropZone.addEventListener('dragover', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
dropZone.classList.add('bg-gray-700');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', function (e) {
|
dropZone.addEventListener('dragleave', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.remove('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('drop', function (e) {
|
dropZone.addEventListener('drop', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.remove('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
const files = e.dataTransfer?.files;
|
const files = e.dataTransfer?.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
handleFileSelect(files);
|
handleFileSelect(files);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fileInput.addEventListener('click', function () {
|
fileInput.addEventListener('click', function () {
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
processBtn.addEventListener('click', mixPages);
|
processBtn.addEventListener('click', mixPages);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,336 +2,385 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
|
||||||
const EXTENSIONS = ['.cbz', '.cbr'];
|
const EXTENSIONS = ['.cbz', '.cbr'];
|
||||||
const TOOL_NAME = 'CBZ';
|
const TOOL_NAME = 'CBZ';
|
||||||
const ALL_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp', '.avif', '.jxl', '.heic', '.heif'];
|
const ALL_IMAGE_EXTENSIONS = [
|
||||||
|
'.jpg',
|
||||||
|
'.jpeg',
|
||||||
|
'.png',
|
||||||
|
'.gif',
|
||||||
|
'.bmp',
|
||||||
|
'.tiff',
|
||||||
|
'.tif',
|
||||||
|
'.webp',
|
||||||
|
'.avif',
|
||||||
|
'.jxl',
|
||||||
|
'.heic',
|
||||||
|
'.heif',
|
||||||
|
];
|
||||||
|
|
||||||
const IMAGE_SIGNATURES = {
|
const IMAGE_SIGNATURES = {
|
||||||
jpeg: [0xFF, 0xD8, 0xFF],
|
jpeg: [0xff, 0xd8, 0xff],
|
||||||
png: [0x89, 0x50, 0x4E, 0x47],
|
png: [0x89, 0x50, 0x4e, 0x47],
|
||||||
gif: [0x47, 0x49, 0x46],
|
gif: [0x47, 0x49, 0x46],
|
||||||
bmp: [0x42, 0x4D],
|
bmp: [0x42, 0x4d],
|
||||||
webp: [0x52, 0x49, 0x46, 0x46],
|
webp: [0x52, 0x49, 0x46, 0x46],
|
||||||
avif: [0x00, 0x00, 0x00],
|
avif: [0x00, 0x00, 0x00],
|
||||||
};
|
};
|
||||||
|
|
||||||
function matchesSignature(data: Uint8Array, signature: number[], offset = 0): boolean {
|
function matchesSignature(
|
||||||
for (let i = 0; i < signature.length; i++) {
|
data: Uint8Array,
|
||||||
if (data[offset + i] !== signature[i]) return false;
|
signature: number[],
|
||||||
}
|
offset = 0
|
||||||
return true;
|
): boolean {
|
||||||
|
for (let i = 0; i < signature.length; i++) {
|
||||||
|
if (data[offset + i] !== signature[i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectImageFormat(data: Uint8Array): 'jpeg' | 'png' | 'gif' | 'bmp' | 'webp' | 'avif' | 'unknown' {
|
function detectImageFormat(
|
||||||
if (data.length < 12) return 'unknown';
|
data: Uint8Array
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.jpeg)) return 'jpeg';
|
): 'jpeg' | 'png' | 'gif' | 'bmp' | 'webp' | 'avif' | 'unknown' {
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.png)) return 'png';
|
if (data.length < 12) return 'unknown';
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.gif)) return 'gif';
|
if (matchesSignature(data, IMAGE_SIGNATURES.jpeg)) return 'jpeg';
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.bmp)) return 'bmp';
|
if (matchesSignature(data, IMAGE_SIGNATURES.png)) return 'png';
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.webp) &&
|
if (matchesSignature(data, IMAGE_SIGNATURES.gif)) return 'gif';
|
||||||
data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50) {
|
if (matchesSignature(data, IMAGE_SIGNATURES.bmp)) return 'bmp';
|
||||||
return 'webp';
|
if (
|
||||||
|
matchesSignature(data, IMAGE_SIGNATURES.webp) &&
|
||||||
|
data[8] === 0x57 &&
|
||||||
|
data[9] === 0x45 &&
|
||||||
|
data[10] === 0x42 &&
|
||||||
|
data[11] === 0x50
|
||||||
|
) {
|
||||||
|
return 'webp';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data[4] === 0x66 &&
|
||||||
|
data[5] === 0x74 &&
|
||||||
|
data[6] === 0x79 &&
|
||||||
|
data[7] === 0x70
|
||||||
|
) {
|
||||||
|
const brand = String.fromCharCode(data[8], data[9], data[10], data[11]);
|
||||||
|
if (
|
||||||
|
brand === 'avif' ||
|
||||||
|
brand === 'avis' ||
|
||||||
|
brand === 'mif1' ||
|
||||||
|
brand === 'miaf'
|
||||||
|
) {
|
||||||
|
return 'avif';
|
||||||
}
|
}
|
||||||
if (data[4] === 0x66 && data[5] === 0x74 && data[6] === 0x79 && data[7] === 0x70) {
|
}
|
||||||
const brand = String.fromCharCode(data[8], data[9], data[10], data[11]);
|
return 'unknown';
|
||||||
if (brand === 'avif' || brand === 'avis' || brand === 'mif1' || brand === 'miaf') {
|
|
||||||
return 'avif';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'unknown';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCbzFile(filename: string): boolean {
|
function isCbzFile(filename: string): boolean {
|
||||||
return filename.toLowerCase().endsWith('.cbz');
|
return filename.toLowerCase().endsWith('.cbz');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertImageToPng(imageData: ArrayBuffer, filename: string): Promise<Blob> {
|
async function convertImageToPng(
|
||||||
return new Promise((resolve, reject) => {
|
imageData: ArrayBuffer,
|
||||||
const blob = new Blob([imageData]);
|
filename: string
|
||||||
const url = URL.createObjectURL(blob);
|
): Promise<Blob> {
|
||||||
const img = new Image();
|
return new Promise((resolve, reject) => {
|
||||||
img.onload = () => {
|
const blob = new Blob([imageData]);
|
||||||
const canvas = document.createElement('canvas');
|
const url = URL.createObjectURL(blob);
|
||||||
canvas.width = img.width;
|
const img = new Image();
|
||||||
canvas.height = img.height;
|
img.onload = () => {
|
||||||
const ctx = canvas.getContext('2d');
|
const canvas = document.createElement('canvas');
|
||||||
if (!ctx) {
|
canvas.width = img.width;
|
||||||
URL.revokeObjectURL(url);
|
canvas.height = img.height;
|
||||||
reject(new Error('Failed to get canvas context'));
|
const ctx = canvas.getContext('2d');
|
||||||
return;
|
if (!ctx) {
|
||||||
}
|
URL.revokeObjectURL(url);
|
||||||
ctx.drawImage(img, 0, 0);
|
reject(new Error('Failed to get canvas context'));
|
||||||
canvas.toBlob((pngBlob) => {
|
return;
|
||||||
URL.revokeObjectURL(url);
|
}
|
||||||
if (pngBlob) {
|
ctx.drawImage(img, 0, 0);
|
||||||
resolve(pngBlob);
|
canvas.toBlob((pngBlob) => {
|
||||||
} else {
|
URL.revokeObjectURL(url);
|
||||||
reject(new Error(`Failed to convert ${filename} to PNG`));
|
if (pngBlob) {
|
||||||
}
|
resolve(pngBlob);
|
||||||
}, 'image/png');
|
} else {
|
||||||
};
|
reject(new Error(`Failed to convert ${filename} to PNG`));
|
||||||
img.onerror = () => {
|
}
|
||||||
URL.revokeObjectURL(url);
|
}, 'image/png');
|
||||||
reject(new Error(`Failed to load image: ${filename}`));
|
};
|
||||||
};
|
img.onerror = () => {
|
||||||
img.src = url;
|
URL.revokeObjectURL(url);
|
||||||
});
|
reject(new Error(`Failed to load image: ${filename}`));
|
||||||
|
};
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertCbzToPdf(file: File): Promise<Blob> {
|
async function convertCbzToPdf(file: File): Promise<Blob> {
|
||||||
const zip = await JSZip.loadAsync(file);
|
const zip = await JSZip.loadAsync(file);
|
||||||
const pdfDoc = await PDFDocument.create();
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
|
||||||
const imageFiles = Object.keys(zip.files)
|
const imageFiles = Object.keys(zip.files)
|
||||||
.filter(name => {
|
.filter((name) => {
|
||||||
if (zip.files[name].dir) return false;
|
if (zip.files[name].dir) return false;
|
||||||
const ext = name.toLowerCase().substring(name.lastIndexOf('.'));
|
const ext = name.toLowerCase().substring(name.lastIndexOf('.'));
|
||||||
return ALL_IMAGE_EXTENSIONS.includes(ext);
|
return ALL_IMAGE_EXTENSIONS.includes(ext);
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
.sort((a, b) =>
|
||||||
|
a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
|
||||||
|
);
|
||||||
|
|
||||||
for (const filename of imageFiles) {
|
for (const filename of imageFiles) {
|
||||||
const zipEntry = zip.files[filename];
|
const zipEntry = zip.files[filename];
|
||||||
const imageData = await zipEntry.async('arraybuffer');
|
const imageData = await zipEntry.async('arraybuffer');
|
||||||
const dataArray = new Uint8Array(imageData);
|
const dataArray = new Uint8Array(imageData);
|
||||||
const actualFormat = detectImageFormat(dataArray);
|
const actualFormat = detectImageFormat(dataArray);
|
||||||
|
|
||||||
let imageBytes: Uint8Array;
|
let imageBytes: Uint8Array;
|
||||||
let embedMethod: 'png' | 'jpg';
|
let embedMethod: 'png' | 'jpg';
|
||||||
|
|
||||||
if (actualFormat === 'jpeg') {
|
if (actualFormat === 'jpeg') {
|
||||||
imageBytes = dataArray;
|
imageBytes = dataArray;
|
||||||
embedMethod = 'jpg';
|
embedMethod = 'jpg';
|
||||||
} else if (actualFormat === 'png') {
|
} else if (actualFormat === 'png') {
|
||||||
imageBytes = dataArray;
|
imageBytes = dataArray;
|
||||||
embedMethod = 'png';
|
embedMethod = 'png';
|
||||||
} else {
|
} else {
|
||||||
const pngBlob = await convertImageToPng(imageData, filename);
|
const pngBlob = await convertImageToPng(imageData, filename);
|
||||||
imageBytes = new Uint8Array(await pngBlob.arrayBuffer());
|
imageBytes = new Uint8Array(await pngBlob.arrayBuffer());
|
||||||
embedMethod = 'png';
|
embedMethod = 'png';
|
||||||
}
|
|
||||||
|
|
||||||
const image = embedMethod === 'png'
|
|
||||||
? await pdfDoc.embedPng(imageBytes)
|
|
||||||
: await pdfDoc.embedJpg(imageBytes);
|
|
||||||
const page = pdfDoc.addPage([image.width, image.height]);
|
|
||||||
page.drawImage(image, {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: image.width,
|
|
||||||
height: image.height,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const image =
|
||||||
return new Blob([pdfBytes.buffer as ArrayBuffer], { type: 'application/pdf' });
|
embedMethod === 'png'
|
||||||
|
? await pdfDoc.embedPng(imageBytes)
|
||||||
|
: await pdfDoc.embedJpg(imageBytes);
|
||||||
|
const page = pdfDoc.addPage([image.width, image.height]);
|
||||||
|
page.drawImage(image, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
return new Blob([pdfBytes.buffer as ArrayBuffer], {
|
||||||
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertCbrToPdf(file: File): Promise<Blob> {
|
async function convertCbrToPdf(file: File): Promise<Blob> {
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
const pymupdf = await loadPyMuPDF();
|
||||||
await pymupdf.load();
|
return await (pymupdf as any).convertToPdf(file, { filetype: 'cbz' });
|
||||||
return await pymupdf.convertToPdf(file, { filetype: 'cbz' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUI = async () => {
|
||||||
|
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||||
|
|
||||||
|
if (state.files.length > 0) {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
|
const file = state.files[index];
|
||||||
|
const fileDiv = document.createElement('div');
|
||||||
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
|
const infoContainer = document.createElement('div');
|
||||||
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('div');
|
||||||
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
|
const metaSpan = document.createElement('div');
|
||||||
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className =
|
||||||
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
createIcons({ icons });
|
||||||
|
fileControls.classList.remove('hidden');
|
||||||
|
processBtn.classList.remove('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
} else {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
processBtn.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateUI = async () => {
|
const resetState = () => {
|
||||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
const convertToPdf = async () => {
|
||||||
fileDisplayArea.innerHTML = '';
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
if (state.files.length === 1) {
|
||||||
const file = state.files[index];
|
const originalFile = state.files[0];
|
||||||
const fileDiv = document.createElement('div');
|
showLoader(`Converting ${originalFile.name}...`);
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
let pdfBlob: Blob;
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
if (isCbzFile(originalFile.name)) {
|
||||||
|
pdfBlob = await convertCbzToPdf(originalFile);
|
||||||
const nameSpan = document.createElement('div');
|
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
|
||||||
nameSpan.textContent = file.name;
|
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
|
||||||
removeBtn.onclick = () => {
|
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
fileControls.classList.remove('hidden');
|
|
||||||
processBtn.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
} else {
|
||||||
fileDisplayArea.innerHTML = '';
|
pdfBlob = await convertCbrToPdf(originalFile);
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
processBtn.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||||
state.files = [];
|
downloadFile(pdfBlob, fileName);
|
||||||
state.pdfDoc = null;
|
hideLoader();
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertToPdf = async () => {
|
showAlert(
|
||||||
try {
|
'Conversion Complete',
|
||||||
if (state.files.length === 0) {
|
`Successfully converted ${originalFile.name} to PDF.`,
|
||||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
'success',
|
||||||
return;
|
() => resetState()
|
||||||
}
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Converting files...');
|
||||||
|
const outputZip = new JSZip();
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const originalFile = state.files[0];
|
const file = state.files[i];
|
||||||
showLoader(`Converting ${originalFile.name}...`);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
let pdfBlob: Blob;
|
let pdfBlob: Blob;
|
||||||
if (isCbzFile(originalFile.name)) {
|
if (isCbzFile(file.name)) {
|
||||||
pdfBlob = await convertCbzToPdf(originalFile);
|
pdfBlob = await convertCbzToPdf(file);
|
||||||
} else {
|
} else {
|
||||||
pdfBlob = await convertCbrToPdf(originalFile);
|
pdfBlob = await convertCbrToPdf(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
downloadFile(pdfBlob, fileName);
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
hideLoader();
|
outputZip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${originalFile.name} to PDF.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showLoader('Converting files...');
|
|
||||||
const outputZip = new JSZip();
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
|
||||||
const file = state.files[i];
|
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
|
||||||
|
|
||||||
let pdfBlob: Blob;
|
|
||||||
if (isCbzFile(file.name)) {
|
|
||||||
pdfBlob = await convertCbzToPdf(file);
|
|
||||||
} else {
|
|
||||||
pdfBlob = await convertCbrToPdf(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
|
||||||
outputZip.file(`${baseName}.pdf`, pdfBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const zipBlob = await outputZip.generateAsync({ type: 'blob' });
|
|
||||||
downloadFile(zipBlob, 'comic-converted.zip');
|
|
||||||
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
const zipBlob = await outputZip.generateAsync({ type: 'blob' });
|
||||||
if (files && files.length > 0) {
|
downloadFile(zipBlob, 'comic-converted.zip');
|
||||||
state.files = [...state.files, ...Array.from(files)];
|
|
||||||
updateUI();
|
hideLoader();
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
state.files = [...state.files, ...Array.from(files)];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fileInput && dropZone) {
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
|
const name = f.name.toLowerCase();
|
||||||
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
|
});
|
||||||
|
if (validFiles.length > 0) {
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
|
handleFileSelect(dataTransfer.files);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
fileInput.addEventListener('click', () => {
|
||||||
fileInput.addEventListener('change', (e) => {
|
fileInput.value = '';
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
if (addMoreBtn) {
|
||||||
e.preventDefault();
|
addMoreBtn.addEventListener('click', () => {
|
||||||
dropZone.classList.add('bg-gray-700');
|
fileInput.click();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
if (clearFilesBtn) {
|
||||||
e.preventDefault();
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
dropZone.classList.remove('bg-gray-700');
|
resetState();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', convertToPdf);
|
||||||
dropZone.classList.remove('bg-gray-700');
|
}
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const validFiles = Array.from(files).filter(f => {
|
|
||||||
const name = f.name.toLowerCase();
|
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
|
||||||
});
|
|
||||||
if (validFiles.length > 0) {
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
|
||||||
handleFileSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
|
||||||
resetState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convertToPdf);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import {
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
import { PDFDocument } from 'pdf-lib';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
@@ -60,8 +61,8 @@ async function performCondenseCompression(
|
|||||||
removeThumbnails?: boolean;
|
removeThumbnails?: boolean;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
// Load PyMuPDF dynamically from user-provided URL
|
||||||
await pymupdf.load();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
const preset =
|
const preset =
|
||||||
CONDENSE_PRESETS[level as keyof typeof CONDENSE_PRESETS] ||
|
CONDENSE_PRESETS[level as keyof typeof CONDENSE_PRESETS] ||
|
||||||
@@ -390,6 +391,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check WASM availability for Condense mode
|
||||||
|
const algorithm = (
|
||||||
|
document.getElementById('compression-algorithm') as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
if (algorithm === 'condense' && !isPyMuPDFAvailable()) {
|
||||||
|
showWasmRequiredDialog('pymupdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
if (state.files.length === 1) {
|
||||||
const originalFile = state.files[0];
|
const originalFile = state.files[0];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { downloadFile } from '../utils/helpers';
|
import { downloadFile } from '../utils/helpers';
|
||||||
|
|
||||||
@@ -10,13 +12,11 @@ interface DeskewResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let selectedFiles: File[] = [];
|
let selectedFiles: File[] = [];
|
||||||
let pymupdf: PyMuPDF | null = null;
|
let pymupdf: any = null;
|
||||||
|
|
||||||
function initPyMuPDF(): PyMuPDF {
|
async function initPyMuPDF(): Promise<any> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = new PyMuPDF({
|
pymupdf = await loadPyMuPDF();
|
||||||
assetPath: import.meta.env.BASE_URL + 'pymupdf-wasm/',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return pymupdf;
|
return pymupdf;
|
||||||
}
|
}
|
||||||
@@ -137,6 +137,12 @@ async function processDeskew(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if PyMuPDF is configured
|
||||||
|
if (!isWasmAvailable('pymupdf')) {
|
||||||
|
showWasmRequiredDialog('pymupdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const thresholdSelect = document.getElementById(
|
const thresholdSelect = document.getElementById(
|
||||||
'deskew-threshold'
|
'deskew-threshold'
|
||||||
) as HTMLSelectElement;
|
) as HTMLSelectElement;
|
||||||
@@ -148,7 +154,7 @@ async function processDeskew(): Promise<void> {
|
|||||||
showLoader('Initializing PyMuPDF...');
|
showLoader('Initializing PyMuPDF...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pdf = initPyMuPDF();
|
const pdf = await initPyMuPDF();
|
||||||
await pdf.load();
|
await pdf.load();
|
||||||
|
|
||||||
for (const file of selectedFiles) {
|
for (const file of selectedFiles) {
|
||||||
|
|||||||
@@ -2,352 +2,396 @@ import { EditAttachmentState, AttachmentInfo } from '@/types';
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||||
|
import {
|
||||||
|
showWasmRequiredDialog,
|
||||||
|
WasmProvider,
|
||||||
|
} from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js');
|
const worker = new Worker(
|
||||||
|
import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js'
|
||||||
|
);
|
||||||
|
|
||||||
const pageState: EditAttachmentState = {
|
const pageState: EditAttachmentState = {
|
||||||
file: null,
|
file: null,
|
||||||
allAttachments: [],
|
allAttachments: [],
|
||||||
attachmentsToRemove: new Set(),
|
attachmentsToRemove: new Set(),
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.file = null;
|
pageState.file = null;
|
||||||
pageState.allAttachments = [];
|
pageState.allAttachments = [];
|
||||||
pageState.attachmentsToRemove.clear();
|
pageState.attachmentsToRemove.clear();
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const attachmentsList = document.getElementById('attachments-list');
|
const attachmentsList = document.getElementById('attachments-list');
|
||||||
if (attachmentsList) attachmentsList.innerHTML = '';
|
if (attachmentsList) attachmentsList.innerHTML = '';
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) processBtn.classList.add('hidden');
|
if (processBtn) processBtn.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
worker.onmessage = function (e) {
|
worker.onmessage = function (e) {
|
||||||
const data = e.data;
|
const data = e.data;
|
||||||
|
|
||||||
if (data.status === 'success' && data.attachments !== undefined) {
|
if (data.status === 'success' && data.attachments !== undefined) {
|
||||||
pageState.allAttachments = data.attachments.map(function (att: any) {
|
pageState.allAttachments = data.attachments.map(function (att: any) {
|
||||||
return {
|
return {
|
||||||
...att,
|
...att,
|
||||||
data: new Uint8Array(att.data)
|
data: new Uint8Array(att.data),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
displayAttachments(data.attachments);
|
displayAttachments(data.attachments);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
|
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
|
const originalName =
|
||||||
downloadFile(
|
pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
|
||||||
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
downloadFile(
|
||||||
`${originalName}_edited.pdf`
|
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
||||||
);
|
`${originalName}_edited.pdf`
|
||||||
|
);
|
||||||
|
|
||||||
showAlert('Success', 'Attachments updated successfully!', 'success', function () {
|
showAlert(
|
||||||
resetState();
|
'Success',
|
||||||
});
|
'Attachments updated successfully!',
|
||||||
} else if (data.status === 'error') {
|
'success',
|
||||||
hideLoader();
|
function () {
|
||||||
showAlert('Error', data.message || 'Unknown error occurred.');
|
resetState();
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', data.message || 'Unknown error occurred.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.onerror = function (error) {
|
worker.onerror = function (error) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
console.error('Worker error:', error);
|
console.error('Worker error:', error);
|
||||||
showAlert('Error', 'Worker error occurred. Check console for details.');
|
showAlert('Error', 'Worker error occurred. Check console for details.');
|
||||||
};
|
};
|
||||||
|
|
||||||
function displayAttachments(attachments: AttachmentInfo[]) {
|
function displayAttachments(attachments: AttachmentInfo[]) {
|
||||||
const attachmentsList = document.getElementById('attachments-list');
|
const attachmentsList = document.getElementById('attachments-list');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (!attachmentsList) return;
|
if (!attachmentsList) return;
|
||||||
|
|
||||||
attachmentsList.innerHTML = '';
|
attachmentsList.innerHTML = '';
|
||||||
|
|
||||||
if (attachments.length === 0) {
|
if (attachments.length === 0) {
|
||||||
const noAttachments = document.createElement('p');
|
const noAttachments = document.createElement('p');
|
||||||
noAttachments.className = 'text-gray-400 text-center py-4';
|
noAttachments.className = 'text-gray-400 text-center py-4';
|
||||||
noAttachments.textContent = 'No attachments found in this PDF.';
|
noAttachments.textContent = 'No attachments found in this PDF.';
|
||||||
attachmentsList.appendChild(noAttachments);
|
attachmentsList.appendChild(noAttachments);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controls container
|
||||||
|
const controlsContainer = document.createElement('div');
|
||||||
|
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
|
||||||
|
|
||||||
|
const removeAllBtn = document.createElement('button');
|
||||||
|
removeAllBtn.className =
|
||||||
|
'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
|
||||||
|
removeAllBtn.textContent = 'Remove All Attachments';
|
||||||
|
removeAllBtn.onclick = function () {
|
||||||
|
if (pageState.allAttachments.length === 0) return;
|
||||||
|
|
||||||
|
const allSelected = pageState.allAttachments.every(function (attachment) {
|
||||||
|
return pageState.attachmentsToRemove.has(attachment.index);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allSelected) {
|
||||||
|
pageState.allAttachments.forEach(function (attachment) {
|
||||||
|
pageState.attachmentsToRemove.delete(attachment.index);
|
||||||
|
const element = document.querySelector(
|
||||||
|
`[data-attachment-index="${attachment.index}"]`
|
||||||
|
);
|
||||||
|
if (element) {
|
||||||
|
element.classList.remove('opacity-50', 'line-through');
|
||||||
|
const btn = element.querySelector('button');
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.remove('bg-gray-600');
|
||||||
|
btn.classList.add('bg-red-600');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
removeAllBtn.textContent = 'Remove All Attachments';
|
||||||
|
} else {
|
||||||
|
pageState.allAttachments.forEach(function (attachment) {
|
||||||
|
pageState.attachmentsToRemove.add(attachment.index);
|
||||||
|
const element = document.querySelector(
|
||||||
|
`[data-attachment-index="${attachment.index}"]`
|
||||||
|
);
|
||||||
|
if (element) {
|
||||||
|
element.classList.add('opacity-50', 'line-through');
|
||||||
|
const btn = element.querySelector('button');
|
||||||
|
if (btn) {
|
||||||
|
btn.classList.add('bg-gray-600');
|
||||||
|
btn.classList.remove('bg-red-600');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
removeAllBtn.textContent = 'Deselect All';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
controlsContainer.appendChild(removeAllBtn);
|
||||||
|
attachmentsList.appendChild(controlsContainer);
|
||||||
|
|
||||||
|
// Attachment items
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
const attachmentDiv = document.createElement('div');
|
||||||
|
attachmentDiv.className =
|
||||||
|
'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
|
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
|
||||||
|
|
||||||
|
const infoDiv = document.createElement('div');
|
||||||
|
infoDiv.className = 'flex-1';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('span');
|
||||||
|
nameSpan.className = 'text-white font-medium block';
|
||||||
|
nameSpan.textContent = attachment.name;
|
||||||
|
|
||||||
|
const levelSpan = document.createElement('span');
|
||||||
|
levelSpan.className = 'text-gray-400 text-sm block';
|
||||||
|
if (attachment.page === 0) {
|
||||||
|
levelSpan.textContent = 'Document-level attachment';
|
||||||
|
} else {
|
||||||
|
levelSpan.textContent = `Page ${attachment.page} attachment`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Controls container
|
infoDiv.append(nameSpan, levelSpan);
|
||||||
const controlsContainer = document.createElement('div');
|
|
||||||
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
|
|
||||||
|
|
||||||
const removeAllBtn = document.createElement('button');
|
const actionsDiv = document.createElement('div');
|
||||||
removeAllBtn.className = 'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
|
actionsDiv.className = 'flex items-center gap-2';
|
||||||
removeAllBtn.textContent = 'Remove All Attachments';
|
|
||||||
removeAllBtn.onclick = function () {
|
|
||||||
if (pageState.allAttachments.length === 0) return;
|
|
||||||
|
|
||||||
const allSelected = pageState.allAttachments.every(function (attachment) {
|
const removeBtn = document.createElement('button');
|
||||||
return pageState.attachmentsToRemove.has(attachment.index);
|
removeBtn.className = `${pageState.attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
|
||||||
});
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
|
removeBtn.title = 'Remove attachment';
|
||||||
|
removeBtn.onclick = function () {
|
||||||
|
if (pageState.attachmentsToRemove.has(attachment.index)) {
|
||||||
|
pageState.attachmentsToRemove.delete(attachment.index);
|
||||||
|
attachmentDiv.classList.remove('opacity-50', 'line-through');
|
||||||
|
removeBtn.classList.remove('bg-gray-600');
|
||||||
|
removeBtn.classList.add('bg-red-600');
|
||||||
|
} else {
|
||||||
|
pageState.attachmentsToRemove.add(attachment.index);
|
||||||
|
attachmentDiv.classList.add('opacity-50', 'line-through');
|
||||||
|
removeBtn.classList.add('bg-gray-600');
|
||||||
|
removeBtn.classList.remove('bg-red-600');
|
||||||
|
}
|
||||||
|
|
||||||
if (allSelected) {
|
const allSelected = pageState.allAttachments.every(function (att) {
|
||||||
pageState.allAttachments.forEach(function (attachment) {
|
return pageState.attachmentsToRemove.has(att.index);
|
||||||
pageState.attachmentsToRemove.delete(attachment.index);
|
});
|
||||||
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
|
removeAllBtn.textContent = allSelected
|
||||||
if (element) {
|
? 'Deselect All'
|
||||||
element.classList.remove('opacity-50', 'line-through');
|
: 'Remove All Attachments';
|
||||||
const btn = element.querySelector('button');
|
|
||||||
if (btn) {
|
|
||||||
btn.classList.remove('bg-gray-600');
|
|
||||||
btn.classList.add('bg-red-600');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
removeAllBtn.textContent = 'Remove All Attachments';
|
|
||||||
} else {
|
|
||||||
pageState.allAttachments.forEach(function (attachment) {
|
|
||||||
pageState.attachmentsToRemove.add(attachment.index);
|
|
||||||
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
|
|
||||||
if (element) {
|
|
||||||
element.classList.add('opacity-50', 'line-through');
|
|
||||||
const btn = element.querySelector('button');
|
|
||||||
if (btn) {
|
|
||||||
btn.classList.add('bg-gray-600');
|
|
||||||
btn.classList.remove('bg-red-600');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
removeAllBtn.textContent = 'Deselect All';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
controlsContainer.appendChild(removeAllBtn);
|
actionsDiv.append(removeBtn);
|
||||||
attachmentsList.appendChild(controlsContainer);
|
attachmentDiv.append(infoDiv, actionsDiv);
|
||||||
|
attachmentsList.appendChild(attachmentDiv);
|
||||||
|
}
|
||||||
|
|
||||||
// Attachment items
|
createIcons({ icons });
|
||||||
for (const attachment of attachments) {
|
|
||||||
const attachmentDiv = document.createElement('div');
|
|
||||||
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
|
||||||
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
|
|
||||||
|
|
||||||
const infoDiv = document.createElement('div');
|
if (processBtn) processBtn.classList.remove('hidden');
|
||||||
infoDiv.className = 'flex-1';
|
|
||||||
|
|
||||||
const nameSpan = document.createElement('span');
|
|
||||||
nameSpan.className = 'text-white font-medium block';
|
|
||||||
nameSpan.textContent = attachment.name;
|
|
||||||
|
|
||||||
const levelSpan = document.createElement('span');
|
|
||||||
levelSpan.className = 'text-gray-400 text-sm block';
|
|
||||||
if (attachment.page === 0) {
|
|
||||||
levelSpan.textContent = 'Document-level attachment';
|
|
||||||
} else {
|
|
||||||
levelSpan.textContent = `Page ${attachment.page} attachment`;
|
|
||||||
}
|
|
||||||
|
|
||||||
infoDiv.append(nameSpan, levelSpan);
|
|
||||||
|
|
||||||
const actionsDiv = document.createElement('div');
|
|
||||||
actionsDiv.className = 'flex items-center gap-2';
|
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
|
||||||
removeBtn.className = `${pageState.attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
|
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
|
||||||
removeBtn.title = 'Remove attachment';
|
|
||||||
removeBtn.onclick = function () {
|
|
||||||
if (pageState.attachmentsToRemove.has(attachment.index)) {
|
|
||||||
pageState.attachmentsToRemove.delete(attachment.index);
|
|
||||||
attachmentDiv.classList.remove('opacity-50', 'line-through');
|
|
||||||
removeBtn.classList.remove('bg-gray-600');
|
|
||||||
removeBtn.classList.add('bg-red-600');
|
|
||||||
} else {
|
|
||||||
pageState.attachmentsToRemove.add(attachment.index);
|
|
||||||
attachmentDiv.classList.add('opacity-50', 'line-through');
|
|
||||||
removeBtn.classList.add('bg-gray-600');
|
|
||||||
removeBtn.classList.remove('bg-red-600');
|
|
||||||
}
|
|
||||||
|
|
||||||
const allSelected = pageState.allAttachments.every(function (att) {
|
|
||||||
return pageState.attachmentsToRemove.has(att.index);
|
|
||||||
});
|
|
||||||
removeAllBtn.textContent = allSelected ? 'Deselect All' : 'Remove All Attachments';
|
|
||||||
};
|
|
||||||
|
|
||||||
actionsDiv.append(removeBtn);
|
|
||||||
attachmentDiv.append(infoDiv, actionsDiv);
|
|
||||||
attachmentsList.appendChild(attachmentDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
|
|
||||||
if (processBtn) processBtn.classList.remove('hidden');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAttachments() {
|
async function loadAttachments() {
|
||||||
if (!pageState.file) return;
|
if (!pageState.file) return;
|
||||||
|
|
||||||
showLoader('Loading attachments...');
|
showLoader('Loading attachments...');
|
||||||
|
|
||||||
try {
|
// Check if CPDF is configured
|
||||||
const fileBuffer = await pageState.file.arrayBuffer();
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const message = {
|
try {
|
||||||
command: 'get-attachments',
|
const fileBuffer = await pageState.file.arrayBuffer();
|
||||||
fileBuffer: fileBuffer,
|
|
||||||
fileName: pageState.file.name
|
|
||||||
};
|
|
||||||
|
|
||||||
worker.postMessage(message, [fileBuffer]);
|
const message = {
|
||||||
} catch (error) {
|
command: 'get-attachments',
|
||||||
console.error('Error loading attachments:', error);
|
fileBuffer: fileBuffer,
|
||||||
hideLoader();
|
fileName: pageState.file.name,
|
||||||
showAlert('Error', 'Failed to load attachments from PDF.');
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
}
|
};
|
||||||
|
|
||||||
|
worker.postMessage(message, [fileBuffer]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading attachments:', error);
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', 'Failed to load attachments from PDF.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveChanges() {
|
async function saveChanges() {
|
||||||
if (!pageState.file) {
|
if (!pageState.file) {
|
||||||
showAlert('Error', 'No PDF file loaded.');
|
showAlert('Error', 'No PDF file loaded.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageState.attachmentsToRemove.size === 0) {
|
if (pageState.attachmentsToRemove.size === 0) {
|
||||||
showAlert('No Changes', 'No attachments selected for removal.');
|
showAlert('No Changes', 'No attachments selected for removal.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Processing attachments...');
|
showLoader('Processing attachments...');
|
||||||
|
|
||||||
try {
|
// Check if CPDF is configured (double check)
|
||||||
const fileBuffer = await pageState.file.arrayBuffer();
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const message = {
|
try {
|
||||||
command: 'edit-attachments',
|
const fileBuffer = await pageState.file.arrayBuffer();
|
||||||
fileBuffer: fileBuffer,
|
|
||||||
fileName: pageState.file.name,
|
|
||||||
attachmentsToRemove: Array.from(pageState.attachmentsToRemove)
|
|
||||||
};
|
|
||||||
|
|
||||||
worker.postMessage(message, [fileBuffer]);
|
const message = {
|
||||||
} catch (error) {
|
command: 'edit-attachments',
|
||||||
console.error('Error editing attachments:', error);
|
fileBuffer: fileBuffer,
|
||||||
hideLoader();
|
fileName: pageState.file.name,
|
||||||
showAlert('Error', 'Failed to edit attachments.');
|
attachmentsToRemove: Array.from(pageState.attachmentsToRemove),
|
||||||
}
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage(message, [fileBuffer]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error editing attachments:', error);
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', 'Failed to edit attachments.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.file) {
|
if (pageState.file) {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = pageState.file.name;
|
nameSpan.textContent = pageState.file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
|
|
||||||
await loadAttachments();
|
await loadAttachments();
|
||||||
} else {
|
} else {
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
if (
|
||||||
pageState.file = file;
|
file.type === 'application/pdf' ||
|
||||||
updateUI();
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
}
|
) {
|
||||||
|
pageState.file = file;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInput && dropZone) {
|
||||||
|
fileInput.addEventListener('change', function (e) {
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragover', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const pdfFiles = Array.from(files).filter(function (f) {
|
||||||
|
return (
|
||||||
|
f.type === 'application/pdf' ||
|
||||||
|
f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
if (pdfFiles.length > 0) {
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.items.add(pdfFiles[0]);
|
||||||
|
handleFileSelect(dataTransfer.files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
fileInput.addEventListener('click', function () {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.value = '';
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', saveChanges);
|
||||||
dropZone.classList.add('bg-gray-700');
|
}
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const pdfFiles = Array.from(files).filter(function (f) {
|
|
||||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
|
||||||
});
|
|
||||||
if (pdfFiles.length > 0) {
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
dataTransfer.items.add(pdfFiles[0]);
|
|
||||||
handleFileSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', function () {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', saveChanges);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { parseEmailFile, renderEmailToHtml } from './email-to-pdf.js';
|
import { parseEmailFile, renderEmailToHtml } from './email-to-pdf.js';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
const EXTENSIONS = ['.eml', '.msg'];
|
const EXTENSIONS = ['.eml', '.msg'];
|
||||||
const TOOL_NAME = 'Email';
|
const TOOL_NAME = 'Email';
|
||||||
@@ -109,8 +110,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const includeAttachments = includeAttachmentsCheckbox?.checked ?? true;
|
const includeAttachments = includeAttachmentsCheckbox?.checked ?? true;
|
||||||
|
|
||||||
showLoader('Loading PDF engine...');
|
showLoader('Loading PDF engine...');
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
const pymupdf = await loadPyMuPDF();
|
||||||
await pymupdf.load();
|
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
if (state.files.length === 1) {
|
||||||
const originalFile = state.files[0];
|
const originalFile = state.files[0];
|
||||||
|
|||||||
@@ -2,205 +2,216 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
const FILETYPE = 'epub';
|
const FILETYPE = 'epub';
|
||||||
const EXTENSIONS = ['.epub'];
|
const EXTENSIONS = ['.epub'];
|
||||||
const TOOL_NAME = 'EPUB';
|
const TOOL_NAME = 'EPUB';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertOptions = document.getElementById('convert-options');
|
||||||
|
|
||||||
|
// ... (existing listeners)
|
||||||
|
|
||||||
|
const updateUI = async () => {
|
||||||
|
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||||
|
|
||||||
|
if (state.files.length > 0) {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
|
const file = state.files[index];
|
||||||
|
const fileDiv = document.createElement('div');
|
||||||
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
|
const infoContainer = document.createElement('div');
|
||||||
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('div');
|
||||||
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
|
const metaSpan = document.createElement('div');
|
||||||
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className =
|
||||||
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
createIcons({ icons });
|
||||||
|
fileControls.classList.remove('hidden');
|
||||||
|
if (convertOptions) convertOptions.classList.remove('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
} else {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
if (convertOptions) convertOptions.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const convertOptions = document.getElementById('convert-options');
|
const resetState = () => {
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
// ... (existing listeners)
|
const convertToPdf = async () => {
|
||||||
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
showLoader('Loading engine...');
|
||||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length === 1) {
|
||||||
fileDisplayArea.innerHTML = '';
|
const originalFile = state.files[0];
|
||||||
|
showLoader(`Converting ${originalFile.name}...`);
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||||
const file = state.files[index];
|
filetype: FILETYPE,
|
||||||
const fileDiv = document.createElement('div');
|
});
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
downloadFile(pdfBlob, fileName);
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
hideLoader();
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
showAlert(
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
'Conversion Complete',
|
||||||
nameSpan.textContent = file.name;
|
`Successfully converted ${originalFile.name} to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Converting files...');
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
const file = state.files[i];
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||||
|
filetype: FILETYPE,
|
||||||
const removeBtn = document.createElement('button');
|
});
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
removeBtn.onclick = () => {
|
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
fileControls.classList.remove('hidden');
|
|
||||||
if (convertOptions) convertOptions.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
if (convertOptions) convertOptions.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
state.files = [];
|
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
||||||
state.pdfDoc = null;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertToPdf = async () => {
|
hideLoader();
|
||||||
try {
|
|
||||||
if (state.files.length === 0) {
|
|
||||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showAlert(
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
'Conversion Complete',
|
||||||
await pymupdf.load();
|
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
const originalFile = state.files[0];
|
if (files && files.length > 0) {
|
||||||
showLoader(`Converting ${originalFile.name}...`);
|
state.files = [...state.files, ...Array.from(files)];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
if (fileInput && dropZone) {
|
||||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
downloadFile(pdfBlob, fileName);
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
hideLoader();
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
showAlert(
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
'Conversion Complete',
|
e.preventDefault();
|
||||||
`Successfully converted ${originalFile.name} to PDF.`,
|
dropZone.classList.remove('bg-gray-700');
|
||||||
'success',
|
});
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showLoader('Converting files...');
|
|
||||||
const JSZip = (await import('jszip')).default;
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
const file = state.files[i];
|
e.preventDefault();
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
if (files && files.length > 0) {
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
const name = f.name.toLowerCase();
|
||||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
}
|
});
|
||||||
|
if (validFiles.length > 0) {
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const dataTransfer = new DataTransfer();
|
||||||
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
|
handleFileSelect(dataTransfer.files);
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
fileInput.addEventListener('click', () => {
|
||||||
if (files && files.length > 0) {
|
fileInput.value = '';
|
||||||
state.files = [...state.files, ...Array.from(files)];
|
});
|
||||||
updateUI();
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (addMoreBtn) {
|
||||||
fileInput.addEventListener('change', (e) => {
|
addMoreBtn.addEventListener('click', () => {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
fileInput.click();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
if (clearFilesBtn) {
|
||||||
e.preventDefault();
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
dropZone.classList.add('bg-gray-700');
|
resetState();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', convertToPdf);
|
||||||
dropZone.classList.remove('bg-gray-700');
|
}
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const validFiles = Array.from(files).filter(f => {
|
|
||||||
const name = f.name.toLowerCase();
|
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
|
||||||
});
|
|
||||||
if (validFiles.length > 0) {
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
|
||||||
handleFileSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
|
||||||
resetState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convertToPdf);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,259 +2,297 @@ import { showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||||
|
import {
|
||||||
|
showWasmRequiredDialog,
|
||||||
|
WasmProvider,
|
||||||
|
} from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js');
|
const worker = new Worker(
|
||||||
|
import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js'
|
||||||
|
);
|
||||||
|
|
||||||
interface ExtractState {
|
interface ExtractState {
|
||||||
files: File[];
|
files: File[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageState: ExtractState = {
|
const pageState: ExtractState = {
|
||||||
files: [],
|
files: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function resetState() {
|
function resetState() {
|
||||||
pageState.files = [];
|
pageState.files = [];
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
|
|
||||||
const statusMessage = document.getElementById('status-message');
|
const statusMessage = document.getElementById('status-message');
|
||||||
if (statusMessage) statusMessage.classList.add('hidden');
|
if (statusMessage) statusMessage.classList.add('hidden');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
processBtn.removeAttribute('disabled');
|
processBtn.removeAttribute('disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
function showStatus(
|
||||||
const statusMessage = document.getElementById('status-message') as HTMLElement;
|
message: string,
|
||||||
if (!statusMessage) return;
|
type: 'success' | 'error' | 'info' = 'info'
|
||||||
|
) {
|
||||||
|
const statusMessage = document.getElementById(
|
||||||
|
'status-message'
|
||||||
|
) as HTMLElement;
|
||||||
|
if (!statusMessage) return;
|
||||||
|
|
||||||
statusMessage.textContent = message;
|
statusMessage.textContent = message;
|
||||||
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
|
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
|
||||||
? 'bg-green-900 text-green-200'
|
type === 'success'
|
||||||
: type === 'error'
|
? 'bg-green-900 text-green-200'
|
||||||
? 'bg-red-900 text-red-200'
|
: type === 'error'
|
||||||
: 'bg-blue-900 text-blue-200'
|
? 'bg-red-900 text-red-200'
|
||||||
}`;
|
: 'bg-blue-900 text-blue-200'
|
||||||
statusMessage.classList.remove('hidden');
|
}`;
|
||||||
|
statusMessage.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
worker.onmessage = function (e) {
|
worker.onmessage = function (e) {
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
processBtn.removeAttribute('disabled');
|
processBtn.removeAttribute('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.data.status === 'success') {
|
||||||
|
const attachments = e.data.attachments;
|
||||||
|
|
||||||
|
if (attachments.length === 0) {
|
||||||
|
showAlert(
|
||||||
|
'No Attachments',
|
||||||
|
'The PDF file(s) do not contain any attachments to extract.'
|
||||||
|
);
|
||||||
|
resetState();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.data.status === 'success') {
|
const zip = new JSZip();
|
||||||
const attachments = e.data.attachments;
|
let totalSize = 0;
|
||||||
|
|
||||||
if (attachments.length === 0) {
|
for (const attachment of attachments) {
|
||||||
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
zip.file(attachment.name, new Uint8Array(attachment.data));
|
||||||
resetState();
|
totalSize += attachment.data.byteLength;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const zip = new JSZip();
|
|
||||||
let totalSize = 0;
|
|
||||||
|
|
||||||
for (const attachment of attachments) {
|
|
||||||
zip.file(attachment.name, new Uint8Array(attachment.data));
|
|
||||||
totalSize += attachment.data.byteLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
zip.generateAsync({ type: 'blob' }).then(function (zipBlob) {
|
|
||||||
downloadFile(zipBlob, 'extracted-attachments.zip');
|
|
||||||
|
|
||||||
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
|
|
||||||
|
|
||||||
showStatus(
|
|
||||||
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
|
|
||||||
resetState();
|
|
||||||
});
|
|
||||||
} else if (e.data.status === 'error') {
|
|
||||||
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
|
||||||
console.error('Worker Error:', errorMessage);
|
|
||||||
|
|
||||||
if (errorMessage.includes('No attachments were found')) {
|
|
||||||
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
|
||||||
resetState();
|
|
||||||
} else {
|
|
||||||
showStatus(`Error: ${errorMessage}`, 'error');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
zip.generateAsync({ type: 'blob' }).then(function (zipBlob) {
|
||||||
|
downloadFile(zipBlob, 'extracted-attachments.zip');
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`${attachments.length} attachment(s) extracted successfully!`
|
||||||
|
);
|
||||||
|
|
||||||
|
showStatus(
|
||||||
|
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
|
||||||
|
resetState();
|
||||||
|
});
|
||||||
|
} else if (e.data.status === 'error') {
|
||||||
|
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
||||||
|
console.error('Worker Error:', errorMessage);
|
||||||
|
|
||||||
|
if (errorMessage.includes('No attachments were found')) {
|
||||||
|
showAlert(
|
||||||
|
'No Attachments',
|
||||||
|
'The PDF file(s) do not contain any attachments to extract.'
|
||||||
|
);
|
||||||
|
resetState();
|
||||||
|
} else {
|
||||||
|
showStatus(`Error: ${errorMessage}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.onerror = function (error) {
|
worker.onerror = function (error) {
|
||||||
console.error('Worker error:', error);
|
console.error('Worker error:', error);
|
||||||
showStatus('Worker error occurred. Check console for details.', 'error');
|
showStatus('Worker error occurred. Check console for details.', 'error');
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
processBtn.removeAttribute('disabled');
|
processBtn.removeAttribute('disabled');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function updateUI() {
|
async function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const toolOptions = document.getElementById('tool-options');
|
const toolOptions = document.getElementById('tool-options');
|
||||||
|
|
||||||
if (!fileDisplayArea) return;
|
if (!fileDisplayArea) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (pageState.files.length > 0) {
|
if (pageState.files.length > 0) {
|
||||||
const summaryDiv = document.createElement('div');
|
const summaryDiv = document.createElement('div');
|
||||||
summaryDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
summaryDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const countSpan = document.createElement('div');
|
const countSpan = document.createElement('div');
|
||||||
countSpan.className = 'font-medium text-gray-200 text-sm mb-1';
|
countSpan.className = 'font-medium text-gray-200 text-sm mb-1';
|
||||||
countSpan.textContent = `${pageState.files.length} PDF file(s) selected`;
|
countSpan.textContent = `${pageState.files.length} PDF file(s) selected`;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('div');
|
const sizeSpan = document.createElement('div');
|
||||||
sizeSpan.className = 'text-xs text-gray-400';
|
sizeSpan.className = 'text-xs text-gray-400';
|
||||||
const totalSize = pageState.files.reduce(function (sum, f) { return sum + f.size; }, 0);
|
const totalSize = pageState.files.reduce(function (sum, f) {
|
||||||
sizeSpan.textContent = formatBytes(totalSize);
|
return sum + f.size;
|
||||||
|
}, 0);
|
||||||
|
sizeSpan.textContent = formatBytes(totalSize);
|
||||||
|
|
||||||
infoContainer.append(countSpan, sizeSpan);
|
infoContainer.append(countSpan, sizeSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = function () {
|
removeBtn.onclick = function () {
|
||||||
resetState();
|
resetState();
|
||||||
};
|
};
|
||||||
|
|
||||||
summaryDiv.append(infoContainer, removeBtn);
|
summaryDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(summaryDiv);
|
fileDisplayArea.appendChild(summaryDiv);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
if (toolOptions) toolOptions.classList.add('hidden');
|
if (toolOptions) toolOptions.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractAttachments() {
|
async function extractAttachments() {
|
||||||
if (pageState.files.length === 0) {
|
if (pageState.files.length === 0) {
|
||||||
showStatus('No Files', 'error');
|
showStatus('No Files', 'error');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if CPDF is configured
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
|
processBtn.setAttribute('disabled', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
showStatus('Reading files...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileBuffers: ArrayBuffer[] = [];
|
||||||
|
const fileNames: string[] = [];
|
||||||
|
|
||||||
|
for (const file of pageState.files) {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
fileBuffers.push(buffer);
|
||||||
|
fileNames.push(file.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
showStatus(
|
||||||
|
`Extracting attachments from ${pageState.files.length} file(s)...`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
command: 'extract-attachments',
|
||||||
|
fileBuffers,
|
||||||
|
fileNames,
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
|
};
|
||||||
|
|
||||||
|
const transferables = fileBuffers.map(function (buf) {
|
||||||
|
return buf;
|
||||||
|
});
|
||||||
|
worker.postMessage(message, transferables);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading files:', error);
|
||||||
|
showStatus(
|
||||||
|
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
processBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
processBtn.setAttribute('disabled', 'true');
|
processBtn.removeAttribute('disabled');
|
||||||
}
|
|
||||||
|
|
||||||
showStatus('Reading files...', 'info');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileBuffers: ArrayBuffer[] = [];
|
|
||||||
const fileNames: string[] = [];
|
|
||||||
|
|
||||||
for (const file of pageState.files) {
|
|
||||||
const buffer = await file.arrayBuffer();
|
|
||||||
fileBuffers.push(buffer);
|
|
||||||
fileNames.push(file.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
showStatus(`Extracting attachments from ${pageState.files.length} file(s)...`, 'info');
|
|
||||||
|
|
||||||
const message = {
|
|
||||||
command: 'extract-attachments',
|
|
||||||
fileBuffers,
|
|
||||||
fileNames,
|
|
||||||
};
|
|
||||||
|
|
||||||
const transferables = fileBuffers.map(function (buf) { return buf; });
|
|
||||||
worker.postMessage(message, transferables);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reading files:', error);
|
|
||||||
showStatus(
|
|
||||||
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
|
|
||||||
'error'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
|
||||||
processBtn.removeAttribute('disabled');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileSelect(files: FileList | null) {
|
function handleFileSelect(files: FileList | null) {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const pdfFiles = Array.from(files).filter(function (f) {
|
const pdfFiles = Array.from(files).filter(function (f) {
|
||||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
return (
|
||||||
});
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
if (pdfFiles.length > 0) {
|
);
|
||||||
pageState.files = pdfFiles;
|
});
|
||||||
updateUI();
|
if (pdfFiles.length > 0) {
|
||||||
}
|
pageState.files = pdfFiles;
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', function () {
|
backBtn.addEventListener('click', function () {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (fileInput && dropZone) {
|
||||||
fileInput.addEventListener('change', function (e) {
|
fileInput.addEventListener('change', function (e) {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', function (e) {
|
dropZone.addEventListener('dragover', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
dropZone.classList.add('bg-gray-700');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', function (e) {
|
dropZone.addEventListener('dragleave', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.remove('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('drop', function (e) {
|
dropZone.addEventListener('drop', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.remove('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
const files = e.dataTransfer?.files;
|
const files = e.dataTransfer?.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
handleFileSelect(files);
|
handleFileSelect(files);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fileInput.addEventListener('click', function () {
|
fileInput.addEventListener('click', function () {
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
processBtn.addEventListener('click', extractAttachments);
|
processBtn.addEventListener('click', extractAttachments);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,281 +1,295 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
getPDFDocument,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
|
|
||||||
interface ExtractedImage {
|
interface ExtractedImage {
|
||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
name: string;
|
name: string;
|
||||||
ext: string;
|
ext: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let extractedImages: ExtractedImage[] = [];
|
let extractedImages: ExtractedImage[] = [];
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const extractOptions = document.getElementById('extract-options');
|
const extractOptions = document.getElementById('extract-options');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const imagesContainer = document.getElementById('images-container');
|
const imagesContainer = document.getElementById('images-container');
|
||||||
const imagesGrid = document.getElementById('images-grid');
|
const imagesGrid = document.getElementById('images-grid');
|
||||||
const downloadAllBtn = document.getElementById('download-all-btn');
|
const downloadAllBtn = document.getElementById('download-all-btn');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
const updateUI = async () => {
|
||||||
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls) return;
|
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls)
|
||||||
|
return;
|
||||||
|
|
||||||
// Clear extracted images when files change
|
// Clear extracted images when files change
|
||||||
extractedImages = [];
|
extractedImages = [];
|
||||||
if (imagesContainer) imagesContainer.classList.add('hidden');
|
if (imagesContainer) imagesContainer.classList.add('hidden');
|
||||||
if (imagesGrid) imagesGrid.innerHTML = '';
|
if (imagesGrid) imagesGrid.innerHTML = '';
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
const file = state.files[index];
|
const file = state.files[index];
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
state.files = state.files.filter((_: File, i: number) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
state.files = state.files.filter((_: File, i: number) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
|
||||||
} catch (error) {
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
fileControls.classList.remove('hidden');
|
|
||||||
extractOptions.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
extractOptions.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
|
||||||
state.files = [];
|
|
||||||
state.pdfDoc = null;
|
|
||||||
extractedImages = [];
|
|
||||||
if (imagesContainer) imagesContainer.classList.add('hidden');
|
|
||||||
if (imagesGrid) imagesGrid.innerHTML = '';
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayImages = () => {
|
|
||||||
if (!imagesGrid || !imagesContainer) return;
|
|
||||||
imagesGrid.innerHTML = '';
|
|
||||||
|
|
||||||
extractedImages.forEach((img, index) => {
|
|
||||||
const blob = new Blob([new Uint8Array(img.data)]);
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.className = 'bg-gray-700 rounded-lg overflow-hidden';
|
|
||||||
|
|
||||||
const imgEl = document.createElement('img');
|
|
||||||
imgEl.src = url;
|
|
||||||
imgEl.className = 'w-full h-32 object-cover';
|
|
||||||
|
|
||||||
const info = document.createElement('div');
|
|
||||||
info.className = 'p-2 flex justify-between items-center';
|
|
||||||
|
|
||||||
const name = document.createElement('span');
|
|
||||||
name.className = 'text-xs text-gray-300 truncate';
|
|
||||||
name.textContent = img.name;
|
|
||||||
|
|
||||||
const downloadBtn = document.createElement('button');
|
|
||||||
downloadBtn.className = 'text-indigo-400 hover:text-indigo-300';
|
|
||||||
downloadBtn.innerHTML = '<i data-lucide="download" class="w-4 h-4"></i>';
|
|
||||||
downloadBtn.onclick = () => {
|
|
||||||
downloadFile(blob, img.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
info.append(name, downloadBtn);
|
|
||||||
card.append(imgEl, info);
|
|
||||||
imagesGrid.appendChild(card);
|
|
||||||
});
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
imagesContainer.classList.remove('hidden');
|
|
||||||
};
|
|
||||||
|
|
||||||
const extract = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (state.files.length === 0) {
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
showAlert('No Files', 'Please select at least one PDF file.');
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
return;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||||
}
|
} catch (error) {
|
||||||
|
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||||
showLoader('Loading PDF processor...');
|
|
||||||
await pymupdf.load();
|
|
||||||
|
|
||||||
extractedImages = [];
|
|
||||||
let imgCounter = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
|
||||||
const file = state.files[i];
|
|
||||||
showLoader(`Extracting images from ${file.name}...`);
|
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
|
||||||
const pageCount = doc.pageCount;
|
|
||||||
|
|
||||||
for (let pageIdx = 0; pageIdx < pageCount; pageIdx++) {
|
|
||||||
const page = doc.getPage(pageIdx);
|
|
||||||
const images = page.getImages();
|
|
||||||
|
|
||||||
for (const imgInfo of images) {
|
|
||||||
try {
|
|
||||||
const imgData = page.extractImage(imgInfo.xref);
|
|
||||||
if (imgData && imgData.data) {
|
|
||||||
imgCounter++;
|
|
||||||
extractedImages.push({
|
|
||||||
data: imgData.data,
|
|
||||||
name: `image_${imgCounter}.${imgData.ext || 'png'}`,
|
|
||||||
ext: imgData.ext || 'png'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to extract image:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
doc.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
if (extractedImages.length === 0) {
|
|
||||||
showAlert('No Images Found', 'No embedded images were found in the selected PDF(s).');
|
|
||||||
} else {
|
|
||||||
displayImages();
|
|
||||||
showAlert(
|
|
||||||
'Extraction Complete',
|
|
||||||
`Found ${extractedImages.length} image(s) in ${state.files.length} PDF(s).`,
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during extraction. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const downloadAll = async () => {
|
createIcons({ icons });
|
||||||
if (extractedImages.length === 0) return;
|
fileControls.classList.remove('hidden');
|
||||||
|
extractOptions.classList.remove('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
} else {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
extractOptions.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
showLoader('Creating ZIP archive...');
|
const resetState = () => {
|
||||||
const JSZip = (await import('jszip')).default;
|
state.files = [];
|
||||||
const zip = new JSZip();
|
state.pdfDoc = null;
|
||||||
|
extractedImages = [];
|
||||||
|
if (imagesContainer) imagesContainer.classList.add('hidden');
|
||||||
|
if (imagesGrid) imagesGrid.innerHTML = '';
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
extractedImages.forEach((img) => {
|
const displayImages = () => {
|
||||||
zip.file(img.name, img.data);
|
if (!imagesGrid || !imagesContainer) return;
|
||||||
});
|
imagesGrid.innerHTML = '';
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
extractedImages.forEach((img, index) => {
|
||||||
downloadFile(zipBlob, 'extracted-images.zip');
|
const blob = new Blob([new Uint8Array(img.data)]);
|
||||||
hideLoader();
|
const url = URL.createObjectURL(blob);
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
const card = document.createElement('div');
|
||||||
if (files && files.length > 0) {
|
card.className = 'bg-gray-700 rounded-lg overflow-hidden';
|
||||||
const pdfFiles = Array.from(files).filter(
|
|
||||||
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
|
||||||
);
|
|
||||||
state.files = [...state.files, ...pdfFiles];
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
const imgEl = document.createElement('img');
|
||||||
fileInput.addEventListener('change', (e) => {
|
imgEl.src = url;
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
imgEl.className = 'w-full h-32 object-cover';
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
const info = document.createElement('div');
|
||||||
e.preventDefault();
|
info.className = 'p-2 flex justify-between items-center';
|
||||||
dropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
const name = document.createElement('span');
|
||||||
e.preventDefault();
|
name.className = 'text-xs text-gray-300 truncate';
|
||||||
dropZone.classList.remove('bg-gray-700');
|
name.textContent = img.name;
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
const downloadBtn = document.createElement('button');
|
||||||
e.preventDefault();
|
downloadBtn.className = 'text-indigo-400 hover:text-indigo-300';
|
||||||
dropZone.classList.remove('bg-gray-700');
|
downloadBtn.innerHTML = '<i data-lucide="download" class="w-4 h-4"></i>';
|
||||||
const files = e.dataTransfer?.files;
|
downloadBtn.onclick = () => {
|
||||||
if (files && files.length > 0) {
|
downloadFile(blob, img.name);
|
||||||
handleFileSelect(files);
|
};
|
||||||
|
|
||||||
|
info.append(name, downloadBtn);
|
||||||
|
card.append(imgEl, info);
|
||||||
|
imagesGrid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
createIcons({ icons });
|
||||||
|
imagesContainer.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
const extract = async () => {
|
||||||
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Loading PDF processor...');
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
extractedImages = [];
|
||||||
|
let imgCounter = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
|
const file = state.files[i];
|
||||||
|
showLoader(`Extracting images from ${file.name}...`);
|
||||||
|
|
||||||
|
const doc = await pymupdf.open(file);
|
||||||
|
const pageCount = doc.pageCount;
|
||||||
|
|
||||||
|
for (let pageIdx = 0; pageIdx < pageCount; pageIdx++) {
|
||||||
|
const page = doc.getPage(pageIdx);
|
||||||
|
const images = page.getImages();
|
||||||
|
|
||||||
|
for (const imgInfo of images) {
|
||||||
|
try {
|
||||||
|
const imgData = page.extractImage(imgInfo.xref);
|
||||||
|
if (imgData && imgData.data) {
|
||||||
|
imgCounter++;
|
||||||
|
extractedImages.push({
|
||||||
|
data: imgData.data,
|
||||||
|
name: `image_${imgCounter}.${imgData.ext || 'png'}`,
|
||||||
|
ext: imgData.ext || 'png',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to extract image:', e);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
doc.close();
|
||||||
|
}
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
hideLoader();
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
if (extractedImages.length === 0) {
|
||||||
addMoreBtn.addEventListener('click', () => {
|
showAlert(
|
||||||
fileInput.click();
|
'No Images Found',
|
||||||
});
|
'No embedded images were found in the selected PDF(s).'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displayImages();
|
||||||
|
showAlert(
|
||||||
|
'Extraction Complete',
|
||||||
|
`Found ${extractedImages.length} image(s) in ${state.files.length} PDF(s).`,
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during extraction. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
const downloadAll = async () => {
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
if (extractedImages.length === 0) return;
|
||||||
resetState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
showLoader('Creating ZIP archive...');
|
||||||
processBtn.addEventListener('click', extract);
|
const JSZip = (await import('jszip')).default;
|
||||||
}
|
const zip = new JSZip();
|
||||||
|
|
||||||
if (downloadAllBtn) {
|
extractedImages.forEach((img) => {
|
||||||
downloadAllBtn.addEventListener('click', downloadAll);
|
zip.file(img.name, img.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'extracted-images.zip');
|
||||||
|
hideLoader();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const pdfFiles = Array.from(files).filter(
|
||||||
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
|
state.files = [...state.files, ...pdfFiles];
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fileInput && dropZone) {
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
handleFileSelect(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('click', () => {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
|
resetState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', extract);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadAllBtn) {
|
||||||
|
downloadAllBtn.addEventListener('click', downloadAll);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,240 +2,259 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
|
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const optionsPanel = document.getElementById('options-panel');
|
const optionsPanel = document.getElementById('options-panel');
|
||||||
|
|
||||||
if (!fileDisplayArea || !optionsPanel) return;
|
if (!fileDisplayArea || !optionsPanel) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
optionsPanel.classList.remove('hidden');
|
optionsPanel.classList.remove('hidden');
|
||||||
|
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = resetState;
|
removeBtn.onclick = resetState;
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
} else {
|
} else {
|
||||||
optionsPanel.classList.add('hidden');
|
optionsPanel.classList.add('hidden');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
file = null;
|
file = null;
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
function tableToCsv(rows: (string | null)[][]): string {
|
function tableToCsv(rows: (string | null)[][]): string {
|
||||||
return rows.map(row =>
|
return rows
|
||||||
row.map(cell => {
|
.map((row) =>
|
||||||
const cellStr = cell ?? '';
|
row
|
||||||
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) {
|
.map((cell) => {
|
||||||
return `"${cellStr.replace(/"/g, '""')}"`;
|
const cellStr = cell ?? '';
|
||||||
}
|
if (
|
||||||
return cellStr;
|
cellStr.includes(',') ||
|
||||||
}).join(',')
|
cellStr.includes('"') ||
|
||||||
).join('\n');
|
cellStr.includes('\n')
|
||||||
|
) {
|
||||||
|
return `"${cellStr.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return cellStr;
|
||||||
|
})
|
||||||
|
.join(',')
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extract() {
|
async function extract() {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
showAlert('No File', 'Please upload a PDF file first.');
|
showAlert('No File', 'Please upload a PDF file first.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatRadios = document.querySelectorAll('input[name="export-format"]');
|
||||||
|
let format = 'csv';
|
||||||
|
formatRadios.forEach((radio: Element) => {
|
||||||
|
if ((radio as HTMLInputElement).checked) {
|
||||||
|
format = (radio as HTMLInputElement).value;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const formatRadios = document.querySelectorAll('input[name="export-format"]');
|
try {
|
||||||
let format = 'csv';
|
|
||||||
formatRadios.forEach((radio: Element) => {
|
|
||||||
if ((radio as HTMLInputElement).checked) {
|
|
||||||
format = (radio as HTMLInputElement).value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
showLoader('Loading Engine...');
|
showLoader('Loading Engine...');
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
showLoader('Extracting tables...');
|
||||||
|
|
||||||
try {
|
const doc = await pymupdf.open(file);
|
||||||
await pymupdf.load();
|
const pageCount = doc.pageCount;
|
||||||
showLoader('Extracting tables...');
|
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
interface TableData {
|
||||||
const pageCount = doc.pageCount;
|
page: number;
|
||||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
tableIndex: number;
|
||||||
|
rows: (string | null)[][];
|
||||||
interface TableData {
|
markdown: string;
|
||||||
page: number;
|
rowCount: number;
|
||||||
tableIndex: number;
|
colCount: number;
|
||||||
rows: (string | null)[][];
|
|
||||||
markdown: string;
|
|
||||||
rowCount: number;
|
|
||||||
colCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allTables: TableData[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < pageCount; i++) {
|
|
||||||
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
|
||||||
const page = doc.getPage(i);
|
|
||||||
const tables = page.findTables();
|
|
||||||
|
|
||||||
tables.forEach((table, tableIdx) => {
|
|
||||||
allTables.push({
|
|
||||||
page: i + 1,
|
|
||||||
tableIndex: tableIdx + 1,
|
|
||||||
rows: table.rows,
|
|
||||||
markdown: table.markdown,
|
|
||||||
rowCount: table.rowCount,
|
|
||||||
colCount: table.colCount
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allTables.length === 0) {
|
|
||||||
showAlert('No Tables Found', 'No tables were detected in this PDF.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allTables.length === 1) {
|
|
||||||
const table = allTables[0];
|
|
||||||
let content: string;
|
|
||||||
let ext: string;
|
|
||||||
let mimeType: string;
|
|
||||||
|
|
||||||
if (format === 'csv') {
|
|
||||||
content = tableToCsv(table.rows);
|
|
||||||
ext = 'csv';
|
|
||||||
mimeType = 'text/csv';
|
|
||||||
} else if (format === 'json') {
|
|
||||||
content = JSON.stringify(table.rows, null, 2);
|
|
||||||
ext = 'json';
|
|
||||||
mimeType = 'application/json';
|
|
||||||
} else {
|
|
||||||
content = table.markdown;
|
|
||||||
ext = 'md';
|
|
||||||
mimeType = 'text/markdown';
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([content], { type: mimeType });
|
|
||||||
downloadFile(blob, `${baseName}_table.${ext}`);
|
|
||||||
showAlert('Success', `Extracted 1 table successfully!`, 'success', resetState);
|
|
||||||
} else {
|
|
||||||
showLoader('Creating ZIP file...');
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
allTables.forEach((table, idx) => {
|
|
||||||
const filename = `table_${idx + 1}_page${table.page}`;
|
|
||||||
let content: string;
|
|
||||||
let ext: string;
|
|
||||||
|
|
||||||
if (format === 'csv') {
|
|
||||||
content = tableToCsv(table.rows);
|
|
||||||
ext = 'csv';
|
|
||||||
} else if (format === 'json') {
|
|
||||||
content = JSON.stringify(table.rows, null, 2);
|
|
||||||
ext = 'json';
|
|
||||||
} else {
|
|
||||||
content = table.markdown;
|
|
||||||
ext = 'md';
|
|
||||||
}
|
|
||||||
|
|
||||||
zip.file(`${filename}.${ext}`, content);
|
|
||||||
});
|
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
downloadFile(zipBlob, `${baseName}_tables.zip`);
|
|
||||||
showAlert('Success', `Extracted ${allTables.length} tables successfully!`, 'success', resetState);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
|
||||||
showAlert('Error', `Failed to extract tables. ${message}`);
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allTables: TableData[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
||||||
|
const page = doc.getPage(i);
|
||||||
|
const tables = page.findTables();
|
||||||
|
|
||||||
|
tables.forEach((table, tableIdx) => {
|
||||||
|
allTables.push({
|
||||||
|
page: i + 1,
|
||||||
|
tableIndex: tableIdx + 1,
|
||||||
|
rows: table.rows,
|
||||||
|
markdown: table.markdown,
|
||||||
|
rowCount: table.rowCount,
|
||||||
|
colCount: table.colCount,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTables.length === 0) {
|
||||||
|
showAlert('No Tables Found', 'No tables were detected in this PDF.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allTables.length === 1) {
|
||||||
|
const table = allTables[0];
|
||||||
|
let content: string;
|
||||||
|
let ext: string;
|
||||||
|
let mimeType: string;
|
||||||
|
|
||||||
|
if (format === 'csv') {
|
||||||
|
content = tableToCsv(table.rows);
|
||||||
|
ext = 'csv';
|
||||||
|
mimeType = 'text/csv';
|
||||||
|
} else if (format === 'json') {
|
||||||
|
content = JSON.stringify(table.rows, null, 2);
|
||||||
|
ext = 'json';
|
||||||
|
mimeType = 'application/json';
|
||||||
|
} else {
|
||||||
|
content = table.markdown;
|
||||||
|
ext = 'md';
|
||||||
|
mimeType = 'text/markdown';
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
downloadFile(blob, `${baseName}_table.${ext}`);
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Extracted 1 table successfully!`,
|
||||||
|
'success',
|
||||||
|
resetState
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Creating ZIP file...');
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
allTables.forEach((table, idx) => {
|
||||||
|
const filename = `table_${idx + 1}_page${table.page}`;
|
||||||
|
let content: string;
|
||||||
|
let ext: string;
|
||||||
|
|
||||||
|
if (format === 'csv') {
|
||||||
|
content = tableToCsv(table.rows);
|
||||||
|
ext = 'csv';
|
||||||
|
} else if (format === 'json') {
|
||||||
|
content = JSON.stringify(table.rows, null, 2);
|
||||||
|
ext = 'json';
|
||||||
|
} else {
|
||||||
|
content = table.markdown;
|
||||||
|
ext = 'md';
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.file(`${filename}.${ext}`, content);
|
||||||
|
});
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, `${baseName}_tables.zip`);
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Extracted ${allTables.length} tables successfully!`,
|
||||||
|
'success',
|
||||||
|
resetState
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||||
|
showAlert('Error', `Failed to extract tables. ${message}`);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileSelect = (newFiles: FileList | null) => {
|
||||||
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
|
const validFile = Array.from(newFiles).find(
|
||||||
|
(f) => f.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validFile) {
|
||||||
|
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileSelect = (newFiles: FileList | null) => {
|
file = validFile;
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
updateUI();
|
||||||
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
|
};
|
||||||
|
|
||||||
if (!validFile) {
|
if (fileInput && dropZone) {
|
||||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
fileInput.addEventListener('change', (e) => {
|
||||||
return;
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
}
|
});
|
||||||
|
|
||||||
file = validFile;
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
updateUI();
|
e.preventDefault();
|
||||||
};
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
fileInput.addEventListener('change', (e) => {
|
e.preventDefault();
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
dropZone.classList.remove('bg-gray-700');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
});
|
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
fileInput.addEventListener('click', () => {
|
||||||
e.preventDefault();
|
fileInput.value = '';
|
||||||
dropZone.classList.remove('bg-gray-700');
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', extract);
|
||||||
dropZone.classList.remove('bg-gray-700');
|
}
|
||||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', extract);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,201 +2,212 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
const FILETYPE = 'fb2';
|
const FILETYPE = 'fb2';
|
||||||
const EXTENSIONS = ['.fb2'];
|
const EXTENSIONS = ['.fb2'];
|
||||||
const TOOL_NAME = 'FB2';
|
const TOOL_NAME = 'FB2';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUI = async () => {
|
||||||
|
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||||
|
|
||||||
|
if (state.files.length > 0) {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
|
const file = state.files[index];
|
||||||
|
const fileDiv = document.createElement('div');
|
||||||
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
|
const infoContainer = document.createElement('div');
|
||||||
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('div');
|
||||||
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
|
const metaSpan = document.createElement('div');
|
||||||
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className =
|
||||||
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
createIcons({ icons });
|
||||||
|
fileControls.classList.remove('hidden');
|
||||||
|
processBtn.classList.remove('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
} else {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
processBtn.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateUI = async () => {
|
const resetState = () => {
|
||||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
const convertToPdf = async () => {
|
||||||
fileDisplayArea.innerHTML = '';
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
showLoader('Loading engine...');
|
||||||
const file = state.files[index];
|
const pymupdf = await loadPyMuPDF();
|
||||||
const fileDiv = document.createElement('div');
|
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
if (state.files.length === 1) {
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
const originalFile = state.files[0];
|
||||||
|
showLoader(`Converting ${originalFile.name}...`);
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
filetype: FILETYPE,
|
||||||
nameSpan.textContent = file.name;
|
});
|
||||||
|
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
downloadFile(pdfBlob, fileName);
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
hideLoader();
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${originalFile.name} to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Converting files...');
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
const file = state.files[i];
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
showLoader(
|
||||||
removeBtn.onclick = () => {
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
);
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
filetype: FILETYPE,
|
||||||
}
|
});
|
||||||
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
createIcons({ icons });
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
fileControls.classList.remove('hidden');
|
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
processBtn.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
processBtn.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
state.files = [];
|
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
||||||
state.pdfDoc = null;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertToPdf = async () => {
|
hideLoader();
|
||||||
try {
|
|
||||||
if (state.files.length === 0) {
|
|
||||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showAlert(
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
'Conversion Complete',
|
||||||
await pymupdf.load();
|
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
const originalFile = state.files[0];
|
if (files && files.length > 0) {
|
||||||
showLoader(`Converting ${originalFile.name}...`);
|
state.files = [...state.files, ...Array.from(files)];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
if (fileInput && dropZone) {
|
||||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
downloadFile(pdfBlob, fileName);
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
hideLoader();
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
showAlert(
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
'Conversion Complete',
|
e.preventDefault();
|
||||||
`Successfully converted ${originalFile.name} to PDF.`,
|
dropZone.classList.remove('bg-gray-700');
|
||||||
'success',
|
});
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showLoader('Converting files...');
|
|
||||||
const JSZip = (await import('jszip')).default;
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
const file = state.files[i];
|
e.preventDefault();
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
if (files && files.length > 0) {
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
const name = f.name.toLowerCase();
|
||||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
}
|
});
|
||||||
|
if (validFiles.length > 0) {
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const dataTransfer = new DataTransfer();
|
||||||
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
|
handleFileSelect(dataTransfer.files);
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
fileInput.addEventListener('click', () => {
|
||||||
if (files && files.length > 0) {
|
fileInput.value = '';
|
||||||
state.files = [...state.files, ...Array.from(files)];
|
});
|
||||||
updateUI();
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (addMoreBtn) {
|
||||||
fileInput.addEventListener('change', (e) => {
|
addMoreBtn.addEventListener('click', () => {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
fileInput.click();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
if (clearFilesBtn) {
|
||||||
e.preventDefault();
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
dropZone.classList.add('bg-gray-700');
|
resetState();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', convertToPdf);
|
||||||
dropZone.classList.remove('bg-gray-700');
|
}
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const validFiles = Array.from(files).filter(f => {
|
|
||||||
const name = f.name.toLowerCase();
|
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
|
||||||
});
|
|
||||||
if (validFiles.length > 0) {
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
|
||||||
handleFileSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
|
||||||
resetState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convertToPdf);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { showAlert } from '../ui.js';
|
import { showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { convertFileToOutlines } from '../utils/ghostscript-loader.js';
|
import { convertFileToOutlines } from '../utils/ghostscript-loader.js';
|
||||||
|
import { isGhostscriptAvailable } from '../utils/ghostscript-dynamic-loader.js';
|
||||||
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
@@ -98,6 +100,12 @@ async function processFiles() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if Ghostscript is configured
|
||||||
|
if (!isGhostscriptAvailable()) {
|
||||||
|
showWasmRequiredDialog('ghostscript');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loaderModal = document.getElementById('loader-modal');
|
const loaderModal = document.getElementById('loader-modal');
|
||||||
const loaderText = document.getElementById('loader-text');
|
const loaderText = document.getElementById('loader-text');
|
||||||
|
|
||||||
|
|||||||
@@ -1,275 +1,293 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
import heic2any from 'heic2any';
|
import heic2any from 'heic2any';
|
||||||
|
|
||||||
const SUPPORTED_FORMATS = '.jpg,.jpeg,.png,.bmp,.gif,.tiff,.tif,.pnm,.pgm,.pbm,.ppm,.pam,.jxr,.jpx,.jp2,.psd,.svg,.heic,.heif,.webp';
|
const SUPPORTED_FORMATS =
|
||||||
const SUPPORTED_FORMATS_DISPLAY = 'JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP';
|
'.jpg,.jpeg,.png,.bmp,.gif,.tiff,.tif,.pnm,.pgm,.pbm,.ppm,.pam,.jxr,.jpx,.jp2,.psd,.svg,.heic,.heif,.webp';
|
||||||
|
const SUPPORTED_FORMATS_DISPLAY =
|
||||||
|
'JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP';
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
let pymupdf: PyMuPDF | null = null;
|
let pymupdf: any = null;
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
} else {
|
} else {
|
||||||
initializePage();
|
initializePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializePage() {
|
function initializePage() {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const formatDisplay = document.getElementById('supported-formats');
|
const formatDisplay = document.getElementById('supported-formats');
|
||||||
|
|
||||||
if (formatDisplay) {
|
if (formatDisplay) {
|
||||||
formatDisplay.textContent = SUPPORTED_FORMATS_DISPLAY;
|
formatDisplay.textContent = SUPPORTED_FORMATS_DISPLAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.accept = SUPPORTED_FORMATS;
|
fileInput.accept = SUPPORTED_FORMATS;
|
||||||
fileInput.addEventListener('change', handleFileUpload);
|
fileInput.addEventListener('change', handleFileUpload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dropZone) {
|
if (dropZone) {
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
dropZone.classList.add('bg-gray-700');
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', () => {
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
const droppedFiles = e.dataTransfer?.files;
|
|
||||||
if (droppedFiles && droppedFiles.length > 0) {
|
|
||||||
handleFiles(droppedFiles);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput?.addEventListener('click', () => {
|
|
||||||
if (fileInput) fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput?.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
|
||||||
files = [];
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convertToPdf);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', () => {
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const droppedFiles = e.dataTransfer?.files;
|
||||||
|
if (droppedFiles && droppedFiles.length > 0) {
|
||||||
|
handleFiles(droppedFiles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput?.addEventListener('click', () => {
|
||||||
|
if (fileInput) fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput?.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
|
files = [];
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', convertToPdf);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||||
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileUpload(e: Event) {
|
function handleFileUpload(e: Event) {
|
||||||
const input = e.target as HTMLInputElement;
|
const input = e.target as HTMLInputElement;
|
||||||
if (input.files && input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
handleFiles(input.files);
|
handleFiles(input.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileExtension(filename: string): string {
|
function getFileExtension(filename: string): string {
|
||||||
return '.' + filename.split('.').pop()?.toLowerCase() || '';
|
return '.' + filename.split('.').pop()?.toLowerCase() || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidImageFile(file: File): boolean {
|
function isValidImageFile(file: File): boolean {
|
||||||
const ext = getFileExtension(file.name);
|
const ext = getFileExtension(file.name);
|
||||||
const validExtensions = SUPPORTED_FORMATS.split(',');
|
const validExtensions = SUPPORTED_FORMATS.split(',');
|
||||||
return validExtensions.includes(ext) || file.type.startsWith('image/');
|
return validExtensions.includes(ext) || file.type.startsWith('image/');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFiles(newFiles: FileList) {
|
function handleFiles(newFiles: FileList) {
|
||||||
const validFiles = Array.from(newFiles).filter(isValidImageFile);
|
const validFiles = Array.from(newFiles).filter(isValidImageFile);
|
||||||
|
|
||||||
if (validFiles.length < newFiles.length) {
|
if (validFiles.length < newFiles.length) {
|
||||||
showAlert('Invalid Files', 'Some files were skipped. Only supported image formats are allowed.');
|
showAlert(
|
||||||
}
|
'Invalid Files',
|
||||||
|
'Some files were skipped. Only supported image formats are allowed.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
files = [...files, ...validFiles];
|
files = [...files, ...validFiles];
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
files = [];
|
files = [];
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const optionsDiv = document.getElementById('jpg-to-pdf-options');
|
const optionsDiv = document.getElementById('jpg-to-pdf-options');
|
||||||
|
|
||||||
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
|
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
fileControls.classList.remove('hidden');
|
fileControls.classList.remove('hidden');
|
||||||
optionsDiv.classList.remove('hidden');
|
optionsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('span');
|
const sizeSpan = document.createElement('span');
|
||||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, sizeSpan);
|
infoContainer.append(nameSpan, sizeSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
files = files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
files = files.filter((_, i) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
});
|
});
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
} else {
|
} else {
|
||||||
fileControls.classList.add('hidden');
|
fileControls.classList.add('hidden');
|
||||||
optionsDiv.classList.add('hidden');
|
optionsDiv.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePyMuPDF(): Promise<PyMuPDF> {
|
async function ensurePyMuPDF(): Promise<any> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
pymupdf = await loadPyMuPDF();
|
||||||
await pymupdf.load();
|
}
|
||||||
}
|
return pymupdf;
|
||||||
return pymupdf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preprocessFile(file: File): Promise<File> {
|
async function preprocessFile(file: File): Promise<File> {
|
||||||
const ext = getFileExtension(file.name);
|
const ext = getFileExtension(file.name);
|
||||||
|
|
||||||
if (ext === '.heic' || ext === '.heif') {
|
if (ext === '.heic' || ext === '.heif') {
|
||||||
try {
|
try {
|
||||||
const conversionResult = await heic2any({
|
const conversionResult = await heic2any({
|
||||||
blob: file,
|
blob: file,
|
||||||
toType: 'image/png',
|
toType: 'image/png',
|
||||||
quality: 0.9,
|
quality: 0.9,
|
||||||
});
|
});
|
||||||
|
|
||||||
const blob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult;
|
const blob = Array.isArray(conversionResult)
|
||||||
return new File([blob], file.name.replace(/\.(heic|heif)$/i, '.png'), { type: 'image/png' });
|
? conversionResult[0]
|
||||||
} catch (e) {
|
: conversionResult;
|
||||||
console.error(`Failed to convert HEIC: ${file.name}`, e);
|
return new File([blob], file.name.replace(/\.(heic|heif)$/i, '.png'), {
|
||||||
throw new Error(`Failed to process HEIC file: ${file.name}`);
|
type: 'image/png',
|
||||||
}
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to convert HEIC: ${file.name}`, e);
|
||||||
|
throw new Error(`Failed to process HEIC file: ${file.name}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ext === '.webp') {
|
if (ext === '.webp') {
|
||||||
try {
|
try {
|
||||||
return await new Promise<File>((resolve, reject) => {
|
return await new Promise<File>((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
|
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = img.width;
|
canvas.width = img.width;
|
||||||
canvas.height = img.height;
|
canvas.height = img.height;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
reject(new Error('Canvas context failed'));
|
reject(new Error('Canvas context failed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
if (blob) {
|
if (blob) {
|
||||||
resolve(new File([blob], file.name.replace(/\.webp$/i, '.png'), { type: 'image/png' }));
|
resolve(
|
||||||
} else {
|
new File([blob], file.name.replace(/\.webp$/i, '.png'), {
|
||||||
reject(new Error('Canvas toBlob failed'));
|
type: 'image/png',
|
||||||
}
|
})
|
||||||
}, 'image/png');
|
);
|
||||||
};
|
} else {
|
||||||
|
reject(new Error('Canvas toBlob failed'));
|
||||||
|
}
|
||||||
|
}, 'image/png');
|
||||||
|
};
|
||||||
|
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
reject(new Error('Failed to load WebP image'));
|
reject(new Error('Failed to load WebP image'));
|
||||||
};
|
};
|
||||||
|
|
||||||
img.src = url;
|
img.src = url;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to convert WebP: ${file.name}`, e);
|
console.error(`Failed to convert WebP: ${file.name}`, e);
|
||||||
throw new Error(`Failed to process WebP file: ${file.name}`);
|
throw new Error(`Failed to process WebP file: ${file.name}`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertToPdf() {
|
async function convertToPdf() {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one image file.');
|
showAlert('No Files', 'Please select at least one image file.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Processing images...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const processedFiles: File[] = [];
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const processed = await preprocessFile(file);
|
||||||
|
processedFiles.push(processed);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Processing images...');
|
showLoader('Loading engine...');
|
||||||
|
const mupdf = await ensurePyMuPDF();
|
||||||
|
|
||||||
try {
|
showLoader('Converting images to PDF...');
|
||||||
const processedFiles: File[] = [];
|
const pdfBlob = await mupdf.imagesToPdf(processedFiles);
|
||||||
for (const file of files) {
|
|
||||||
try {
|
|
||||||
const processed = await preprocessFile(file);
|
|
||||||
processedFiles.push(processed);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.warn(error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
downloadFile(pdfBlob, 'images_to_pdf.pdf');
|
||||||
const mupdf = await ensurePyMuPDF();
|
|
||||||
|
|
||||||
showLoader('Converting images to PDF...');
|
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||||
const pdfBlob = await mupdf.imagesToPdf(processedFiles);
|
resetState();
|
||||||
|
});
|
||||||
downloadFile(pdfBlob, 'images_to_pdf.pdf');
|
} catch (e: any) {
|
||||||
|
console.error('[ImageToPDF]', e);
|
||||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
showAlert(
|
||||||
resetState();
|
'Conversion Error',
|
||||||
});
|
e.message || 'Failed to convert images to PDF.'
|
||||||
} catch (e: any) {
|
);
|
||||||
console.error('[ImageToPDF]', e);
|
} finally {
|
||||||
showAlert('Conversion Error', e.message || 'Failed to convert images to PDF.');
|
hideLoader();
|
||||||
} finally {
|
}
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,195 +1,205 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
const SUPPORTED_FORMATS = '.jpg,.jpeg,.jp2,.jpx';
|
const SUPPORTED_FORMATS = '.jpg,.jpeg,.jp2,.jpx';
|
||||||
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/jp2'];
|
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/jp2'];
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
let pymupdf: PyMuPDF | null = null;
|
let pymupdf: any = null;
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
} else {
|
} else {
|
||||||
initializePage();
|
initializePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializePage() {
|
function initializePage() {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.addEventListener('change', handleFileUpload);
|
fileInput.addEventListener('change', handleFileUpload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dropZone) {
|
if (dropZone) {
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
dropZone.classList.add('bg-gray-700');
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', () => {
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
const droppedFiles = e.dataTransfer?.files;
|
|
||||||
if (droppedFiles && droppedFiles.length > 0) {
|
|
||||||
handleFiles(droppedFiles);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput?.addEventListener('click', () => {
|
|
||||||
if (fileInput) fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput?.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
|
||||||
files = [];
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convertToPdf);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', () => {
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const droppedFiles = e.dataTransfer?.files;
|
||||||
|
if (droppedFiles && droppedFiles.length > 0) {
|
||||||
|
handleFiles(droppedFiles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput?.addEventListener('click', () => {
|
||||||
|
if (fileInput) fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput?.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
|
files = [];
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', convertToPdf);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||||
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileUpload(e: Event) {
|
function handleFileUpload(e: Event) {
|
||||||
const input = e.target as HTMLInputElement;
|
const input = e.target as HTMLInputElement;
|
||||||
if (input.files && input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
handleFiles(input.files);
|
handleFiles(input.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileExtension(filename: string): string {
|
function getFileExtension(filename: string): string {
|
||||||
return '.' + (filename.split('.').pop()?.toLowerCase() || '');
|
return '.' + (filename.split('.').pop()?.toLowerCase() || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidImageFile(file: File): boolean {
|
function isValidImageFile(file: File): boolean {
|
||||||
const ext = getFileExtension(file.name);
|
const ext = getFileExtension(file.name);
|
||||||
const validExtensions = SUPPORTED_FORMATS.split(',');
|
const validExtensions = SUPPORTED_FORMATS.split(',');
|
||||||
return validExtensions.includes(ext) || SUPPORTED_MIME_TYPES.includes(file.type);
|
return (
|
||||||
|
validExtensions.includes(ext) || SUPPORTED_MIME_TYPES.includes(file.type)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFiles(newFiles: FileList) {
|
function handleFiles(newFiles: FileList) {
|
||||||
const validFiles = Array.from(newFiles).filter(isValidImageFile);
|
const validFiles = Array.from(newFiles).filter(isValidImageFile);
|
||||||
|
|
||||||
if (validFiles.length < newFiles.length) {
|
if (validFiles.length < newFiles.length) {
|
||||||
showAlert('Invalid Files', 'Some files were skipped. Only JPG, JPEG, JP2, and JPX files are allowed.');
|
showAlert(
|
||||||
}
|
'Invalid Files',
|
||||||
|
'Some files were skipped. Only JPG, JPEG, JP2, and JPX files are allowed.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
files = [...files, ...validFiles];
|
files = [...files, ...validFiles];
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
files = [];
|
files = [];
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const optionsDiv = document.getElementById('jpg-to-pdf-options');
|
const optionsDiv = document.getElementById('jpg-to-pdf-options');
|
||||||
|
|
||||||
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
|
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
fileControls.classList.remove('hidden');
|
fileControls.classList.remove('hidden');
|
||||||
optionsDiv.classList.remove('hidden');
|
optionsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('span');
|
const sizeSpan = document.createElement('span');
|
||||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, sizeSpan);
|
infoContainer.append(nameSpan, sizeSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
files = files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
files = files.filter((_, i) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
});
|
});
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
} else {
|
} else {
|
||||||
fileControls.classList.add('hidden');
|
fileControls.classList.add('hidden');
|
||||||
optionsDiv.classList.add('hidden');
|
optionsDiv.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePyMuPDF(): Promise<PyMuPDF> {
|
async function ensurePyMuPDF(): Promise<any> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
pymupdf = await loadPyMuPDF();
|
||||||
await pymupdf.load();
|
}
|
||||||
}
|
return pymupdf;
|
||||||
return pymupdf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convertToPdf() {
|
async function convertToPdf() {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one JPG or JPEG2000 image.');
|
showAlert('No Files', 'Please select at least one JPG or JPEG2000 image.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showLoader('Loading engine...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mupdf = await ensurePyMuPDF();
|
const mupdf = await ensurePyMuPDF();
|
||||||
|
|
||||||
showLoader('Converting images to PDF...');
|
showLoader('Converting images to PDF...');
|
||||||
|
|
||||||
const pdfBlob = await mupdf.imagesToPdf(files);
|
const pdfBlob = await mupdf.imagesToPdf(files);
|
||||||
|
|
||||||
downloadFile(pdfBlob, 'from_jpgs.pdf');
|
downloadFile(pdfBlob, 'from_jpgs.pdf');
|
||||||
|
|
||||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[JpgToPdf]', e);
|
console.error('[JpgToPdf]', e);
|
||||||
showAlert('Conversion Error', e.message || 'Failed to convert images to PDF.');
|
showAlert(
|
||||||
} finally {
|
'Conversion Error',
|
||||||
hideLoader();
|
e.message || 'Failed to convert images to PDF.'
|
||||||
}
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,101 +1,133 @@
|
|||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip';
|
||||||
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
formatBytes,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers';
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||||
|
import {
|
||||||
|
showWasmRequiredDialog,
|
||||||
|
WasmProvider,
|
||||||
|
} from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/json-to-pdf.worker.js');
|
const worker = new Worker(
|
||||||
|
import.meta.env.BASE_URL + 'workers/json-to-pdf.worker.js'
|
||||||
|
);
|
||||||
|
|
||||||
let selectedFiles: File[] = []
|
let selectedFiles: File[] = [];
|
||||||
|
|
||||||
const jsonFilesInput = document.getElementById('jsonFiles') as HTMLInputElement
|
const jsonFilesInput = document.getElementById('jsonFiles') as HTMLInputElement;
|
||||||
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement
|
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement;
|
||||||
const statusMessage = document.getElementById('status-message') as HTMLDivElement
|
const statusMessage = document.getElementById(
|
||||||
const fileListDiv = document.getElementById('fileList') as HTMLDivElement
|
'status-message'
|
||||||
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement
|
) as HTMLDivElement;
|
||||||
|
const fileListDiv = document.getElementById('fileList') as HTMLDivElement;
|
||||||
|
const backToToolsBtn = document.getElementById(
|
||||||
|
'back-to-tools'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
function showStatus(
|
function showStatus(
|
||||||
message: string,
|
message: string,
|
||||||
type: 'success' | 'error' | 'info' = 'info'
|
type: 'success' | 'error' | 'info' = 'info'
|
||||||
) {
|
) {
|
||||||
statusMessage.textContent = message
|
statusMessage.textContent = message;
|
||||||
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
|
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
|
||||||
? 'bg-green-900 text-green-200'
|
type === 'success'
|
||||||
: type === 'error'
|
? 'bg-green-900 text-green-200'
|
||||||
? 'bg-red-900 text-red-200'
|
: type === 'error'
|
||||||
: 'bg-blue-900 text-blue-200'
|
? 'bg-red-900 text-red-200'
|
||||||
}`
|
: 'bg-blue-900 text-blue-200'
|
||||||
statusMessage.classList.remove('hidden')
|
}`;
|
||||||
|
statusMessage.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideStatus() {
|
function hideStatus() {
|
||||||
statusMessage.classList.add('hidden')
|
statusMessage.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFileList() {
|
function updateFileList() {
|
||||||
fileListDiv.innerHTML = ''
|
fileListDiv.innerHTML = '';
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
fileListDiv.classList.add('hidden')
|
fileListDiv.classList.add('hidden');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileListDiv.classList.remove('hidden')
|
fileListDiv.classList.remove('hidden');
|
||||||
selectedFiles.forEach((file) => {
|
selectedFiles.forEach((file) => {
|
||||||
const fileDiv = document.createElement('div')
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2'
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2';
|
||||||
|
|
||||||
const nameSpan = document.createElement('span')
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200'
|
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||||
nameSpan.textContent = file.name
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('span')
|
const sizeSpan = document.createElement('span');
|
||||||
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'
|
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
|
||||||
sizeSpan.textContent = formatBytes(file.size)
|
sizeSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
fileDiv.append(nameSpan, sizeSpan)
|
fileDiv.append(nameSpan, sizeSpan);
|
||||||
fileListDiv.appendChild(fileDiv)
|
fileListDiv.appendChild(fileDiv);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonFilesInput.addEventListener('change', (e) => {
|
jsonFilesInput.addEventListener('change', (e) => {
|
||||||
const target = e.target as HTMLInputElement
|
const target = e.target as HTMLInputElement;
|
||||||
if (target.files && target.files.length > 0) {
|
if (target.files && target.files.length > 0) {
|
||||||
selectedFiles = Array.from(target.files)
|
selectedFiles = Array.from(target.files);
|
||||||
convertBtn.disabled = selectedFiles.length === 0
|
convertBtn.disabled = selectedFiles.length === 0;
|
||||||
updateFileList()
|
updateFileList();
|
||||||
|
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
showStatus('Please select at least 1 JSON file', 'info')
|
showStatus('Please select at least 1 JSON file', 'info');
|
||||||
} else {
|
} else {
|
||||||
showStatus(`${selectedFiles.length} file(s) selected. Ready to convert!`, 'info')
|
showStatus(
|
||||||
|
`${selectedFiles.length} file(s) selected. Ready to convert!`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
async function convertJSONsToPDF() {
|
async function convertJSONsToPDF() {
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
showStatus('Please select at least 1 JSON file', 'error')
|
showStatus('Please select at least 1 JSON file', 'error');
|
||||||
return
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if CPDF is configured
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
convertBtn.disabled = true
|
convertBtn.disabled = true;
|
||||||
showStatus('Reading files (Main Thread)...', 'info')
|
showStatus('Reading files (Main Thread)...', 'info');
|
||||||
|
|
||||||
const fileBuffers = await Promise.all(
|
const fileBuffers = await Promise.all(
|
||||||
selectedFiles.map(file => readFileAsArrayBuffer(file))
|
selectedFiles.map((file) => readFileAsArrayBuffer(file))
|
||||||
)
|
);
|
||||||
|
|
||||||
showStatus('Converting JSONs to PDFs...', 'info')
|
showStatus('Converting JSONs to PDFs...', 'info');
|
||||||
|
|
||||||
worker.postMessage({
|
|
||||||
command: 'convert',
|
|
||||||
fileBuffers: fileBuffers,
|
|
||||||
fileNames: selectedFiles.map(f => f.name)
|
|
||||||
}, fileBuffers);
|
|
||||||
|
|
||||||
|
worker.postMessage(
|
||||||
|
{
|
||||||
|
command: 'convert',
|
||||||
|
fileBuffers: fileBuffers,
|
||||||
|
fileNames: selectedFiles.map((f) => f.name),
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
|
},
|
||||||
|
fileBuffers
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading files:', error)
|
console.error('Error reading files:', error);
|
||||||
showStatus(`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
|
showStatus(
|
||||||
convertBtn.disabled = false
|
`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
convertBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,42 +135,49 @@ worker.onmessage = async (e: MessageEvent) => {
|
|||||||
convertBtn.disabled = false;
|
convertBtn.disabled = false;
|
||||||
|
|
||||||
if (e.data.status === 'success') {
|
if (e.data.status === 'success') {
|
||||||
const pdfFiles = e.data.pdfFiles as Array<{ name: string, data: ArrayBuffer }>;
|
const pdfFiles = e.data.pdfFiles as Array<{
|
||||||
|
name: string;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showStatus('Creating ZIP file...', 'info')
|
showStatus('Creating ZIP file...', 'info');
|
||||||
|
|
||||||
const zip = new JSZip()
|
const zip = new JSZip();
|
||||||
pdfFiles.forEach(({ name, data }) => {
|
pdfFiles.forEach(({ name, data }) => {
|
||||||
const pdfName = name.replace(/\.json$/i, '.pdf')
|
const pdfName = name.replace(/\.json$/i, '.pdf');
|
||||||
const uint8Array = new Uint8Array(data)
|
const uint8Array = new Uint8Array(data);
|
||||||
zip.file(pdfName, uint8Array)
|
zip.file(pdfName, uint8Array);
|
||||||
})
|
});
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' })
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
const url = URL.createObjectURL(zipBlob)
|
const url = URL.createObjectURL(zipBlob);
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a');
|
||||||
a.href = url
|
a.href = url;
|
||||||
a.download = 'jsons-to-pdf.zip'
|
a.download = 'jsons-to-pdf.zip';
|
||||||
downloadFile(zipBlob, 'jsons-to-pdf.zip')
|
downloadFile(zipBlob, 'jsons-to-pdf.zip');
|
||||||
|
|
||||||
showStatus('✅ JSONs converted to PDF successfully! ZIP download started.', 'success')
|
showStatus(
|
||||||
|
'✅ JSONs converted to PDF successfully! ZIP download started.',
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
|
||||||
selectedFiles = []
|
selectedFiles = [];
|
||||||
jsonFilesInput.value = ''
|
jsonFilesInput.value = '';
|
||||||
fileListDiv.innerHTML = ''
|
fileListDiv.innerHTML = '';
|
||||||
fileListDiv.classList.add('hidden')
|
fileListDiv.classList.add('hidden');
|
||||||
convertBtn.disabled = true
|
convertBtn.disabled = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hideStatus()
|
hideStatus();
|
||||||
}, 3000)
|
}, 3000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating ZIP:', error)
|
console.error('Error creating ZIP:', error);
|
||||||
showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
|
showStatus(
|
||||||
|
`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (e.data.status === 'error') {
|
} else if (e.data.status === 'error') {
|
||||||
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
||||||
console.error('Worker Error:', errorMessage);
|
console.error('Worker Error:', errorMessage);
|
||||||
@@ -148,12 +187,12 @@ worker.onmessage = async (e: MessageEvent) => {
|
|||||||
|
|
||||||
if (backToToolsBtn) {
|
if (backToToolsBtn) {
|
||||||
backToToolsBtn.addEventListener('click', () => {
|
backToToolsBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
convertBtn.addEventListener('click', convertJSONsToPDF)
|
convertBtn.addEventListener('click', convertJSONsToPDF);
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
showStatus('Select JSON files to get started', 'info')
|
showStatus('Select JSON files to get started', 'info');
|
||||||
initializeGlobalShortcuts()
|
initializeGlobalShortcuts();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,201 +2,212 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
const FILETYPE = 'mobi';
|
const FILETYPE = 'mobi';
|
||||||
const EXTENSIONS = ['.mobi'];
|
const EXTENSIONS = ['.mobi'];
|
||||||
const TOOL_NAME = 'MOBI';
|
const TOOL_NAME = 'MOBI';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUI = async () => {
|
||||||
|
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||||
|
|
||||||
|
if (state.files.length > 0) {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
|
const file = state.files[index];
|
||||||
|
const fileDiv = document.createElement('div');
|
||||||
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
|
const infoContainer = document.createElement('div');
|
||||||
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('div');
|
||||||
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
|
const metaSpan = document.createElement('div');
|
||||||
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className =
|
||||||
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
createIcons({ icons });
|
||||||
|
fileControls.classList.remove('hidden');
|
||||||
|
processBtn.classList.remove('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
} else {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
processBtn.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateUI = async () => {
|
const resetState = () => {
|
||||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
const convertToPdf = async () => {
|
||||||
fileDisplayArea.innerHTML = '';
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
showLoader('Loading engine...');
|
||||||
const file = state.files[index];
|
const pymupdf = await loadPyMuPDF();
|
||||||
const fileDiv = document.createElement('div');
|
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
if (state.files.length === 1) {
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
const originalFile = state.files[0];
|
||||||
|
showLoader(`Converting ${originalFile.name}...`);
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
filetype: FILETYPE,
|
||||||
nameSpan.textContent = file.name;
|
});
|
||||||
|
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
downloadFile(pdfBlob, fileName);
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
hideLoader();
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${originalFile.name} to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Converting files...');
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
const file = state.files[i];
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
showLoader(
|
||||||
removeBtn.onclick = () => {
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
);
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
filetype: FILETYPE,
|
||||||
}
|
});
|
||||||
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
createIcons({ icons });
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
fileControls.classList.remove('hidden');
|
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
processBtn.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
processBtn.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
state.files = [];
|
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
||||||
state.pdfDoc = null;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertToPdf = async () => {
|
hideLoader();
|
||||||
try {
|
|
||||||
if (state.files.length === 0) {
|
|
||||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showAlert(
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
'Conversion Complete',
|
||||||
await pymupdf.load();
|
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
const originalFile = state.files[0];
|
if (files && files.length > 0) {
|
||||||
showLoader(`Converting ${originalFile.name}...`);
|
state.files = [...state.files, ...Array.from(files)];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
if (fileInput && dropZone) {
|
||||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
downloadFile(pdfBlob, fileName);
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
hideLoader();
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
showAlert(
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
'Conversion Complete',
|
e.preventDefault();
|
||||||
`Successfully converted ${originalFile.name} to PDF.`,
|
dropZone.classList.remove('bg-gray-700');
|
||||||
'success',
|
});
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showLoader('Converting files...');
|
|
||||||
const JSZip = (await import('jszip')).default;
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
const file = state.files[i];
|
e.preventDefault();
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
if (files && files.length > 0) {
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
const name = f.name.toLowerCase();
|
||||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
}
|
});
|
||||||
|
if (validFiles.length > 0) {
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const dataTransfer = new DataTransfer();
|
||||||
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
|
handleFileSelect(dataTransfer.files);
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
fileInput.addEventListener('click', () => {
|
||||||
if (files && files.length > 0) {
|
fileInput.value = '';
|
||||||
state.files = [...state.files, ...Array.from(files)];
|
});
|
||||||
updateUI();
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (addMoreBtn) {
|
||||||
fileInput.addEventListener('change', (e) => {
|
addMoreBtn.addEventListener('click', () => {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
fileInput.click();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
if (clearFilesBtn) {
|
||||||
e.preventDefault();
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
dropZone.classList.add('bg-gray-700');
|
resetState();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', convertToPdf);
|
||||||
dropZone.classList.remove('bg-gray-700');
|
}
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const validFiles = Array.from(files).filter(f => {
|
|
||||||
const name = f.name.toLowerCase();
|
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
|
||||||
});
|
|
||||||
if (validFiles.length > 0) {
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
|
||||||
handleFileSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
|
||||||
resetState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convertToPdf);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
getPDFDocument,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
|
|
||||||
interface LayerData {
|
interface LayerData {
|
||||||
number: number;
|
number: number;
|
||||||
xref: number;
|
xref: number;
|
||||||
text: string;
|
text: string;
|
||||||
on: boolean;
|
on: boolean;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
depth: number;
|
depth: number;
|
||||||
parentXref: number;
|
parentXref: number;
|
||||||
displayOrder: number;
|
displayOrder: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
let currentFile: File | null = null;
|
let currentFile: File | null = null;
|
||||||
let currentDoc: any = null;
|
let currentDoc: any = null;
|
||||||
@@ -23,151 +27,170 @@ const layersMap = new Map<number, LayerData>();
|
|||||||
let nextDisplayOrder = 0;
|
let nextDisplayOrder = 0;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const processBtnContainer = document.getElementById('process-btn-container');
|
const processBtnContainer = document.getElementById('process-btn-container');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const layersContainer = document.getElementById('layers-container');
|
const layersContainer = document.getElementById('layers-container');
|
||||||
const layersList = document.getElementById('layers-list');
|
const layersList = document.getElementById('layers-list');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUI = async () => {
|
||||||
|
if (!fileDisplayArea || !processBtnContainer || !processBtn) return;
|
||||||
|
|
||||||
|
if (currentFile) {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
const fileDiv = document.createElement('div');
|
||||||
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
|
const infoContainer = document.createElement('div');
|
||||||
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('div');
|
||||||
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = currentFile.name;
|
||||||
|
|
||||||
|
const metaSpan = document.createElement('div');
|
||||||
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
|
metaSpan.textContent = `${formatBytes(currentFile.size)} • Loading pages...`;
|
||||||
|
|
||||||
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className =
|
||||||
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
resetState();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await readFileAsArrayBuffer(currentFile);
|
||||||
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
|
metaSpan.textContent = `${formatBytes(currentFile.size)} • ${pdfDoc.numPages} pages`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading PDF:', error);
|
||||||
|
metaSpan.textContent = `${formatBytes(currentFile.size)} • Could not load page count`;
|
||||||
|
}
|
||||||
|
|
||||||
|
createIcons({ icons });
|
||||||
|
processBtnContainer.classList.remove('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
} else {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
processBtnContainer.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateUI = async () => {
|
const resetState = () => {
|
||||||
if (!fileDisplayArea || !processBtnContainer || !processBtn) return;
|
currentFile = null;
|
||||||
|
currentDoc = null;
|
||||||
|
layersMap.clear();
|
||||||
|
nextDisplayOrder = 0;
|
||||||
|
|
||||||
if (currentFile) {
|
if (dropZone) dropZone.style.display = 'flex';
|
||||||
fileDisplayArea.innerHTML = '';
|
if (layersContainer) layersContainer.classList.add('hidden');
|
||||||
const fileDiv = document.createElement('div');
|
updateUI();
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
};
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const promptForInput = (
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
title: string,
|
||||||
|
message: string,
|
||||||
|
defaultValue: string = ''
|
||||||
|
): Promise<string | null> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const modal = document.getElementById('input-modal');
|
||||||
|
const titleEl = document.getElementById('input-title');
|
||||||
|
const messageEl = document.getElementById('input-message');
|
||||||
|
const inputEl = document.getElementById(
|
||||||
|
'input-value'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const confirmBtn = document.getElementById('input-confirm');
|
||||||
|
const cancelBtn = document.getElementById('input-cancel');
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
if (
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
!modal ||
|
||||||
nameSpan.textContent = currentFile.name;
|
!titleEl ||
|
||||||
|
!messageEl ||
|
||||||
|
!inputEl ||
|
||||||
|
!confirmBtn ||
|
||||||
|
!cancelBtn
|
||||||
|
) {
|
||||||
|
console.error('Input modal elements not found');
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
titleEl.textContent = title;
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
messageEl.textContent = message;
|
||||||
metaSpan.textContent = `${formatBytes(currentFile.size)} • Loading pages...`;
|
inputEl.value = defaultValue;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
const closeModal = () => {
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
confirmBtn.onclick = null;
|
||||||
|
cancelBtn.onclick = null;
|
||||||
|
inputEl.onkeydown = null;
|
||||||
|
};
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const confirm = () => {
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
const val = inputEl.value.trim();
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
closeModal();
|
||||||
removeBtn.onclick = () => {
|
resolve(val);
|
||||||
resetState();
|
};
|
||||||
};
|
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
const cancel = () => {
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
closeModal();
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
confirmBtn.onclick = confirm;
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(currentFile);
|
cancelBtn.onclick = cancel;
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
|
||||||
metaSpan.textContent = `${formatBytes(currentFile.size)} • ${pdfDoc.numPages} pages`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading PDF:', error);
|
|
||||||
metaSpan.textContent = `${formatBytes(currentFile.size)} • Could not load page count`;
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
inputEl.onkeydown = (e) => {
|
||||||
processBtnContainer.classList.remove('hidden');
|
if (e.key === 'Enter') confirm();
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
if (e.key === 'Escape') cancel();
|
||||||
} else {
|
};
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
processBtnContainer.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
modal.classList.remove('hidden');
|
||||||
currentFile = null;
|
inputEl.focus();
|
||||||
currentDoc = null;
|
});
|
||||||
layersMap.clear();
|
};
|
||||||
nextDisplayOrder = 0;
|
|
||||||
|
|
||||||
if (dropZone) dropZone.style.display = 'flex';
|
const renderLayers = () => {
|
||||||
if (layersContainer) layersContainer.classList.add('hidden');
|
if (!layersList) return;
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const promptForInput = (title: string, message: string, defaultValue: string = ''): Promise<string | null> => {
|
const layersArray = Array.from(layersMap.values());
|
||||||
return new Promise((resolve) => {
|
|
||||||
const modal = document.getElementById('input-modal');
|
|
||||||
const titleEl = document.getElementById('input-title');
|
|
||||||
const messageEl = document.getElementById('input-message');
|
|
||||||
const inputEl = document.getElementById('input-value') as HTMLInputElement;
|
|
||||||
const confirmBtn = document.getElementById('input-confirm');
|
|
||||||
const cancelBtn = document.getElementById('input-cancel');
|
|
||||||
|
|
||||||
if (!modal || !titleEl || !messageEl || !inputEl || !confirmBtn || !cancelBtn) {
|
if (layersArray.length === 0) {
|
||||||
console.error('Input modal elements not found');
|
layersList.innerHTML = `
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
titleEl.textContent = title;
|
|
||||||
messageEl.textContent = message;
|
|
||||||
inputEl.value = defaultValue;
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
modal.classList.add('hidden');
|
|
||||||
confirmBtn.onclick = null;
|
|
||||||
cancelBtn.onclick = null;
|
|
||||||
inputEl.onkeydown = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirm = () => {
|
|
||||||
const val = inputEl.value.trim();
|
|
||||||
closeModal();
|
|
||||||
resolve(val);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
closeModal();
|
|
||||||
resolve(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
confirmBtn.onclick = confirm;
|
|
||||||
cancelBtn.onclick = cancel;
|
|
||||||
|
|
||||||
inputEl.onkeydown = (e) => {
|
|
||||||
if (e.key === 'Enter') confirm();
|
|
||||||
if (e.key === 'Escape') cancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
modal.classList.remove('hidden');
|
|
||||||
inputEl.focus();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderLayers = () => {
|
|
||||||
if (!layersList) return;
|
|
||||||
|
|
||||||
const layersArray = Array.from(layersMap.values());
|
|
||||||
|
|
||||||
if (layersArray.length === 0) {
|
|
||||||
layersList.innerHTML = `
|
|
||||||
<div class="layers-empty">
|
<div class="layers-empty">
|
||||||
<p>This PDF has no layers (OCG).</p>
|
<p>This PDF has no layers (OCG).</p>
|
||||||
<p>Add a new layer to get started!</p>
|
<p>Add a new layer to get started!</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort layers by displayOrder
|
// Sort layers by displayOrder
|
||||||
const sortedLayers = layersArray.sort((a, b) => a.displayOrder - b.displayOrder);
|
const sortedLayers = layersArray.sort(
|
||||||
|
(a, b) => a.displayOrder - b.displayOrder
|
||||||
|
);
|
||||||
|
|
||||||
layersList.innerHTML = sortedLayers.map((layer: LayerData) => `
|
layersList.innerHTML = sortedLayers
|
||||||
|
.map(
|
||||||
|
(layer: LayerData) => `
|
||||||
<div class="layer-item" data-number="${layer.number}" style="padding-left: ${layer.depth * 24 + 8}px;">
|
<div class="layer-item" data-number="${layer.number}" style="padding-left: ${layer.depth * 24 + 8}px;">
|
||||||
<label class="layer-toggle">
|
<label class="layer-toggle">
|
||||||
<input type="checkbox" ${layer.on ? 'checked' : ''} ${layer.locked ? 'disabled' : ''} data-xref="${layer.xref}" />
|
<input type="checkbox" ${layer.on ? 'checked' : ''} ${layer.locked ? 'disabled' : ''} data-xref="${layer.xref}" />
|
||||||
@@ -179,238 +202,261 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
${!layer.locked ? `<button class="layer-delete" data-xref="${layer.xref}" title="Delete layer">✕</button>` : ''}
|
${!layer.locked ? `<button class="layer-delete" data-xref="${layer.xref}" title="Delete layer">✕</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
|
||||||
// Attach toggle handlers
|
// Attach toggle handlers
|
||||||
layersList.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
|
layersList
|
||||||
checkbox.addEventListener('change', (e) => {
|
.querySelectorAll('input[type="checkbox"]')
|
||||||
const target = e.target as HTMLInputElement;
|
.forEach((checkbox) => {
|
||||||
const xref = parseInt(target.dataset.xref || '0');
|
checkbox.addEventListener('change', (e) => {
|
||||||
const isOn = target.checked;
|
const target = e.target as HTMLInputElement;
|
||||||
|
const xref = parseInt(target.dataset.xref || '0');
|
||||||
|
const isOn = target.checked;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
currentDoc.setLayerVisibility(xref, isOn);
|
currentDoc.setLayerVisibility(xref, isOn);
|
||||||
const layer = Array.from(layersMap.values()).find(l => l.xref === xref);
|
const layer = Array.from(layersMap.values()).find(
|
||||||
if (layer) {
|
(l) => l.xref === xref
|
||||||
layer.on = isOn;
|
);
|
||||||
}
|
if (layer) {
|
||||||
} catch (err) {
|
layer.on = isOn;
|
||||||
console.error('Failed to set layer visibility:', err);
|
}
|
||||||
target.checked = !isOn;
|
} catch (err) {
|
||||||
showAlert('Error', 'Failed to toggle layer visibility');
|
console.error('Failed to set layer visibility:', err);
|
||||||
}
|
target.checked = !isOn;
|
||||||
});
|
showAlert('Error', 'Failed to toggle layer visibility');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Attach delete handlers
|
// Attach delete handlers
|
||||||
layersList.querySelectorAll('.layer-delete').forEach((btn) => {
|
layersList.querySelectorAll('.layer-delete').forEach((btn) => {
|
||||||
btn.addEventListener('click', (e) => {
|
btn.addEventListener('click', (e) => {
|
||||||
const target = e.target as HTMLButtonElement;
|
const target = e.target as HTMLButtonElement;
|
||||||
const xref = parseInt(target.dataset.xref || '0');
|
const xref = parseInt(target.dataset.xref || '0');
|
||||||
const layer = Array.from(layersMap.values()).find(l => l.xref === xref);
|
const layer = Array.from(layersMap.values()).find(
|
||||||
|
(l) => l.xref === xref
|
||||||
|
);
|
||||||
|
|
||||||
if (!layer) {
|
if (!layer) {
|
||||||
showAlert('Error', 'Layer not found');
|
showAlert('Error', 'Layer not found');
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
currentDoc.deleteOCG(layer.number);
|
|
||||||
layersMap.delete(layer.number);
|
|
||||||
renderLayers();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to delete layer:', err);
|
|
||||||
showAlert('Error', 'Failed to delete layer');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
layersList.querySelectorAll('.layer-add-child').forEach((btn) => {
|
|
||||||
btn.addEventListener('click', async (e) => {
|
|
||||||
const target = e.target as HTMLButtonElement;
|
|
||||||
const parentXref = parseInt(target.dataset.xref || '0');
|
|
||||||
const parentLayer = Array.from(layersMap.values()).find(l => l.xref === parentXref);
|
|
||||||
|
|
||||||
const childName = await promptForInput('Add Child Layer', `Enter name for child layer under "${parentLayer?.text || 'Layer'}":`);
|
|
||||||
|
|
||||||
if (!childName || !childName.trim()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const childXref = currentDoc.addOCGWithParent(childName.trim(), parentXref);
|
|
||||||
const parentDisplayOrder = parentLayer?.displayOrder || 0;
|
|
||||||
layersMap.forEach((l) => {
|
|
||||||
if (l.displayOrder > parentDisplayOrder) {
|
|
||||||
l.displayOrder += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
layersMap.set(childXref, {
|
|
||||||
number: childXref,
|
|
||||||
xref: childXref,
|
|
||||||
text: childName.trim(),
|
|
||||||
on: true,
|
|
||||||
locked: false,
|
|
||||||
depth: (parentLayer?.depth || 0) + 1,
|
|
||||||
parentXref: parentXref,
|
|
||||||
displayOrder: parentDisplayOrder + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
renderLayers();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to add child layer:', err);
|
|
||||||
showAlert('Error', 'Failed to add child layer');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadLayers = async () => {
|
|
||||||
if (!currentFile) {
|
|
||||||
showAlert('No File', 'Please select a PDF file.');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Loading engine...');
|
currentDoc.deleteOCG(layer.number);
|
||||||
await pymupdf.load();
|
layersMap.delete(layer.number);
|
||||||
|
renderLayers();
|
||||||
showLoader(`Loading layers from ${currentFile.name}...`);
|
} catch (err) {
|
||||||
currentDoc = await pymupdf.open(currentFile);
|
console.error('Failed to delete layer:', err);
|
||||||
|
showAlert('Error', 'Failed to delete layer');
|
||||||
showLoader('Reading layer configuration...');
|
|
||||||
const existingLayers = currentDoc.getLayerConfig();
|
|
||||||
|
|
||||||
// Reset and populate layers map
|
|
||||||
layersMap.clear();
|
|
||||||
nextDisplayOrder = 0;
|
|
||||||
|
|
||||||
existingLayers.forEach((layer: any) => {
|
|
||||||
layersMap.set(layer.number, {
|
|
||||||
number: layer.number,
|
|
||||||
xref: layer.xref ?? layer.number,
|
|
||||||
text: layer.text,
|
|
||||||
on: layer.on,
|
|
||||||
locked: layer.locked,
|
|
||||||
depth: layer.depth ?? 0,
|
|
||||||
parentXref: layer.parentXref ?? 0,
|
|
||||||
displayOrder: layer.displayOrder ?? nextDisplayOrder++
|
|
||||||
});
|
|
||||||
if ((layer.displayOrder ?? -1) >= nextDisplayOrder) {
|
|
||||||
nextDisplayOrder = layer.displayOrder + 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
// Hide upload zone, show layers container
|
|
||||||
if (dropZone) dropZone.style.display = 'none';
|
|
||||||
if (processBtnContainer) processBtnContainer.classList.add('hidden');
|
|
||||||
if (layersContainer) layersContainer.classList.remove('hidden');
|
|
||||||
|
|
||||||
renderLayers();
|
|
||||||
setupLayerHandlers();
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', error.message || 'Failed to load PDF layers');
|
|
||||||
console.error('Layers error:', error);
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const setupLayerHandlers = () => {
|
layersList.querySelectorAll('.layer-add-child').forEach((btn) => {
|
||||||
const addLayerBtn = document.getElementById('add-layer-btn');
|
btn.addEventListener('click', async (e) => {
|
||||||
const newLayerInput = document.getElementById('new-layer-name') as HTMLInputElement;
|
const target = e.target as HTMLButtonElement;
|
||||||
const saveLayersBtn = document.getElementById('save-layers-btn');
|
const parentXref = parseInt(target.dataset.xref || '0');
|
||||||
|
const parentLayer = Array.from(layersMap.values()).find(
|
||||||
|
(l) => l.xref === parentXref
|
||||||
|
);
|
||||||
|
|
||||||
if (addLayerBtn && newLayerInput) {
|
const childName = await promptForInput(
|
||||||
addLayerBtn.onclick = () => {
|
'Add Child Layer',
|
||||||
const name = newLayerInput.value.trim();
|
`Enter name for child layer under "${parentLayer?.text || 'Layer'}":`
|
||||||
if (!name) {
|
);
|
||||||
showAlert('Invalid Name', 'Please enter a layer name');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
if (!childName || !childName.trim()) return;
|
||||||
const xref = currentDoc.addOCG(name);
|
|
||||||
newLayerInput.value = '';
|
|
||||||
|
|
||||||
const newDisplayOrder = nextDisplayOrder++;
|
try {
|
||||||
layersMap.set(xref, {
|
const childXref = currentDoc.addOCGWithParent(
|
||||||
number: xref,
|
childName.trim(),
|
||||||
xref: xref,
|
parentXref
|
||||||
text: name,
|
);
|
||||||
on: true,
|
const parentDisplayOrder = parentLayer?.displayOrder || 0;
|
||||||
locked: false,
|
layersMap.forEach((l) => {
|
||||||
depth: 0,
|
if (l.displayOrder > parentDisplayOrder) {
|
||||||
parentXref: 0,
|
l.displayOrder += 1;
|
||||||
displayOrder: newDisplayOrder
|
|
||||||
});
|
|
||||||
|
|
||||||
renderLayers();
|
|
||||||
} catch (err: any) {
|
|
||||||
showAlert('Error', 'Failed to add layer: ' + err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (saveLayersBtn) {
|
|
||||||
saveLayersBtn.onclick = () => {
|
|
||||||
try {
|
|
||||||
showLoader('Saving PDF with layer changes...');
|
|
||||||
const pdfBytes = currentDoc.save();
|
|
||||||
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
|
||||||
const outName = currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
|
|
||||||
downloadFile(blob, outName);
|
|
||||||
hideLoader();
|
|
||||||
resetState();
|
|
||||||
showAlert('Success', 'PDF with layer changes saved!', 'success');
|
|
||||||
} catch (err: any) {
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', 'Failed to save PDF: ' + err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const file = files[0];
|
|
||||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
|
||||||
currentFile = file;
|
|
||||||
updateUI();
|
|
||||||
} else {
|
|
||||||
showAlert('Invalid File', 'Please select a PDF file.');
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
layersMap.set(childXref, {
|
||||||
|
number: childXref,
|
||||||
|
xref: childXref,
|
||||||
|
text: childName.trim(),
|
||||||
|
on: true,
|
||||||
|
locked: false,
|
||||||
|
depth: (parentLayer?.depth || 0) + 1,
|
||||||
|
parentXref: parentXref,
|
||||||
|
displayOrder: parentDisplayOrder + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderLayers();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to add child layer:', err);
|
||||||
|
showAlert('Error', 'Failed to add child layer');
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
const loadLayers = async () => {
|
||||||
fileInput.addEventListener('change', (e) => {
|
if (!currentFile) {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
showAlert('No File', 'Please select a PDF file.');
|
||||||
});
|
return;
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processBtn) {
|
try {
|
||||||
processBtn.addEventListener('click', loadLayers);
|
showLoader('Loading engine...');
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
showLoader(`Loading layers from ${currentFile.name}...`);
|
||||||
|
currentDoc = await pymupdf.open(currentFile);
|
||||||
|
|
||||||
|
showLoader('Reading layer configuration...');
|
||||||
|
const existingLayers = currentDoc.getLayerConfig();
|
||||||
|
|
||||||
|
// Reset and populate layers map
|
||||||
|
layersMap.clear();
|
||||||
|
nextDisplayOrder = 0;
|
||||||
|
|
||||||
|
existingLayers.forEach((layer: any) => {
|
||||||
|
layersMap.set(layer.number, {
|
||||||
|
number: layer.number,
|
||||||
|
xref: layer.xref ?? layer.number,
|
||||||
|
text: layer.text,
|
||||||
|
on: layer.on,
|
||||||
|
locked: layer.locked,
|
||||||
|
depth: layer.depth ?? 0,
|
||||||
|
parentXref: layer.parentXref ?? 0,
|
||||||
|
displayOrder: layer.displayOrder ?? nextDisplayOrder++,
|
||||||
|
});
|
||||||
|
if ((layer.displayOrder ?? -1) >= nextDisplayOrder) {
|
||||||
|
nextDisplayOrder = layer.displayOrder + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
// Hide upload zone, show layers container
|
||||||
|
if (dropZone) dropZone.style.display = 'none';
|
||||||
|
if (processBtnContainer) processBtnContainer.classList.add('hidden');
|
||||||
|
if (layersContainer) layersContainer.classList.remove('hidden');
|
||||||
|
|
||||||
|
renderLayers();
|
||||||
|
setupLayerHandlers();
|
||||||
|
} catch (error: any) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', error.message || 'Failed to load PDF layers');
|
||||||
|
console.error('Layers error:', error);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupLayerHandlers = () => {
|
||||||
|
const addLayerBtn = document.getElementById('add-layer-btn');
|
||||||
|
const newLayerInput = document.getElementById(
|
||||||
|
'new-layer-name'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const saveLayersBtn = document.getElementById('save-layers-btn');
|
||||||
|
|
||||||
|
if (addLayerBtn && newLayerInput) {
|
||||||
|
addLayerBtn.onclick = () => {
|
||||||
|
const name = newLayerInput.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
showAlert('Invalid Name', 'Please enter a layer name');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const xref = currentDoc.addOCG(name);
|
||||||
|
newLayerInput.value = '';
|
||||||
|
|
||||||
|
const newDisplayOrder = nextDisplayOrder++;
|
||||||
|
layersMap.set(xref, {
|
||||||
|
number: xref,
|
||||||
|
xref: xref,
|
||||||
|
text: name,
|
||||||
|
on: true,
|
||||||
|
locked: false,
|
||||||
|
depth: 0,
|
||||||
|
parentXref: 0,
|
||||||
|
displayOrder: newDisplayOrder,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderLayers();
|
||||||
|
} catch (err: any) {
|
||||||
|
showAlert('Error', 'Failed to add layer: ' + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveLayersBtn) {
|
||||||
|
saveLayersBtn.onclick = () => {
|
||||||
|
try {
|
||||||
|
showLoader('Saving PDF with layer changes...');
|
||||||
|
const pdfBytes = currentDoc.save();
|
||||||
|
const blob = new Blob([new Uint8Array(pdfBytes)], {
|
||||||
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
|
const outName =
|
||||||
|
currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
|
||||||
|
downloadFile(blob, outName);
|
||||||
|
hideLoader();
|
||||||
|
resetState();
|
||||||
|
showAlert('Success', 'PDF with layer changes saved!', 'success');
|
||||||
|
} catch (err: any) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', 'Failed to save PDF: ' + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const file = files[0];
|
||||||
|
if (
|
||||||
|
file.type === 'application/pdf' ||
|
||||||
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
|
) {
|
||||||
|
currentFile = file;
|
||||||
|
updateUI();
|
||||||
|
} else {
|
||||||
|
showAlert('Invalid File', 'Please select a PDF file.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fileInput && dropZone) {
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('click', () => {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', loadLayers);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,171 +2,186 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
|
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const optionsPanel = document.getElementById('options-panel');
|
const optionsPanel = document.getElementById('options-panel');
|
||||||
|
|
||||||
if (!fileDisplayArea || !optionsPanel) return;
|
if (!fileDisplayArea || !optionsPanel) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
optionsPanel.classList.remove('hidden');
|
optionsPanel.classList.remove('hidden');
|
||||||
|
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = resetState;
|
removeBtn.onclick = resetState;
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
} else {
|
} else {
|
||||||
optionsPanel.classList.add('hidden');
|
optionsPanel.classList.add('hidden');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
file = null;
|
file = null;
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
function tableToCsv(rows: (string | null)[][]): string {
|
function tableToCsv(rows: (string | null)[][]): string {
|
||||||
return rows.map(row =>
|
return rows
|
||||||
row.map(cell => {
|
.map((row) =>
|
||||||
const cellStr = cell ?? '';
|
row
|
||||||
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) {
|
.map((cell) => {
|
||||||
return `"${cellStr.replace(/"/g, '""')}"`;
|
const cellStr = cell ?? '';
|
||||||
}
|
if (
|
||||||
return cellStr;
|
cellStr.includes(',') ||
|
||||||
}).join(',')
|
cellStr.includes('"') ||
|
||||||
).join('\n');
|
cellStr.includes('\n')
|
||||||
|
) {
|
||||||
|
return `"${cellStr.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return cellStr;
|
||||||
|
})
|
||||||
|
.join(',')
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convert() {
|
async function convert() {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
showAlert('No File', 'Please upload a PDF file first.');
|
showAlert('No File', 'Please upload a PDF file first.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Loading Engine...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
showLoader('Extracting tables...');
|
||||||
|
|
||||||
|
const doc = await pymupdf.open(file);
|
||||||
|
const pageCount = doc.pageCount;
|
||||||
|
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
const allRows: (string | null)[][] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
||||||
|
const page = doc.getPage(i);
|
||||||
|
const tables = page.findTables();
|
||||||
|
|
||||||
|
tables.forEach((table) => {
|
||||||
|
allRows.push(...table.rows);
|
||||||
|
allRows.push([]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading Engine...');
|
if (allRows.length === 0) {
|
||||||
|
showAlert('No Tables Found', 'No tables were detected in this PDF.');
|
||||||
try {
|
return;
|
||||||
await pymupdf.load();
|
|
||||||
showLoader('Extracting tables...');
|
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
|
||||||
const pageCount = doc.pageCount;
|
|
||||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
|
||||||
|
|
||||||
const allRows: (string | null)[][] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < pageCount; i++) {
|
|
||||||
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
|
||||||
const page = doc.getPage(i);
|
|
||||||
const tables = page.findTables();
|
|
||||||
|
|
||||||
tables.forEach((table) => {
|
|
||||||
allRows.push(...table.rows);
|
|
||||||
allRows.push([]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allRows.length === 0) {
|
|
||||||
showAlert('No Tables Found', 'No tables were detected in this PDF.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const csvContent = tableToCsv(allRows.filter(row => row.length > 0));
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
downloadFile(blob, `${baseName}.csv`);
|
|
||||||
showAlert('Success', 'PDF converted to CSV successfully!', 'success', resetState);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
|
||||||
showAlert('Error', `Failed to convert PDF to CSV. ${message}`);
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const csvContent = tableToCsv(allRows.filter((row) => row.length > 0));
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
downloadFile(blob, `${baseName}.csv`);
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'PDF converted to CSV successfully!',
|
||||||
|
'success',
|
||||||
|
resetState
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||||
|
showAlert('Error', `Failed to convert PDF to CSV. ${message}`);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileSelect = (newFiles: FileList | null) => {
|
||||||
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
|
const validFile = Array.from(newFiles).find(
|
||||||
|
(f) => f.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validFile) {
|
||||||
|
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileSelect = (newFiles: FileList | null) => {
|
file = validFile;
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
updateUI();
|
||||||
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
|
};
|
||||||
|
|
||||||
if (!validFile) {
|
if (fileInput && dropZone) {
|
||||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
fileInput.addEventListener('change', (e) => {
|
||||||
return;
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
}
|
});
|
||||||
|
|
||||||
file = validFile;
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
updateUI();
|
e.preventDefault();
|
||||||
};
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
fileInput.addEventListener('change', (e) => {
|
e.preventDefault();
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
dropZone.classList.remove('bg-gray-700');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
});
|
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
fileInput.addEventListener('click', () => {
|
||||||
e.preventDefault();
|
fileInput.value = '';
|
||||||
dropZone.classList.remove('bg-gray-700');
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', convert);
|
||||||
dropZone.classList.remove('bg-gray-700');
|
}
|
||||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convert);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,203 +1,216 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
getPDFDocument,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const convertOptions = document.getElementById('convert-options');
|
const convertOptions = document.getElementById('convert-options');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
const updateUI = async () => {
|
||||||
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls) return;
|
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls)
|
||||||
|
return;
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
const file = state.files[index];
|
const file = state.files[index];
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
state.files = state.files.filter((_: File, i: number) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
state.files = state.files.filter((_: File, i: number) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
|
||||||
} catch (error) {
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
fileControls.classList.remove('hidden');
|
|
||||||
convertOptions.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
convertOptions.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
|
||||||
state.files = [];
|
|
||||||
state.pdfDoc = null;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const convert = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (state.files.length === 0) {
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
showAlert('No Files', 'Please select at least one PDF file.');
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
return;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||||
}
|
} catch (error) {
|
||||||
|
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||||
showLoader('Loading PDF converter...');
|
|
||||||
await pymupdf.load();
|
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
|
||||||
const file = state.files[0];
|
|
||||||
showLoader(`Converting ${file.name}...`);
|
|
||||||
|
|
||||||
const docxBlob = await pymupdf.pdfToDocx(file);
|
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '.docx';
|
|
||||||
|
|
||||||
downloadFile(docxBlob, outName);
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${file.name} to DOCX.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showLoader('Converting multiple PDFs...');
|
|
||||||
const JSZip = (await import('jszip')).default;
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
|
||||||
const file = state.files[i];
|
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
|
||||||
|
|
||||||
const docxBlob = await pymupdf.pdfToDocx(file);
|
|
||||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
|
||||||
const arrayBuffer = await docxBlob.arrayBuffer();
|
|
||||||
zip.file(`${baseName}.docx`, arrayBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Creating ZIP archive...');
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
|
|
||||||
downloadFile(zipBlob, 'converted-documents.zip');
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${state.files.length} PDF(s) to DOCX.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
createIcons({ icons });
|
||||||
if (files && files.length > 0) {
|
fileControls.classList.remove('hidden');
|
||||||
const pdfFiles = Array.from(files).filter(
|
convertOptions.classList.remove('hidden');
|
||||||
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
);
|
} else {
|
||||||
state.files = [...state.files, ...pdfFiles];
|
fileDisplayArea.innerHTML = '';
|
||||||
updateUI();
|
fileControls.classList.add('hidden');
|
||||||
|
convertOptions.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
const convert = async () => {
|
||||||
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Loading PDF converter...');
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
if (state.files.length === 1) {
|
||||||
|
const file = state.files[0];
|
||||||
|
showLoader(`Converting ${file.name}...`);
|
||||||
|
|
||||||
|
const docxBlob = await pymupdf.pdfToDocx(file);
|
||||||
|
const outName = file.name.replace(/\.pdf$/i, '') + '.docx';
|
||||||
|
|
||||||
|
downloadFile(docxBlob, outName);
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${file.name} to DOCX.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Converting multiple PDFs...');
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
|
const file = state.files[i];
|
||||||
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const docxBlob = await pymupdf.pdfToDocx(file);
|
||||||
|
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
|
const arrayBuffer = await docxBlob.arrayBuffer();
|
||||||
|
zip.file(`${baseName}.docx`, arrayBuffer);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
showLoader('Creating ZIP archive...');
|
||||||
fileInput.addEventListener('change', (e) => {
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
downloadFile(zipBlob, 'converted-documents.zip');
|
||||||
e.preventDefault();
|
hideLoader();
|
||||||
dropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
showAlert(
|
||||||
e.preventDefault();
|
'Conversion Complete',
|
||||||
dropZone.classList.remove('bg-gray-700');
|
`Successfully converted ${state.files.length} PDF(s) to DOCX.`,
|
||||||
});
|
'success',
|
||||||
|
() => resetState()
|
||||||
dropZone.addEventListener('drop', (e) => {
|
);
|
||||||
e.preventDefault();
|
}
|
||||||
dropZone.classList.remove('bg-gray-700');
|
} catch (e: any) {
|
||||||
const files = e.dataTransfer?.files;
|
hideLoader();
|
||||||
if (files && files.length > 0) {
|
showAlert(
|
||||||
handleFileSelect(files);
|
'Error',
|
||||||
}
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
});
|
);
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (addMoreBtn) {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
addMoreBtn.addEventListener('click', () => {
|
if (files && files.length > 0) {
|
||||||
fileInput.click();
|
const pdfFiles = Array.from(files).filter(
|
||||||
});
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
|
state.files = [...state.files, ...pdfFiles];
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
if (fileInput && dropZone) {
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
fileInput.addEventListener('change', (e) => {
|
||||||
resetState();
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
processBtn.addEventListener('click', convert);
|
e.preventDefault();
|
||||||
}
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
handleFileSelect(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('click', () => {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
|
resetState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', convert);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,182 +1,194 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
|
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
let file: File | null = null;
|
let file: File | null = null;
|
||||||
|
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const optionsPanel = document.getElementById('options-panel');
|
const optionsPanel = document.getElementById('options-panel');
|
||||||
|
|
||||||
if (!fileDisplayArea || !optionsPanel) return;
|
if (!fileDisplayArea || !optionsPanel) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
optionsPanel.classList.remove('hidden');
|
optionsPanel.classList.remove('hidden');
|
||||||
|
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = resetState;
|
removeBtn.onclick = resetState;
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
} else {
|
} else {
|
||||||
optionsPanel.classList.add('hidden');
|
optionsPanel.classList.add('hidden');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
file = null;
|
file = null;
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
async function convert() {
|
async function convert() {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
showAlert('No File', 'Please upload a PDF file first.');
|
showAlert('No File', 'Please upload a PDF file first.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Loading Engine...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
showLoader('Extracting tables...');
|
||||||
|
|
||||||
|
const doc = await pymupdf.open(file);
|
||||||
|
const pageCount = doc.pageCount;
|
||||||
|
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
interface TableData {
|
||||||
|
page: number;
|
||||||
|
rows: (string | null)[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading Engine...');
|
const allTables: TableData[] = [];
|
||||||
|
|
||||||
try {
|
for (let i = 0; i < pageCount; i++) {
|
||||||
await pymupdf.load();
|
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
||||||
showLoader('Extracting tables...');
|
const page = doc.getPage(i);
|
||||||
|
const tables = page.findTables();
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
tables.forEach((table) => {
|
||||||
const pageCount = doc.pageCount;
|
allTables.push({
|
||||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
page: i + 1,
|
||||||
|
rows: table.rows,
|
||||||
interface TableData {
|
});
|
||||||
page: number;
|
});
|
||||||
rows: (string | null)[][];
|
|
||||||
}
|
|
||||||
|
|
||||||
const allTables: TableData[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < pageCount; i++) {
|
|
||||||
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
|
|
||||||
const page = doc.getPage(i);
|
|
||||||
const tables = page.findTables();
|
|
||||||
|
|
||||||
tables.forEach((table) => {
|
|
||||||
allTables.push({
|
|
||||||
page: i + 1,
|
|
||||||
rows: table.rows
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allTables.length === 0) {
|
|
||||||
showAlert('No Tables Found', 'No tables were detected in this PDF.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Creating Excel file...');
|
|
||||||
|
|
||||||
const workbook = XLSX.utils.book_new();
|
|
||||||
|
|
||||||
if (allTables.length === 1) {
|
|
||||||
const worksheet = XLSX.utils.aoa_to_sheet(allTables[0].rows);
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Table');
|
|
||||||
} else {
|
|
||||||
allTables.forEach((table, idx) => {
|
|
||||||
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(0, 31);
|
|
||||||
const worksheet = XLSX.utils.aoa_to_sheet(table.rows);
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const xlsxData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
|
||||||
const blob = new Blob([xlsxData], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
|
||||||
downloadFile(blob, `${baseName}.xlsx`);
|
|
||||||
showAlert('Success', `Extracted ${allTables.length} table(s) to Excel!`, 'success', resetState);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
|
||||||
showAlert('Error', `Failed to convert PDF to Excel. ${message}`);
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allTables.length === 0) {
|
||||||
|
showAlert('No Tables Found', 'No tables were detected in this PDF.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Creating Excel file...');
|
||||||
|
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
|
||||||
|
if (allTables.length === 1) {
|
||||||
|
const worksheet = XLSX.utils.aoa_to_sheet(allTables[0].rows);
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Table');
|
||||||
|
} else {
|
||||||
|
allTables.forEach((table, idx) => {
|
||||||
|
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(
|
||||||
|
0,
|
||||||
|
31
|
||||||
|
);
|
||||||
|
const worksheet = XLSX.utils.aoa_to_sheet(table.rows);
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const xlsxData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||||
|
const blob = new Blob([xlsxData], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
|
downloadFile(blob, `${baseName}.xlsx`);
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Extracted ${allTables.length} table(s) to Excel!`,
|
||||||
|
'success',
|
||||||
|
resetState
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||||
|
showAlert('Error', `Failed to convert PDF to Excel. ${message}`);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileSelect = (newFiles: FileList | null) => {
|
||||||
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
|
const validFile = Array.from(newFiles).find(
|
||||||
|
(f) => f.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validFile) {
|
||||||
|
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileSelect = (newFiles: FileList | null) => {
|
file = validFile;
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
updateUI();
|
||||||
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
|
};
|
||||||
|
|
||||||
if (!validFile) {
|
if (fileInput && dropZone) {
|
||||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
fileInput.addEventListener('change', (e) => {
|
||||||
return;
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
}
|
});
|
||||||
|
|
||||||
file = validFile;
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
updateUI();
|
e.preventDefault();
|
||||||
};
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
fileInput.addEventListener('change', (e) => {
|
e.preventDefault();
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
dropZone.classList.remove('bg-gray-700');
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
});
|
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
fileInput.addEventListener('click', () => {
|
||||||
e.preventDefault();
|
fileInput.value = '';
|
||||||
dropZone.classList.remove('bg-gray-700');
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', convert);
|
||||||
dropZone.classList.remove('bg-gray-700');
|
}
|
||||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convert);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,101 +1,133 @@
|
|||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip';
|
||||||
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
formatBytes,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers';
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||||
|
import {
|
||||||
|
showWasmRequiredDialog,
|
||||||
|
WasmProvider,
|
||||||
|
} from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/pdf-to-json.worker.js');
|
const worker = new Worker(
|
||||||
|
import.meta.env.BASE_URL + 'workers/pdf-to-json.worker.js'
|
||||||
|
);
|
||||||
|
|
||||||
let selectedFiles: File[] = []
|
let selectedFiles: File[] = [];
|
||||||
|
|
||||||
const pdfFilesInput = document.getElementById('pdfFiles') as HTMLInputElement
|
const pdfFilesInput = document.getElementById('pdfFiles') as HTMLInputElement;
|
||||||
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement
|
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement;
|
||||||
const statusMessage = document.getElementById('status-message') as HTMLDivElement
|
const statusMessage = document.getElementById(
|
||||||
const fileListDiv = document.getElementById('fileList') as HTMLDivElement
|
'status-message'
|
||||||
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement
|
) as HTMLDivElement;
|
||||||
|
const fileListDiv = document.getElementById('fileList') as HTMLDivElement;
|
||||||
|
const backToToolsBtn = document.getElementById(
|
||||||
|
'back-to-tools'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
function showStatus(
|
function showStatus(
|
||||||
message: string,
|
message: string,
|
||||||
type: 'success' | 'error' | 'info' = 'info'
|
type: 'success' | 'error' | 'info' = 'info'
|
||||||
) {
|
) {
|
||||||
statusMessage.textContent = message
|
statusMessage.textContent = message;
|
||||||
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
|
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
|
||||||
? 'bg-green-900 text-green-200'
|
type === 'success'
|
||||||
: type === 'error'
|
? 'bg-green-900 text-green-200'
|
||||||
? 'bg-red-900 text-red-200'
|
: type === 'error'
|
||||||
: 'bg-blue-900 text-blue-200'
|
? 'bg-red-900 text-red-200'
|
||||||
}`
|
: 'bg-blue-900 text-blue-200'
|
||||||
statusMessage.classList.remove('hidden')
|
}`;
|
||||||
|
statusMessage.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideStatus() {
|
function hideStatus() {
|
||||||
statusMessage.classList.add('hidden')
|
statusMessage.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFileList() {
|
function updateFileList() {
|
||||||
fileListDiv.innerHTML = ''
|
fileListDiv.innerHTML = '';
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
fileListDiv.classList.add('hidden')
|
fileListDiv.classList.add('hidden');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileListDiv.classList.remove('hidden')
|
fileListDiv.classList.remove('hidden');
|
||||||
selectedFiles.forEach((file) => {
|
selectedFiles.forEach((file) => {
|
||||||
const fileDiv = document.createElement('div')
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2'
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2';
|
||||||
|
|
||||||
const nameSpan = document.createElement('span')
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200'
|
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||||
nameSpan.textContent = file.name
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('span')
|
const sizeSpan = document.createElement('span');
|
||||||
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'
|
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
|
||||||
sizeSpan.textContent = formatBytes(file.size)
|
sizeSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
fileDiv.append(nameSpan, sizeSpan)
|
fileDiv.append(nameSpan, sizeSpan);
|
||||||
fileListDiv.appendChild(fileDiv)
|
fileListDiv.appendChild(fileDiv);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfFilesInput.addEventListener('change', (e) => {
|
pdfFilesInput.addEventListener('change', (e) => {
|
||||||
const target = e.target as HTMLInputElement
|
const target = e.target as HTMLInputElement;
|
||||||
if (target.files && target.files.length > 0) {
|
if (target.files && target.files.length > 0) {
|
||||||
selectedFiles = Array.from(target.files)
|
selectedFiles = Array.from(target.files);
|
||||||
convertBtn.disabled = selectedFiles.length === 0
|
convertBtn.disabled = selectedFiles.length === 0;
|
||||||
updateFileList()
|
updateFileList();
|
||||||
|
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
showStatus('Please select at least 1 PDF file', 'info')
|
showStatus('Please select at least 1 PDF file', 'info');
|
||||||
} else {
|
} else {
|
||||||
showStatus(`${selectedFiles.length} file(s) selected. Ready to convert!`, 'info')
|
showStatus(
|
||||||
|
`${selectedFiles.length} file(s) selected. Ready to convert!`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
async function convertPDFsToJSON() {
|
async function convertPDFsToJSON() {
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
showStatus('Please select at least 1 PDF file', 'error')
|
showStatus('Please select at least 1 PDF file', 'error');
|
||||||
return
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if CPDF is configured
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
convertBtn.disabled = true
|
convertBtn.disabled = true;
|
||||||
showStatus('Reading files (Main Thread)...', 'info')
|
showStatus('Reading files (Main Thread)...', 'info');
|
||||||
|
|
||||||
const fileBuffers = await Promise.all(
|
const fileBuffers = await Promise.all(
|
||||||
selectedFiles.map(file => readFileAsArrayBuffer(file))
|
selectedFiles.map((file) => readFileAsArrayBuffer(file))
|
||||||
)
|
);
|
||||||
|
|
||||||
showStatus('Converting PDFs to JSON..', 'info')
|
showStatus('Converting PDFs to JSON..', 'info');
|
||||||
|
|
||||||
worker.postMessage({
|
|
||||||
command: 'convert',
|
|
||||||
fileBuffers: fileBuffers,
|
|
||||||
fileNames: selectedFiles.map(f => f.name)
|
|
||||||
}, fileBuffers);
|
|
||||||
|
|
||||||
|
worker.postMessage(
|
||||||
|
{
|
||||||
|
command: 'convert',
|
||||||
|
fileBuffers: fileBuffers,
|
||||||
|
fileNames: selectedFiles.map((f) => f.name),
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
|
},
|
||||||
|
fileBuffers
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading files:', error)
|
console.error('Error reading files:', error);
|
||||||
showStatus(`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
|
showStatus(
|
||||||
convertBtn.disabled = false
|
`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
convertBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,38 +135,45 @@ worker.onmessage = async (e: MessageEvent) => {
|
|||||||
convertBtn.disabled = false;
|
convertBtn.disabled = false;
|
||||||
|
|
||||||
if (e.data.status === 'success') {
|
if (e.data.status === 'success') {
|
||||||
const jsonFiles = e.data.jsonFiles as Array<{ name: string, data: ArrayBuffer }>;
|
const jsonFiles = e.data.jsonFiles as Array<{
|
||||||
|
name: string;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showStatus('Creating ZIP file...', 'info')
|
showStatus('Creating ZIP file...', 'info');
|
||||||
|
|
||||||
const zip = new JSZip()
|
const zip = new JSZip();
|
||||||
jsonFiles.forEach(({ name, data }) => {
|
jsonFiles.forEach(({ name, data }) => {
|
||||||
const jsonName = name.replace(/\.pdf$/i, '.json')
|
const jsonName = name.replace(/\.pdf$/i, '.json');
|
||||||
const uint8Array = new Uint8Array(data)
|
const uint8Array = new Uint8Array(data);
|
||||||
zip.file(jsonName, uint8Array)
|
zip.file(jsonName, uint8Array);
|
||||||
})
|
});
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' })
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'pdfs-to-json.zip')
|
downloadFile(zipBlob, 'pdfs-to-json.zip');
|
||||||
|
|
||||||
showStatus('✅ PDFs converted to JSON successfully! ZIP download started.', 'success')
|
showStatus(
|
||||||
|
'✅ PDFs converted to JSON successfully! ZIP download started.',
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
|
||||||
selectedFiles = []
|
selectedFiles = [];
|
||||||
pdfFilesInput.value = ''
|
pdfFilesInput.value = '';
|
||||||
fileListDiv.innerHTML = ''
|
fileListDiv.innerHTML = '';
|
||||||
fileListDiv.classList.add('hidden')
|
fileListDiv.classList.add('hidden');
|
||||||
convertBtn.disabled = true
|
convertBtn.disabled = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hideStatus()
|
hideStatus();
|
||||||
}, 3000)
|
}, 3000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating ZIP:', error)
|
console.error('Error creating ZIP:', error);
|
||||||
showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
|
showStatus(
|
||||||
|
`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
|
'error'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (e.data.status === 'error') {
|
} else if (e.data.status === 'error') {
|
||||||
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
||||||
console.error('Worker Error:', errorMessage);
|
console.error('Worker Error:', errorMessage);
|
||||||
@@ -144,11 +183,11 @@ worker.onmessage = async (e: MessageEvent) => {
|
|||||||
|
|
||||||
if (backToToolsBtn) {
|
if (backToToolsBtn) {
|
||||||
backToToolsBtn.addEventListener('click', () => {
|
backToToolsBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
convertBtn.addEventListener('click', convertPDFsToJSON)
|
convertBtn.addEventListener('click', convertPDFsToJSON);
|
||||||
|
|
||||||
showStatus('Select PDF files to get started', 'info')
|
showStatus('Select PDF files to get started', 'info');
|
||||||
initializeGlobalShortcuts()
|
initializeGlobalShortcuts();
|
||||||
|
|||||||
@@ -1,206 +1,221 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
getPDFDocument,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const convertOptions = document.getElementById('convert-options');
|
const convertOptions = document.getElementById('convert-options');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const includeImagesCheckbox = document.getElementById('include-images') as HTMLInputElement;
|
const includeImagesCheckbox = document.getElementById(
|
||||||
|
'include-images'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
const updateUI = async () => {
|
||||||
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls) return;
|
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls)
|
||||||
|
return;
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
const file = state.files[index];
|
const file = state.files[index];
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
state.files = state.files.filter((_: File, i: number) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
state.files = state.files.filter((_: File, i: number) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
|
||||||
} catch (error) {
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
fileControls.classList.remove('hidden');
|
|
||||||
convertOptions.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
convertOptions.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
|
||||||
state.files = [];
|
|
||||||
state.pdfDoc = null;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const convert = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (state.files.length === 0) {
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
showAlert('No Files', 'Please select at least one PDF file.');
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
return;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||||
}
|
} catch (error) {
|
||||||
|
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||||
showLoader('Loading PDF converter...');
|
|
||||||
await pymupdf.load();
|
|
||||||
|
|
||||||
const includeImages = includeImagesCheckbox?.checked ?? false;
|
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
|
||||||
const file = state.files[0];
|
|
||||||
showLoader(`Converting ${file.name}...`);
|
|
||||||
|
|
||||||
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '.md';
|
|
||||||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
|
||||||
|
|
||||||
downloadFile(blob, outName);
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${file.name} to Markdown.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showLoader('Converting multiple PDFs...');
|
|
||||||
const JSZip = (await import('jszip')).default;
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
|
||||||
const file = state.files[i];
|
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
|
||||||
|
|
||||||
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
|
||||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
|
||||||
zip.file(`${baseName}.md`, markdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Creating ZIP archive...');
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
|
|
||||||
downloadFile(zipBlob, 'markdown-files.zip');
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${state.files.length} PDF(s) to Markdown.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
createIcons({ icons });
|
||||||
if (files && files.length > 0) {
|
fileControls.classList.remove('hidden');
|
||||||
const pdfFiles = Array.from(files).filter(
|
convertOptions.classList.remove('hidden');
|
||||||
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
);
|
} else {
|
||||||
state.files = [...state.files, ...pdfFiles];
|
fileDisplayArea.innerHTML = '';
|
||||||
updateUI();
|
fileControls.classList.add('hidden');
|
||||||
|
convertOptions.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
const convert = async () => {
|
||||||
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Loading PDF converter...');
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
const includeImages = includeImagesCheckbox?.checked ?? false;
|
||||||
|
|
||||||
|
if (state.files.length === 1) {
|
||||||
|
const file = state.files[0];
|
||||||
|
showLoader(`Converting ${file.name}...`);
|
||||||
|
|
||||||
|
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
||||||
|
const outName = file.name.replace(/\.pdf$/i, '') + '.md';
|
||||||
|
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||||||
|
|
||||||
|
downloadFile(blob, outName);
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${file.name} to Markdown.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Converting multiple PDFs...');
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
|
const file = state.files[i];
|
||||||
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
||||||
|
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
|
zip.file(`${baseName}.md`, markdown);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
showLoader('Creating ZIP archive...');
|
||||||
fileInput.addEventListener('change', (e) => {
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
downloadFile(zipBlob, 'markdown-files.zip');
|
||||||
e.preventDefault();
|
hideLoader();
|
||||||
dropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
showAlert(
|
||||||
e.preventDefault();
|
'Conversion Complete',
|
||||||
dropZone.classList.remove('bg-gray-700');
|
`Successfully converted ${state.files.length} PDF(s) to Markdown.`,
|
||||||
});
|
'success',
|
||||||
|
() => resetState()
|
||||||
dropZone.addEventListener('drop', (e) => {
|
);
|
||||||
e.preventDefault();
|
}
|
||||||
dropZone.classList.remove('bg-gray-700');
|
} catch (e: any) {
|
||||||
const files = e.dataTransfer?.files;
|
hideLoader();
|
||||||
if (files && files.length > 0) {
|
showAlert(
|
||||||
handleFileSelect(files);
|
'Error',
|
||||||
}
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
});
|
);
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (addMoreBtn) {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
addMoreBtn.addEventListener('click', () => {
|
if (files && files.length > 0) {
|
||||||
fileInput.click();
|
const pdfFiles = Array.from(files).filter(
|
||||||
});
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
|
state.files = [...state.files, ...pdfFiles];
|
||||||
|
updateUI();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
if (fileInput && dropZone) {
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
fileInput.addEventListener('change', (e) => {
|
||||||
resetState();
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
processBtn.addEventListener('click', convert);
|
e.preventDefault();
|
||||||
}
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
handleFileSelect(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('click', () => {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
|
resetState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', convert);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,228 +1,270 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
downloadFile,
|
||||||
readFileAsArrayBuffer,
|
readFileAsArrayBuffer,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
getPDFDocument,
|
getPDFDocument,
|
||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
|
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const optionsContainer = document.getElementById('options-container');
|
const optionsContainer = document.getElementById('options-container');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const pdfaLevelSelect = document.getElementById('pdfa-level') as HTMLSelectElement;
|
const pdfaLevelSelect = document.getElementById(
|
||||||
|
'pdfa-level'
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
const updateUI = async () => {
|
||||||
if (!fileDisplayArea || !optionsContainer || !processBtn || !fileControls) return;
|
if (!fileDisplayArea || !optionsContainer || !processBtn || !fileControls)
|
||||||
|
return;
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
const file = state.files[index];
|
const file = state.files[index];
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading PDF:', error);
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
fileControls.classList.remove('hidden');
|
|
||||||
optionsContainer.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
optionsContainer.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
|
||||||
state.files = [];
|
|
||||||
state.pdfDoc = null;
|
|
||||||
|
|
||||||
if (pdfaLevelSelect) pdfaLevelSelect.value = 'PDF/A-2b';
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertToPdfA = async () => {
|
|
||||||
const level = pdfaLevelSelect.value as PdfALevel;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (state.files.length === 0) {
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
showAlert('No Files', 'Please select at least one PDF file.');
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
hideLoader();
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||||
return;
|
} catch (error) {
|
||||||
}
|
console.error('Error loading PDF:', error);
|
||||||
|
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
createIcons({ icons });
|
||||||
const originalFile = state.files[0];
|
fileControls.classList.remove('hidden');
|
||||||
|
optionsContainer.classList.remove('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
} else {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
optionsContainer.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
showLoader('Initializing Ghostscript...');
|
const resetState = () => {
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
|
||||||
const convertedBlob = await convertFileToPdfA(
|
if (pdfaLevelSelect) pdfaLevelSelect.value = 'PDF/A-2b';
|
||||||
originalFile,
|
|
||||||
level,
|
|
||||||
(msg) => showLoader(msg)
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileName = originalFile.name.replace(/\.pdf$/i, '') + '_pdfa.pdf';
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
downloadFile(convertedBlob, fileName);
|
const convertToPdfA = async () => {
|
||||||
|
const level = pdfaLevelSelect.value as PdfALevel;
|
||||||
|
|
||||||
hideLoader();
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showAlert(
|
if (state.files.length === 1) {
|
||||||
'Conversion Complete',
|
const originalFile = state.files[0];
|
||||||
`Successfully converted ${originalFile.name} to ${level}.`,
|
const preFlattenCheckbox = document.getElementById(
|
||||||
'success',
|
'pre-flatten'
|
||||||
() => resetState()
|
) as HTMLInputElement;
|
||||||
);
|
const shouldPreFlatten = preFlattenCheckbox?.checked || false;
|
||||||
} else {
|
|
||||||
showLoader('Converting multiple PDFs to PDF/A...');
|
|
||||||
const JSZip = (await import('jszip')).default;
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
let fileToConvert = originalFile;
|
||||||
const file = state.files[i];
|
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
|
||||||
|
|
||||||
const convertedBlob = await convertFileToPdfA(
|
// Pre-flatten using PyMuPDF rasterization if checkbox is checked
|
||||||
file,
|
if (shouldPreFlatten) {
|
||||||
level,
|
if (!isPyMuPDFAvailable()) {
|
||||||
(msg) => showLoader(msg)
|
showWasmRequiredDialog('pymupdf');
|
||||||
);
|
|
||||||
|
|
||||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
|
||||||
const blobBuffer = await convertedBlob.arrayBuffer();
|
|
||||||
zip.file(`${baseName}_pdfa.pdf`, blobBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
|
|
||||||
downloadFile(zipBlob, 'pdfa-converted.zip');
|
|
||||||
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${state.files.length} PDF(s) to ${level}.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert(
|
return;
|
||||||
'Error',
|
}
|
||||||
`An error occurred during conversion. Error: ${e.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
showLoader('Pre-flattening PDF...');
|
||||||
if (files && files.length > 0) {
|
const pymupdf = await loadPyMuPDF();
|
||||||
state.files = [...state.files, ...Array.from(files)];
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
// Rasterize PDF to images and back to PDF (300 DPI for quality)
|
||||||
fileInput.addEventListener('change', (e) => {
|
const flattenedBlob = await (pymupdf as any).rasterizePdf(
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
originalFile,
|
||||||
});
|
{
|
||||||
|
dpi: 300,
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
format: 'png',
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
|
||||||
if (pdfFiles.length > 0) {
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
pdfFiles.forEach(f => dataTransfer.items.add(f));
|
|
||||||
handleFileSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
// Clear value on click to allow re-selecting the same file
|
fileToConvert = new File([flattenedBlob], originalFile.name, {
|
||||||
fileInput.addEventListener('click', () => {
|
type: 'application/pdf',
|
||||||
fileInput.value = '';
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
showLoader('Initializing Ghostscript...');
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
const convertedBlob = await convertFileToPdfA(
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
fileToConvert,
|
||||||
resetState();
|
level,
|
||||||
});
|
(msg) => showLoader(msg)
|
||||||
}
|
);
|
||||||
|
|
||||||
if (processBtn) {
|
const fileName = originalFile.name.replace(/\.pdf$/i, '') + '_pdfa.pdf';
|
||||||
processBtn.addEventListener('click', convertToPdfA);
|
|
||||||
|
downloadFile(convertedBlob, fileName);
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${originalFile.name} to ${level}.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Converting multiple PDFs to PDF/A...');
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
|
const file = state.files[i];
|
||||||
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const convertedBlob = await convertFileToPdfA(file, level, (msg) =>
|
||||||
|
showLoader(msg)
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
|
const blobBuffer = await convertedBlob.arrayBuffer();
|
||||||
|
zip.file(`${baseName}_pdfa.pdf`, blobBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
|
||||||
|
downloadFile(zipBlob, 'pdfa-converted.zip');
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${state.files.length} PDF(s) to ${level}.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
state.files = [...state.files, ...Array.from(files)];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fileInput && dropZone) {
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const pdfFiles = Array.from(files).filter(
|
||||||
|
(f) =>
|
||||||
|
f.type === 'application/pdf' ||
|
||||||
|
f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
|
if (pdfFiles.length > 0) {
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
pdfFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
|
handleFileSelect(dataTransfer.files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear value on click to allow re-selecting the same file
|
||||||
|
fileInput.addEventListener('click', () => {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
|
resetState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', convertToPdfA);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,201 +2,237 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
let pymupdf: any = null;
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
|
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const optionsPanel = document.getElementById('options-panel');
|
const optionsPanel = document.getElementById('options-panel');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
|
|
||||||
if (!fileDisplayArea || !optionsPanel) return;
|
if (!fileDisplayArea || !optionsPanel) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
optionsPanel.classList.remove('hidden');
|
optionsPanel.classList.remove('hidden');
|
||||||
if (fileControls) fileControls.classList.remove('hidden');
|
if (fileControls) fileControls.classList.remove('hidden');
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
files = files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
files = files.filter((_, i) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
});
|
});
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
} else {
|
} else {
|
||||||
optionsPanel.classList.add('hidden');
|
optionsPanel.classList.add('hidden');
|
||||||
if (fileControls) fileControls.classList.add('hidden');
|
if (fileControls) fileControls.classList.add('hidden');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
files = [];
|
files = [];
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
async function convert() {
|
async function convert() {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
showAlert('No Files', 'Please upload at least one PDF file.');
|
showAlert('No Files', 'Please upload at least one PDF file.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if PyMuPDF is configured
|
||||||
|
if (!isPyMuPDFAvailable()) {
|
||||||
|
showWasmRequiredDialog('pymupdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Loading Engine...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load PyMuPDF dynamically if not already loaded
|
||||||
|
if (!pymupdf) {
|
||||||
|
pymupdf = await loadPyMuPDF();
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading Engine...');
|
const isSingleFile = files.length === 1;
|
||||||
|
|
||||||
try {
|
if (isSingleFile) {
|
||||||
await pymupdf.load();
|
const doc = await pymupdf.open(files[0]);
|
||||||
|
const pageCount = doc.pageCount;
|
||||||
|
const baseName = files[0].name.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
const isSingleFile = files.length === 1;
|
if (pageCount === 1) {
|
||||||
|
showLoader('Converting to SVG...');
|
||||||
if (isSingleFile) {
|
const page = doc.getPage(0);
|
||||||
const doc = await pymupdf.open(files[0]);
|
const svgContent = page.toSvg();
|
||||||
const pageCount = doc.pageCount;
|
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||||
const baseName = files[0].name.replace(/\.[^/.]+$/, '');
|
downloadFile(svgBlob, `${baseName}.svg`);
|
||||||
|
showAlert(
|
||||||
if (pageCount === 1) {
|
'Success',
|
||||||
showLoader('Converting to SVG...');
|
'PDF converted to SVG successfully!',
|
||||||
const page = doc.getPage(0);
|
'success',
|
||||||
const svgContent = page.toSvg();
|
() => resetState()
|
||||||
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
|
);
|
||||||
downloadFile(svgBlob, `${baseName}.svg`);
|
} else {
|
||||||
showAlert('Success', 'PDF converted to SVG successfully!', 'success', () => resetState());
|
const zip = new JSZip();
|
||||||
} else {
|
for (let i = 0; i < pageCount; i++) {
|
||||||
const zip = new JSZip();
|
showLoader(`Converting page ${i + 1} of ${pageCount}...`);
|
||||||
for (let i = 0; i < pageCount; i++) {
|
const page = doc.getPage(i);
|
||||||
showLoader(`Converting page ${i + 1} of ${pageCount}...`);
|
const svgContent = page.toSvg();
|
||||||
const page = doc.getPage(i);
|
zip.file(`page_${i + 1}.svg`, svgContent);
|
||||||
const svgContent = page.toSvg();
|
|
||||||
zip.file(`page_${i + 1}.svg`, svgContent);
|
|
||||||
}
|
|
||||||
showLoader('Creating ZIP file...');
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
downloadFile(zipBlob, `${baseName}_svg.zip`);
|
|
||||||
showAlert('Success', `Converted ${pageCount} pages to SVG!`, 'success', () => resetState());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const zip = new JSZip();
|
|
||||||
let totalPages = 0;
|
|
||||||
|
|
||||||
for (let f = 0; f < files.length; f++) {
|
|
||||||
const file = files[f];
|
|
||||||
showLoader(`Processing file ${f + 1} of ${files.length}...`);
|
|
||||||
const doc = await pymupdf.open(file);
|
|
||||||
const pageCount = doc.pageCount;
|
|
||||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
|
||||||
|
|
||||||
for (let i = 0; i < pageCount; i++) {
|
|
||||||
showLoader(`File ${f + 1}/${files.length}: Page ${i + 1}/${pageCount}`);
|
|
||||||
const page = doc.getPage(i);
|
|
||||||
const svgContent = page.toSvg();
|
|
||||||
const fileName = pageCount === 1 ? `${baseName}.svg` : `${baseName}_page_${i + 1}.svg`;
|
|
||||||
zip.file(fileName, svgContent);
|
|
||||||
totalPages++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Creating ZIP file...');
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
downloadFile(zipBlob, 'pdf_to_svg.zip');
|
|
||||||
showAlert('Success', `Converted ${files.length} files (${totalPages} pages) to SVG!`, 'success', () => resetState());
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
showLoader('Creating ZIP file...');
|
||||||
console.error(e);
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
downloadFile(zipBlob, `${baseName}_svg.zip`);
|
||||||
showAlert('Error', `Failed to convert PDF to SVG. ${message}`);
|
showAlert(
|
||||||
} finally {
|
'Success',
|
||||||
hideLoader();
|
`Converted ${pageCount} pages to SVG!`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const zip = new JSZip();
|
||||||
|
let totalPages = 0;
|
||||||
|
|
||||||
|
for (let f = 0; f < files.length; f++) {
|
||||||
|
const file = files[f];
|
||||||
|
showLoader(`Processing file ${f + 1} of ${files.length}...`);
|
||||||
|
const doc = await pymupdf.open(file);
|
||||||
|
const pageCount = doc.pageCount;
|
||||||
|
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
showLoader(
|
||||||
|
`File ${f + 1}/${files.length}: Page ${i + 1}/${pageCount}`
|
||||||
|
);
|
||||||
|
const page = doc.getPage(i);
|
||||||
|
const svgContent = page.toSvg();
|
||||||
|
const fileName =
|
||||||
|
pageCount === 1
|
||||||
|
? `${baseName}.svg`
|
||||||
|
: `${baseName}_page_${i + 1}.svg`;
|
||||||
|
zip.file(fileName, svgContent);
|
||||||
|
totalPages++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Creating ZIP file...');
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'pdf_to_svg.zip');
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Converted ${files.length} files (${totalPages} pages) to SVG!`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||||
|
showAlert('Error', `Failed to convert PDF to SVG. ${message}`);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileSelect = (newFiles: FileList | null, replace = false) => {
|
||||||
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
|
const validFiles = Array.from(newFiles).filter(
|
||||||
|
(file) => file.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validFiles.length === 0) {
|
||||||
|
showAlert('Invalid Files', 'Please upload PDF files.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFileSelect = (newFiles: FileList | null, replace = false) => {
|
if (replace) {
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
files = validFiles;
|
||||||
const validFiles = Array.from(newFiles).filter(
|
} else {
|
||||||
(file) => file.type === 'application/pdf'
|
files = [...files, ...validFiles];
|
||||||
);
|
|
||||||
|
|
||||||
if (validFiles.length === 0) {
|
|
||||||
showAlert('Invalid Files', 'Please upload PDF files.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replace) {
|
|
||||||
files = validFiles;
|
|
||||||
} else {
|
|
||||||
files = [...files, ...validFiles];
|
|
||||||
}
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
|
||||||
fileInput.addEventListener('change', (e) => {
|
|
||||||
handleFileSelect((e.target as HTMLInputElement).files, files.length === 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
handleFileSelect(e.dataTransfer?.files ?? null, files.length === 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput?.click());
|
if (fileInput && dropZone) {
|
||||||
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
fileInput.addEventListener('change', (e) => {
|
||||||
if (processBtn) processBtn.addEventListener('click', convert);
|
handleFileSelect(
|
||||||
|
(e.target as HTMLInputElement).files,
|
||||||
|
files.length === 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
handleFileSelect(e.dataTransfer?.files ?? null, files.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('click', () => {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn)
|
||||||
|
addMoreBtn.addEventListener('click', () => fileInput?.click());
|
||||||
|
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
||||||
|
if (processBtn) processBtn.addEventListener('click', convert);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,212 +1,233 @@
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
let pymupdf: PyMuPDF | null = null;
|
let pymupdf: any = null;
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
} else {
|
} else {
|
||||||
initializePage();
|
initializePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializePage() {
|
function initializePage() {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.addEventListener('change', handleFileUpload);
|
fileInput.addEventListener('change', handleFileUpload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dropZone) {
|
if (dropZone) {
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dropZone.classList.add('bg-gray-600');
|
dropZone.classList.add('bg-gray-600');
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', () => {
|
|
||||||
dropZone.classList.remove('bg-gray-600');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-600');
|
|
||||||
const droppedFiles = e.dataTransfer?.files;
|
|
||||||
if (droppedFiles && droppedFiles.length > 0) {
|
|
||||||
handleFiles(droppedFiles);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput?.addEventListener('click', () => {
|
|
||||||
if (fileInput) fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput?.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
|
||||||
files = [];
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', extractText);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', () => {
|
||||||
|
dropZone.classList.remove('bg-gray-600');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-600');
|
||||||
|
const droppedFiles = e.dataTransfer?.files;
|
||||||
|
if (droppedFiles && droppedFiles.length > 0) {
|
||||||
|
handleFiles(droppedFiles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput?.addEventListener('click', () => {
|
||||||
|
if (fileInput) fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput?.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
|
files = [];
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', extractText);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||||
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileUpload(e: Event) {
|
function handleFileUpload(e: Event) {
|
||||||
const input = e.target as HTMLInputElement;
|
const input = e.target as HTMLInputElement;
|
||||||
if (input.files && input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
handleFiles(input.files);
|
handleFiles(input.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFiles(newFiles: FileList) {
|
function handleFiles(newFiles: FileList) {
|
||||||
const validFiles = Array.from(newFiles).filter(file =>
|
const validFiles = Array.from(newFiles).filter(
|
||||||
file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')
|
(file) =>
|
||||||
|
file.type === 'application/pdf' ||
|
||||||
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validFiles.length < newFiles.length) {
|
||||||
|
showAlert(
|
||||||
|
'Invalid Files',
|
||||||
|
'Some files were skipped. Only PDF files are allowed.'
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (validFiles.length < newFiles.length) {
|
if (validFiles.length > 0) {
|
||||||
showAlert('Invalid Files', 'Some files were skipped. Only PDF files are allowed.');
|
files = [...files, ...validFiles];
|
||||||
}
|
updateUI();
|
||||||
|
}
|
||||||
if (validFiles.length > 0) {
|
|
||||||
files = [...files, ...validFiles];
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
files = [];
|
files = [];
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateUI() {
|
function updateUI() {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const extractOptions = document.getElementById('extract-options');
|
const extractOptions = document.getElementById('extract-options');
|
||||||
|
|
||||||
if (!fileDisplayArea || !fileControls || !extractOptions) return;
|
if (!fileDisplayArea || !fileControls || !extractOptions) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
fileControls.classList.remove('hidden');
|
fileControls.classList.remove('hidden');
|
||||||
extractOptions.classList.remove('hidden');
|
extractOptions.classList.remove('hidden');
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('span');
|
const sizeSpan = document.createElement('span');
|
||||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, sizeSpan);
|
infoContainer.append(nameSpan, sizeSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
files = files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
files = files.filter((_, i) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
});
|
});
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
} else {
|
} else {
|
||||||
fileControls.classList.add('hidden');
|
fileControls.classList.add('hidden');
|
||||||
extractOptions.classList.add('hidden');
|
extractOptions.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensurePyMuPDF(): Promise<PyMuPDF> {
|
async function ensurePyMuPDF(): Promise<any> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
pymupdf = await loadPyMuPDF();
|
||||||
await pymupdf.load();
|
}
|
||||||
}
|
return pymupdf;
|
||||||
return pymupdf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractText() {
|
async function extractText() {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one PDF file.');
|
showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showLoader('Loading engine...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mupdf = await ensurePyMuPDF();
|
const mupdf = await ensurePyMuPDF();
|
||||||
|
|
||||||
if (files.length === 1) {
|
if (files.length === 1) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
showLoader(`Extracting text from ${file.name}...`);
|
showLoader(`Extracting text from ${file.name}...`);
|
||||||
|
|
||||||
const fullText = await mupdf.pdfToText(file);
|
const fullText = await mupdf.pdfToText(file);
|
||||||
|
|
||||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
const textBlob = new Blob([fullText], { type: 'text/plain;charset=utf-8' });
|
const textBlob = new Blob([fullText], {
|
||||||
downloadFile(textBlob, `${baseName}.txt`);
|
type: 'text/plain;charset=utf-8',
|
||||||
|
});
|
||||||
|
downloadFile(textBlob, `${baseName}.txt`);
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Success', 'Text extracted successfully!', 'success', () => {
|
showAlert('Success', 'Text extracted successfully!', 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showLoader('Extracting text from multiple files...');
|
showLoader('Extracting text from multiple files...');
|
||||||
|
|
||||||
const JSZip = (await import('jszip')).default;
|
const JSZip = (await import('jszip')).default;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
showLoader(`Extracting text from file ${i + 1}/${files.length}: ${file.name}...`);
|
showLoader(
|
||||||
|
`Extracting text from file ${i + 1}/${files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
const fullText = await mupdf.pdfToText(file);
|
const fullText = await mupdf.pdfToText(file);
|
||||||
|
|
||||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
zip.file(`${baseName}.txt`, fullText);
|
zip.file(`${baseName}.txt`, fullText);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'pdf-to-text.zip');
|
downloadFile(zipBlob, 'pdf-to-text.zip');
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Success', `Extracted text from ${files.length} PDF files!`, 'success', () => {
|
showAlert(
|
||||||
resetState();
|
'Success',
|
||||||
});
|
`Extracted text from ${files.length} PDF files!`,
|
||||||
|
'success',
|
||||||
|
() => {
|
||||||
|
resetState();
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
);
|
||||||
console.error('[PDFToText]', e);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Extraction Error', e.message || 'Failed to extract text from PDF.');
|
|
||||||
}
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[PDFToText]', e);
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Extraction Error',
|
||||||
|
e.message || 'Failed to extract text from PDF.'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,204 +1,237 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
getPDFDocument,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const extractOptions = document.getElementById('extract-options');
|
const extractOptions = document.getElementById('extract-options');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
const updateUI = async () => {
|
||||||
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls) return;
|
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls)
|
||||||
|
return;
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
const file = state.files[index];
|
const file = state.files[index];
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading PDF:', error);
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
fileControls.classList.remove('hidden');
|
|
||||||
extractOptions.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
extractOptions.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
|
||||||
state.files = [];
|
|
||||||
state.pdfDoc = null;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractForAI = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (state.files.length === 0) {
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
showAlert('No Files', 'Please select at least one PDF file.');
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
return;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error('Error loading PDF:', error);
|
||||||
showLoader('Loading engine...');
|
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||||
await pymupdf.load();
|
|
||||||
|
|
||||||
const total = state.files.length;
|
|
||||||
let completed = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
if (total === 1) {
|
|
||||||
const file = state.files[0];
|
|
||||||
showLoader(`Extracting ${file.name} for AI...`);
|
|
||||||
|
|
||||||
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
|
||||||
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
|
||||||
downloadFile(new Blob([jsonContent], { type: 'application/json' }), outName);
|
|
||||||
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Extraction Complete', `Successfully extracted PDF for AI/LLM use.`, 'success', () => resetState());
|
|
||||||
} else {
|
|
||||||
// Multiple files - create ZIP
|
|
||||||
const JSZip = (await import('jszip')).default;
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (const file of state.files) {
|
|
||||||
try {
|
|
||||||
showLoader(`Extracting ${file.name} for AI (${completed + 1}/${total})...`);
|
|
||||||
|
|
||||||
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
|
||||||
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
|
||||||
zip.file(outName, jsonContent);
|
|
||||||
|
|
||||||
completed++;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to extract ${file.name}:`, error);
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Creating ZIP archive...');
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
|
|
||||||
downloadFile(zipBlob, 'pdf-for-ai.zip');
|
|
||||||
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
if (failed === 0) {
|
|
||||||
showAlert('Extraction Complete', `Successfully extracted ${completed} PDF(s) for AI/LLM use.`, 'success', () => resetState());
|
|
||||||
} else {
|
|
||||||
showAlert('Extraction Partial', `Extracted ${completed} PDF(s), failed ${failed}.`, 'warning', () => resetState());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during extraction. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
createIcons({ icons });
|
||||||
if (files && files.length > 0) {
|
fileControls.classList.remove('hidden');
|
||||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
extractOptions.classList.remove('hidden');
|
||||||
if (pdfFiles.length > 0) {
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
state.files = [...state.files, ...pdfFiles];
|
} else {
|
||||||
updateUI();
|
fileDisplayArea.innerHTML = '';
|
||||||
}
|
fileControls.classList.add('hidden');
|
||||||
|
extractOptions.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractForAI = async () => {
|
||||||
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Loading engine...');
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
const total = state.files.length;
|
||||||
|
let completed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
if (total === 1) {
|
||||||
|
const file = state.files[0];
|
||||||
|
showLoader(`Extracting ${file.name} for AI...`);
|
||||||
|
|
||||||
|
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
||||||
|
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
||||||
|
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
||||||
|
downloadFile(
|
||||||
|
new Blob([jsonContent], { type: 'application/json' }),
|
||||||
|
outName
|
||||||
|
);
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Extraction Complete',
|
||||||
|
`Successfully extracted PDF for AI/LLM use.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Multiple files - create ZIP
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
for (const file of state.files) {
|
||||||
|
try {
|
||||||
|
showLoader(
|
||||||
|
`Extracting ${file.name} for AI (${completed + 1}/${total})...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
||||||
|
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
||||||
|
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
||||||
|
zip.file(outName, jsonContent);
|
||||||
|
|
||||||
|
completed++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to extract ${file.name}:`, error);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
showLoader('Creating ZIP archive...');
|
||||||
fileInput.addEventListener('change', (e) => {
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
downloadFile(zipBlob, 'pdf-for-ai.zip');
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
hideLoader();
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
if (failed === 0) {
|
||||||
e.preventDefault();
|
showAlert(
|
||||||
dropZone.classList.remove('bg-gray-700');
|
'Extraction Complete',
|
||||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
`Successfully extracted ${completed} PDF(s) for AI/LLM use.`,
|
||||||
});
|
'success',
|
||||||
|
() => resetState()
|
||||||
fileInput.addEventListener('click', () => {
|
);
|
||||||
fileInput.value = '';
|
} else {
|
||||||
});
|
showAlert(
|
||||||
|
'Extraction Partial',
|
||||||
|
`Extracted ${completed} PDF(s), failed ${failed}.`,
|
||||||
|
'warning',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during extraction. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (addMoreBtn) {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
addMoreBtn.addEventListener('click', () => {
|
if (files && files.length > 0) {
|
||||||
fileInput.click();
|
const pdfFiles = Array.from(files).filter(
|
||||||
});
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
|
if (pdfFiles.length > 0) {
|
||||||
|
state.files = [...state.files, ...pdfFiles];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
if (fileInput && dropZone) {
|
||||||
clearFilesBtn.addEventListener('click', resetState);
|
fileInput.addEventListener('change', (e) => {
|
||||||
}
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
if (processBtn) {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
processBtn.addEventListener('click', extractForAI);
|
e.preventDefault();
|
||||||
}
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('click', () => {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', resetState);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', extractForAI);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,133 +2,165 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
const ACCEPTED_EXTENSIONS = ['.psd'];
|
const ACCEPTED_EXTENSIONS = ['.psd'];
|
||||||
const FILETYPE_NAME = 'PSD';
|
const FILETYPE_NAME = 'PSD';
|
||||||
|
|
||||||
let pymupdf: PyMuPDF | null = null;
|
let pymupdf: any = null;
|
||||||
|
|
||||||
async function ensurePyMuPDF(): Promise<PyMuPDF> {
|
async function ensurePyMuPDF(): Promise<any> {
|
||||||
if (!pymupdf) {
|
if (!pymupdf) {
|
||||||
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
pymupdf = await loadPyMuPDF();
|
||||||
await pymupdf.load();
|
}
|
||||||
}
|
return pymupdf;
|
||||||
return pymupdf;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUI = async () => {
|
||||||
|
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||||
|
if (state.files.length > 0) {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
|
const file = state.files[index];
|
||||||
|
const fileDiv = document.createElement('div');
|
||||||
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
const infoContainer = document.createElement('div');
|
||||||
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
const nameSpan = document.createElement('div');
|
||||||
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
const metaSpan = document.createElement('div');
|
||||||
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className =
|
||||||
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
}
|
||||||
|
createIcons({ icons });
|
||||||
|
fileControls.classList.remove('hidden');
|
||||||
|
processBtn.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
processBtn.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateUI = async () => {
|
const resetState = () => {
|
||||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
state.files = [];
|
||||||
if (state.files.length > 0) {
|
updateUI();
|
||||||
fileDisplayArea.innerHTML = '';
|
};
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
|
||||||
const file = state.files[index];
|
|
||||||
const fileDiv = document.createElement('div');
|
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
|
||||||
const infoContainer = document.createElement('div');
|
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
|
||||||
const nameSpan = document.createElement('div');
|
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
|
||||||
nameSpan.textContent = file.name;
|
|
||||||
const metaSpan = document.createElement('div');
|
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
|
||||||
const removeBtn = document.createElement('button');
|
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
|
||||||
removeBtn.onclick = () => {
|
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
|
||||||
}
|
|
||||||
createIcons({ icons });
|
|
||||||
fileControls.classList.remove('hidden');
|
|
||||||
processBtn.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
processBtn.classList.add('hidden');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
const convert = async () => {
|
||||||
state.files = [];
|
if (state.files.length === 0) {
|
||||||
|
showAlert(
|
||||||
|
'No Files',
|
||||||
|
`Please select at least one ${FILETYPE_NAME} file.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
showLoader('Loading engine...');
|
||||||
|
const mupdf = await ensurePyMuPDF();
|
||||||
|
|
||||||
|
if (state.files.length === 1) {
|
||||||
|
const file = state.files[0];
|
||||||
|
showLoader(`Converting ${file.name}...`);
|
||||||
|
const pdfBlob = await mupdf.imageToPdf(file, { imageType: 'psd' });
|
||||||
|
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||||
|
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${file.name} to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Converting multiple files...');
|
||||||
|
const pdfBlob = await mupdf.imagesToPdf(state.files);
|
||||||
|
downloadFile(pdfBlob, 'psd_to_pdf.pdf');
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${state.files.length} PSD files to a single PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
hideLoader();
|
||||||
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||||
|
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const validFiles = Array.from(files).filter((file) => {
|
||||||
|
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||||
|
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||||
|
});
|
||||||
|
if (validFiles.length > 0) {
|
||||||
|
state.files = [...state.files, ...validFiles];
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
}
|
||||||
|
|
||||||
const convert = async () => {
|
|
||||||
if (state.files.length === 0) {
|
|
||||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
showLoader('Loading engine...');
|
|
||||||
const mupdf = await ensurePyMuPDF();
|
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
|
||||||
const file = state.files[0];
|
|
||||||
showLoader(`Converting ${file.name}...`);
|
|
||||||
const pdfBlob = await mupdf.imageToPdf(file, { imageType: 'psd' });
|
|
||||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
|
||||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
|
||||||
} else {
|
|
||||||
showLoader('Converting multiple files...');
|
|
||||||
const pdfBlob = await mupdf.imagesToPdf(state.files);
|
|
||||||
downloadFile(pdfBlob, 'psd_to_pdf.pdf');
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} PSD files to a single PDF.`, 'success', () => resetState());
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
hideLoader();
|
|
||||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
||||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const validFiles = Array.from(files).filter(file => {
|
|
||||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
|
||||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
|
||||||
});
|
|
||||||
if (validFiles.length > 0) {
|
|
||||||
state.files = [...state.files, ...validFiles];
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
|
||||||
fileInput.addEventListener('change', (e) => handleFileSelect((e.target as HTMLInputElement).files));
|
|
||||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
|
|
||||||
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); });
|
|
||||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); handleFileSelect(e.dataTransfer?.files ?? null); });
|
|
||||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
|
||||||
}
|
}
|
||||||
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput.click());
|
};
|
||||||
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
|
||||||
if (processBtn) processBtn.addEventListener('click', convert);
|
if (fileInput && dropZone) {
|
||||||
|
fileInput.addEventListener('change', (e) =>
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files)
|
||||||
|
);
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||||
|
});
|
||||||
|
fileInput.addEventListener('click', () => {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||||
|
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
||||||
|
if (processBtn) processBtn.addEventListener('click', convert);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,219 +1,262 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
getPDFDocument,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const rasterizeOptions = document.getElementById('rasterize-options');
|
const rasterizeOptions = document.getElementById('rasterize-options');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = async () => {
|
const updateUI = async () => {
|
||||||
if (!fileDisplayArea || !rasterizeOptions || !processBtn || !fileControls) return;
|
if (!fileDisplayArea || !rasterizeOptions || !processBtn || !fileControls)
|
||||||
|
return;
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
const file = state.files[index];
|
const file = state.files[index];
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
const infoContainer = document.createElement('div');
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const nameSpan = document.createElement('div');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
removeBtn.className =
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
removeBtn.onclick = () => {
|
||||||
updateUI();
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
};
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
try {
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
|
||||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading PDF:', error);
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
fileControls.classList.remove('hidden');
|
|
||||||
rasterizeOptions.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
rasterizeOptions.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
|
||||||
state.files = [];
|
|
||||||
state.pdfDoc = null;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const rasterize = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (state.files.length === 0) {
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
showAlert('No Files', 'Please select at least one PDF file.');
|
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
return;
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error('Error loading PDF:', error);
|
||||||
showLoader('Loading engine...');
|
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||||
await pymupdf.load();
|
|
||||||
|
|
||||||
// Get options from UI
|
|
||||||
const dpi = parseInt((document.getElementById('rasterize-dpi') as HTMLSelectElement).value) || 150;
|
|
||||||
const format = (document.getElementById('rasterize-format') as HTMLSelectElement).value as 'png' | 'jpeg';
|
|
||||||
const grayscale = (document.getElementById('rasterize-grayscale') as HTMLInputElement).checked;
|
|
||||||
|
|
||||||
const total = state.files.length;
|
|
||||||
let completed = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
if (total === 1) {
|
|
||||||
const file = state.files[0];
|
|
||||||
showLoader(`Rasterizing ${file.name}...`);
|
|
||||||
|
|
||||||
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
|
||||||
dpi,
|
|
||||||
format,
|
|
||||||
grayscale,
|
|
||||||
quality: 95
|
|
||||||
});
|
|
||||||
|
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
|
||||||
downloadFile(rasterizedBlob, outName);
|
|
||||||
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Rasterization Complete', `Successfully rasterized PDF at ${dpi} DPI.`, 'success', () => resetState());
|
|
||||||
} else {
|
|
||||||
// Multiple files - create ZIP
|
|
||||||
const JSZip = (await import('jszip')).default;
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (const file of state.files) {
|
|
||||||
try {
|
|
||||||
showLoader(`Rasterizing ${file.name} (${completed + 1}/${total})...`);
|
|
||||||
|
|
||||||
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
|
||||||
dpi,
|
|
||||||
format,
|
|
||||||
grayscale,
|
|
||||||
quality: 95
|
|
||||||
});
|
|
||||||
|
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
|
||||||
zip.file(outName, rasterizedBlob);
|
|
||||||
|
|
||||||
completed++;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to rasterize ${file.name}:`, error);
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Creating ZIP archive...');
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
|
|
||||||
downloadFile(zipBlob, 'rasterized-pdfs.zip');
|
|
||||||
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
if (failed === 0) {
|
|
||||||
showAlert('Rasterization Complete', `Successfully rasterized ${completed} PDF(s) at ${dpi} DPI.`, 'success', () => resetState());
|
|
||||||
} else {
|
|
||||||
showAlert('Rasterization Partial', `Rasterized ${completed} PDF(s), failed ${failed}.`, 'warning', () => resetState());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during rasterization. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
createIcons({ icons });
|
||||||
if (files && files.length > 0) {
|
fileControls.classList.remove('hidden');
|
||||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
rasterizeOptions.classList.remove('hidden');
|
||||||
if (pdfFiles.length > 0) {
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
state.files = [...state.files, ...pdfFiles];
|
} else {
|
||||||
updateUI();
|
fileDisplayArea.innerHTML = '';
|
||||||
}
|
fileControls.classList.add('hidden');
|
||||||
|
rasterizeOptions.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetState = () => {
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rasterize = async () => {
|
||||||
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPyMuPDFAvailable()) {
|
||||||
|
showWasmRequiredDialog('pymupdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Loading engine...');
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
// Get options from UI
|
||||||
|
const dpi =
|
||||||
|
parseInt(
|
||||||
|
(document.getElementById('rasterize-dpi') as HTMLSelectElement).value
|
||||||
|
) || 150;
|
||||||
|
const format = (
|
||||||
|
document.getElementById('rasterize-format') as HTMLSelectElement
|
||||||
|
).value as 'png' | 'jpeg';
|
||||||
|
const grayscale = (
|
||||||
|
document.getElementById('rasterize-grayscale') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
|
||||||
|
const total = state.files.length;
|
||||||
|
let completed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
if (total === 1) {
|
||||||
|
const file = state.files[0];
|
||||||
|
showLoader(`Rasterizing ${file.name}...`);
|
||||||
|
|
||||||
|
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
||||||
|
dpi,
|
||||||
|
format,
|
||||||
|
grayscale,
|
||||||
|
quality: 95,
|
||||||
|
});
|
||||||
|
|
||||||
|
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
||||||
|
downloadFile(rasterizedBlob, outName);
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Rasterization Complete',
|
||||||
|
`Successfully rasterized PDF at ${dpi} DPI.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Multiple files - create ZIP
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
for (const file of state.files) {
|
||||||
|
try {
|
||||||
|
showLoader(
|
||||||
|
`Rasterizing ${file.name} (${completed + 1}/${total})...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
||||||
|
dpi,
|
||||||
|
format,
|
||||||
|
grayscale,
|
||||||
|
quality: 95,
|
||||||
|
});
|
||||||
|
|
||||||
|
const outName =
|
||||||
|
file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
||||||
|
zip.file(outName, rasterizedBlob);
|
||||||
|
|
||||||
|
completed++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to rasterize ${file.name}:`, error);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
showLoader('Creating ZIP archive...');
|
||||||
fileInput.addEventListener('change', (e) => {
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
downloadFile(zipBlob, 'rasterized-pdfs.zip');
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.add('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
hideLoader();
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
if (failed === 0) {
|
||||||
e.preventDefault();
|
showAlert(
|
||||||
dropZone.classList.remove('bg-gray-700');
|
'Rasterization Complete',
|
||||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
`Successfully rasterized ${completed} PDF(s) at ${dpi} DPI.`,
|
||||||
});
|
'success',
|
||||||
|
() => resetState()
|
||||||
fileInput.addEventListener('click', () => {
|
);
|
||||||
fileInput.value = '';
|
} else {
|
||||||
});
|
showAlert(
|
||||||
|
'Rasterization Partial',
|
||||||
|
`Rasterized ${completed} PDF(s), failed ${failed}.`,
|
||||||
|
'warning',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during rasterization. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (addMoreBtn) {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
addMoreBtn.addEventListener('click', () => {
|
if (files && files.length > 0) {
|
||||||
fileInput.click();
|
const pdfFiles = Array.from(files).filter(
|
||||||
});
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
|
if (pdfFiles.length > 0) {
|
||||||
|
state.files = [...state.files, ...pdfFiles];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
if (fileInput && dropZone) {
|
||||||
clearFilesBtn.addEventListener('click', resetState);
|
fileInput.addEventListener('change', (e) => {
|
||||||
}
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
if (processBtn) {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
processBtn.addEventListener('click', rasterize);
|
e.preventDefault();
|
||||||
}
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('click', () => {
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', resetState);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', rasterize);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,14 @@
|
|||||||
import { downloadFile, formatBytes } from "../utils/helpers";
|
import { downloadFile, formatBytes } from '../utils/helpers';
|
||||||
import { initializeGlobalShortcuts } from "../utils/shortcuts-init.js";
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||||
|
import {
|
||||||
|
showWasmRequiredDialog,
|
||||||
|
WasmProvider,
|
||||||
|
} from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
|
const worker = new Worker(
|
||||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js');
|
import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js'
|
||||||
|
);
|
||||||
|
|
||||||
let pdfFile: File | null = null;
|
let pdfFile: File | null = null;
|
||||||
|
|
||||||
@@ -55,12 +61,13 @@ function showStatus(
|
|||||||
type: 'success' | 'error' | 'info' = 'info'
|
type: 'success' | 'error' | 'info' = 'info'
|
||||||
) {
|
) {
|
||||||
statusMessage.textContent = message;
|
statusMessage.textContent = message;
|
||||||
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
|
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
|
||||||
? 'bg-green-900 text-green-200'
|
type === 'success'
|
||||||
: type === 'error'
|
? 'bg-green-900 text-green-200'
|
||||||
? 'bg-red-900 text-red-200'
|
: type === 'error'
|
||||||
: 'bg-blue-900 text-blue-200'
|
? 'bg-red-900 text-red-200'
|
||||||
}`;
|
: 'bg-blue-900 text-blue-200'
|
||||||
|
}`;
|
||||||
statusMessage.classList.remove('hidden');
|
statusMessage.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +137,12 @@ async function generateTableOfContents() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if CPDF is configured
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
generateBtn.disabled = true;
|
generateBtn.disabled = true;
|
||||||
showStatus('Reading file (Main Thread)...', 'info');
|
showStatus('Reading file (Main Thread)...', 'info');
|
||||||
@@ -143,13 +156,14 @@ async function generateTableOfContents() {
|
|||||||
const fontFamily = parseInt(fontFamilySelect.value, 10);
|
const fontFamily = parseInt(fontFamilySelect.value, 10);
|
||||||
const addBookmark = addBookmarkCheckbox.checked;
|
const addBookmark = addBookmarkCheckbox.checked;
|
||||||
|
|
||||||
const message: GenerateTOCMessage = {
|
const message = {
|
||||||
command: 'generate-toc',
|
command: 'generate-toc',
|
||||||
pdfData: arrayBuffer,
|
pdfData: arrayBuffer,
|
||||||
title,
|
title,
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
addBookmark,
|
addBookmark,
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.postMessage(message, [arrayBuffer]);
|
worker.postMessage(message, [arrayBuffer]);
|
||||||
@@ -171,7 +185,10 @@ worker.onmessage = (e: MessageEvent<TOCWorkerResponse>) => {
|
|||||||
const pdfBytes = new Uint8Array(pdfBytesBuffer);
|
const pdfBytes = new Uint8Array(pdfBytesBuffer);
|
||||||
|
|
||||||
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
||||||
downloadFile(blob, pdfFile?.name.replace('.pdf', '_with_toc.pdf') || 'output_with_toc.pdf');
|
downloadFile(
|
||||||
|
blob,
|
||||||
|
pdfFile?.name.replace('.pdf', '_with_toc.pdf') || 'output_with_toc.pdf'
|
||||||
|
);
|
||||||
|
|
||||||
showStatus(
|
showStatus(
|
||||||
'Table of contents generated successfully! Download started.',
|
'Table of contents generated successfully! Download started.',
|
||||||
|
|||||||
@@ -1,252 +1,280 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
let currentMode: 'upload' | 'text' = 'upload';
|
let currentMode: 'upload' | 'text' = 'upload';
|
||||||
|
|
||||||
// RTL character detection pattern (Arabic, Hebrew, Persian, etc.)
|
// RTL character detection pattern (Arabic, Hebrew, Persian, etc.)
|
||||||
const RTL_PATTERN = /[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u08A0-\u08FF\uFB1D-\uFB4F\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
const RTL_PATTERN =
|
||||||
|
/[\u0590-\u05FF\u0600-\u06FF\u0700-\u074F\u0750-\u077F\u0780-\u07BF\u07C0-\u07FF\u08A0-\u08FF\uFB1D-\uFB4F\uFB50-\uFDFF\uFE70-\uFEFF]/;
|
||||||
|
|
||||||
function hasRtlCharacters(text: string): boolean {
|
function hasRtlCharacters(text: string): boolean {
|
||||||
return RTL_PATTERN.test(text);
|
return RTL_PATTERN.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
|
|
||||||
if (!fileDisplayArea || !fileControls || !dropZone) return;
|
if (!fileDisplayArea || !fileControls || !dropZone) return;
|
||||||
|
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
if (files.length > 0 && currentMode === 'upload') {
|
if (files.length > 0 && currentMode === 'upload') {
|
||||||
dropZone.classList.add('hidden');
|
dropZone.classList.add('hidden');
|
||||||
fileControls.classList.remove('hidden');
|
fileControls.classList.remove('hidden');
|
||||||
|
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const infoSpan = document.createElement('span');
|
const infoSpan = document.createElement('span');
|
||||||
infoSpan.className = 'truncate font-medium text-gray-200';
|
infoSpan.className = 'truncate font-medium text-gray-200';
|
||||||
infoSpan.textContent = file.name;
|
infoSpan.textContent = file.name;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('span');
|
const sizeSpan = document.createElement('span');
|
||||||
sizeSpan.className = 'text-gray-400 text-xs ml-2';
|
sizeSpan.className = 'text-gray-400 text-xs ml-2';
|
||||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300';
|
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300';
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.onclick = () => {
|
removeBtn.onclick = () => {
|
||||||
files = files.filter((_, i) => i !== index);
|
files = files.filter((_, i) => i !== index);
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
fileDiv.append(infoSpan, sizeSpan, removeBtn);
|
fileDiv.append(infoSpan, sizeSpan, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
});
|
});
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
} else {
|
} else {
|
||||||
dropZone.classList.remove('hidden');
|
dropZone.classList.remove('hidden');
|
||||||
fileControls.classList.add('hidden');
|
fileControls.classList.add('hidden');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
files = [];
|
files = [];
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
|
const textInput = document.getElementById(
|
||||||
if (fileInput) fileInput.value = '';
|
'text-input'
|
||||||
if (textInput) textInput.value = '';
|
) as HTMLTextAreaElement;
|
||||||
updateUI();
|
if (fileInput) fileInput.value = '';
|
||||||
|
if (textInput) textInput.value = '';
|
||||||
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
async function convert() {
|
async function convert() {
|
||||||
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement).value) || 12;
|
const fontSize =
|
||||||
const pageSizeKey = (document.getElementById('page-size') as HTMLSelectElement).value;
|
parseInt(
|
||||||
const fontName = (document.getElementById('font-family') as HTMLSelectElement)?.value || 'helv';
|
(document.getElementById('font-size') as HTMLInputElement).value
|
||||||
const textColor = (document.getElementById('text-color') as HTMLInputElement)?.value || '#000000';
|
) || 12;
|
||||||
|
const pageSizeKey = (
|
||||||
|
document.getElementById('page-size') as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
const fontName =
|
||||||
|
(document.getElementById('font-family') as HTMLSelectElement)?.value ||
|
||||||
|
'helv';
|
||||||
|
const textColor =
|
||||||
|
(document.getElementById('text-color') as HTMLInputElement)?.value ||
|
||||||
|
'#000000';
|
||||||
|
|
||||||
if (currentMode === 'upload' && files.length === 0) {
|
if (currentMode === 'upload' && files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one text file.');
|
showAlert('No Files', 'Please select at least one text file.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentMode === 'text') {
|
||||||
|
const textInput = document.getElementById(
|
||||||
|
'text-input'
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
if (!textInput.value.trim()) {
|
||||||
|
showAlert('No Text', 'Please enter some text to convert.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Loading engine...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
let textContent = '';
|
||||||
|
|
||||||
|
if (currentMode === 'upload') {
|
||||||
|
for (const file of files) {
|
||||||
|
const text = await file.text();
|
||||||
|
textContent += text + '\n\n';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const textInput = document.getElementById(
|
||||||
|
'text-input'
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
textContent = textInput.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentMode === 'text') {
|
showLoader('Creating PDF...');
|
||||||
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
|
|
||||||
if (!textInput.value.trim()) {
|
|
||||||
showAlert('No Text', 'Please enter some text to convert.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
const pdfBlob = await pymupdf.textToPdf(textContent, {
|
||||||
|
fontSize,
|
||||||
|
pageSize: pageSizeKey as 'a4' | 'letter' | 'legal' | 'a3' | 'a5',
|
||||||
|
fontName: fontName as 'helv' | 'tiro' | 'cour' | 'times',
|
||||||
|
textColor,
|
||||||
|
margins: 72,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
downloadFile(pdfBlob, 'text_to_pdf.pdf');
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
|
||||||
await pymupdf.load();
|
|
||||||
|
|
||||||
let textContent = '';
|
showAlert(
|
||||||
|
'Success',
|
||||||
if (currentMode === 'upload') {
|
'Text converted to PDF successfully!',
|
||||||
for (const file of files) {
|
'success',
|
||||||
const text = await file.text();
|
() => {
|
||||||
textContent += text + '\n\n';
|
resetState();
|
||||||
}
|
}
|
||||||
} else {
|
);
|
||||||
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
|
} catch (e: any) {
|
||||||
textContent = textInput.value;
|
console.error('[TxtToPDF] Error:', e);
|
||||||
}
|
showAlert('Error', `Failed to convert text to PDF. ${e.message || ''}`);
|
||||||
|
} finally {
|
||||||
showLoader('Creating PDF...');
|
hideLoader();
|
||||||
|
}
|
||||||
const pdfBlob = await pymupdf.textToPdf(textContent, {
|
|
||||||
fontSize,
|
|
||||||
pageSize: pageSizeKey as 'a4' | 'letter' | 'legal' | 'a3' | 'a5',
|
|
||||||
fontName: fontName as 'helv' | 'tiro' | 'cour' | 'times',
|
|
||||||
textColor,
|
|
||||||
margins: 72
|
|
||||||
});
|
|
||||||
|
|
||||||
downloadFile(pdfBlob, 'text_to_pdf.pdf');
|
|
||||||
|
|
||||||
showAlert('Success', 'Text converted to PDF successfully!', 'success', () => {
|
|
||||||
resetState();
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('[TxtToPDF] Error:', e);
|
|
||||||
showAlert('Error', `Failed to convert text to PDF. ${e.message || ''}`);
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update textarea direction based on RTL detection
|
// Update textarea direction based on RTL detection
|
||||||
function updateTextareaDirection(textarea: HTMLTextAreaElement) {
|
function updateTextareaDirection(textarea: HTMLTextAreaElement) {
|
||||||
const text = textarea.value;
|
const text = textarea.value;
|
||||||
if (hasRtlCharacters(text)) {
|
if (hasRtlCharacters(text)) {
|
||||||
textarea.style.direction = 'rtl';
|
textarea.style.direction = 'rtl';
|
||||||
textarea.style.textAlign = 'right';
|
textarea.style.textAlign = 'right';
|
||||||
} else {
|
} else {
|
||||||
textarea.style.direction = 'ltr';
|
textarea.style.direction = 'ltr';
|
||||||
textarea.style.textAlign = 'left';
|
textarea.style.textAlign = 'left';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
const uploadModeBtn = document.getElementById('txt-mode-upload-btn');
|
const uploadModeBtn = document.getElementById('txt-mode-upload-btn');
|
||||||
const textModeBtn = document.getElementById('txt-mode-text-btn');
|
const textModeBtn = document.getElementById('txt-mode-text-btn');
|
||||||
const uploadPanel = document.getElementById('txt-upload-panel');
|
const uploadPanel = document.getElementById('txt-upload-panel');
|
||||||
const textPanel = document.getElementById('txt-text-panel');
|
const textPanel = document.getElementById('txt-text-panel');
|
||||||
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
|
const textInput = document.getElementById(
|
||||||
|
'text-input'
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
// Back to Tools
|
// Back to Tools
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode switching
|
||||||
|
if (uploadModeBtn && textModeBtn && uploadPanel && textPanel) {
|
||||||
|
uploadModeBtn.addEventListener('click', () => {
|
||||||
|
currentMode = 'upload';
|
||||||
|
uploadModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||||
|
uploadModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
|
textModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
|
textModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
|
uploadPanel.classList.remove('hidden');
|
||||||
|
textPanel.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
textModeBtn.addEventListener('click', () => {
|
||||||
|
currentMode = 'text';
|
||||||
|
textModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||||
|
textModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
|
uploadModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
|
uploadModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
|
textPanel.classList.remove('hidden');
|
||||||
|
uploadPanel.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// RTL auto-detection for textarea
|
||||||
|
if (textInput) {
|
||||||
|
textInput.addEventListener('input', () => {
|
||||||
|
updateTextareaDirection(textInput);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// File handling
|
||||||
|
const handleFileSelect = (newFiles: FileList | null) => {
|
||||||
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
|
const validFiles = Array.from(newFiles).filter(
|
||||||
|
(file) =>
|
||||||
|
file.name.toLowerCase().endsWith('.txt') || file.type === 'text/plain'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validFiles.length < newFiles.length) {
|
||||||
|
showAlert(
|
||||||
|
'Invalid Files',
|
||||||
|
'Some files were skipped. Only text files are allowed.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode switching
|
if (validFiles.length > 0) {
|
||||||
if (uploadModeBtn && textModeBtn && uploadPanel && textPanel) {
|
files = [...files, ...validFiles];
|
||||||
uploadModeBtn.addEventListener('click', () => {
|
updateUI();
|
||||||
currentMode = 'upload';
|
|
||||||
uploadModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
|
||||||
uploadModeBtn.classList.add('bg-indigo-600', 'text-white');
|
|
||||||
textModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
|
||||||
textModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
|
||||||
uploadPanel.classList.remove('hidden');
|
|
||||||
textPanel.classList.add('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
textModeBtn.addEventListener('click', () => {
|
|
||||||
currentMode = 'text';
|
|
||||||
textModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
|
||||||
textModeBtn.classList.add('bg-indigo-600', 'text-white');
|
|
||||||
uploadModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
|
||||||
uploadModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
|
||||||
textPanel.classList.remove('hidden');
|
|
||||||
uploadPanel.classList.add('hidden');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// RTL auto-detection for textarea
|
if (fileInput && dropZone) {
|
||||||
if (textInput) {
|
fileInput.addEventListener('change', (e) => {
|
||||||
textInput.addEventListener('input', () => {
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
updateTextareaDirection(textInput);
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// File handling
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
const handleFileSelect = (newFiles: FileList | null) => {
|
e.preventDefault();
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
dropZone.classList.add('bg-gray-700');
|
||||||
const validFiles = Array.from(newFiles).filter(
|
});
|
||||||
(file) => file.name.toLowerCase().endsWith('.txt') || file.type === 'text/plain'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validFiles.length < newFiles.length) {
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
showAlert('Invalid Files', 'Some files were skipped. Only text files are allowed.');
|
e.preventDefault();
|
||||||
}
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
files = [...files, ...validFiles];
|
e.preventDefault();
|
||||||
updateUI();
|
dropZone.classList.remove('bg-gray-700');
|
||||||
}
|
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||||
};
|
});
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
fileInput.addEventListener('click', () => {
|
||||||
fileInput.addEventListener('change', (e) => {
|
fileInput.value = '';
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
if (addMoreBtn && fileInput) {
|
||||||
e.preventDefault();
|
addMoreBtn.addEventListener('click', () => {
|
||||||
dropZone.classList.add('bg-gray-700');
|
fileInput.click();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
if (clearFilesBtn) {
|
||||||
e.preventDefault();
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
dropZone.classList.remove('bg-gray-700');
|
files = [];
|
||||||
});
|
updateUI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', convert);
|
||||||
dropZone.classList.remove('bg-gray-700');
|
}
|
||||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
createIcons({ icons });
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn && fileInput) {
|
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
|
||||||
files = [];
|
|
||||||
updateUI();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convert);
|
|
||||||
}
|
|
||||||
|
|
||||||
createIcons({ icons });
|
|
||||||
});
|
});
|
||||||
|
|||||||
219
src/js/logic/wasm-settings-page.ts
Normal file
219
src/js/logic/wasm-settings-page.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import { createIcons, icons } from 'lucide';
|
||||||
|
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||||
|
import { WasmProvider, type WasmPackage } from '../utils/wasm-provider.js';
|
||||||
|
import { clearPyMuPDFCache } from '../utils/pymupdf-loader.js';
|
||||||
|
import { clearGhostscriptCache } from '../utils/ghostscript-dynamic-loader.js';
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
|
} else {
|
||||||
|
initializePage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializePage() {
|
||||||
|
createIcons({ icons });
|
||||||
|
|
||||||
|
document.querySelectorAll('.copy-btn').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const url = btn.getAttribute('data-copy');
|
||||||
|
if (url) {
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
const svg = btn.querySelector('svg');
|
||||||
|
if (svg) {
|
||||||
|
const checkIcon = document.createElement('i');
|
||||||
|
checkIcon.setAttribute('data-lucide', 'check');
|
||||||
|
checkIcon.className = 'w-3.5 h-3.5';
|
||||||
|
svg.replaceWith(checkIcon);
|
||||||
|
createIcons({ icons });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const newSvg = btn.querySelector('svg');
|
||||||
|
if (newSvg) {
|
||||||
|
const copyIcon = document.createElement('i');
|
||||||
|
copyIcon.setAttribute('data-lucide', 'copy');
|
||||||
|
copyIcon.className = 'w-3.5 h-3.5';
|
||||||
|
newSvg.replaceWith(copyIcon);
|
||||||
|
createIcons({ icons });
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const pymupdfUrl = document.getElementById('pymupdf-url') as HTMLInputElement;
|
||||||
|
const pymupdfTest = document.getElementById(
|
||||||
|
'pymupdf-test'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const pymupdfStatus = document.getElementById(
|
||||||
|
'pymupdf-status'
|
||||||
|
) as HTMLSpanElement;
|
||||||
|
|
||||||
|
const ghostscriptUrl = document.getElementById(
|
||||||
|
'ghostscript-url'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const ghostscriptTest = document.getElementById(
|
||||||
|
'ghostscript-test'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const ghostscriptStatus = document.getElementById(
|
||||||
|
'ghostscript-status'
|
||||||
|
) as HTMLSpanElement;
|
||||||
|
|
||||||
|
const cpdfUrl = document.getElementById('cpdf-url') as HTMLInputElement;
|
||||||
|
const cpdfTest = document.getElementById('cpdf-test') as HTMLButtonElement;
|
||||||
|
const cpdfStatus = document.getElementById('cpdf-status') as HTMLSpanElement;
|
||||||
|
|
||||||
|
const saveBtn = document.getElementById('save-btn') as HTMLButtonElement;
|
||||||
|
const clearBtn = document.getElementById('clear-btn') as HTMLButtonElement;
|
||||||
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
|
backBtn?.addEventListener('click', () => {
|
||||||
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
|
});
|
||||||
|
|
||||||
|
loadConfiguration();
|
||||||
|
|
||||||
|
function loadConfiguration() {
|
||||||
|
const config = WasmProvider.getAllProviders();
|
||||||
|
|
||||||
|
if (config.pymupdf) {
|
||||||
|
pymupdfUrl.value = config.pymupdf;
|
||||||
|
updateStatus('pymupdf', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.ghostscript) {
|
||||||
|
ghostscriptUrl.value = config.ghostscript;
|
||||||
|
updateStatus('ghostscript', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.cpdf) {
|
||||||
|
cpdfUrl.value = config.cpdf;
|
||||||
|
updateStatus('cpdf', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(
|
||||||
|
packageName: WasmPackage,
|
||||||
|
configured: boolean,
|
||||||
|
testing = false
|
||||||
|
) {
|
||||||
|
const statusMap: Record<WasmPackage, HTMLSpanElement> = {
|
||||||
|
pymupdf: pymupdfStatus,
|
||||||
|
ghostscript: ghostscriptStatus,
|
||||||
|
cpdf: cpdfStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusEl = statusMap[packageName];
|
||||||
|
if (!statusEl) return;
|
||||||
|
|
||||||
|
if (testing) {
|
||||||
|
statusEl.textContent = 'Testing...';
|
||||||
|
statusEl.className =
|
||||||
|
'text-xs px-2 py-1 rounded-full bg-yellow-600/30 text-yellow-300';
|
||||||
|
} else if (configured) {
|
||||||
|
statusEl.textContent = 'Configured';
|
||||||
|
statusEl.className =
|
||||||
|
'text-xs px-2 py-1 rounded-full bg-green-600/30 text-green-300';
|
||||||
|
} else {
|
||||||
|
statusEl.textContent = 'Not Configured';
|
||||||
|
statusEl.className =
|
||||||
|
'text-xs px-2 py-1 rounded-full bg-gray-600 text-gray-300';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testConnection(packageName: WasmPackage, url: string) {
|
||||||
|
if (!url.trim()) {
|
||||||
|
showAlert('Empty URL', 'Please enter a URL to test.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(packageName, false, true);
|
||||||
|
|
||||||
|
const result = await WasmProvider.validateUrl(packageName, url);
|
||||||
|
|
||||||
|
if (result.valid) {
|
||||||
|
updateStatus(packageName, true);
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Connection to ${WasmProvider.getPackageDisplayName(packageName)} successful!`,
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updateStatus(packageName, false);
|
||||||
|
showAlert(
|
||||||
|
'Connection Failed',
|
||||||
|
result.error || 'Could not connect to the URL.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pymupdfTest?.addEventListener('click', () => {
|
||||||
|
testConnection('pymupdf', pymupdfUrl.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
ghostscriptTest?.addEventListener('click', () => {
|
||||||
|
testConnection('ghostscript', ghostscriptUrl.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
cpdfTest?.addEventListener('click', () => {
|
||||||
|
testConnection('cpdf', cpdfUrl.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
saveBtn?.addEventListener('click', async () => {
|
||||||
|
showLoader('Saving configuration...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (pymupdfUrl.value.trim()) {
|
||||||
|
WasmProvider.setUrl('pymupdf', pymupdfUrl.value.trim());
|
||||||
|
updateStatus('pymupdf', true);
|
||||||
|
} else {
|
||||||
|
WasmProvider.removeUrl('pymupdf');
|
||||||
|
updateStatus('pymupdf', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ghostscriptUrl.value.trim()) {
|
||||||
|
WasmProvider.setUrl('ghostscript', ghostscriptUrl.value.trim());
|
||||||
|
updateStatus('ghostscript', true);
|
||||||
|
} else {
|
||||||
|
WasmProvider.removeUrl('ghostscript');
|
||||||
|
updateStatus('ghostscript', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cpdfUrl.value.trim()) {
|
||||||
|
WasmProvider.setUrl('cpdf', cpdfUrl.value.trim());
|
||||||
|
updateStatus('cpdf', true);
|
||||||
|
} else {
|
||||||
|
WasmProvider.removeUrl('cpdf');
|
||||||
|
updateStatus('cpdf', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Saved', 'Configuration saved successfully!', 'success');
|
||||||
|
} catch (e: unknown) {
|
||||||
|
hideLoader();
|
||||||
|
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||||
|
showAlert('Error', `Failed to save configuration: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clearBtn?.addEventListener('click', () => {
|
||||||
|
WasmProvider.clearAll();
|
||||||
|
|
||||||
|
clearPyMuPDFCache();
|
||||||
|
clearGhostscriptCache();
|
||||||
|
|
||||||
|
pymupdfUrl.value = '';
|
||||||
|
ghostscriptUrl.value = '';
|
||||||
|
cpdfUrl.value = '';
|
||||||
|
|
||||||
|
updateStatus('pymupdf', false);
|
||||||
|
updateStatus('ghostscript', false);
|
||||||
|
updateStatus('cpdf', false);
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Cleared',
|
||||||
|
'All configurations and cached modules have been cleared.',
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,201 +2,212 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
|
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
|
||||||
|
|
||||||
const FILETYPE = 'xps';
|
const FILETYPE = 'xps';
|
||||||
const EXTENSIONS = ['.xps', '.oxps'];
|
const EXTENSIONS = ['.xps', '.oxps'];
|
||||||
const TOOL_NAME = 'XPS';
|
const TOOL_NAME = 'XPS';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
if (backBtn) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
backBtn.addEventListener('click', () => {
|
||||||
window.location.href = import.meta.env.BASE_URL;
|
window.location.href = import.meta.env.BASE_URL;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUI = async () => {
|
||||||
|
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
||||||
|
|
||||||
|
if (state.files.length > 0) {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
|
||||||
|
for (let index = 0; index < state.files.length; index++) {
|
||||||
|
const file = state.files[index];
|
||||||
|
const fileDiv = document.createElement('div');
|
||||||
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
|
const infoContainer = document.createElement('div');
|
||||||
|
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('div');
|
||||||
|
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
|
const metaSpan = document.createElement('div');
|
||||||
|
metaSpan.className = 'text-xs text-gray-400';
|
||||||
|
metaSpan.textContent = formatBytes(file.size);
|
||||||
|
|
||||||
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className =
|
||||||
|
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||||
|
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
|
removeBtn.onclick = () => {
|
||||||
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
createIcons({ icons });
|
||||||
|
fileControls.classList.remove('hidden');
|
||||||
|
processBtn.classList.remove('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
} else {
|
||||||
|
fileDisplayArea.innerHTML = '';
|
||||||
|
fileControls.classList.add('hidden');
|
||||||
|
processBtn.classList.add('hidden');
|
||||||
|
(processBtn as HTMLButtonElement).disabled = true;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateUI = async () => {
|
const resetState = () => {
|
||||||
if (!fileDisplayArea || !processBtn || !fileControls) return;
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
if (state.files.length > 0) {
|
const convertToPdf = async () => {
|
||||||
fileDisplayArea.innerHTML = '';
|
try {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let index = 0; index < state.files.length; index++) {
|
showLoader('Loading engine...');
|
||||||
const file = state.files[index];
|
const pymupdf = await loadPyMuPDF();
|
||||||
const fileDiv = document.createElement('div');
|
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
|
||||||
|
|
||||||
const infoContainer = document.createElement('div');
|
if (state.files.length === 1) {
|
||||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
const originalFile = state.files[0];
|
||||||
|
showLoader(`Converting ${originalFile.name}...`);
|
||||||
|
|
||||||
const nameSpan = document.createElement('div');
|
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
filetype: FILETYPE,
|
||||||
nameSpan.textContent = file.name;
|
});
|
||||||
|
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
downloadFile(pdfBlob, fileName);
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
hideLoader();
|
||||||
metaSpan.textContent = formatBytes(file.size);
|
|
||||||
|
|
||||||
infoContainer.append(nameSpan, metaSpan);
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${originalFile.name} to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showLoader('Converting files...');
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
const file = state.files[i];
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
showLoader(
|
||||||
removeBtn.onclick = () => {
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
);
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
fileDiv.append(infoContainer, removeBtn);
|
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
filetype: FILETYPE,
|
||||||
}
|
});
|
||||||
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
createIcons({ icons });
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
fileControls.classList.remove('hidden');
|
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
processBtn.classList.remove('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
} else {
|
|
||||||
fileDisplayArea.innerHTML = '';
|
|
||||||
fileControls.classList.add('hidden');
|
|
||||||
processBtn.classList.add('hidden');
|
|
||||||
(processBtn as HTMLButtonElement).disabled = true;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = () => {
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
state.files = [];
|
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
||||||
state.pdfDoc = null;
|
|
||||||
updateUI();
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertToPdf = async () => {
|
hideLoader();
|
||||||
try {
|
|
||||||
if (state.files.length === 0) {
|
|
||||||
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showAlert(
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
'Conversion Complete',
|
||||||
await pymupdf.load();
|
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
const originalFile = state.files[0];
|
if (files && files.length > 0) {
|
||||||
showLoader(`Converting ${originalFile.name}...`);
|
state.files = [...state.files, ...Array.from(files)];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
if (fileInput && dropZone) {
|
||||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
handleFileSelect((e.target as HTMLInputElement).files);
|
||||||
|
});
|
||||||
|
|
||||||
downloadFile(pdfBlob, fileName);
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
hideLoader();
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
showAlert(
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
'Conversion Complete',
|
e.preventDefault();
|
||||||
`Successfully converted ${originalFile.name} to PDF.`,
|
dropZone.classList.remove('bg-gray-700');
|
||||||
'success',
|
});
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showLoader('Converting files...');
|
|
||||||
const JSZip = (await import('jszip')).default;
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
dropZone.addEventListener('drop', (e) => {
|
||||||
const file = state.files[i];
|
e.preventDefault();
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
if (files && files.length > 0) {
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
const name = f.name.toLowerCase();
|
||||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
}
|
});
|
||||||
|
if (validFiles.length > 0) {
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const dataTransfer = new DataTransfer();
|
||||||
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
|
handleFileSelect(dataTransfer.files);
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
showAlert(
|
|
||||||
'Conversion Complete',
|
|
||||||
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
|
|
||||||
'success',
|
|
||||||
() => resetState()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
fileInput.addEventListener('click', () => {
|
||||||
if (files && files.length > 0) {
|
fileInput.value = '';
|
||||||
state.files = [...state.files, ...Array.from(files)];
|
});
|
||||||
updateUI();
|
}
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (addMoreBtn) {
|
||||||
fileInput.addEventListener('change', (e) => {
|
addMoreBtn.addEventListener('click', () => {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files);
|
fileInput.click();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
if (clearFilesBtn) {
|
||||||
e.preventDefault();
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
dropZone.classList.add('bg-gray-700');
|
resetState();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dropZone.addEventListener('dragleave', (e) => {
|
if (processBtn) {
|
||||||
e.preventDefault();
|
processBtn.addEventListener('click', convertToPdf);
|
||||||
dropZone.classList.remove('bg-gray-700');
|
}
|
||||||
});
|
|
||||||
|
|
||||||
dropZone.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
dropZone.classList.remove('bg-gray-700');
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const validFiles = Array.from(files).filter(f => {
|
|
||||||
const name = f.name.toLowerCase();
|
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
|
||||||
});
|
|
||||||
if (validFiles.length > 0) {
|
|
||||||
const dataTransfer = new DataTransfer();
|
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
|
||||||
handleFileSelect(dataTransfer.files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
|
||||||
fileInput.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addMoreBtn) {
|
|
||||||
addMoreBtn.addEventListener('click', () => {
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearFilesBtn) {
|
|
||||||
clearFilesBtn.addEventListener('click', () => {
|
|
||||||
resetState();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processBtn) {
|
|
||||||
processBtn.addEventListener('click', convertToPdf);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,35 @@
|
|||||||
|
import { WasmProvider } from './wasm-provider';
|
||||||
|
|
||||||
let cpdfLoaded = false;
|
let cpdfLoaded = false;
|
||||||
let cpdfLoadPromise: Promise<void> | null = null;
|
let cpdfLoadPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
//TODO: @ALAM,is it better to use a worker to load the cpdf library?
|
function getCpdfUrl(): string | undefined {
|
||||||
// or just use the browser version?
|
const userUrl = WasmProvider.getUrl('cpdf');
|
||||||
export async function ensureCpdfLoaded(): Promise<void> {
|
if (userUrl) {
|
||||||
|
const baseUrl = userUrl.endsWith('/') ? userUrl : `${userUrl}/`;
|
||||||
|
return `${baseUrl}coherentpdf.browser.min.js`;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCpdfAvailable(): boolean {
|
||||||
|
return WasmProvider.isConfigured('cpdf');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isCpdfLoaded(): Promise<void> {
|
||||||
if (cpdfLoaded) return;
|
if (cpdfLoaded) return;
|
||||||
|
|
||||||
if (cpdfLoadPromise) {
|
if (cpdfLoadPromise) {
|
||||||
return cpdfLoadPromise;
|
return cpdfLoadPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cpdfUrl = getCpdfUrl();
|
||||||
|
if (!cpdfUrl) {
|
||||||
|
throw new Error(
|
||||||
|
'CoherentPDF is not configured. Please configure it in WASM Settings.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
cpdfLoadPromise = new Promise((resolve, reject) => {
|
cpdfLoadPromise = new Promise((resolve, reject) => {
|
||||||
if (typeof (window as any).coherentpdf !== 'undefined') {
|
if (typeof (window as any).coherentpdf !== 'undefined') {
|
||||||
cpdfLoaded = true;
|
cpdfLoaded = true;
|
||||||
@@ -18,13 +38,14 @@ export async function ensureCpdfLoaded(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = import.meta.env.BASE_URL + 'coherentpdf.browser.min.js';
|
script.src = cpdfUrl;
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
cpdfLoaded = true;
|
cpdfLoaded = true;
|
||||||
|
console.log('[CPDF] Loaded from:', script.src);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
script.onerror = () => {
|
script.onerror = () => {
|
||||||
reject(new Error('Failed to load CoherentPDF library'));
|
reject(new Error('Failed to load CoherentPDF library from: ' + cpdfUrl));
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
});
|
});
|
||||||
@@ -32,11 +53,7 @@ export async function ensureCpdfLoaded(): Promise<void> {
|
|||||||
return cpdfLoadPromise;
|
return cpdfLoadPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the cpdf instance, ensuring it's loaded first
|
|
||||||
*/
|
|
||||||
export async function getCpdf(): Promise<any> {
|
export async function getCpdf(): Promise<any> {
|
||||||
await ensureCpdfLoaded();
|
await isCpdfLoaded();
|
||||||
return (window as any).coherentpdf;
|
return (window as any).coherentpdf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
89
src/js/utils/ghostscript-dynamic-loader.ts
Normal file
89
src/js/utils/ghostscript-dynamic-loader.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { WasmProvider } from './wasm-provider.js';
|
||||||
|
|
||||||
|
let cachedGS: any = null;
|
||||||
|
let loadPromise: Promise<any> | null = null;
|
||||||
|
|
||||||
|
export interface GhostscriptInterface {
|
||||||
|
convertToPDFA(pdfBuffer: ArrayBuffer, profile: string): Promise<ArrayBuffer>;
|
||||||
|
fontToOutline(pdfBuffer: ArrayBuffer): Promise<ArrayBuffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadGhostscript(): Promise<GhostscriptInterface> {
|
||||||
|
if (cachedGS) {
|
||||||
|
return cachedGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadPromise) {
|
||||||
|
return loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPromise = (async () => {
|
||||||
|
const baseUrl = WasmProvider.getUrl('ghostscript');
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error(
|
||||||
|
'Ghostscript is not configured. Please configure it in Advanced Settings.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wrapperUrl = `${normalizedUrl}gs.js`;
|
||||||
|
|
||||||
|
await loadScript(wrapperUrl);
|
||||||
|
|
||||||
|
const globalScope =
|
||||||
|
typeof globalThis !== 'undefined' ? globalThis : window;
|
||||||
|
|
||||||
|
if (typeof (globalScope as any).loadGS === 'function') {
|
||||||
|
cachedGS = await (globalScope as any).loadGS({
|
||||||
|
baseUrl: normalizedUrl,
|
||||||
|
});
|
||||||
|
} else if (typeof (globalScope as any).GhostscriptWASM === 'function') {
|
||||||
|
cachedGS = new (globalScope as any).GhostscriptWASM(normalizedUrl);
|
||||||
|
await cachedGS.init?.();
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'Ghostscript wrapper did not expose expected interface. Expected loadGS() or GhostscriptWASM class.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedGS;
|
||||||
|
} catch (error: any) {
|
||||||
|
loadPromise = null;
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load Ghostscript from ${normalizedUrl}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadScript(url: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (document.querySelector(`script[src="${url}"]`)) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = url;
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.async = true;
|
||||||
|
|
||||||
|
script.onload = () => resolve();
|
||||||
|
script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
|
||||||
|
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGhostscriptAvailable(): boolean {
|
||||||
|
return WasmProvider.isConfigured('ghostscript');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearGhostscriptCache(): void {
|
||||||
|
cachedGS = null;
|
||||||
|
loadPromise = null;
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* PDF/A Conversion using Ghostscript WASM
|
* PDF/A Conversion using Ghostscript WASM
|
||||||
* * Converts PDFs to PDF/A-1b, PDF/A-2b, or PDF/A-3b format.
|
* Converts PDFs to PDF/A-1b, PDF/A-2b, or PDF/A-3b format.
|
||||||
|
* Requires user to configure Ghostscript URL in WASM Settings.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import loadWASM from '@bentopdf/gs-wasm';
|
import {
|
||||||
import { getWasmBaseUrl, fetchWasmFile } from '../config/wasm-cdn-config.js';
|
getWasmBaseUrl,
|
||||||
|
fetchWasmFile,
|
||||||
|
isWasmAvailable,
|
||||||
|
} from '../config/wasm-cdn-config.js';
|
||||||
import { PDFDocument, PDFDict, PDFName, PDFArray } from 'pdf-lib';
|
import { PDFDocument, PDFDict, PDFName, PDFArray } from 'pdf-lib';
|
||||||
|
|
||||||
interface GhostscriptModule {
|
interface GhostscriptModule {
|
||||||
@@ -34,6 +38,12 @@ export async function convertToPdfA(
|
|||||||
level: PdfALevel = 'PDF/A-2b',
|
level: PdfALevel = 'PDF/A-2b',
|
||||||
onProgress?: (msg: string) => void
|
onProgress?: (msg: string) => void
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
|
if (!isWasmAvailable('ghostscript')) {
|
||||||
|
throw new Error(
|
||||||
|
'Ghostscript is not configured. Please configure it in WASM Settings.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onProgress?.('Loading Ghostscript...');
|
onProgress?.('Loading Ghostscript...');
|
||||||
|
|
||||||
let gs: GhostscriptModule;
|
let gs: GhostscriptModule;
|
||||||
@@ -41,11 +51,16 @@ export async function convertToPdfA(
|
|||||||
if (cachedGsModule) {
|
if (cachedGsModule) {
|
||||||
gs = cachedGsModule;
|
gs = cachedGsModule;
|
||||||
} else {
|
} else {
|
||||||
const gsBaseUrl = getWasmBaseUrl('ghostscript');
|
const gsBaseUrl = getWasmBaseUrl('ghostscript')!;
|
||||||
|
const libUrl = `${gsBaseUrl}dist/index.js`;
|
||||||
|
const module = await import(/* @vite-ignore */ libUrl);
|
||||||
|
const loadWASM = module.loadGhostscriptWASM || module.default;
|
||||||
|
|
||||||
gs = (await loadWASM({
|
gs = (await loadWASM({
|
||||||
|
baseUrl: `${gsBaseUrl}assets/`,
|
||||||
locateFile: (path: string) => {
|
locateFile: (path: string) => {
|
||||||
if (path.endsWith('.wasm')) {
|
if (path.endsWith('.wasm')) {
|
||||||
return gsBaseUrl + 'gs.wasm';
|
return gsBaseUrl + 'assets/gs.wasm';
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
},
|
},
|
||||||
@@ -73,11 +88,12 @@ export async function convertToPdfA(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const iccFileName = 'sRGB_IEC61966-2-1_no_black_scaling.icc';
|
const iccFileName = 'sRGB_IEC61966-2-1_no_black_scaling.icc';
|
||||||
const response = await fetchWasmFile('ghostscript', iccFileName);
|
const iccLocalPath = `${import.meta.env.BASE_URL}ghostscript-wasm/${iccFileName}`;
|
||||||
|
const response = await fetch(iccLocalPath);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to fetch ICC profile: ${iccFileName}. Ensure it is in your assets folder.`
|
`Failed to fetch ICC profile from ${iccLocalPath}: HTTP ${response.status}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +378,12 @@ export async function convertFontsToOutlines(
|
|||||||
pdfData: Uint8Array,
|
pdfData: Uint8Array,
|
||||||
onProgress?: (msg: string) => void
|
onProgress?: (msg: string) => void
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
|
if (!isWasmAvailable('ghostscript')) {
|
||||||
|
throw new Error(
|
||||||
|
'Ghostscript is not configured. Please configure it in WASM Settings.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onProgress?.('Loading Ghostscript...');
|
onProgress?.('Loading Ghostscript...');
|
||||||
|
|
||||||
let gs: GhostscriptModule;
|
let gs: GhostscriptModule;
|
||||||
@@ -369,11 +391,16 @@ export async function convertFontsToOutlines(
|
|||||||
if (cachedGsModule) {
|
if (cachedGsModule) {
|
||||||
gs = cachedGsModule;
|
gs = cachedGsModule;
|
||||||
} else {
|
} else {
|
||||||
const gsBaseUrl = getWasmBaseUrl('ghostscript');
|
const gsBaseUrl = getWasmBaseUrl('ghostscript')!;
|
||||||
|
const libUrl = `${gsBaseUrl}dist/index.js`;
|
||||||
|
const module = await import(/* @vite-ignore */ libUrl);
|
||||||
|
const loadWASM = module.loadGhostscriptWASM || module.default;
|
||||||
|
|
||||||
gs = (await loadWASM({
|
gs = (await loadWASM({
|
||||||
|
baseUrl: `${gsBaseUrl}assets/`,
|
||||||
locateFile: (path: string) => {
|
locateFile: (path: string) => {
|
||||||
if (path.endsWith('.wasm')) {
|
if (path.endsWith('.wasm')) {
|
||||||
return gsBaseUrl + 'gs.wasm';
|
return gsBaseUrl + 'assets/gs.wasm';
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
},
|
},
|
||||||
|
|||||||
87
src/js/utils/pymupdf-loader.ts
Normal file
87
src/js/utils/pymupdf-loader.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { WasmProvider } from './wasm-provider.js';
|
||||||
|
|
||||||
|
let cachedPyMuPDF: any = null;
|
||||||
|
let loadPromise: Promise<any> | null = null;
|
||||||
|
|
||||||
|
export interface PyMuPDFInterface {
|
||||||
|
load(): Promise<void>;
|
||||||
|
compressPdf(
|
||||||
|
file: Blob,
|
||||||
|
options: any
|
||||||
|
): Promise<{ blob: Blob; compressedSize: number }>;
|
||||||
|
convertToPdf(file: Blob, ext: string): Promise<Blob>;
|
||||||
|
extractText(file: Blob, options?: any): Promise<string>;
|
||||||
|
extractImages(file: Blob): Promise<Array<{ data: Uint8Array; ext: string }>>;
|
||||||
|
extractTables(file: Blob): Promise<any[]>;
|
||||||
|
toSvg(file: Blob, pageNum: number): Promise<string>;
|
||||||
|
renderPageToImage(file: Blob, pageNum: number, scale: number): Promise<Blob>;
|
||||||
|
getPageCount(file: Blob): Promise<number>;
|
||||||
|
rasterizePdf(file: Blob | File, options: any): Promise<Blob>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadPyMuPDF(): Promise<any> {
|
||||||
|
if (cachedPyMuPDF) {
|
||||||
|
return cachedPyMuPDF;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadPromise) {
|
||||||
|
return loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPromise = (async () => {
|
||||||
|
if (!WasmProvider.isConfigured('pymupdf')) {
|
||||||
|
throw new Error(
|
||||||
|
'PyMuPDF is not configured. Please configure it in Advanced Settings.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!WasmProvider.isConfigured('ghostscript')) {
|
||||||
|
throw new Error(
|
||||||
|
'Ghostscript is not configured. PyMuPDF requires Ghostscript for some operations. Please configure both in Advanced Settings.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pymupdfUrl = WasmProvider.getUrl('pymupdf')!;
|
||||||
|
const gsUrl = WasmProvider.getUrl('ghostscript')!;
|
||||||
|
const normalizedPymupdf = pymupdfUrl.endsWith('/')
|
||||||
|
? pymupdfUrl
|
||||||
|
: `${pymupdfUrl}/`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wrapperUrl = `${normalizedPymupdf}dist/index.js`;
|
||||||
|
const module = await import(/* @vite-ignore */ wrapperUrl);
|
||||||
|
|
||||||
|
if (typeof module.PyMuPDF !== 'function') {
|
||||||
|
throw new Error(
|
||||||
|
'PyMuPDF module did not export expected PyMuPDF class.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedPyMuPDF = new module.PyMuPDF({
|
||||||
|
assetPath: `${normalizedPymupdf}assets/`,
|
||||||
|
ghostscriptUrl: gsUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
await cachedPyMuPDF.load();
|
||||||
|
|
||||||
|
console.log('[PyMuPDF Loader] Successfully loaded from CDN');
|
||||||
|
return cachedPyMuPDF;
|
||||||
|
} catch (error: any) {
|
||||||
|
loadPromise = null;
|
||||||
|
throw new Error(`Failed to load PyMuPDF from CDN: ${error.message}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return loadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPyMuPDFAvailable(): boolean {
|
||||||
|
return (
|
||||||
|
WasmProvider.isConfigured('pymupdf') &&
|
||||||
|
WasmProvider.isConfigured('ghostscript')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearPyMuPDFCache(): void {
|
||||||
|
cachedPyMuPDF = null;
|
||||||
|
loadPromise = null;
|
||||||
|
}
|
||||||
@@ -1,132 +1,159 @@
|
|||||||
import { getLibreOfficeConverter } from './libreoffice-loader.js';
|
import { getLibreOfficeConverter } from './libreoffice-loader.js';
|
||||||
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
|
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
||||||
import loadGsWASM from '@bentopdf/gs-wasm';
|
|
||||||
import { setCachedGsModule } from './ghostscript-loader.js';
|
|
||||||
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
|
|
||||||
|
|
||||||
export enum PreloadStatus {
|
export enum PreloadStatus {
|
||||||
IDLE = 'idle',
|
IDLE = 'idle',
|
||||||
LOADING = 'loading',
|
LOADING = 'loading',
|
||||||
READY = 'ready',
|
READY = 'ready',
|
||||||
ERROR = 'error'
|
ERROR = 'error',
|
||||||
|
UNAVAILABLE = 'unavailable',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PreloadState {
|
interface PreloadState {
|
||||||
libreoffice: PreloadStatus;
|
libreoffice: PreloadStatus;
|
||||||
pymupdf: PreloadStatus;
|
pymupdf: PreloadStatus;
|
||||||
ghostscript: PreloadStatus;
|
ghostscript: PreloadStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preloadState: PreloadState = {
|
const preloadState: PreloadState = {
|
||||||
libreoffice: PreloadStatus.IDLE,
|
libreoffice: PreloadStatus.IDLE,
|
||||||
pymupdf: PreloadStatus.IDLE,
|
pymupdf: PreloadStatus.IDLE,
|
||||||
ghostscript: PreloadStatus.IDLE
|
ghostscript: PreloadStatus.IDLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
let pymupdfInstance: PyMuPDF | null = null;
|
|
||||||
|
|
||||||
export function getPreloadStatus(): Readonly<PreloadState> {
|
export function getPreloadStatus(): Readonly<PreloadState> {
|
||||||
return { ...preloadState };
|
return { ...preloadState };
|
||||||
}
|
|
||||||
|
|
||||||
export function getPymupdfInstance(): PyMuPDF | null {
|
|
||||||
return pymupdfInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function preloadLibreOffice(): Promise<void> {
|
|
||||||
if (preloadState.libreoffice !== PreloadStatus.IDLE) return;
|
|
||||||
|
|
||||||
preloadState.libreoffice = PreloadStatus.LOADING;
|
|
||||||
console.log('[Preloader] Starting LibreOffice WASM preload...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const converter = getLibreOfficeConverter();
|
|
||||||
await converter.initialize();
|
|
||||||
preloadState.libreoffice = PreloadStatus.READY;
|
|
||||||
console.log('[Preloader] LibreOffice WASM ready');
|
|
||||||
} catch (e) {
|
|
||||||
preloadState.libreoffice = PreloadStatus.ERROR;
|
|
||||||
console.warn('[Preloader] LibreOffice preload failed:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preloadPyMuPDF(): Promise<void> {
|
async function preloadPyMuPDF(): Promise<void> {
|
||||||
if (preloadState.pymupdf !== PreloadStatus.IDLE) return;
|
if (preloadState.pymupdf !== PreloadStatus.IDLE) return;
|
||||||
|
|
||||||
preloadState.pymupdf = PreloadStatus.LOADING;
|
if (!isWasmAvailable('pymupdf')) {
|
||||||
console.log('[Preloader] Starting PyMuPDF preload...');
|
preloadState.pymupdf = PreloadStatus.UNAVAILABLE;
|
||||||
|
console.log('[Preloader] PyMuPDF not configured, skipping preload');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
preloadState.pymupdf = PreloadStatus.LOADING;
|
||||||
const pymupdfBaseUrl = getWasmBaseUrl('pymupdf');
|
console.log('[Preloader] Starting PyMuPDF preload...');
|
||||||
pymupdfInstance = new PyMuPDF(pymupdfBaseUrl);
|
|
||||||
await pymupdfInstance.load();
|
try {
|
||||||
preloadState.pymupdf = PreloadStatus.READY;
|
const pymupdfBaseUrl = getWasmBaseUrl('pymupdf')!;
|
||||||
console.log('[Preloader] PyMuPDF ready');
|
const gsBaseUrl = getWasmBaseUrl('ghostscript');
|
||||||
} catch (e) {
|
const normalizedUrl = pymupdfBaseUrl.endsWith('/')
|
||||||
preloadState.pymupdf = PreloadStatus.ERROR;
|
? pymupdfBaseUrl
|
||||||
console.warn('[Preloader] PyMuPDF preload failed:', e);
|
: `${pymupdfBaseUrl}/`;
|
||||||
}
|
|
||||||
|
const wrapperUrl = `${normalizedUrl}dist/index.js`;
|
||||||
|
const module = await import(/* @vite-ignore */ wrapperUrl);
|
||||||
|
|
||||||
|
const pymupdfInstance = new module.PyMuPDF({
|
||||||
|
assetPath: `${normalizedUrl}assets/`,
|
||||||
|
ghostscriptUrl: gsBaseUrl || '',
|
||||||
|
});
|
||||||
|
await pymupdfInstance.load();
|
||||||
|
preloadState.pymupdf = PreloadStatus.READY;
|
||||||
|
console.log('[Preloader] PyMuPDF ready');
|
||||||
|
} catch (e) {
|
||||||
|
preloadState.pymupdf = PreloadStatus.ERROR;
|
||||||
|
console.warn('[Preloader] PyMuPDF preload failed:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preloadGhostscript(): Promise<void> {
|
async function preloadGhostscript(): Promise<void> {
|
||||||
if (preloadState.ghostscript !== PreloadStatus.IDLE) return;
|
if (preloadState.ghostscript !== PreloadStatus.IDLE) return;
|
||||||
|
|
||||||
preloadState.ghostscript = PreloadStatus.LOADING;
|
if (!isWasmAvailable('ghostscript')) {
|
||||||
console.log('[Preloader] Starting Ghostscript WASM preload...');
|
preloadState.ghostscript = PreloadStatus.UNAVAILABLE;
|
||||||
|
console.log('[Preloader] Ghostscript not configured, skipping preload');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
preloadState.ghostscript = PreloadStatus.LOADING;
|
||||||
const gsBaseUrl = getWasmBaseUrl('ghostscript');
|
console.log('[Preloader] Starting Ghostscript WASM preload...');
|
||||||
const gsModule = await loadGsWASM({
|
|
||||||
locateFile: (path: string) => {
|
try {
|
||||||
if (path.endsWith('.wasm')) {
|
const gsBaseUrl = getWasmBaseUrl('ghostscript')!;
|
||||||
return gsBaseUrl + 'gs.wasm';
|
|
||||||
}
|
let packageBaseUrl = gsBaseUrl;
|
||||||
return path;
|
if (packageBaseUrl.endsWith('/assets/')) {
|
||||||
},
|
packageBaseUrl = packageBaseUrl.slice(0, -8);
|
||||||
print: () => { },
|
} else if (packageBaseUrl.endsWith('/assets')) {
|
||||||
printErr: () => { },
|
packageBaseUrl = packageBaseUrl.slice(0, -7);
|
||||||
});
|
|
||||||
setCachedGsModule(gsModule as any);
|
|
||||||
preloadState.ghostscript = PreloadStatus.READY;
|
|
||||||
console.log('[Preloader] Ghostscript WASM ready');
|
|
||||||
} catch (e) {
|
|
||||||
preloadState.ghostscript = PreloadStatus.ERROR;
|
|
||||||
console.warn('[Preloader] Ghostscript preload failed:', e);
|
|
||||||
}
|
}
|
||||||
|
const normalizedUrl = packageBaseUrl.endsWith('/')
|
||||||
|
? packageBaseUrl
|
||||||
|
: `${packageBaseUrl}/`;
|
||||||
|
|
||||||
|
const libUrl = `${normalizedUrl}dist/index.js`;
|
||||||
|
const module = await import(/* @vite-ignore */ libUrl);
|
||||||
|
const loadGsWASM = module.loadGhostscriptWASM || module.default;
|
||||||
|
const { setCachedGsModule } = await import('./ghostscript-loader.js');
|
||||||
|
|
||||||
|
const gsModule = await loadGsWASM({
|
||||||
|
baseUrl: `${normalizedUrl}assets/`,
|
||||||
|
locateFile: (path: string) => {
|
||||||
|
if (path.endsWith('.wasm')) {
|
||||||
|
return `${normalizedUrl}assets/gs.wasm`;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
},
|
||||||
|
print: () => {},
|
||||||
|
printErr: () => {},
|
||||||
|
});
|
||||||
|
setCachedGsModule(gsModule as any);
|
||||||
|
preloadState.ghostscript = PreloadStatus.READY;
|
||||||
|
console.log('[Preloader] Ghostscript WASM ready');
|
||||||
|
} catch (e) {
|
||||||
|
preloadState.ghostscript = PreloadStatus.ERROR;
|
||||||
|
console.warn('[Preloader] Ghostscript preload failed:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleIdleTask(task: () => Promise<void>): void {
|
function scheduleIdleTask(task: () => Promise<void>): void {
|
||||||
if ('requestIdleCallback' in window) {
|
if ('requestIdleCallback' in window) {
|
||||||
requestIdleCallback(() => task(), { timeout: 5000 });
|
requestIdleCallback(() => task(), { timeout: 5000 });
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => task(), 1000);
|
setTimeout(() => task(), 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startBackgroundPreload(): void {
|
export function startBackgroundPreload(): void {
|
||||||
console.log('[Preloader] Scheduling background WASM preloads...');
|
console.log('[Preloader] Scheduling background WASM preloads...');
|
||||||
|
|
||||||
const libreOfficePages = [
|
const libreOfficePages = [
|
||||||
'word-to-pdf', 'excel-to-pdf', 'ppt-to-pdf', 'powerpoint-to-pdf',
|
'word-to-pdf',
|
||||||
'docx-to-pdf', 'xlsx-to-pdf', 'pptx-to-pdf', 'csv-to-pdf',
|
'excel-to-pdf',
|
||||||
'rtf-to-pdf', 'odt-to-pdf', 'ods-to-pdf', 'odp-to-pdf'
|
'ppt-to-pdf',
|
||||||
];
|
'powerpoint-to-pdf',
|
||||||
|
'docx-to-pdf',
|
||||||
|
'xlsx-to-pdf',
|
||||||
|
'pptx-to-pdf',
|
||||||
|
'csv-to-pdf',
|
||||||
|
'rtf-to-pdf',
|
||||||
|
'odt-to-pdf',
|
||||||
|
'ods-to-pdf',
|
||||||
|
'odp-to-pdf',
|
||||||
|
];
|
||||||
|
|
||||||
const currentPath = window.location.pathname;
|
const currentPath = window.location.pathname;
|
||||||
const isLibreOfficePage = libreOfficePages.some(page => currentPath.includes(page));
|
const isLibreOfficePage = libreOfficePages.some((page) =>
|
||||||
|
currentPath.includes(page)
|
||||||
|
);
|
||||||
|
|
||||||
if (isLibreOfficePage) {
|
if (isLibreOfficePage) {
|
||||||
console.log('[Preloader] Skipping preloads on LibreOffice page to save memory');
|
console.log(
|
||||||
return;
|
'[Preloader] Skipping preloads on LibreOffice page to save memory'
|
||||||
}
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
scheduleIdleTask(async () => {
|
scheduleIdleTask(async () => {
|
||||||
console.log('[Preloader] Starting sequential WASM preloads...');
|
console.log('[Preloader] Starting sequential WASM preloads...');
|
||||||
|
|
||||||
await preloadPyMuPDF();
|
await preloadPyMuPDF();
|
||||||
await preloadGhostscript();
|
await preloadGhostscript();
|
||||||
|
|
||||||
console.log('[Preloader] Sequential preloads complete (LibreOffice skipped - loaded on demand)');
|
console.log('[Preloader] Sequential preloads complete');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
328
src/js/utils/wasm-provider.ts
Normal file
328
src/js/utils/wasm-provider.ts
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
export type WasmPackage = 'pymupdf' | 'ghostscript' | 'cpdf';
|
||||||
|
|
||||||
|
interface WasmProviderConfig {
|
||||||
|
pymupdf?: string;
|
||||||
|
ghostscript?: string;
|
||||||
|
cpdf?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'bentopdf:wasm-providers';
|
||||||
|
|
||||||
|
class WasmProviderManager {
|
||||||
|
private config: WasmProviderConfig;
|
||||||
|
private validationCache: Map<WasmPackage, boolean> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.config = this.loadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadConfig(): WasmProviderConfig {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
return JSON.parse(stored);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
'[WasmProvider] Failed to load config from localStorage:',
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveConfig(): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.config));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[WasmProvider] Failed to save config to localStorage:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl(packageName: WasmPackage): string | undefined {
|
||||||
|
return this.config[packageName];
|
||||||
|
}
|
||||||
|
|
||||||
|
setUrl(packageName: WasmPackage, url: string): void {
|
||||||
|
const normalizedUrl = url.endsWith('/') ? url : `${url}/`;
|
||||||
|
this.config[packageName] = normalizedUrl;
|
||||||
|
this.validationCache.delete(packageName);
|
||||||
|
this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUrl(packageName: WasmPackage): void {
|
||||||
|
delete this.config[packageName];
|
||||||
|
this.validationCache.delete(packageName);
|
||||||
|
this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
isConfigured(packageName: WasmPackage): boolean {
|
||||||
|
return !!this.config[packageName];
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAnyProvider(): boolean {
|
||||||
|
return Object.keys(this.config).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateUrl(
|
||||||
|
packageName: WasmPackage,
|
||||||
|
url?: string
|
||||||
|
): Promise<{ valid: boolean; error?: string }> {
|
||||||
|
const testUrl = url || this.config[packageName];
|
||||||
|
if (!testUrl) {
|
||||||
|
return { valid: false, error: 'No URL configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(testUrl);
|
||||||
|
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: 'URL must start with http:// or https://',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error:
|
||||||
|
'Invalid URL format. Please enter a valid URL (e.g., https://example.com/wasm/)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedUrl = testUrl.endsWith('/') ? testUrl : `${testUrl}/`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const testFiles: Record<WasmPackage, string> = {
|
||||||
|
pymupdf: 'dist/index.js',
|
||||||
|
ghostscript: 'gs.js',
|
||||||
|
cpdf: 'coherentpdf.browser.min.js',
|
||||||
|
};
|
||||||
|
|
||||||
|
const testFile = testFiles[packageName];
|
||||||
|
const fullUrl = `${normalizedUrl}${testFile}`;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s
|
||||||
|
|
||||||
|
const response = await fetch(fullUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
mode: 'cors',
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Could not find ${testFile} at the specified URL (HTTP ${response.status}). Make sure the file exists.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
if (reader) {
|
||||||
|
try {
|
||||||
|
await reader.read();
|
||||||
|
reader.cancel();
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `File exists but could not be read. Check CORS configuration.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
contentType &&
|
||||||
|
!contentType.includes('javascript') &&
|
||||||
|
!contentType.includes('application/octet-stream') &&
|
||||||
|
!contentType.includes('text/')
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `The URL returned unexpected content type: ${contentType}. Expected a JavaScript file.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url || url === this.config[packageName]) {
|
||||||
|
this.validationCache.set(packageName, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||||
|
|
||||||
|
if (
|
||||||
|
errorMessage.includes('Failed to fetch') ||
|
||||||
|
errorMessage.includes('NetworkError')
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error:
|
||||||
|
'Network error: Could not connect to the URL. Check that the URL is correct and the server allows CORS requests.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: `Network error: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllProviders(): WasmProviderConfig {
|
||||||
|
return { ...this.config };
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAll(): void {
|
||||||
|
this.config = {};
|
||||||
|
this.validationCache.clear();
|
||||||
|
this.saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPackageDisplayName(packageName: WasmPackage): string {
|
||||||
|
const names: Record<WasmPackage, string> = {
|
||||||
|
pymupdf: 'PyMuPDF (Document Processing)',
|
||||||
|
ghostscript: 'Ghostscript (PDF/A Conversion)',
|
||||||
|
cpdf: 'CoherentPDF (Bookmarks & Metadata)',
|
||||||
|
};
|
||||||
|
return names[packageName];
|
||||||
|
}
|
||||||
|
|
||||||
|
getPackageFeatures(packageName: WasmPackage): string[] {
|
||||||
|
const features: Record<WasmPackage, string[]> = {
|
||||||
|
pymupdf: [
|
||||||
|
'PDF to Text',
|
||||||
|
'PDF to Markdown',
|
||||||
|
'PDF to SVG',
|
||||||
|
'PDF to Images (High Quality)',
|
||||||
|
'PDF to DOCX',
|
||||||
|
'PDF to Excel/CSV',
|
||||||
|
'Extract Images',
|
||||||
|
'Extract Tables',
|
||||||
|
'EPUB/MOBI/FB2/XPS/CBZ to PDF',
|
||||||
|
'Image Compression',
|
||||||
|
'Deskew PDF',
|
||||||
|
'PDF Layers',
|
||||||
|
],
|
||||||
|
ghostscript: ['PDF/A Conversion', 'Font to Outline'],
|
||||||
|
cpdf: [
|
||||||
|
'Merge PDF',
|
||||||
|
'Alternate Merge',
|
||||||
|
'Split by Bookmarks',
|
||||||
|
'Table of Contents',
|
||||||
|
'PDF to JSON',
|
||||||
|
'JSON to PDF',
|
||||||
|
'Add/Edit/Extract Attachments',
|
||||||
|
'Edit Bookmarks',
|
||||||
|
'PDF Metadata',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return features[packageName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WasmProvider = new WasmProviderManager();
|
||||||
|
|
||||||
|
export function showWasmRequiredDialog(
|
||||||
|
packageName: WasmPackage,
|
||||||
|
onConfigure?: () => void
|
||||||
|
): void {
|
||||||
|
const displayName = WasmProvider.getPackageDisplayName(packageName);
|
||||||
|
const features = WasmProvider.getPackageFeatures(packageName);
|
||||||
|
|
||||||
|
// Create modal
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className =
|
||||||
|
'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4';
|
||||||
|
overlay.id = 'wasm-required-modal';
|
||||||
|
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className =
|
||||||
|
'bg-gray-800 rounded-2xl max-w-md w-full shadow-2xl border border-gray-700';
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<div class="w-12 h-12 rounded-full bg-amber-500/20 flex items-center justify-center">
|
||||||
|
<svg class="w-6 h-6 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-white">Advanced Feature Required</h3>
|
||||||
|
<p class="text-sm text-gray-400">External processing module needed</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-gray-300 mb-4">
|
||||||
|
This feature requires <strong class="text-white">${displayName}</strong> to be configured.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-gray-700/50 rounded-lg p-4 mb-4">
|
||||||
|
<p class="text-sm text-gray-400 mb-2">Features enabled by this module:</p>
|
||||||
|
<ul class="text-sm text-gray-300 space-y-1">
|
||||||
|
${features
|
||||||
|
.slice(0, 4)
|
||||||
|
.map(
|
||||||
|
(f) =>
|
||||||
|
`<li class="flex items-center gap-2"><span class="text-green-400">✓</span> ${f}</li>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
${features.length > 4 ? `<li class="text-gray-500">+ ${features.length - 4} more...</li>` : ''}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-gray-500 mb-4">
|
||||||
|
This module is licensed under AGPL-3.0. By configuring it, you agree to its license terms.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-700 p-4 flex gap-3">
|
||||||
|
<button id="wasm-modal-cancel" class="flex-1 px-4 py-2.5 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 transition-colors font-medium">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button id="wasm-modal-configure" class="flex-1 px-4 py-2.5 rounded-lg bg-gradient-to-r from-blue-600 to-blue-500 text-white hover:from-blue-500 hover:to-blue-400 transition-all font-medium">
|
||||||
|
Configure
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
overlay.appendChild(modal);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
const cancelBtn = modal.querySelector('#wasm-modal-cancel');
|
||||||
|
const configureBtn = modal.querySelector('#wasm-modal-configure');
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
overlay.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelBtn?.addEventListener('click', closeModal);
|
||||||
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay) closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
configureBtn?.addEventListener('click', () => {
|
||||||
|
closeModal();
|
||||||
|
if (onConfigure) {
|
||||||
|
onConfigure();
|
||||||
|
} else {
|
||||||
|
window.location.href = `${import.meta.env.BASE_URL}wasm-settings.html`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireWasm(
|
||||||
|
packageName: WasmPackage,
|
||||||
|
onAvailable?: () => void
|
||||||
|
): boolean {
|
||||||
|
if (WasmProvider.isConfigured(packageName)) {
|
||||||
|
onAvailable?.();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
showWasmRequiredDialog(packageName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -191,6 +191,29 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 bg-gray-700/50 p-4 rounded-lg border border-gray-600"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="pre-flatten"
|
||||||
|
class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500 focus:ring-2"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="pre-flatten"
|
||||||
|
class="text-sm font-medium text-gray-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
Pre-flatten PDF (recommended for complex files)
|
||||||
|
</label>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">
|
||||||
|
Converts the PDF to images first, ensuring better PDF/A
|
||||||
|
compliance. Recommended if validation fails on the normal
|
||||||
|
conversion.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button id="process-btn" class="btn-gradient w-full mt-4">
|
<button id="process-btn" class="btn-gradient w-full mt-4">
|
||||||
Convert to PDF/A
|
Convert to PDF/A
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
332
src/pages/wasm-settings.html
Normal file
332
src/pages/wasm-settings.html
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<!-- Primary Meta Tags -->
|
||||||
|
<title>
|
||||||
|
Advanced Features Settings - Configure WASM Modules | BentoPDF
|
||||||
|
</title>
|
||||||
|
<meta
|
||||||
|
name="title"
|
||||||
|
content="Advanced Features Settings - Configure WASM Modules | BentoPDF"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Configure advanced PDF processing modules for BentoPDF. Enable features like PDF/A conversion, document extraction, and more."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content="wasm settings, pdf processing, advanced features"
|
||||||
|
/>
|
||||||
|
<meta name="author" content="BentoPDF" />
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
|
||||||
|
<!-- Canonical URL -->
|
||||||
|
<link rel="canonical" href="https://www.bentopdf.com/wasm-settings.html" />
|
||||||
|
|
||||||
|
<!-- Mobile Web App -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="WASM Settings" />
|
||||||
|
|
||||||
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Web App Manifest -->
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
|
||||||
|
<!-- Favicons -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="192x192"
|
||||||
|
href="/images/favicon-192x192.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="512x512"
|
||||||
|
href="/images/favicon-512x512.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/images/apple-touch-icon.png"
|
||||||
|
/>
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="32x32" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="antialiased bg-gray-900">
|
||||||
|
{{> navbar }}
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="uploader"
|
||||||
|
class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="tool-uploader"
|
||||||
|
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
id="back-to-tools"
|
||||||
|
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
|
||||||
|
>
|
||||||
|
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||||
|
<span class="cursor-pointer" data-i18n="tools.backToTools">
|
||||||
|
Back to Tools
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h1 class="text-2xl font-bold text-white mb-2">
|
||||||
|
Advanced Features Settings
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-400 mb-6">
|
||||||
|
Configure external processing modules to enable advanced PDF features.
|
||||||
|
These modules are optional and licensed separately.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Info Banner -->
|
||||||
|
<div
|
||||||
|
class="bg-amber-900/30 border border-amber-600/50 rounded-lg p-4 mb-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<i
|
||||||
|
data-lucide="info"
|
||||||
|
class="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5"
|
||||||
|
></i>
|
||||||
|
<div>
|
||||||
|
<p class="text-amber-200 text-sm">
|
||||||
|
<strong>Why is this needed?</strong> Some advanced features
|
||||||
|
require external processing modules that are licensed under
|
||||||
|
AGPL-3.0. By providing your own module URLs, you can enable
|
||||||
|
these features.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PyMuPDF Section -->
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="bg-gray-700/50 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-white">PyMuPDF</h3>
|
||||||
|
<p class="text-xs text-gray-400">Document Processing Engine</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
id="pymupdf-status"
|
||||||
|
class="text-xs px-2 py-1 rounded-full bg-gray-600 text-gray-300"
|
||||||
|
>
|
||||||
|
Not Configured
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-400 mb-3">
|
||||||
|
Enables: PDF to Text, Markdown, SVG, DOCX, Excel • Extract
|
||||||
|
Images/Tables • Format Conversion
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="pymupdf-url"
|
||||||
|
placeholder="https://your-cdn.com/pymupdf-wasm/"
|
||||||
|
class="flex-1 bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="pymupdf-test"
|
||||||
|
class="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mt-2">
|
||||||
|
<span class="text-xs text-gray-500">Recommended:</span>
|
||||||
|
<code
|
||||||
|
class="text-xs text-indigo-400 bg-gray-800 px-2 py-1 rounded flex-1 truncate"
|
||||||
|
>https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.1.9/</code
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-copy="https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.1.9/"
|
||||||
|
class="copy-btn p-1.5 bg-gray-600 hover:bg-gray-500 rounded text-gray-300 hover:text-white transition-colors"
|
||||||
|
title="Copy to clipboard"
|
||||||
|
>
|
||||||
|
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ghostscript Section -->
|
||||||
|
<div class="bg-gray-700/50 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-white">Ghostscript</h3>
|
||||||
|
<p class="text-xs text-gray-400">PDF/A Conversion Engine</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
id="ghostscript-status"
|
||||||
|
class="text-xs px-2 py-1 rounded-full bg-gray-600 text-gray-300"
|
||||||
|
>
|
||||||
|
Not Configured
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-400 mb-3">
|
||||||
|
Enables: PDF/A-1b, PDF/A-2b, PDF/A-3b Conversion • Font to Outline
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="ghostscript-url"
|
||||||
|
placeholder="https://your-cdn.com/ghostscript-wasm/"
|
||||||
|
class="flex-1 bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="ghostscript-test"
|
||||||
|
class="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mt-2">
|
||||||
|
<span class="text-xs text-gray-500">Recommended:</span>
|
||||||
|
<code
|
||||||
|
class="text-xs text-indigo-400 bg-gray-800 px-2 py-1 rounded flex-1 truncate"
|
||||||
|
>https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/</code
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-copy="https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/"
|
||||||
|
class="copy-btn p-1.5 bg-gray-600 hover:bg-gray-500 rounded text-gray-300 hover:text-white transition-colors"
|
||||||
|
title="Copy to clipboard"
|
||||||
|
>
|
||||||
|
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CPDF Section -->
|
||||||
|
<div class="bg-gray-700/50 rounded-lg p-4 border border-gray-600">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-white">CoherentPDF</h3>
|
||||||
|
<p class="text-xs text-gray-400">Bookmarks & Metadata Engine</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
id="cpdf-status"
|
||||||
|
class="text-xs px-2 py-1 rounded-full bg-gray-600 text-gray-300"
|
||||||
|
>
|
||||||
|
Not Configured
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-400 mb-3">
|
||||||
|
Enables: Split by Bookmarks • Edit Bookmarks • PDF Metadata
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="cpdf-url"
|
||||||
|
placeholder="https://your-cdn.com/cpdf/"
|
||||||
|
class="flex-1 bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 text-sm"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="cpdf-test"
|
||||||
|
class="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 mt-2">
|
||||||
|
<span class="text-xs text-gray-500">Recommended:</span>
|
||||||
|
<code
|
||||||
|
class="text-xs text-indigo-400 bg-gray-800 px-2 py-1 rounded flex-1 truncate"
|
||||||
|
>https://cdn.jsdelivr.net/npm/coherentpdf/dist/</code
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
data-copy="https://cdn.jsdelivr.net/npm/coherentpdf/dist/"
|
||||||
|
class="copy-btn p-1.5 bg-gray-600 hover:bg-gray-500 rounded text-gray-300 hover:text-white transition-colors"
|
||||||
|
title="Copy to clipboard"
|
||||||
|
>
|
||||||
|
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex gap-3 mt-6">
|
||||||
|
<button id="save-btn" class="btn-gradient flex-1">
|
||||||
|
Save Configuration
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="clear-btn"
|
||||||
|
class="px-6 py-2.5 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg font-medium transition-colors border border-red-600/50"
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- License Notice -->
|
||||||
|
<div
|
||||||
|
class="mt-6 p-4 bg-gray-700/30 rounded-lg border border-gray-600/50"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-gray-500">
|
||||||
|
<strong class="text-gray-400">License Notice:</strong> The external
|
||||||
|
modules (PyMuPDF, Ghostscript, CoherentPDF) are licensed under
|
||||||
|
AGPL-3.0 or similar copyleft licenses. By configuring and using
|
||||||
|
these modules, you agree to their respective license terms. BentoPDF
|
||||||
|
is compatible with any Ghostscript WASM and PyMuPDF WASM
|
||||||
|
implementation that follows the expected interface.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loader Modal -->
|
||||||
|
<div
|
||||||
|
id="loader-modal"
|
||||||
|
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl"
|
||||||
|
>
|
||||||
|
<div class="solid-spinner"></div>
|
||||||
|
<p
|
||||||
|
id="loader-text"
|
||||||
|
class="text-white text-lg font-medium"
|
||||||
|
data-i18n="loader.processing"
|
||||||
|
>
|
||||||
|
Processing...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert Modal -->
|
||||||
|
<div
|
||||||
|
id="alert-modal"
|
||||||
|
class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700"
|
||||||
|
>
|
||||||
|
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">
|
||||||
|
Alert
|
||||||
|
</h3>
|
||||||
|
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||||
|
<button
|
||||||
|
id="alert-ok"
|
||||||
|
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200"
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{> footer }}
|
||||||
|
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||||
|
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||||
|
<script type="module" src="/src/version.ts"></script>
|
||||||
|
<script type="module" src="/src/js/logic/wasm-settings-page.ts"></script>
|
||||||
|
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||||
|
<script type="module" src="/src/js/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -81,6 +81,11 @@
|
|||||||
>Privacy Policy</a
|
>Privacy Policy</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="wasm-settings.html" class="hover:text-indigo-400"
|
||||||
|
>Advanced Settings</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -274,34 +274,6 @@ export default defineConfig(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const staticCopyTargets = [
|
const staticCopyTargets = [
|
||||||
{
|
|
||||||
src: 'node_modules/@bentopdf/pymupdf-wasm/assets/*.wasm',
|
|
||||||
dest: 'pymupdf-wasm',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'node_modules/@bentopdf/pymupdf-wasm/assets/*.js',
|
|
||||||
dest: 'pymupdf-wasm',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'node_modules/@bentopdf/pymupdf-wasm/assets/*.whl',
|
|
||||||
dest: 'pymupdf-wasm',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'node_modules/@bentopdf/pymupdf-wasm/assets/*.zip',
|
|
||||||
dest: 'pymupdf-wasm',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'node_modules/@bentopdf/pymupdf-wasm/assets/*.json',
|
|
||||||
dest: 'pymupdf-wasm',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'node_modules/@bentopdf/gs-wasm/assets/*.wasm',
|
|
||||||
dest: 'ghostscript-wasm',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'node_modules/@bentopdf/gs-wasm/assets/*.js',
|
|
||||||
dest: 'ghostscript-wasm',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
src: 'node_modules/embedpdf-snippet/dist/pdfium.wasm',
|
src: 'node_modules/embedpdf-snippet/dist/pdfium.wasm',
|
||||||
dest: 'embedpdf',
|
dest: 'embedpdf',
|
||||||
|
|||||||
Reference in New Issue
Block a user