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" }
|
||||||
|
# ]
|
||||||
@@ -13,7 +13,7 @@ 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** |
|
||||||
@@ -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
|
||||||
@@ -48,7 +68,7 @@ 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 | ✅ |
|
||||||
@@ -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,11 +118,47 @@ 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 |
|
||||||
|
|
||||||
|
|||||||
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',
|
status: 'success',
|
||||||
modifiedPDF: buffer
|
modifiedPDF: buffer,
|
||||||
}, [buffer]);
|
},
|
||||||
|
[buffer]
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: error instanceof Error
|
message:
|
||||||
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: 'Unknown error occurred while adding attachments.'
|
: '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,
|
||||||
|
|||||||
1
public/workers/alternate-merge.worker.d.ts
vendored
1
public/workers/alternate-merge.worker.d.ts
vendored
@@ -8,6 +8,7 @@ interface InterleaveFile {
|
|||||||
interface InterleaveMessage {
|
interface InterleaveMessage {
|
||||||
command: 'interleave';
|
command: 'interleave';
|
||||||
files: InterleaveFile[];
|
files: InterleaveFile[];
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InterleaveSuccessResponse {
|
interface InterleaveSuccessResponse {
|
||||||
|
|||||||
@@ -1,8 +1,46 @@
|
|||||||
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();
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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') {
|
if (command === 'interleave') {
|
||||||
interleavePDFs(files);
|
interleavePDFs(files);
|
||||||
@@ -16,7 +54,7 @@ function interleavePDFs(files) {
|
|||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
@@ -43,22 +81,29 @@ function interleavePDFs(files) {
|
|||||||
throw new Error('No valid pages to merge.');
|
throw new Error('No valid pages to merge.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedPdf = coherentpdf.mergeSame(pdfsToMerge, true, true, rangesToMerge);
|
const mergedPdf = coherentpdf.mergeSame(
|
||||||
|
pdfsToMerge,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
rangesToMerge
|
||||||
|
);
|
||||||
|
|
||||||
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
||||||
const buffer = mergedPdfBytes.buffer;
|
const buffer = mergedPdfBytes.buffer;
|
||||||
coherentpdf.deletePdf(mergedPdf);
|
coherentpdf.deletePdf(mergedPdf);
|
||||||
loadedPdfs.forEach(pdf => coherentpdf.deletePdf(pdf));
|
loadedPdfs.forEach((pdf) => coherentpdf.deletePdf(pdf));
|
||||||
|
|
||||||
self.postMessage({
|
self.postMessage(
|
||||||
|
{
|
||||||
status: 'success',
|
status: 'success',
|
||||||
pdfBytes: buffer
|
pdfBytes: buffer,
|
||||||
}, [buffer]);
|
},
|
||||||
|
[buffer]
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: error.message || 'Unknown error during interleave merge'
|
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 instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: 'Unknown error occurred during attachment listing.'
|
: '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 instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: 'Unknown error occurred during attachment editing.'
|
: '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 instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: 'Unknown error occurred during attachment extraction.'
|
: '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,9 +34,8 @@ 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. ` +
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
1
public/workers/merge.worker.d.ts
vendored
1
public/workers/merge.worker.d.ts
vendored
@@ -18,6 +18,7 @@ interface MergeMessage {
|
|||||||
command: 'merge';
|
command: 'merge';
|
||||||
files: MergeFile[];
|
files: MergeFile[];
|
||||||
jobs: MergeJob[];
|
jobs: MergeJob[];
|
||||||
|
cpdfUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MergeSuccessResponse {
|
interface MergeSuccessResponse {
|
||||||
|
|||||||
@@ -1,8 +1,46 @@
|
|||||||
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();
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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') {
|
if (command === 'merge') {
|
||||||
mergePDFs(files, jobs);
|
mergePDFs(files, jobs);
|
||||||
@@ -17,7 +55,7 @@ function mergePDFs(files, jobs) {
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,23 +87,30 @@ function mergePDFs(files, jobs) {
|
|||||||
throw new Error('No valid files or pages to merge.');
|
throw new Error('No valid files or pages to merge.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedPdf = coherentpdf.mergeSame(pdfsToMerge, true, true, rangesToMerge);
|
const mergedPdf = coherentpdf.mergeSame(
|
||||||
|
pdfsToMerge,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
rangesToMerge
|
||||||
|
);
|
||||||
|
|
||||||
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
||||||
const buffer = mergedPdfBytes.buffer;
|
const buffer = mergedPdfBytes.buffer;
|
||||||
|
|
||||||
coherentpdf.deletePdf(mergedPdf);
|
coherentpdf.deletePdf(mergedPdf);
|
||||||
Object.values(loadedPdfs).forEach(pdf => coherentpdf.deletePdf(pdf));
|
Object.values(loadedPdfs).forEach((pdf) => coherentpdf.deletePdf(pdf));
|
||||||
|
|
||||||
self.postMessage({
|
self.postMessage(
|
||||||
|
{
|
||||||
status: 'success',
|
status: 'success',
|
||||||
pdfBytes: buffer
|
pdfBytes: buffer,
|
||||||
}, [buffer]);
|
},
|
||||||
|
[buffer]
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: error.message || 'Unknown error during merge'
|
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}`
|
||||||
export type WasmPackage = 'ghostscript' | 'pymupdf';
|
);
|
||||||
|
return userUrl;
|
||||||
export function getWasmBaseUrl(packageName: WasmPackage): string {
|
|
||||||
if (USE_CDN) {
|
|
||||||
return CDN_URLS[packageName];
|
|
||||||
}
|
}
|
||||||
return LOCAL_PATHS[packageName];
|
|
||||||
|
console.warn(
|
||||||
|
`[WASM Config] No URL configured for ${packageName}. Feature unavailable.`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
console.log(`[WASM] Fetching: ${url}`);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch ${fileName}: HTTP ${response.status}`);
|
throw new Error(`Failed to fetch ${fileName}: HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// use this to debug
|
|
||||||
export function getWasmConfigInfo() {
|
export function getWasmConfigInfo() {
|
||||||
return {
|
return {
|
||||||
cdnEnabled: USE_CDN,
|
|
||||||
packageVersions: PACKAGE_VERSIONS,
|
packageVersions: PACKAGE_VERSIONS,
|
||||||
cdnUrls: CDN_URLS,
|
configuredProviders: WasmProvider.getAllProviders(),
|
||||||
localPaths: LOCAL_PATHS,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,3 @@ export const PACKAGE_VERSIONS = {
|
|||||||
ghostscript: '0.1.0',
|
ghostscript: '0.1.0',
|
||||||
pymupdf: '0.1.9',
|
pymupdf: '0.1.9',
|
||||||
} as const;
|
} 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;
|
|
||||||
@@ -3,8 +3,15 @@ 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,
|
||||||
@@ -26,10 +33,14 @@ function resetState() {
|
|||||||
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(
|
||||||
|
'attachment-files-input'
|
||||||
|
) as HTMLInputElement;
|
||||||
if (attachmentInput) attachmentInput.value = '';
|
if (attachmentInput) attachmentInput.value = '';
|
||||||
|
|
||||||
const attachmentLevelOptions = document.getElementById('attachment-level-options');
|
const attachmentLevelOptions = document.getElementById(
|
||||||
|
'attachment-level-options'
|
||||||
|
);
|
||||||
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
||||||
|
|
||||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||||
@@ -41,7 +52,9 @@ function resetState() {
|
|||||||
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(
|
||||||
|
'input[name="attachment-level"][value="document"]'
|
||||||
|
) as HTMLInputElement;
|
||||||
if (documentRadio) documentRadio.checked = true;
|
if (documentRadio) documentRadio.checked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,15 +64,21 @@ worker.onmessage = function (e) {
|
|||||||
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 =
|
||||||
|
pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
|
||||||
downloadFile(
|
downloadFile(
|
||||||
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
||||||
`${originalName}_with_attachments.pdf`
|
`${originalName}_with_attachments.pdf`
|
||||||
);
|
);
|
||||||
|
|
||||||
showAlert('Success', `${pageState.attachments.length} file(s) attached successfully.`, 'success', function () {
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`${pageState.attachments.length} file(s) attached successfully.`,
|
||||||
|
'success',
|
||||||
|
function () {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} else if (data.status === 'error') {
|
} else if (data.status === 'error') {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', data.message || 'Unknown error occurred.');
|
showAlert('Error', data.message || 'Unknown error occurred.');
|
||||||
@@ -82,7 +101,8 @@ async function updateUI() {
|
|||||||
|
|
||||||
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';
|
||||||
@@ -114,7 +134,7 @@ async function updateUI() {
|
|||||||
|
|
||||||
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();
|
||||||
@@ -139,7 +159,9 @@ async function updateUI() {
|
|||||||
|
|
||||||
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(
|
||||||
|
'attachment-level-options'
|
||||||
|
);
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (!attachmentFileList) return;
|
if (!attachmentFileList) return;
|
||||||
@@ -148,7 +170,8 @@ function updateAttachmentList() {
|
|||||||
|
|
||||||
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';
|
||||||
@@ -163,7 +186,8 @@ function updateAttachmentList() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (pageState.attachments.length > 0) {
|
if (pageState.attachments.length > 0) {
|
||||||
if (attachmentLevelOptions) attachmentLevelOptions.classList.remove('hidden');
|
if (attachmentLevelOptions)
|
||||||
|
attachmentLevelOptions.classList.remove('hidden');
|
||||||
if (processBtn) processBtn.classList.remove('hidden');
|
if (processBtn) processBtn.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
||||||
@@ -182,18 +206,32 @@ async function addAttachments() {
|
|||||||
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(
|
||||||
|
'attachment-page-range'
|
||||||
|
) as HTMLInputElement;
|
||||||
pageRange = pageRangeInput?.value?.trim() || '';
|
pageRange = pageRangeInput?.value?.trim() || '';
|
||||||
|
|
||||||
if (!pageRange) {
|
if (!pageRange) {
|
||||||
showAlert('Error', 'Please specify a page range for page-level attachments.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Please specify a page range for page-level attachments.'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,7 +246,9 @@ async function addAttachments() {
|
|||||||
|
|
||||||
for (let i = 0; i < pageState.attachments.length; i++) {
|
for (let i = 0; i < pageState.attachments.length; i++) {
|
||||||
const file = pageState.attachments[i];
|
const file = pageState.attachments[i];
|
||||||
showLoader(`Reading ${file.name} (${i + 1}/${pageState.attachments.length})...`);
|
showLoader(
|
||||||
|
`Reading ${file.name} (${i + 1}/${pageState.attachments.length})...`
|
||||||
|
);
|
||||||
|
|
||||||
const fileBuffer = await file.arrayBuffer();
|
const fileBuffer = await file.arrayBuffer();
|
||||||
attachmentBuffers.push(fileBuffer);
|
attachmentBuffers.push(fileBuffer);
|
||||||
@@ -223,12 +263,12 @@ async function addAttachments() {
|
|||||||
attachmentBuffers: attachmentBuffers,
|
attachmentBuffers: attachmentBuffers,
|
||||||
attachmentNames: attachmentNames,
|
attachmentNames: attachmentNames,
|
||||||
attachmentLevel: attachmentLevel,
|
attachmentLevel: attachmentLevel,
|
||||||
pageRange: pageRange
|
pageRange: pageRange,
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
const transferables = [pdfBuffer, ...attachmentBuffers];
|
const transferables = [pdfBuffer, ...attachmentBuffers];
|
||||||
worker.postMessage(message, transferables);
|
worker.postMessage(message, transferables);
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error attaching files:', error);
|
console.error('Error attaching files:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -239,7 +279,10 @@ async function addAttachments() {
|
|||||||
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 (
|
||||||
|
file.type === 'application/pdf' ||
|
||||||
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
|
) {
|
||||||
pageState.file = file;
|
pageState.file = file;
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
@@ -256,7 +299,9 @@ function handleAttachmentSelect(files: FileList | null) {
|
|||||||
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(
|
||||||
|
'attachment-files-input'
|
||||||
|
) as HTMLInputElement;
|
||||||
const attachmentDropZone = document.getElementById('attachment-drop-zone');
|
const attachmentDropZone = document.getElementById('attachment-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');
|
||||||
@@ -289,7 +334,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const files = e.dataTransfer?.files;
|
const files = e.dataTransfer?.files;
|
||||||
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) {
|
if (pdfFiles.length > 0) {
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
@@ -333,7 +381,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
|
const attachmentLevelRadios = document.querySelectorAll(
|
||||||
|
'input[name="attachment-level"]'
|
||||||
|
);
|
||||||
attachmentLevelRadios.forEach(function (radio) {
|
attachmentLevelRadios.forEach(function (radio) {
|
||||||
radio.addEventListener('change', function (e) {
|
radio.addEventListener('change', function (e) {
|
||||||
const value = (e.target as HTMLInputElement).value;
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ 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: [],
|
||||||
@@ -10,7 +15,9 @@ const pageState: AlternateMergeState = {
|
|||||||
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 = [];
|
||||||
@@ -42,7 +49,8 @@ async function updateUI() {
|
|||||||
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';
|
||||||
@@ -74,7 +82,8 @@ async function updateUI() {
|
|||||||
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 =
|
||||||
|
'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
|
||||||
li.dataset.fileName = file.name;
|
li.dataset.fileName = file.name;
|
||||||
|
|
||||||
const infoDiv = document.createElement('div');
|
const infoDiv = document.createElement('div');
|
||||||
@@ -91,7 +100,8 @@ async function updateUI() {
|
|||||||
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 =
|
||||||
|
'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>`;
|
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);
|
||||||
@@ -121,7 +131,16 @@ async function updateUI() {
|
|||||||
|
|
||||||
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(
|
||||||
|
'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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +150,11 @@ async function mixPages() {
|
|||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
if (!fileList) throw new Error('File list not found');
|
if (!fileList) throw new Error('File list not found');
|
||||||
|
|
||||||
const sortedFileNames = Array.from(fileList.children).map(function (li) {
|
const sortedFileNames = Array.from(fileList.children)
|
||||||
|
.map(function (li) {
|
||||||
return (li as HTMLElement).dataset.fileName;
|
return (li as HTMLElement).dataset.fileName;
|
||||||
}).filter(Boolean) as string[];
|
})
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
interface InterleaveFile {
|
interface InterleaveFile {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -156,19 +177,30 @@ async function mixPages() {
|
|||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
command: 'interleave',
|
command: 'interleave',
|
||||||
files: filesToMerge
|
files: filesToMerge,
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
alternateMergeWorker.postMessage(message, filesToMerge.map(function (f) { return f.data; }));
|
alternateMergeWorker.postMessage(
|
||||||
|
message,
|
||||||
|
filesToMerge.map(function (f) {
|
||||||
|
return f.data;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
alternateMergeWorker.onmessage = function (e: MessageEvent) {
|
alternateMergeWorker.onmessage = function (e: MessageEvent) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
if (e.data.status === 'success') {
|
if (e.data.status === 'success') {
|
||||||
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
||||||
downloadFile(blob, 'alternated-mixed.pdf');
|
downloadFile(blob, 'alternated-mixed.pdf');
|
||||||
showAlert('Success', 'PDFs have been mixed successfully!', 'success', function () {
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'PDFs have been mixed successfully!',
|
||||||
|
'success',
|
||||||
|
function () {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error('Worker interleave error:', e.data.message);
|
console.error('Worker interleave error:', e.data.message);
|
||||||
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
|
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
|
||||||
@@ -180,7 +212,6 @@ async function mixPages() {
|
|||||||
console.error('Worker error:', e);
|
console.error('Worker error:', e);
|
||||||
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Alternate Merge error:', e);
|
console.error('Alternate Merge error:', e);
|
||||||
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||||
@@ -191,7 +222,9 @@ async function mixPages() {
|
|||||||
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) {
|
if (pdfFiles.length > 0) {
|
||||||
pageState.files = pdfFiles;
|
pageState.files = pdfFiles;
|
||||||
|
|||||||
@@ -2,44 +2,79 @@ 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(
|
||||||
|
data: Uint8Array,
|
||||||
|
signature: number[],
|
||||||
|
offset = 0
|
||||||
|
): boolean {
|
||||||
for (let i = 0; i < signature.length; i++) {
|
for (let i = 0; i < signature.length; i++) {
|
||||||
if (data[offset + i] !== signature[i]) return false;
|
if (data[offset + i] !== signature[i]) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectImageFormat(data: Uint8Array): 'jpeg' | 'png' | 'gif' | 'bmp' | 'webp' | 'avif' | 'unknown' {
|
function detectImageFormat(
|
||||||
|
data: Uint8Array
|
||||||
|
): 'jpeg' | 'png' | 'gif' | 'bmp' | 'webp' | 'avif' | 'unknown' {
|
||||||
if (data.length < 12) return 'unknown';
|
if (data.length < 12) return 'unknown';
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.jpeg)) return 'jpeg';
|
if (matchesSignature(data, IMAGE_SIGNATURES.jpeg)) return 'jpeg';
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.png)) return 'png';
|
if (matchesSignature(data, IMAGE_SIGNATURES.png)) return 'png';
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.gif)) return 'gif';
|
if (matchesSignature(data, IMAGE_SIGNATURES.gif)) return 'gif';
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.bmp)) return 'bmp';
|
if (matchesSignature(data, IMAGE_SIGNATURES.bmp)) return 'bmp';
|
||||||
if (matchesSignature(data, IMAGE_SIGNATURES.webp) &&
|
if (
|
||||||
data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50) {
|
matchesSignature(data, IMAGE_SIGNATURES.webp) &&
|
||||||
|
data[8] === 0x57 &&
|
||||||
|
data[9] === 0x45 &&
|
||||||
|
data[10] === 0x42 &&
|
||||||
|
data[11] === 0x50
|
||||||
|
) {
|
||||||
return 'webp';
|
return 'webp';
|
||||||
}
|
}
|
||||||
if (data[4] === 0x66 && data[5] === 0x74 && data[6] === 0x79 && data[7] === 0x70) {
|
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]);
|
const brand = String.fromCharCode(data[8], data[9], data[10], data[11]);
|
||||||
if (brand === 'avif' || brand === 'avis' || brand === 'mif1' || brand === 'miaf') {
|
if (
|
||||||
|
brand === 'avif' ||
|
||||||
|
brand === 'avis' ||
|
||||||
|
brand === 'mif1' ||
|
||||||
|
brand === 'miaf'
|
||||||
|
) {
|
||||||
return 'avif';
|
return 'avif';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +85,10 @@ 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(
|
||||||
|
imageData: ArrayBuffer,
|
||||||
|
filename: string
|
||||||
|
): Promise<Blob> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const blob = new Blob([imageData]);
|
const blob = new Blob([imageData]);
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -88,12 +126,14 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
|
|||||||
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];
|
||||||
@@ -116,7 +156,8 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
|
|||||||
embedMethod = 'png';
|
embedMethod = 'png';
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = embedMethod === 'png'
|
const image =
|
||||||
|
embedMethod === 'png'
|
||||||
? await pdfDoc.embedPng(imageBytes)
|
? await pdfDoc.embedPng(imageBytes)
|
||||||
: await pdfDoc.embedJpg(imageBytes);
|
: await pdfDoc.embedJpg(imageBytes);
|
||||||
const page = pdfDoc.addPage([image.width, image.height]);
|
const page = pdfDoc.addPage([image.width, image.height]);
|
||||||
@@ -129,13 +170,14 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
return new Blob([pdfBytes.buffer as ArrayBuffer], { type: 'application/pdf' });
|
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', () => {
|
||||||
@@ -163,7 +205,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -179,7 +222,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
@@ -242,7 +286,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
let pdfBlob: Blob;
|
let pdfBlob: Blob;
|
||||||
if (isCbzFile(file.name)) {
|
if (isCbzFile(file.name)) {
|
||||||
@@ -271,7 +317,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -302,13 +351,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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) {
|
||||||
const validFiles = Array.from(files).filter(f => {
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
const name = f.name.toLowerCase();
|
const name = f.name.toLowerCase();
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
});
|
});
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
handleFileSelect(dataTransfer.files);
|
handleFileSelect(dataTransfer.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 +2,15 @@ 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,
|
||||||
@@ -39,7 +46,7 @@ worker.onmessage = function (e) {
|
|||||||
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),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,15 +55,21 @@ worker.onmessage = function (e) {
|
|||||||
} 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 =
|
||||||
|
pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
|
||||||
downloadFile(
|
downloadFile(
|
||||||
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
||||||
`${originalName}_edited.pdf`
|
`${originalName}_edited.pdf`
|
||||||
);
|
);
|
||||||
|
|
||||||
showAlert('Success', 'Attachments updated successfully!', 'success', function () {
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'Attachments updated successfully!',
|
||||||
|
'success',
|
||||||
|
function () {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} else if (data.status === 'error') {
|
} else if (data.status === 'error') {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', data.message || 'Unknown error occurred.');
|
showAlert('Error', data.message || 'Unknown error occurred.');
|
||||||
@@ -90,7 +103,8 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
|||||||
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
|
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
|
||||||
|
|
||||||
const removeAllBtn = document.createElement('button');
|
const removeAllBtn = document.createElement('button');
|
||||||
removeAllBtn.className = 'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
|
removeAllBtn.className =
|
||||||
|
'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
|
||||||
removeAllBtn.textContent = 'Remove All Attachments';
|
removeAllBtn.textContent = 'Remove All Attachments';
|
||||||
removeAllBtn.onclick = function () {
|
removeAllBtn.onclick = function () {
|
||||||
if (pageState.allAttachments.length === 0) return;
|
if (pageState.allAttachments.length === 0) return;
|
||||||
@@ -102,7 +116,9 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
|||||||
if (allSelected) {
|
if (allSelected) {
|
||||||
pageState.allAttachments.forEach(function (attachment) {
|
pageState.allAttachments.forEach(function (attachment) {
|
||||||
pageState.attachmentsToRemove.delete(attachment.index);
|
pageState.attachmentsToRemove.delete(attachment.index);
|
||||||
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
|
const element = document.querySelector(
|
||||||
|
`[data-attachment-index="${attachment.index}"]`
|
||||||
|
);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.classList.remove('opacity-50', 'line-through');
|
element.classList.remove('opacity-50', 'line-through');
|
||||||
const btn = element.querySelector('button');
|
const btn = element.querySelector('button');
|
||||||
@@ -116,7 +132,9 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
|||||||
} else {
|
} else {
|
||||||
pageState.allAttachments.forEach(function (attachment) {
|
pageState.allAttachments.forEach(function (attachment) {
|
||||||
pageState.attachmentsToRemove.add(attachment.index);
|
pageState.attachmentsToRemove.add(attachment.index);
|
||||||
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
|
const element = document.querySelector(
|
||||||
|
`[data-attachment-index="${attachment.index}"]`
|
||||||
|
);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.classList.add('opacity-50', 'line-through');
|
element.classList.add('opacity-50', 'line-through');
|
||||||
const btn = element.querySelector('button');
|
const btn = element.querySelector('button');
|
||||||
@@ -136,7 +154,8 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
|||||||
// Attachment items
|
// Attachment items
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
const attachmentDiv = document.createElement('div');
|
const attachmentDiv = document.createElement('div');
|
||||||
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
attachmentDiv.className =
|
||||||
|
'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
|
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
|
||||||
|
|
||||||
const infoDiv = document.createElement('div');
|
const infoDiv = document.createElement('div');
|
||||||
@@ -179,7 +198,9 @@ function displayAttachments(attachments: AttachmentInfo[]) {
|
|||||||
const allSelected = pageState.allAttachments.every(function (att) {
|
const allSelected = pageState.allAttachments.every(function (att) {
|
||||||
return pageState.attachmentsToRemove.has(att.index);
|
return pageState.attachmentsToRemove.has(att.index);
|
||||||
});
|
});
|
||||||
removeAllBtn.textContent = allSelected ? 'Deselect All' : 'Remove All Attachments';
|
removeAllBtn.textContent = allSelected
|
||||||
|
? 'Deselect All'
|
||||||
|
: 'Remove All Attachments';
|
||||||
};
|
};
|
||||||
|
|
||||||
actionsDiv.append(removeBtn);
|
actionsDiv.append(removeBtn);
|
||||||
@@ -197,13 +218,21 @@ async function loadAttachments() {
|
|||||||
|
|
||||||
showLoader('Loading attachments...');
|
showLoader('Loading attachments...');
|
||||||
|
|
||||||
|
// Check if CPDF is configured
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileBuffer = await pageState.file.arrayBuffer();
|
const fileBuffer = await pageState.file.arrayBuffer();
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
command: 'get-attachments',
|
command: 'get-attachments',
|
||||||
fileBuffer: fileBuffer,
|
fileBuffer: fileBuffer,
|
||||||
fileName: pageState.file.name
|
fileName: pageState.file.name,
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.postMessage(message, [fileBuffer]);
|
worker.postMessage(message, [fileBuffer]);
|
||||||
@@ -227,6 +256,13 @@ async function saveChanges() {
|
|||||||
|
|
||||||
showLoader('Processing attachments...');
|
showLoader('Processing attachments...');
|
||||||
|
|
||||||
|
// Check if CPDF is configured (double check)
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileBuffer = await pageState.file.arrayBuffer();
|
const fileBuffer = await pageState.file.arrayBuffer();
|
||||||
|
|
||||||
@@ -234,7 +270,8 @@ async function saveChanges() {
|
|||||||
command: 'edit-attachments',
|
command: 'edit-attachments',
|
||||||
fileBuffer: fileBuffer,
|
fileBuffer: fileBuffer,
|
||||||
fileName: pageState.file.name,
|
fileName: pageState.file.name,
|
||||||
attachmentsToRemove: Array.from(pageState.attachmentsToRemove)
|
attachmentsToRemove: Array.from(pageState.attachmentsToRemove),
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
worker.postMessage(message, [fileBuffer]);
|
worker.postMessage(message, [fileBuffer]);
|
||||||
@@ -255,7 +292,8 @@ async function updateUI() {
|
|||||||
|
|
||||||
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';
|
||||||
@@ -292,7 +330,10 @@ async function updateUI() {
|
|||||||
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 (
|
||||||
|
file.type === 'application/pdf' ||
|
||||||
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
|
) {
|
||||||
pageState.file = file;
|
pageState.file = file;
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
@@ -332,7 +373,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const files = e.dataTransfer?.files;
|
const files = e.dataTransfer?.files;
|
||||||
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) {
|
if (pdfFiles.length > 0) {
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
|
|||||||
@@ -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,8 +2,9 @@ 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'];
|
||||||
@@ -38,7 +39,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -54,7 +56,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
@@ -91,14 +94,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showLoader('Loading 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];
|
||||||
showLoader(`Converting ${originalFile.name}...`);
|
showLoader(`Converting ${originalFile.name}...`);
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||||
|
filetype: FILETYPE,
|
||||||
|
});
|
||||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||||
|
|
||||||
downloadFile(pdfBlob, fileName);
|
downloadFile(pdfBlob, fileName);
|
||||||
@@ -117,9 +121,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||||
|
filetype: FILETYPE,
|
||||||
|
});
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
@@ -140,7 +148,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,13 +182,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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) {
|
||||||
const validFiles = Array.from(files).filter(f => {
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
const name = f.name.toLowerCase();
|
const name = f.name.toLowerCase();
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
});
|
});
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
handleFileSelect(dataTransfer.files);
|
handleFileSelect(dataTransfer.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ 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[];
|
||||||
@@ -35,12 +42,18 @@ function resetState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
function showStatus(
|
||||||
const statusMessage = document.getElementById('status-message') as HTMLElement;
|
message: string,
|
||||||
|
type: 'success' | 'error' | 'info' = 'info'
|
||||||
|
) {
|
||||||
|
const statusMessage = document.getElementById(
|
||||||
|
'status-message'
|
||||||
|
) as HTMLElement;
|
||||||
if (!statusMessage) return;
|
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 ${
|
||||||
|
type === 'success'
|
||||||
? 'bg-green-900 text-green-200'
|
? 'bg-green-900 text-green-200'
|
||||||
: type === 'error'
|
: type === 'error'
|
||||||
? 'bg-red-900 text-red-200'
|
? 'bg-red-900 text-red-200'
|
||||||
@@ -60,7 +73,10 @@ worker.onmessage = function (e) {
|
|||||||
const attachments = e.data.attachments;
|
const attachments = e.data.attachments;
|
||||||
|
|
||||||
if (attachments.length === 0) {
|
if (attachments.length === 0) {
|
||||||
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
showAlert(
|
||||||
|
'No Attachments',
|
||||||
|
'The PDF file(s) do not contain any attachments to extract.'
|
||||||
|
);
|
||||||
resetState();
|
resetState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -76,7 +92,10 @@ worker.onmessage = function (e) {
|
|||||||
zip.generateAsync({ type: 'blob' }).then(function (zipBlob) {
|
zip.generateAsync({ type: 'blob' }).then(function (zipBlob) {
|
||||||
downloadFile(zipBlob, 'extracted-attachments.zip');
|
downloadFile(zipBlob, 'extracted-attachments.zip');
|
||||||
|
|
||||||
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`${attachments.length} attachment(s) extracted successfully!`
|
||||||
|
);
|
||||||
|
|
||||||
showStatus(
|
showStatus(
|
||||||
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
|
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
|
||||||
@@ -90,7 +109,10 @@ worker.onmessage = function (e) {
|
|||||||
console.error('Worker Error:', errorMessage);
|
console.error('Worker Error:', errorMessage);
|
||||||
|
|
||||||
if (errorMessage.includes('No attachments were found')) {
|
if (errorMessage.includes('No attachments were found')) {
|
||||||
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
showAlert(
|
||||||
|
'No Attachments',
|
||||||
|
'The PDF file(s) do not contain any attachments to extract.'
|
||||||
|
);
|
||||||
resetState();
|
resetState();
|
||||||
} else {
|
} else {
|
||||||
showStatus(`Error: ${errorMessage}`, 'error');
|
showStatus(`Error: ${errorMessage}`, 'error');
|
||||||
@@ -119,7 +141,8 @@ async function updateUI() {
|
|||||||
|
|
||||||
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';
|
||||||
@@ -130,7 +153,9 @@ async function updateUI() {
|
|||||||
|
|
||||||
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) {
|
||||||
|
return sum + f.size;
|
||||||
|
}, 0);
|
||||||
sizeSpan.textContent = formatBytes(totalSize);
|
sizeSpan.textContent = formatBytes(totalSize);
|
||||||
|
|
||||||
infoContainer.append(countSpan, sizeSpan);
|
infoContainer.append(countSpan, sizeSpan);
|
||||||
@@ -158,6 +183,12 @@ async function extractAttachments() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if CPDF is configured
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
processBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
processBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
@@ -176,17 +207,22 @@ async function extractAttachments() {
|
|||||||
fileNames.push(file.name);
|
fileNames.push(file.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
showStatus(`Extracting attachments from ${pageState.files.length} file(s)...`, 'info');
|
showStatus(
|
||||||
|
`Extracting attachments from ${pageState.files.length} file(s)...`,
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
command: 'extract-attachments',
|
command: 'extract-attachments',
|
||||||
fileBuffers,
|
fileBuffers,
|
||||||
fileNames,
|
fileNames,
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
const transferables = fileBuffers.map(function (buf) { return buf; });
|
const transferables = fileBuffers.map(function (buf) {
|
||||||
|
return buf;
|
||||||
|
});
|
||||||
worker.postMessage(message, transferables);
|
worker.postMessage(message, transferables);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading files:', error);
|
console.error('Error reading files:', error);
|
||||||
showStatus(
|
showStatus(
|
||||||
@@ -204,7 +240,9 @@ async function extractAttachments() {
|
|||||||
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) {
|
if (pdfFiles.length > 0) {
|
||||||
pageState.files = pdfFiles;
|
pageState.files = pdfFiles;
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
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;
|
||||||
@@ -36,7 +40,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = [];
|
||||||
@@ -49,7 +54,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -65,7 +71,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_: File, i: number) => i !== index);
|
state.files = state.files.filter((_: File, i: number) => i !== index);
|
||||||
@@ -151,7 +158,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading PDF processor...');
|
showLoader('Loading PDF processor...');
|
||||||
await pymupdf.load();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
extractedImages = [];
|
extractedImages = [];
|
||||||
let imgCounter = 0;
|
let imgCounter = 0;
|
||||||
@@ -175,7 +182,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
extractedImages.push({
|
extractedImages.push({
|
||||||
data: imgData.data,
|
data: imgData.data,
|
||||||
name: `image_${imgCounter}.${imgData.ext || 'png'}`,
|
name: `image_${imgCounter}.${imgData.ext || 'png'}`,
|
||||||
ext: imgData.ext || 'png'
|
ext: imgData.ext || 'png',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -189,7 +196,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
if (extractedImages.length === 0) {
|
if (extractedImages.length === 0) {
|
||||||
showAlert('No Images Found', 'No embedded images were found in the selected PDF(s).');
|
showAlert(
|
||||||
|
'No Images Found',
|
||||||
|
'No embedded images were found in the selected PDF(s).'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
displayImages();
|
displayImages();
|
||||||
showAlert(
|
showAlert(
|
||||||
@@ -200,7 +210,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during extraction. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during extraction. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,7 +236,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const handleFileSelect = (files: FileList | null) => {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const pdfFiles = Array.from(files).filter(
|
const pdfFiles = Array.from(files).filter(
|
||||||
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
);
|
);
|
||||||
state.files = [...state.files, ...pdfFiles];
|
state.files = [...state.files, ...pdfFiles];
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ 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 = () => {
|
||||||
@@ -20,7 +19,8 @@ const updateUI = () => {
|
|||||||
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';
|
||||||
@@ -57,15 +57,23 @@ const resetState = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function tableToCsv(rows: (string | null)[][]): string {
|
function tableToCsv(rows: (string | null)[][]): string {
|
||||||
return rows.map(row =>
|
return rows
|
||||||
row.map(cell => {
|
.map((row) =>
|
||||||
|
row
|
||||||
|
.map((cell) => {
|
||||||
const cellStr = cell ?? '';
|
const cellStr = cell ?? '';
|
||||||
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) {
|
if (
|
||||||
|
cellStr.includes(',') ||
|
||||||
|
cellStr.includes('"') ||
|
||||||
|
cellStr.includes('\n')
|
||||||
|
) {
|
||||||
return `"${cellStr.replace(/"/g, '""')}"`;
|
return `"${cellStr.replace(/"/g, '""')}"`;
|
||||||
}
|
}
|
||||||
return cellStr;
|
return cellStr;
|
||||||
}).join(',')
|
})
|
||||||
).join('\n');
|
.join(',')
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extract() {
|
async function extract() {
|
||||||
@@ -82,10 +90,9 @@ async function extract() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
showLoader('Loading Engine...');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pymupdf.load();
|
showLoader('Loading Engine...');
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
showLoader('Extracting tables...');
|
showLoader('Extracting tables...');
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
const doc = await pymupdf.open(file);
|
||||||
@@ -115,7 +122,7 @@ async function extract() {
|
|||||||
rows: table.rows,
|
rows: table.rows,
|
||||||
markdown: table.markdown,
|
markdown: table.markdown,
|
||||||
rowCount: table.rowCount,
|
rowCount: table.rowCount,
|
||||||
colCount: table.colCount
|
colCount: table.colCount,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -147,7 +154,12 @@ async function extract() {
|
|||||||
|
|
||||||
const blob = new Blob([content], { type: mimeType });
|
const blob = new Blob([content], { type: mimeType });
|
||||||
downloadFile(blob, `${baseName}_table.${ext}`);
|
downloadFile(blob, `${baseName}_table.${ext}`);
|
||||||
showAlert('Success', `Extracted 1 table successfully!`, 'success', resetState);
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Extracted 1 table successfully!`,
|
||||||
|
'success',
|
||||||
|
resetState
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showLoader('Creating ZIP file...');
|
showLoader('Creating ZIP file...');
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
@@ -173,7 +185,12 @@ async function extract() {
|
|||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, `${baseName}_tables.zip`);
|
downloadFile(zipBlob, `${baseName}_tables.zip`);
|
||||||
showAlert('Success', `Extracted ${allTables.length} tables successfully!`, 'success', resetState);
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Extracted ${allTables.length} tables successfully!`,
|
||||||
|
'success',
|
||||||
|
resetState
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -198,7 +215,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const handleFileSelect = (newFiles: FileList | null) => {
|
const handleFileSelect = (newFiles: FileList | null) => {
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
|
const validFile = Array.from(newFiles).find(
|
||||||
|
(f) => f.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
|
||||||
if (!validFile) {
|
if (!validFile) {
|
||||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ 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'];
|
||||||
@@ -34,7 +35,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -50,7 +52,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
@@ -87,14 +90,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showLoader('Loading 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];
|
||||||
showLoader(`Converting ${originalFile.name}...`);
|
showLoader(`Converting ${originalFile.name}...`);
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||||
|
filetype: FILETYPE,
|
||||||
|
});
|
||||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||||
|
|
||||||
downloadFile(pdfBlob, fileName);
|
downloadFile(pdfBlob, fileName);
|
||||||
@@ -113,9 +117,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||||
|
filetype: FILETYPE,
|
||||||
|
});
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
@@ -136,7 +144,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,13 +178,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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) {
|
||||||
const validFiles = Array.from(files).filter(f => {
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
const name = f.name.toLowerCase();
|
const name = f.name.toLowerCase();
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
});
|
});
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
handleFileSelect(dataTransfer.files);
|
handleFileSelect(dataTransfer.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,15 +1,18 @@
|
|||||||
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);
|
||||||
@@ -103,7 +106,10 @@ 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) {
|
||||||
@@ -132,7 +138,8 @@ function updateUI() {
|
|||||||
|
|
||||||
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';
|
||||||
@@ -148,7 +155,8 @@ function updateUI() {
|
|||||||
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 =
|
||||||
|
'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 = () => {
|
removeBtn.onclick = () => {
|
||||||
files = files.filter((_, i) => i !== index);
|
files = files.filter((_, i) => i !== index);
|
||||||
@@ -165,10 +173,9 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -184,8 +191,12 @@ async function preprocessFile(file: File): Promise<File> {
|
|||||||
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]
|
||||||
|
: conversionResult;
|
||||||
|
return new File([blob], file.name.replace(/\.(heic|heif)$/i, '.png'), {
|
||||||
|
type: 'image/png',
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to convert HEIC: ${file.name}`, e);
|
console.error(`Failed to convert HEIC: ${file.name}`, e);
|
||||||
throw new Error(`Failed to process HEIC file: ${file.name}`);
|
throw new Error(`Failed to process HEIC file: ${file.name}`);
|
||||||
@@ -212,7 +223,11 @@ async function preprocessFile(file: File): Promise<File> {
|
|||||||
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(
|
||||||
|
new File([blob], file.name.replace(/\.webp$/i, '.png'), {
|
||||||
|
type: 'image/png',
|
||||||
|
})
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('Canvas toBlob failed'));
|
reject(new Error('Canvas toBlob failed'));
|
||||||
}
|
}
|
||||||
@@ -268,7 +283,10 @@ async function convertToPdf() {
|
|||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[ImageToPDF]', e);
|
console.error('[ImageToPDF]', e);
|
||||||
showAlert('Conversion Error', e.message || 'Failed to convert images to PDF.');
|
showAlert(
|
||||||
|
'Conversion Error',
|
||||||
|
e.message || 'Failed to convert images to PDF.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
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);
|
||||||
@@ -89,14 +90,19 @@ function getFileExtension(filename: string): string {
|
|||||||
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) {
|
||||||
@@ -125,7 +131,8 @@ function updateUI() {
|
|||||||
|
|
||||||
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';
|
||||||
@@ -141,7 +148,8 @@ function updateUI() {
|
|||||||
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 =
|
||||||
|
'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 = () => {
|
removeBtn.onclick = () => {
|
||||||
files = files.filter((_, i) => i !== index);
|
files = files.filter((_, i) => i !== index);
|
||||||
@@ -158,10 +166,9 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -188,7 +195,10 @@ async function convertToPdf() {
|
|||||||
});
|
});
|
||||||
} 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(
|
||||||
|
'Conversion Error',
|
||||||
|
e.message || 'Failed to convert images to PDF.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
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 ${
|
||||||
|
type === 'success'
|
||||||
? 'bg-green-900 text-green-200'
|
? 'bg-green-900 text-green-200'
|
||||||
: type === 'error'
|
: type === 'error'
|
||||||
? 'bg-red-900 text-red-200'
|
? 'bg-red-900 text-red-200'
|
||||||
: 'bg-blue-900 text-blue-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({
|
worker.postMessage(
|
||||||
|
{
|
||||||
command: 'convert',
|
command: 'convert',
|
||||||
fileBuffers: fileBuffers,
|
fileBuffers: fileBuffers,
|
||||||
fileNames: selectedFiles.map(f => f.name)
|
fileNames: selectedFiles.map((f) => f.name),
|
||||||
}, fileBuffers);
|
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();
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
getPDFDocument,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
|
import {
|
||||||
|
renderPagesProgressively,
|
||||||
|
cleanupLazyRendering,
|
||||||
|
} from '../utils/render-utils.js';
|
||||||
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||||
|
import {
|
||||||
|
showWasmRequiredDialog,
|
||||||
|
WasmProvider,
|
||||||
|
} from '../utils/wasm-provider.js';
|
||||||
|
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
|
||||||
interface MergeState {
|
interface MergeState {
|
||||||
pdfDocs: Record<string, any>;
|
pdfDocs: Record<string, any>;
|
||||||
@@ -35,7 +50,9 @@ const mergeState: MergeState = {
|
|||||||
mergeSuccess: false,
|
mergeSuccess: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mergeWorker = new Worker(import.meta.env.BASE_URL + 'workers/merge.worker.js');
|
const mergeWorker = new Worker(
|
||||||
|
import.meta.env.BASE_URL + 'workers/merge.worker.js'
|
||||||
|
);
|
||||||
|
|
||||||
function initializeFileListSortable() {
|
function initializeFileListSortable() {
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
@@ -122,7 +139,11 @@ async function renderPageMergeThumbnails() {
|
|||||||
let currentPageNumber = 0;
|
let currentPageNumber = 0;
|
||||||
|
|
||||||
// Function to create wrapper element for each page
|
// Function to create wrapper element for each page
|
||||||
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => {
|
const createWrapper = (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
pageNumber: number,
|
||||||
|
fileName?: string
|
||||||
|
) => {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className =
|
wrapper.className =
|
||||||
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
||||||
@@ -146,7 +167,9 @@ async function renderPageMergeThumbnails() {
|
|||||||
const fileNamePara = document.createElement('p');
|
const fileNamePara = document.createElement('p');
|
||||||
fileNamePara.className =
|
fileNamePara.className =
|
||||||
'text-xs text-gray-400 truncate w-full text-center';
|
'text-xs text-gray-400 truncate w-full text-center';
|
||||||
const fullTitle = fileName ? `${fileName} (page ${pageNumber})` : `Page ${pageNumber}`;
|
const fullTitle = fileName
|
||||||
|
? `${fileName} (page ${pageNumber})`
|
||||||
|
: `Page ${pageNumber}`;
|
||||||
fileNamePara.title = fullTitle;
|
fileNamePara.title = fullTitle;
|
||||||
fileNamePara.textContent = fileName
|
fileNamePara.textContent = fileName
|
||||||
? `${fileName.substring(0, 10)}... (p${pageNumber})`
|
? `${fileName.substring(0, 10)}... (p${pageNumber})`
|
||||||
@@ -162,7 +185,10 @@ async function renderPageMergeThumbnails() {
|
|||||||
if (!pdfjsDoc) continue;
|
if (!pdfjsDoc) continue;
|
||||||
|
|
||||||
// Create a wrapper function that includes the file name
|
// Create a wrapper function that includes the file name
|
||||||
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
const createWrapperWithFileName = (
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
pageNumber: number
|
||||||
|
) => {
|
||||||
return createWrapper(canvas, pageNumber, file.name);
|
return createWrapper(canvas, pageNumber, file.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,13 +203,11 @@ async function renderPageMergeThumbnails() {
|
|||||||
lazyLoadMargin: '300px',
|
lazyLoadMargin: '300px',
|
||||||
onProgress: (current, total) => {
|
onProgress: (current, total) => {
|
||||||
currentPageNumber++;
|
currentPageNumber++;
|
||||||
showLoader(
|
showLoader(`Rendering page previews...`);
|
||||||
`Rendering page previews...`
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onBatchComplete: () => {
|
onBatchComplete: () => {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -253,8 +277,13 @@ const resetState = async () => {
|
|||||||
await updateUI();
|
await updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export async function merge() {
|
export async function merge() {
|
||||||
|
// Check if CPDF is configured
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showLoader('Merging PDFs...');
|
showLoader('Merging PDFs...');
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -269,14 +298,18 @@ export async function merge() {
|
|||||||
|
|
||||||
const sortedFiles = Array.from(fileList.children)
|
const sortedFiles = Array.from(fileList.children)
|
||||||
.map((li) => {
|
.map((li) => {
|
||||||
return state.files.find((f) => f.name === (li as HTMLElement).dataset.fileName);
|
return state.files.find(
|
||||||
|
(f) => f.name === (li as HTMLElement).dataset.fileName
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
for (const file of sortedFiles) {
|
for (const file of sortedFiles) {
|
||||||
if (!file) continue;
|
if (!file) continue;
|
||||||
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||||
const rangeInput = document.getElementById(`range-${safeFileName}`) as HTMLInputElement;
|
const rangeInput = document.getElementById(
|
||||||
|
`range-${safeFileName}`
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
uniqueFileNames.add(file.name);
|
uniqueFileNames.add(file.name);
|
||||||
|
|
||||||
@@ -284,12 +317,12 @@ export async function merge() {
|
|||||||
jobs.push({
|
jobs.push({
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
rangeType: 'specific',
|
rangeType: 'specific',
|
||||||
rangeString: rangeInput.value.trim()
|
rangeString: rangeInput.value.trim(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
jobs.push({
|
jobs.push({
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
rangeType: 'all'
|
rangeType: 'all',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,7 +363,7 @@ export async function merge() {
|
|||||||
jobs.push({
|
jobs.push({
|
||||||
fileName: current.fileName,
|
fileName: current.fileName,
|
||||||
rangeType: 'single',
|
rangeType: 'single',
|
||||||
pageIndex: current.pageIndex
|
pageIndex: current.pageIndex,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Range of pages
|
// Range of pages
|
||||||
@@ -338,7 +371,7 @@ export async function merge() {
|
|||||||
fileName: current.fileName,
|
fileName: current.fileName,
|
||||||
rangeType: 'range',
|
rangeType: 'range',
|
||||||
startPage: current.pageIndex + 1,
|
startPage: current.pageIndex + 1,
|
||||||
endPage: endPage + 1
|
endPage: endPage + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,10 +394,14 @@ export async function merge() {
|
|||||||
const message: MergeMessage = {
|
const message: MergeMessage = {
|
||||||
command: 'merge',
|
command: 'merge',
|
||||||
files: filesToMerge,
|
files: filesToMerge,
|
||||||
jobs: jobs
|
jobs: jobs,
|
||||||
|
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
|
||||||
};
|
};
|
||||||
|
|
||||||
mergeWorker.postMessage(message, filesToMerge.map(f => f.data));
|
mergeWorker.postMessage(
|
||||||
|
message,
|
||||||
|
filesToMerge.map((f) => f.data)
|
||||||
|
);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
mergeWorker.onmessage = (e: MessageEvent<MergeResponse>) => {
|
mergeWorker.onmessage = (e: MessageEvent<MergeResponse>) => {
|
||||||
@@ -373,9 +410,14 @@ export async function merge() {
|
|||||||
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
||||||
downloadFile(blob, 'merged.pdf');
|
downloadFile(blob, 'merged.pdf');
|
||||||
mergeState.mergeSuccess = true;
|
mergeState.mergeSuccess = true;
|
||||||
showAlert('Success', 'PDFs merged successfully!', 'success', async () => {
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'PDFs merged successfully!',
|
||||||
|
'success',
|
||||||
|
async () => {
|
||||||
await resetState();
|
await resetState();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error('Worker merge error:', e.data.message);
|
console.error('Worker merge error:', e.data.message);
|
||||||
showAlert('Error', e.data.message || 'Failed to merge PDFs.');
|
showAlert('Error', e.data.message || 'Failed to merge PDFs.');
|
||||||
@@ -387,7 +429,6 @@ export async function merge() {
|
|||||||
console.error('Worker error:', e);
|
console.error('Worker error:', e);
|
||||||
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Merge error:', e);
|
console.error('Merge error:', e);
|
||||||
showAlert(
|
showAlert(
|
||||||
@@ -400,7 +441,9 @@ export async function merge() {
|
|||||||
|
|
||||||
export async function refreshMergeUI() {
|
export async function refreshMergeUI() {
|
||||||
document.getElementById('merge-options')?.classList.remove('hidden');
|
document.getElementById('merge-options')?.classList.remove('hidden');
|
||||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
if (processBtn) processBtn.disabled = false;
|
if (processBtn) processBtn.disabled = false;
|
||||||
|
|
||||||
const wasInPageMode = mergeState.activeMode === 'page';
|
const wasInPageMode = mergeState.activeMode === 'page';
|
||||||
@@ -432,7 +475,8 @@ export async function refreshMergeUI() {
|
|||||||
const pagePanel = document.getElementById('page-mode-panel');
|
const pagePanel = document.getElementById('page-mode-panel');
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
|
|
||||||
if (!fileModeBtn || !pageModeBtn || !filePanel || !pagePanel || !fileList) return;
|
if (!fileModeBtn || !pageModeBtn || !filePanel || !pagePanel || !fileList)
|
||||||
|
return;
|
||||||
|
|
||||||
fileList.textContent = ''; // Clear list safely
|
fileList.textContent = ''; // Clear list safely
|
||||||
(state.files as File[]).forEach((f, index) => {
|
(state.files as File[]).forEach((f, index) => {
|
||||||
@@ -481,7 +525,8 @@ export async function refreshMergeUI() {
|
|||||||
inputWrapper.append(label, input);
|
inputWrapper.append(label, input);
|
||||||
|
|
||||||
const deleteBtn = document.createElement('button');
|
const deleteBtn = document.createElement('button');
|
||||||
deleteBtn.className = 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 self-end';
|
deleteBtn.className =
|
||||||
|
'text-red-400 hover:text-red-300 p-2 flex-shrink-0 self-end';
|
||||||
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
deleteBtn.title = 'Remove file';
|
deleteBtn.title = 'Remove file';
|
||||||
deleteBtn.onclick = (e) => {
|
deleteBtn.onclick = (e) => {
|
||||||
@@ -565,8 +610,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (fileInput && dropZone) {
|
||||||
fileInput.addEventListener('change', async (e) => {
|
fileInput.addEventListener('change', async (e) => {
|
||||||
const files = (e.target as HTMLInputElement).files;
|
const files = (e.target as HTMLInputElement).files;
|
||||||
@@ -591,7 +634,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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) {
|
||||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
const pdfFiles = Array.from(files).filter(
|
||||||
|
(f) =>
|
||||||
|
f.type === 'application/pdf' ||
|
||||||
|
f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
if (pdfFiles.length > 0) {
|
if (pdfFiles.length > 0) {
|
||||||
state.files = [...state.files, ...pdfFiles];
|
state.files = [...state.files, ...pdfFiles];
|
||||||
await updateUI();
|
await updateUI();
|
||||||
@@ -599,7 +646,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
fileInput.addEventListener('click', () => {
|
fileInput.addEventListener('click', () => {
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
});
|
});
|
||||||
@@ -624,6 +670,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
await merge();
|
await merge();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ 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'];
|
||||||
@@ -34,7 +35,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -50,7 +52,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
@@ -87,14 +90,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showLoader('Loading 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];
|
||||||
showLoader(`Converting ${originalFile.name}...`);
|
showLoader(`Converting ${originalFile.name}...`);
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||||
|
filetype: FILETYPE,
|
||||||
|
});
|
||||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||||
|
|
||||||
downloadFile(pdfBlob, fileName);
|
downloadFile(pdfBlob, fileName);
|
||||||
@@ -113,9 +117,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||||
|
filetype: FILETYPE,
|
||||||
|
});
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
@@ -136,7 +144,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,13 +178,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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) {
|
||||||
const validFiles = Array.from(files).filter(f => {
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
const name = f.name.toLowerCase();
|
const name = f.name.toLowerCase();
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
});
|
});
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
handleFileSelect(dataTransfer.files);
|
handleFileSelect(dataTransfer.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
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;
|
||||||
@@ -15,7 +19,7 @@ interface LayerData {
|
|||||||
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;
|
||||||
@@ -44,7 +48,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (currentFile) {
|
if (currentFile) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
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';
|
||||||
@@ -60,7 +65,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
resetState();
|
resetState();
|
||||||
@@ -99,16 +105,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
|
|
||||||
const promptForInput = (title: string, message: string, defaultValue: string = ''): Promise<string | null> => {
|
const promptForInput = (
|
||||||
|
title: string,
|
||||||
|
message: string,
|
||||||
|
defaultValue: string = ''
|
||||||
|
): Promise<string | null> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const modal = document.getElementById('input-modal');
|
const modal = document.getElementById('input-modal');
|
||||||
const titleEl = document.getElementById('input-title');
|
const titleEl = document.getElementById('input-title');
|
||||||
const messageEl = document.getElementById('input-message');
|
const messageEl = document.getElementById('input-message');
|
||||||
const inputEl = document.getElementById('input-value') as HTMLInputElement;
|
const inputEl = document.getElementById(
|
||||||
|
'input-value'
|
||||||
|
) as HTMLInputElement;
|
||||||
const confirmBtn = document.getElementById('input-confirm');
|
const confirmBtn = document.getElementById('input-confirm');
|
||||||
const cancelBtn = document.getElementById('input-cancel');
|
const cancelBtn = document.getElementById('input-cancel');
|
||||||
|
|
||||||
if (!modal || !titleEl || !messageEl || !inputEl || !confirmBtn || !cancelBtn) {
|
if (
|
||||||
|
!modal ||
|
||||||
|
!titleEl ||
|
||||||
|
!messageEl ||
|
||||||
|
!inputEl ||
|
||||||
|
!confirmBtn ||
|
||||||
|
!cancelBtn
|
||||||
|
) {
|
||||||
console.error('Input modal elements not found');
|
console.error('Input modal elements not found');
|
||||||
resolve(null);
|
resolve(null);
|
||||||
return;
|
return;
|
||||||
@@ -165,9 +184,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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,10 +202,14 @@ 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
|
||||||
|
.querySelectorAll('input[type="checkbox"]')
|
||||||
|
.forEach((checkbox) => {
|
||||||
checkbox.addEventListener('change', (e) => {
|
checkbox.addEventListener('change', (e) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
const xref = parseInt(target.dataset.xref || '0');
|
const xref = parseInt(target.dataset.xref || '0');
|
||||||
@@ -190,7 +217,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
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(
|
||||||
|
(l) => l.xref === xref
|
||||||
|
);
|
||||||
if (layer) {
|
if (layer) {
|
||||||
layer.on = isOn;
|
layer.on = isOn;
|
||||||
}
|
}
|
||||||
@@ -207,7 +236,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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');
|
||||||
@@ -229,14 +260,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
const target = e.target as HTMLButtonElement;
|
const target = e.target as HTMLButtonElement;
|
||||||
const parentXref = parseInt(target.dataset.xref || '0');
|
const parentXref = parseInt(target.dataset.xref || '0');
|
||||||
const parentLayer = Array.from(layersMap.values()).find(l => l.xref === parentXref);
|
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'}":`);
|
const childName = await promptForInput(
|
||||||
|
'Add Child Layer',
|
||||||
|
`Enter name for child layer under "${parentLayer?.text || 'Layer'}":`
|
||||||
|
);
|
||||||
|
|
||||||
if (!childName || !childName.trim()) return;
|
if (!childName || !childName.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const childXref = currentDoc.addOCGWithParent(childName.trim(), parentXref);
|
const childXref = currentDoc.addOCGWithParent(
|
||||||
|
childName.trim(),
|
||||||
|
parentXref
|
||||||
|
);
|
||||||
const parentDisplayOrder = parentLayer?.displayOrder || 0;
|
const parentDisplayOrder = parentLayer?.displayOrder || 0;
|
||||||
layersMap.forEach((l) => {
|
layersMap.forEach((l) => {
|
||||||
if (l.displayOrder > parentDisplayOrder) {
|
if (l.displayOrder > parentDisplayOrder) {
|
||||||
@@ -252,7 +291,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
depth: (parentLayer?.depth || 0) + 1,
|
depth: (parentLayer?.depth || 0) + 1,
|
||||||
parentXref: parentXref,
|
parentXref: parentXref,
|
||||||
displayOrder: parentDisplayOrder + 1
|
displayOrder: parentDisplayOrder + 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
renderLayers();
|
renderLayers();
|
||||||
@@ -272,7 +311,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Loading engine...');
|
showLoader('Loading engine...');
|
||||||
await pymupdf.load();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
showLoader(`Loading layers from ${currentFile.name}...`);
|
showLoader(`Loading layers from ${currentFile.name}...`);
|
||||||
currentDoc = await pymupdf.open(currentFile);
|
currentDoc = await pymupdf.open(currentFile);
|
||||||
@@ -293,7 +332,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
locked: layer.locked,
|
locked: layer.locked,
|
||||||
depth: layer.depth ?? 0,
|
depth: layer.depth ?? 0,
|
||||||
parentXref: layer.parentXref ?? 0,
|
parentXref: layer.parentXref ?? 0,
|
||||||
displayOrder: layer.displayOrder ?? nextDisplayOrder++
|
displayOrder: layer.displayOrder ?? nextDisplayOrder++,
|
||||||
});
|
});
|
||||||
if ((layer.displayOrder ?? -1) >= nextDisplayOrder) {
|
if ((layer.displayOrder ?? -1) >= nextDisplayOrder) {
|
||||||
nextDisplayOrder = layer.displayOrder + 1;
|
nextDisplayOrder = layer.displayOrder + 1;
|
||||||
@@ -309,7 +348,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
renderLayers();
|
renderLayers();
|
||||||
setupLayerHandlers();
|
setupLayerHandlers();
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', error.message || 'Failed to load PDF layers');
|
showAlert('Error', error.message || 'Failed to load PDF layers');
|
||||||
@@ -319,7 +357,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const setupLayerHandlers = () => {
|
const setupLayerHandlers = () => {
|
||||||
const addLayerBtn = document.getElementById('add-layer-btn');
|
const addLayerBtn = document.getElementById('add-layer-btn');
|
||||||
const newLayerInput = document.getElementById('new-layer-name') as HTMLInputElement;
|
const newLayerInput = document.getElementById(
|
||||||
|
'new-layer-name'
|
||||||
|
) as HTMLInputElement;
|
||||||
const saveLayersBtn = document.getElementById('save-layers-btn');
|
const saveLayersBtn = document.getElementById('save-layers-btn');
|
||||||
|
|
||||||
if (addLayerBtn && newLayerInput) {
|
if (addLayerBtn && newLayerInput) {
|
||||||
@@ -343,7 +383,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
locked: false,
|
locked: false,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
parentXref: 0,
|
parentXref: 0,
|
||||||
displayOrder: newDisplayOrder
|
displayOrder: newDisplayOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
renderLayers();
|
renderLayers();
|
||||||
@@ -358,8 +398,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try {
|
try {
|
||||||
showLoader('Saving PDF with layer changes...');
|
showLoader('Saving PDF with layer changes...');
|
||||||
const pdfBytes = currentDoc.save();
|
const pdfBytes = currentDoc.save();
|
||||||
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
const blob = new Blob([new Uint8Array(pdfBytes)], {
|
||||||
const outName = currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
|
const outName =
|
||||||
|
currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
|
||||||
downloadFile(blob, outName);
|
downloadFile(blob, outName);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
resetState();
|
resetState();
|
||||||
@@ -375,7 +418,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const handleFileSelect = (files: FileList | null) => {
|
const 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 (
|
||||||
|
file.type === 'application/pdf' ||
|
||||||
|
file.name.toLowerCase().endsWith('.pdf')
|
||||||
|
) {
|
||||||
currentFile = file;
|
currentFile = file;
|
||||||
updateUI();
|
updateUI();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ 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 = () => {
|
||||||
@@ -20,7 +19,8 @@ const updateUI = () => {
|
|||||||
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';
|
||||||
@@ -57,15 +57,23 @@ const resetState = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function tableToCsv(rows: (string | null)[][]): string {
|
function tableToCsv(rows: (string | null)[][]): string {
|
||||||
return rows.map(row =>
|
return rows
|
||||||
row.map(cell => {
|
.map((row) =>
|
||||||
|
row
|
||||||
|
.map((cell) => {
|
||||||
const cellStr = cell ?? '';
|
const cellStr = cell ?? '';
|
||||||
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) {
|
if (
|
||||||
|
cellStr.includes(',') ||
|
||||||
|
cellStr.includes('"') ||
|
||||||
|
cellStr.includes('\n')
|
||||||
|
) {
|
||||||
return `"${cellStr.replace(/"/g, '""')}"`;
|
return `"${cellStr.replace(/"/g, '""')}"`;
|
||||||
}
|
}
|
||||||
return cellStr;
|
return cellStr;
|
||||||
}).join(',')
|
})
|
||||||
).join('\n');
|
.join(',')
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function convert() {
|
async function convert() {
|
||||||
@@ -77,7 +85,7 @@ async function convert() {
|
|||||||
showLoader('Loading Engine...');
|
showLoader('Loading Engine...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pymupdf.load();
|
const pymupdf = await loadPyMuPDF();
|
||||||
showLoader('Extracting tables...');
|
showLoader('Extracting tables...');
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
const doc = await pymupdf.open(file);
|
||||||
@@ -102,10 +110,15 @@ async function convert() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const csvContent = tableToCsv(allRows.filter(row => row.length > 0));
|
const csvContent = tableToCsv(allRows.filter((row) => row.length > 0));
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
downloadFile(blob, `${baseName}.csv`);
|
downloadFile(blob, `${baseName}.csv`);
|
||||||
showAlert('Success', 'PDF converted to CSV successfully!', 'success', resetState);
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'PDF converted to CSV successfully!',
|
||||||
|
'success',
|
||||||
|
resetState
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||||
@@ -129,7 +142,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const handleFileSelect = (newFiles: FileList | null) => {
|
const handleFileSelect = (newFiles: FileList | null) => {
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
|
const validFile = Array.from(newFiles).find(
|
||||||
|
(f) => f.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
|
||||||
if (!validFile) {
|
if (!validFile) {
|
||||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
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;
|
||||||
@@ -25,7 +29,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = '';
|
||||||
@@ -33,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -49,7 +55,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_: File, i: number) => i !== index);
|
state.files = state.files.filter((_: File, i: number) => i !== index);
|
||||||
@@ -94,7 +101,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading PDF converter...');
|
showLoader('Loading PDF converter...');
|
||||||
await pymupdf.load();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
if (state.files.length === 1) {
|
if (state.files.length === 1) {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
@@ -119,7 +126,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
const docxBlob = await pymupdf.pdfToDocx(file);
|
const docxBlob = await pymupdf.pdfToDocx(file);
|
||||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
@@ -142,14 +151,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const pdfFiles = Array.from(files).filter(
|
const pdfFiles = Array.from(files).filter(
|
||||||
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
);
|
);
|
||||||
state.files = [...state.files, ...pdfFiles];
|
state.files = [...state.files, ...pdfFiles];
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
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 = () => {
|
||||||
@@ -20,7 +19,8 @@ const updateUI = () => {
|
|||||||
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';
|
||||||
@@ -65,7 +65,7 @@ async function convert() {
|
|||||||
showLoader('Loading Engine...');
|
showLoader('Loading Engine...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pymupdf.load();
|
const pymupdf = await loadPyMuPDF();
|
||||||
showLoader('Extracting tables...');
|
showLoader('Extracting tables...');
|
||||||
|
|
||||||
const doc = await pymupdf.open(file);
|
const doc = await pymupdf.open(file);
|
||||||
@@ -87,7 +87,7 @@ async function convert() {
|
|||||||
tables.forEach((table) => {
|
tables.forEach((table) => {
|
||||||
allTables.push({
|
allTables.push({
|
||||||
page: i + 1,
|
page: i + 1,
|
||||||
rows: table.rows
|
rows: table.rows,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -106,16 +106,26 @@ async function convert() {
|
|||||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Table');
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Table');
|
||||||
} else {
|
} else {
|
||||||
allTables.forEach((table, idx) => {
|
allTables.forEach((table, idx) => {
|
||||||
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(0, 31);
|
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(
|
||||||
|
0,
|
||||||
|
31
|
||||||
|
);
|
||||||
const worksheet = XLSX.utils.aoa_to_sheet(table.rows);
|
const worksheet = XLSX.utils.aoa_to_sheet(table.rows);
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const xlsxData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
const xlsxData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
||||||
const blob = new Blob([xlsxData], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
const blob = new Blob([xlsxData], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
});
|
||||||
downloadFile(blob, `${baseName}.xlsx`);
|
downloadFile(blob, `${baseName}.xlsx`);
|
||||||
showAlert('Success', `Extracted ${allTables.length} table(s) to Excel!`, 'success', resetState);
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Extracted ${allTables.length} table(s) to Excel!`,
|
||||||
|
'success',
|
||||||
|
resetState
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
const message = e instanceof Error ? e.message : 'Unknown error';
|
const message = e instanceof Error ? e.message : 'Unknown error';
|
||||||
@@ -139,7 +149,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const handleFileSelect = (newFiles: FileList | null) => {
|
const handleFileSelect = (newFiles: FileList | null) => {
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
|
const validFile = Array.from(newFiles).find(
|
||||||
|
(f) => f.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
|
||||||
if (!validFile) {
|
if (!validFile) {
|
||||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||||
|
|||||||
@@ -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 ${
|
||||||
|
type === 'success'
|
||||||
? 'bg-green-900 text-green-200'
|
? 'bg-green-900 text-green-200'
|
||||||
: type === 'error'
|
: type === 'error'
|
||||||
? 'bg-red-900 text-red-200'
|
? 'bg-red-900 text-red-200'
|
||||||
: 'bg-blue-900 text-blue-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({
|
worker.postMessage(
|
||||||
|
{
|
||||||
command: 'convert',
|
command: 'convert',
|
||||||
fileBuffers: fileBuffers,
|
fileBuffers: fileBuffers,
|
||||||
fileNames: selectedFiles.map(f => f.name)
|
fileNames: selectedFiles.map((f) => f.name),
|
||||||
}, fileBuffers);
|
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,11 +1,15 @@
|
|||||||
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;
|
||||||
@@ -17,7 +21,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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', () => {
|
||||||
@@ -26,7 +32,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = '';
|
||||||
@@ -34,7 +41,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -50,7 +58,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_: File, i: number) => i !== index);
|
state.files = state.files.filter((_: File, i: number) => i !== index);
|
||||||
@@ -95,7 +104,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading PDF converter...');
|
showLoader('Loading PDF converter...');
|
||||||
await pymupdf.load();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
const includeImages = includeImagesCheckbox?.checked ?? false;
|
const includeImages = includeImagesCheckbox?.checked ?? false;
|
||||||
|
|
||||||
@@ -123,7 +132,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
|
||||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
@@ -145,14 +156,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const pdfFiles = Array.from(files).filter(
|
const pdfFiles = Array.from(files).filter(
|
||||||
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
);
|
);
|
||||||
state.files = [...state.files, ...pdfFiles];
|
state.files = [...state.files, ...pdfFiles];
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
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;
|
||||||
@@ -19,7 +21,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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', () => {
|
||||||
@@ -28,7 +32,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = '';
|
||||||
@@ -36,7 +41,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -52,7 +58,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
@@ -105,11 +112,42 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (state.files.length === 1) {
|
if (state.files.length === 1) {
|
||||||
const originalFile = state.files[0];
|
const originalFile = state.files[0];
|
||||||
|
const preFlattenCheckbox = document.getElementById(
|
||||||
|
'pre-flatten'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const shouldPreFlatten = preFlattenCheckbox?.checked || false;
|
||||||
|
|
||||||
|
let fileToConvert = originalFile;
|
||||||
|
|
||||||
|
// Pre-flatten using PyMuPDF rasterization if checkbox is checked
|
||||||
|
if (shouldPreFlatten) {
|
||||||
|
if (!isPyMuPDFAvailable()) {
|
||||||
|
showWasmRequiredDialog('pymupdf');
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Pre-flattening PDF...');
|
||||||
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
|
// Rasterize PDF to images and back to PDF (300 DPI for quality)
|
||||||
|
const flattenedBlob = await (pymupdf as any).rasterizePdf(
|
||||||
|
originalFile,
|
||||||
|
{
|
||||||
|
dpi: 300,
|
||||||
|
format: 'png',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
fileToConvert = new File([flattenedBlob], originalFile.name, {
|
||||||
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
showLoader('Initializing Ghostscript...');
|
showLoader('Initializing Ghostscript...');
|
||||||
|
|
||||||
const convertedBlob = await convertFileToPdfA(
|
const convertedBlob = await convertFileToPdfA(
|
||||||
originalFile,
|
fileToConvert,
|
||||||
level,
|
level,
|
||||||
(msg) => showLoader(msg)
|
(msg) => showLoader(msg)
|
||||||
);
|
);
|
||||||
@@ -133,12 +171,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
const convertedBlob = await convertFileToPdfA(
|
const convertedBlob = await convertFileToPdfA(file, level, (msg) =>
|
||||||
file,
|
showLoader(msg)
|
||||||
level,
|
|
||||||
(msg) => showLoader(msg)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
@@ -195,10 +233,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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) {
|
||||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
const pdfFiles = Array.from(files).filter(
|
||||||
|
(f) =>
|
||||||
|
f.type === 'application/pdf' ||
|
||||||
|
f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
if (pdfFiles.length > 0) {
|
if (pdfFiles.length > 0) {
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
pdfFiles.forEach(f => dataTransfer.items.add(f));
|
pdfFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
handleFileSelect(dataTransfer.files);
|
handleFileSelect(dataTransfer.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ 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 = () => {
|
||||||
@@ -23,7 +24,8 @@ const updateUI = () => {
|
|||||||
|
|
||||||
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';
|
||||||
@@ -39,7 +41,8 @@ const updateUI = () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
files = files.filter((_, i) => i !== index);
|
files = files.filter((_, i) => i !== index);
|
||||||
@@ -70,10 +73,19 @@ async function convert() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if PyMuPDF is configured
|
||||||
|
if (!isPyMuPDFAvailable()) {
|
||||||
|
showWasmRequiredDialog('pymupdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showLoader('Loading Engine...');
|
showLoader('Loading Engine...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pymupdf.load();
|
// Load PyMuPDF dynamically if not already loaded
|
||||||
|
if (!pymupdf) {
|
||||||
|
pymupdf = await loadPyMuPDF();
|
||||||
|
}
|
||||||
|
|
||||||
const isSingleFile = files.length === 1;
|
const isSingleFile = files.length === 1;
|
||||||
|
|
||||||
@@ -88,7 +100,12 @@ async function convert() {
|
|||||||
const svgContent = page.toSvg();
|
const svgContent = page.toSvg();
|
||||||
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
|
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||||
downloadFile(svgBlob, `${baseName}.svg`);
|
downloadFile(svgBlob, `${baseName}.svg`);
|
||||||
showAlert('Success', 'PDF converted to SVG successfully!', 'success', () => resetState());
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'PDF converted to SVG successfully!',
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
for (let i = 0; i < pageCount; i++) {
|
for (let i = 0; i < pageCount; i++) {
|
||||||
@@ -100,7 +117,12 @@ async function convert() {
|
|||||||
showLoader('Creating ZIP file...');
|
showLoader('Creating ZIP file...');
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, `${baseName}_svg.zip`);
|
downloadFile(zipBlob, `${baseName}_svg.zip`);
|
||||||
showAlert('Success', `Converted ${pageCount} pages to SVG!`, 'success', () => resetState());
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Converted ${pageCount} pages to SVG!`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
@@ -114,10 +136,15 @@ async function convert() {
|
|||||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||||
|
|
||||||
for (let i = 0; i < pageCount; i++) {
|
for (let i = 0; i < pageCount; i++) {
|
||||||
showLoader(`File ${f + 1}/${files.length}: Page ${i + 1}/${pageCount}`);
|
showLoader(
|
||||||
|
`File ${f + 1}/${files.length}: Page ${i + 1}/${pageCount}`
|
||||||
|
);
|
||||||
const page = doc.getPage(i);
|
const page = doc.getPage(i);
|
||||||
const svgContent = page.toSvg();
|
const svgContent = page.toSvg();
|
||||||
const fileName = pageCount === 1 ? `${baseName}.svg` : `${baseName}_page_${i + 1}.svg`;
|
const fileName =
|
||||||
|
pageCount === 1
|
||||||
|
? `${baseName}.svg`
|
||||||
|
: `${baseName}_page_${i + 1}.svg`;
|
||||||
zip.file(fileName, svgContent);
|
zip.file(fileName, svgContent);
|
||||||
totalPages++;
|
totalPages++;
|
||||||
}
|
}
|
||||||
@@ -126,7 +153,12 @@ async function convert() {
|
|||||||
showLoader('Creating ZIP file...');
|
showLoader('Creating ZIP file...');
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'pdf_to_svg.zip');
|
downloadFile(zipBlob, 'pdf_to_svg.zip');
|
||||||
showAlert('Success', `Converted ${files.length} files (${totalPages} pages) to SVG!`, 'success', () => resetState());
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`Converted ${files.length} files (${totalPages} pages) to SVG!`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -172,7 +204,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (fileInput && dropZone) {
|
||||||
fileInput.addEventListener('change', (e) => {
|
fileInput.addEventListener('change', (e) => {
|
||||||
handleFileSelect((e.target as HTMLInputElement).files, files.length === 0);
|
handleFileSelect(
|
||||||
|
(e.target as HTMLInputElement).files,
|
||||||
|
files.length === 0
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
dropZone.addEventListener('dragover', (e) => {
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
@@ -196,7 +231,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput?.click());
|
if (addMoreBtn)
|
||||||
|
addMoreBtn.addEventListener('click', () => fileInput?.click());
|
||||||
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
||||||
if (processBtn) processBtn.addEventListener('click', convert);
|
if (processBtn) processBtn.addEventListener('click', convert);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
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);
|
||||||
@@ -20,7 +21,9 @@ function initializePage() {
|
|||||||
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);
|
||||||
@@ -80,12 +83,17 @@ function handleFileUpload(e: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (validFiles.length < newFiles.length) {
|
||||||
showAlert('Invalid Files', 'Some files were skipped. Only PDF files are allowed.');
|
showAlert(
|
||||||
|
'Invalid Files',
|
||||||
|
'Some files were skipped. Only PDF files are allowed.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
@@ -114,7 +122,8 @@ function updateUI() {
|
|||||||
|
|
||||||
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';
|
||||||
@@ -130,7 +139,8 @@ function updateUI() {
|
|||||||
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 =
|
||||||
|
'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 = () => {
|
removeBtn.onclick = () => {
|
||||||
files = files.filter((_, i) => i !== index);
|
files = files.filter((_, i) => i !== index);
|
||||||
@@ -147,10 +157,9 @@ function updateUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -173,7 +182,9 @@ async function extractText() {
|
|||||||
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], {
|
||||||
|
type: 'text/plain;charset=utf-8',
|
||||||
|
});
|
||||||
downloadFile(textBlob, `${baseName}.txt`);
|
downloadFile(textBlob, `${baseName}.txt`);
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -188,7 +199,9 @@ async function extractText() {
|
|||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -200,13 +213,21 @@ async function extractText() {
|
|||||||
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(
|
||||||
|
'Success',
|
||||||
|
`Extracted text from ${files.length} PDF files!`,
|
||||||
|
'success',
|
||||||
|
() => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[PDFToText]', e);
|
console.error('[PDFToText]', e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Extraction Error', e.message || 'Failed to extract text from PDF.');
|
showAlert(
|
||||||
|
'Extraction Error',
|
||||||
|
e.message || 'Failed to extract text from PDF.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
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;
|
||||||
@@ -25,7 +29,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = '';
|
||||||
@@ -33,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -49,7 +55,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
@@ -95,7 +102,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showLoader('Loading engine...');
|
||||||
await pymupdf.load();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
const total = state.files.length;
|
const total = state.files.length;
|
||||||
let completed = 0;
|
let completed = 0;
|
||||||
@@ -108,10 +115,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
||||||
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
const jsonContent = JSON.stringify(llamaDocs, null, 2);
|
||||||
downloadFile(new Blob([jsonContent], { type: 'application/json' }), outName);
|
downloadFile(
|
||||||
|
new Blob([jsonContent], { type: 'application/json' }),
|
||||||
|
outName
|
||||||
|
);
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Extraction Complete', `Successfully extracted PDF for AI/LLM use.`, 'success', () => resetState());
|
showAlert(
|
||||||
|
'Extraction Complete',
|
||||||
|
`Successfully extracted PDF for AI/LLM use.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Multiple files - create ZIP
|
// Multiple files - create ZIP
|
||||||
const JSZip = (await import('jszip')).default;
|
const JSZip = (await import('jszip')).default;
|
||||||
@@ -119,7 +134,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
try {
|
try {
|
||||||
showLoader(`Extracting ${file.name} for AI (${completed + 1}/${total})...`);
|
showLoader(
|
||||||
|
`Extracting ${file.name} for AI (${completed + 1}/${total})...`
|
||||||
|
);
|
||||||
|
|
||||||
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
|
||||||
@@ -141,20 +158,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
if (failed === 0) {
|
if (failed === 0) {
|
||||||
showAlert('Extraction Complete', `Successfully extracted ${completed} PDF(s) for AI/LLM use.`, 'success', () => resetState());
|
showAlert(
|
||||||
|
'Extraction Complete',
|
||||||
|
`Successfully extracted ${completed} PDF(s) for AI/LLM use.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showAlert('Extraction Partial', `Extracted ${completed} PDF(s), failed ${failed}.`, 'warning', () => resetState());
|
showAlert(
|
||||||
|
'Extraction Partial',
|
||||||
|
`Extracted ${completed} PDF(s), failed ${failed}.`,
|
||||||
|
'warning',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during extraction. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during extraction. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
const pdfFiles = Array.from(files).filter(
|
||||||
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
if (pdfFiles.length > 0) {
|
if (pdfFiles.length > 0) {
|
||||||
state.files = [...state.files, ...pdfFiles];
|
state.files = [...state.files, ...pdfFiles];
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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');
|
||||||
@@ -52,7 +53,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
@@ -78,7 +80,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const convert = async () => {
|
const convert = async () => {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
|
showAlert(
|
||||||
|
'No Files',
|
||||||
|
`Please select at least one ${FILETYPE_NAME} file.`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -92,25 +97,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
const baseName = file.name.replace(/\.[^/.]+$/, '');
|
||||||
downloadFile(pdfBlob, `${baseName}.pdf`);
|
downloadFile(pdfBlob, `${baseName}.pdf`);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${file.name} to PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showLoader('Converting multiple files...');
|
showLoader('Converting multiple files...');
|
||||||
const pdfBlob = await mupdf.imagesToPdf(state.files);
|
const pdfBlob = await mupdf.imagesToPdf(state.files);
|
||||||
downloadFile(pdfBlob, 'psd_to_pdf.pdf');
|
downloadFile(pdfBlob, 'psd_to_pdf.pdf');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Conversion Complete', `Successfully converted ${state.files.length} PSD files to a single PDF.`, 'success', () => resetState());
|
showAlert(
|
||||||
|
'Conversion Complete',
|
||||||
|
`Successfully converted ${state.files.length} PSD files to a single PDF.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||||
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const validFiles = Array.from(files).filter(file => {
|
const validFiles = Array.from(files).filter((file) => {
|
||||||
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
|
||||||
return ACCEPTED_EXTENSIONS.includes(ext);
|
return ACCEPTED_EXTENSIONS.includes(ext);
|
||||||
});
|
});
|
||||||
@@ -122,11 +140,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (fileInput && dropZone) {
|
if (fileInput && dropZone) {
|
||||||
fileInput.addEventListener('change', (e) => handleFileSelect((e.target as HTMLInputElement).files));
|
fileInput.addEventListener('change', (e) =>
|
||||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
|
handleFileSelect((e.target as HTMLInputElement).files)
|
||||||
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); });
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
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 (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||||
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
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;
|
||||||
@@ -25,7 +29,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 = '';
|
||||||
@@ -33,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -49,7 +55,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
@@ -94,13 +101,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isPyMuPDFAvailable()) {
|
||||||
|
showWasmRequiredDialog('pymupdf');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showLoader('Loading engine...');
|
||||||
await pymupdf.load();
|
const pymupdf = await loadPyMuPDF();
|
||||||
|
|
||||||
// Get options from UI
|
// Get options from UI
|
||||||
const dpi = parseInt((document.getElementById('rasterize-dpi') as HTMLSelectElement).value) || 150;
|
const dpi =
|
||||||
const format = (document.getElementById('rasterize-format') as HTMLSelectElement).value as 'png' | 'jpeg';
|
parseInt(
|
||||||
const grayscale = (document.getElementById('rasterize-grayscale') as HTMLInputElement).checked;
|
(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;
|
const total = state.files.length;
|
||||||
let completed = 0;
|
let completed = 0;
|
||||||
@@ -114,14 +133,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
dpi,
|
dpi,
|
||||||
format,
|
format,
|
||||||
grayscale,
|
grayscale,
|
||||||
quality: 95
|
quality: 95,
|
||||||
});
|
});
|
||||||
|
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
||||||
downloadFile(rasterizedBlob, outName);
|
downloadFile(rasterizedBlob, outName);
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Rasterization Complete', `Successfully rasterized PDF at ${dpi} DPI.`, 'success', () => resetState());
|
showAlert(
|
||||||
|
'Rasterization Complete',
|
||||||
|
`Successfully rasterized PDF at ${dpi} DPI.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Multiple files - create ZIP
|
// Multiple files - create ZIP
|
||||||
const JSZip = (await import('jszip')).default;
|
const JSZip = (await import('jszip')).default;
|
||||||
@@ -129,16 +153,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
try {
|
try {
|
||||||
showLoader(`Rasterizing ${file.name} (${completed + 1}/${total})...`);
|
showLoader(
|
||||||
|
`Rasterizing ${file.name} (${completed + 1}/${total})...`
|
||||||
|
);
|
||||||
|
|
||||||
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
|
||||||
dpi,
|
dpi,
|
||||||
format,
|
format,
|
||||||
grayscale,
|
grayscale,
|
||||||
quality: 95
|
quality: 95,
|
||||||
});
|
});
|
||||||
|
|
||||||
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
const outName =
|
||||||
|
file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
|
||||||
zip.file(outName, rasterizedBlob);
|
zip.file(outName, rasterizedBlob);
|
||||||
|
|
||||||
completed++;
|
completed++;
|
||||||
@@ -156,20 +183,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
if (failed === 0) {
|
if (failed === 0) {
|
||||||
showAlert('Rasterization Complete', `Successfully rasterized ${completed} PDF(s) at ${dpi} DPI.`, 'success', () => resetState());
|
showAlert(
|
||||||
|
'Rasterization Complete',
|
||||||
|
`Successfully rasterized ${completed} PDF(s) at ${dpi} DPI.`,
|
||||||
|
'success',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showAlert('Rasterization Partial', `Rasterized ${completed} PDF(s), failed ${failed}.`, 'warning', () => resetState());
|
showAlert(
|
||||||
|
'Rasterization Partial',
|
||||||
|
`Rasterized ${completed} PDF(s), failed ${failed}.`,
|
||||||
|
'warning',
|
||||||
|
() => resetState()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during rasterization. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during rasterization. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = (files: FileList | null) => {
|
const handleFileSelect = (files: FileList | null) => {
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
const pdfFiles = Array.from(files).filter(
|
||||||
|
(f) =>
|
||||||
|
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
if (pdfFiles.length > 0) {
|
if (pdfFiles.length > 0) {
|
||||||
state.files = [...state.files, ...pdfFiles];
|
state.files = [...state.files, ...pdfFiles];
|
||||||
updateUI();
|
updateUI();
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { downloadFile, getPDFDocument, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
getPDFDocument,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
|
import {
|
||||||
|
renderPagesProgressively,
|
||||||
|
cleanupLazyRendering,
|
||||||
|
} from '../utils/render-utils.js';
|
||||||
|
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
|
||||||
|
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
|
import.meta.url
|
||||||
|
).toString();
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
let visualSelectorRendered = false;
|
let visualSelectorRendered = false;
|
||||||
@@ -21,7 +34,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const backBtn = document.getElementById('back-to-tools');
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
// Split Mode Elements
|
// Split Mode Elements
|
||||||
const splitModeSelect = document.getElementById('split-mode') as HTMLSelectElement;
|
const splitModeSelect = document.getElementById(
|
||||||
|
'split-mode'
|
||||||
|
) as HTMLSelectElement;
|
||||||
const rangePanel = document.getElementById('range-panel');
|
const rangePanel = document.getElementById('range-panel');
|
||||||
const visualPanel = document.getElementById('visual-select-panel');
|
const visualPanel = document.getElementById('visual-select-panel');
|
||||||
const evenOddPanel = document.getElementById('even-odd-panel');
|
const evenOddPanel = document.getElementById('even-odd-panel');
|
||||||
@@ -43,7 +58,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (fileDisplayArea) {
|
if (fileDisplayArea) {
|
||||||
fileDisplayArea.innerHTML = '';
|
fileDisplayArea.innerHTML = '';
|
||||||
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';
|
||||||
@@ -60,7 +76,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Add remove button
|
// Add remove button
|
||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = [];
|
state.files = [];
|
||||||
@@ -76,7 +93,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try {
|
try {
|
||||||
if (!state.pdfDoc) {
|
if (!state.pdfDoc) {
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file) as ArrayBuffer;
|
const arrayBuffer = (await readFileAsArrayBuffer(
|
||||||
|
file
|
||||||
|
)) as ArrayBuffer;
|
||||||
state.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
state.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
@@ -92,7 +111,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (splitOptions) splitOptions.classList.remove('hidden');
|
if (splitOptions) splitOptions.classList.remove('hidden');
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
if (splitOptions) splitOptions.classList.add('hidden');
|
if (splitOptions) splitOptions.classList.add('hidden');
|
||||||
@@ -119,7 +137,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// If pdfDoc is not loaded yet (e.g. page refresh), try to load it from the first file
|
// If pdfDoc is not loaded yet (e.g. page refresh), try to load it from the first file
|
||||||
if (state.files.length > 0) {
|
if (state.files.length > 0) {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file) as ArrayBuffer;
|
const arrayBuffer = (await readFileAsArrayBuffer(
|
||||||
|
file
|
||||||
|
)) as ArrayBuffer;
|
||||||
state.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
state.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No PDF document loaded');
|
throw new Error('No PDF document loaded');
|
||||||
@@ -172,11 +192,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Render pages progressively with lazy loading
|
// Render pages progressively with lazy loading
|
||||||
await renderPagesProgressively(
|
await renderPagesProgressively(pdf, container, createWrapper, {
|
||||||
pdf,
|
|
||||||
container,
|
|
||||||
createWrapper,
|
|
||||||
{
|
|
||||||
batchSize: 8,
|
batchSize: 8,
|
||||||
useLazyLoading: true,
|
useLazyLoading: true,
|
||||||
lazyLoadMargin: '400px',
|
lazyLoadMargin: '400px',
|
||||||
@@ -185,9 +201,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
},
|
},
|
||||||
onBatchComplete: () => {
|
onBatchComplete: () => {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error rendering visual selector:', error);
|
console.error('Error rendering visual selector:', error);
|
||||||
showAlert('Error', 'Failed to render page previews.');
|
showAlert('Error', 'Failed to render page previews.');
|
||||||
@@ -203,7 +218,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
state.pdfDoc = null;
|
state.pdfDoc = null;
|
||||||
|
|
||||||
// Reset visual selection
|
// Reset visual selection
|
||||||
document.querySelectorAll('.page-thumbnail-wrapper.selected').forEach(el => {
|
document
|
||||||
|
.querySelectorAll('.page-thumbnail-wrapper.selected')
|
||||||
|
.forEach((el) => {
|
||||||
el.classList.remove('selected', 'border-indigo-500');
|
el.classList.remove('selected', 'border-indigo-500');
|
||||||
el.classList.add('border-transparent');
|
el.classList.add('border-transparent');
|
||||||
});
|
});
|
||||||
@@ -212,14 +229,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (container) container.innerHTML = '';
|
if (container) container.innerHTML = '';
|
||||||
|
|
||||||
// Reset inputs
|
// Reset inputs
|
||||||
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement;
|
const pageRangeInput = document.getElementById(
|
||||||
|
'page-range'
|
||||||
|
) as HTMLInputElement;
|
||||||
if (pageRangeInput) pageRangeInput.value = '';
|
if (pageRangeInput) pageRangeInput.value = '';
|
||||||
|
|
||||||
const nValueInput = document.getElementById('split-n-value') as HTMLInputElement;
|
const nValueInput = document.getElementById(
|
||||||
|
'split-n-value'
|
||||||
|
) as HTMLInputElement;
|
||||||
if (nValueInput) nValueInput.value = '5';
|
if (nValueInput) nValueInput.value = '5';
|
||||||
|
|
||||||
// Reset radio buttons to default (range)
|
// Reset radio buttons to default (range)
|
||||||
const rangeRadio = document.querySelector('input[name="split-mode"][value="range"]') as HTMLInputElement;
|
const rangeRadio = document.querySelector(
|
||||||
|
'input[name="split-mode"][value="range"]'
|
||||||
|
) as HTMLInputElement;
|
||||||
if (rangeRadio) {
|
if (rangeRadio) {
|
||||||
rangeRadio.checked = true;
|
rangeRadio.checked = true;
|
||||||
rangeRadio.dispatchEvent(new Event('change'));
|
rangeRadio.dispatchEvent(new Event('change'));
|
||||||
@@ -237,8 +260,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const split = async () => {
|
const split = async () => {
|
||||||
const splitMode = splitModeSelect.value;
|
const splitMode = splitModeSelect.value;
|
||||||
const downloadAsZip =
|
const downloadAsZip =
|
||||||
(document.getElementById('download-as-zip') as HTMLInputElement)?.checked ||
|
(document.getElementById('download-as-zip') as HTMLInputElement)
|
||||||
false;
|
?.checked || false;
|
||||||
|
|
||||||
showLoader('Splitting PDF...');
|
showLoader('Splitting PDF...');
|
||||||
|
|
||||||
@@ -250,7 +273,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
switch (splitMode) {
|
switch (splitMode) {
|
||||||
case 'range':
|
case 'range':
|
||||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
const pageRangeInput = (
|
||||||
|
document.getElementById('page-range') as HTMLInputElement
|
||||||
|
).value;
|
||||||
if (!pageRangeInput) throw new Error('Choose a valid page range.');
|
if (!pageRangeInput) throw new Error('Choose a valid page range.');
|
||||||
const ranges = pageRangeInput.split(',');
|
const ranges = pageRangeInput.split(',');
|
||||||
|
|
||||||
@@ -273,7 +298,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
for (let i = start; i <= end; i++) groupIndices.push(i - 1);
|
for (let i = start; i <= end; i++) groupIndices.push(i - 1);
|
||||||
} else {
|
} else {
|
||||||
const pageNum = Number(trimmedRange);
|
const pageNum = Number(trimmedRange);
|
||||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages)
|
||||||
|
continue;
|
||||||
groupIndices.push(pageNum - 1);
|
groupIndices.push(pageNum - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +322,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const minPage = Math.min(...group) + 1;
|
const minPage = Math.min(...group) + 1;
|
||||||
const maxPage = Math.max(...group) + 1;
|
const maxPage = Math.max(...group) + 1;
|
||||||
const filename = minPage === maxPage
|
const filename =
|
||||||
|
minPage === maxPage
|
||||||
? `page-${minPage}.pdf`
|
? `page-${minPage}.pdf`
|
||||||
: `pages-${minPage}-${maxPage}.pdf`;
|
: `pages-${minPage}-${maxPage}.pdf`;
|
||||||
zip.file(filename, pdfBytes);
|
zip.file(filename, pdfBytes);
|
||||||
@@ -305,9 +332,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'split-pages.zip');
|
downloadFile(zipBlob, 'split-pages.zip');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Success', `PDF split into ${rangeGroups.length} files successfully!`, 'success', () => {
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`PDF split into ${rangeGroups.length} files successfully!`,
|
||||||
|
'success',
|
||||||
|
() => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -316,10 +348,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const choiceElement = document.querySelector(
|
const choiceElement = document.querySelector(
|
||||||
'input[name="even-odd-choice"]:checked'
|
'input[name="even-odd-choice"]:checked'
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
if (!choiceElement) throw new Error('Please select even or odd pages.');
|
if (!choiceElement)
|
||||||
|
throw new Error('Please select even or odd pages.');
|
||||||
const choice = choiceElement.value;
|
const choice = choiceElement.value;
|
||||||
for (let i = 0; i < totalPages; i++) {
|
for (let i = 0; i < totalPages; i++) {
|
||||||
if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i);
|
if (choice === 'even' && (i + 1) % 2 === 0)
|
||||||
|
indicesToExtract.push(i);
|
||||||
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -329,10 +363,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
case 'visual':
|
case 'visual':
|
||||||
indicesToExtract = Array.from(
|
indicesToExtract = Array.from(
|
||||||
document.querySelectorAll('.page-thumbnail-wrapper.selected')
|
document.querySelectorAll('.page-thumbnail-wrapper.selected')
|
||||||
)
|
).map((el) => parseInt((el as HTMLElement).dataset.pageIndex || '0'));
|
||||||
.map((el) => parseInt((el as HTMLElement).dataset.pageIndex || '0'));
|
|
||||||
break;
|
break;
|
||||||
case 'bookmarks':
|
case 'bookmarks':
|
||||||
|
// Check if CPDF is configured
|
||||||
|
if (!isCpdfAvailable()) {
|
||||||
|
showWasmRequiredDialog('cpdf');
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { getCpdf } = await import('../utils/cpdf-helper.js');
|
const { getCpdf } = await import('../utils/cpdf-helper.js');
|
||||||
const cpdf = await getCpdf();
|
const cpdf = await getCpdf();
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
@@ -340,7 +379,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
cpdf.startGetBookmarkInfo(pdf);
|
cpdf.startGetBookmarkInfo(pdf);
|
||||||
const bookmarkCount = cpdf.numberBookmarks();
|
const bookmarkCount = cpdf.numberBookmarks();
|
||||||
const bookmarkLevel = (document.getElementById('bookmark-level') as HTMLSelectElement)?.value;
|
const bookmarkLevel = (
|
||||||
|
document.getElementById('bookmark-level') as HTMLSelectElement
|
||||||
|
)?.value;
|
||||||
|
|
||||||
const splitPages: number[] = [];
|
const splitPages: number[] = [];
|
||||||
for (let i = 0; i < bookmarkCount; i++) {
|
for (let i = 0; i < bookmarkCount; i++) {
|
||||||
@@ -365,11 +406,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < splitPages.length; i++) {
|
for (let i = 0; i < splitPages.length; i++) {
|
||||||
const startPage = i === 0 ? 0 : splitPages[i];
|
const startPage = i === 0 ? 0 : splitPages[i];
|
||||||
const endPage = i < splitPages.length - 1 ? splitPages[i + 1] - 1 : totalPages - 1;
|
const endPage =
|
||||||
|
i < splitPages.length - 1
|
||||||
|
? splitPages[i + 1] - 1
|
||||||
|
: totalPages - 1;
|
||||||
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, idx) => startPage + idx);
|
const pageIndices = Array.from(
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
{ length: endPage - startPage + 1 },
|
||||||
|
(_, idx) => startPage + idx
|
||||||
|
);
|
||||||
|
const copiedPages = await newPdf.copyPages(
|
||||||
|
state.pdfDoc,
|
||||||
|
pageIndices
|
||||||
|
);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
const pdfBytes2 = await newPdf.save();
|
const pdfBytes2 = await newPdf.save();
|
||||||
zip.file(`split-${i + 1}.pdf`, pdfBytes2);
|
zip.file(`split-${i + 1}.pdf`, pdfBytes2);
|
||||||
@@ -384,7 +434,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
case 'n-times':
|
case 'n-times':
|
||||||
const nValue = parseInt((document.getElementById('split-n-value') as HTMLInputElement)?.value || '5');
|
const nValue = parseInt(
|
||||||
|
(document.getElementById('split-n-value') as HTMLInputElement)
|
||||||
|
?.value || '5'
|
||||||
|
);
|
||||||
if (nValue < 1) throw new Error('N must be at least 1.');
|
if (nValue < 1) throw new Error('N must be at least 1.');
|
||||||
|
|
||||||
const zip2 = new JSZip();
|
const zip2 = new JSZip();
|
||||||
@@ -393,10 +446,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
for (let i = 0; i < numSplits; i++) {
|
for (let i = 0; i < numSplits; i++) {
|
||||||
const startPage = i * nValue;
|
const startPage = i * nValue;
|
||||||
const endPage = Math.min(startPage + nValue - 1, totalPages - 1);
|
const endPage = Math.min(startPage + nValue - 1, totalPages - 1);
|
||||||
const pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, idx) => startPage + idx);
|
const pageIndices = Array.from(
|
||||||
|
{ length: endPage - startPage + 1 },
|
||||||
|
(_, idx) => startPage + idx
|
||||||
|
);
|
||||||
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
const copiedPages = await newPdf.copyPages(
|
||||||
|
state.pdfDoc,
|
||||||
|
pageIndices
|
||||||
|
);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
const pdfBytes3 = await newPdf.save();
|
const pdfBytes3 = await newPdf.save();
|
||||||
zip2.file(`split-${i + 1}.pdf`, pdfBytes3);
|
zip2.file(`split-${i + 1}.pdf`, pdfBytes3);
|
||||||
@@ -412,7 +471,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uniqueIndices = [...new Set(indicesToExtract)];
|
const uniqueIndices = [...new Set(indicesToExtract)];
|
||||||
if (uniqueIndices.length === 0 && splitMode !== 'bookmarks' && splitMode !== 'n-times') {
|
if (
|
||||||
|
uniqueIndices.length === 0 &&
|
||||||
|
splitMode !== 'bookmarks' &&
|
||||||
|
splitMode !== 'n-times'
|
||||||
|
) {
|
||||||
throw new Error('No pages were selected for splitting.');
|
throw new Error('No pages were selected for splitting.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,7 +518,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
showAlert('Success', 'PDF split successfully!', 'success', () => {
|
showAlert('Success', 'PDF split successfully!', 'success', () => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert(
|
showAlert(
|
||||||
@@ -495,7 +557,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
dropZone.classList.remove('bg-gray-700');
|
dropZone.classList.remove('bg-gray-700');
|
||||||
const files = e.dataTransfer?.files;
|
const files = e.dataTransfer?.files;
|
||||||
if (files) {
|
if (files) {
|
||||||
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
const pdfFiles = Array.from(files).filter(
|
||||||
|
(f) =>
|
||||||
|
f.type === 'application/pdf' ||
|
||||||
|
f.name.toLowerCase().endsWith('.pdf')
|
||||||
|
);
|
||||||
if (pdfFiles.length > 0) {
|
if (pdfFiles.length > 0) {
|
||||||
// Take only the first PDF
|
// Take only the first PDF
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
@@ -551,7 +617,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const updateWarning = () => {
|
const updateWarning = () => {
|
||||||
if (!state.pdfDoc) return;
|
if (!state.pdfDoc) return;
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
const nValue = parseInt((document.getElementById('split-n-value') as HTMLInputElement)?.value || '5');
|
const nValue = parseInt(
|
||||||
|
(document.getElementById('split-n-value') as HTMLInputElement)
|
||||||
|
?.value || '5'
|
||||||
|
);
|
||||||
const remainder = totalPages % nValue;
|
const remainder = totalPages % nValue;
|
||||||
if (remainder !== 0 && nTimesWarning) {
|
if (remainder !== 0 && nTimesWarning) {
|
||||||
nTimesWarning.classList.remove('hidden');
|
nTimesWarning.classList.remove('hidden');
|
||||||
@@ -565,7 +634,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateWarning();
|
updateWarning();
|
||||||
document.getElementById('split-n-value')?.addEventListener('input', updateWarning);
|
document
|
||||||
|
.getElementById('split-n-value')
|
||||||
|
?.addEventListener('input', updateWarning);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +61,8 @@ 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 ${
|
||||||
|
type === 'success'
|
||||||
? 'bg-green-900 text-green-200'
|
? 'bg-green-900 text-green-200'
|
||||||
: type === 'error'
|
: type === 'error'
|
||||||
? 'bg-red-900 text-red-200'
|
? 'bg-red-900 text-red-200'
|
||||||
@@ -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,14 +1,16 @@
|
|||||||
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);
|
||||||
@@ -29,7 +31,8 @@ const updateUI = () => {
|
|||||||
|
|
||||||
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';
|
||||||
@@ -60,17 +63,28 @@ const updateUI = () => {
|
|||||||
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(
|
||||||
|
'text-input'
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
if (fileInput) fileInput.value = '';
|
if (fileInput) fileInput.value = '';
|
||||||
if (textInput) textInput.value = '';
|
if (textInput) textInput.value = '';
|
||||||
updateUI();
|
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.');
|
||||||
@@ -78,7 +92,9 @@ async function convert() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentMode === 'text') {
|
if (currentMode === 'text') {
|
||||||
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
|
const textInput = document.getElementById(
|
||||||
|
'text-input'
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
if (!textInput.value.trim()) {
|
if (!textInput.value.trim()) {
|
||||||
showAlert('No Text', 'Please enter some text to convert.');
|
showAlert('No Text', 'Please enter some text to convert.');
|
||||||
return;
|
return;
|
||||||
@@ -88,8 +104,7 @@ async function convert() {
|
|||||||
showLoader('Loading engine...');
|
showLoader('Loading engine...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
|
const pymupdf = await loadPyMuPDF();
|
||||||
await pymupdf.load();
|
|
||||||
|
|
||||||
let textContent = '';
|
let textContent = '';
|
||||||
|
|
||||||
@@ -99,7 +114,9 @@ async function convert() {
|
|||||||
textContent += text + '\n\n';
|
textContent += text + '\n\n';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
|
const textInput = document.getElementById(
|
||||||
|
'text-input'
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
textContent = textInput.value;
|
textContent = textInput.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,14 +127,19 @@ async function convert() {
|
|||||||
pageSize: pageSizeKey as 'a4' | 'letter' | 'legal' | 'a3' | 'a5',
|
pageSize: pageSizeKey as 'a4' | 'letter' | 'legal' | 'a3' | 'a5',
|
||||||
fontName: fontName as 'helv' | 'tiro' | 'cour' | 'times',
|
fontName: fontName as 'helv' | 'tiro' | 'cour' | 'times',
|
||||||
textColor,
|
textColor,
|
||||||
margins: 72
|
margins: 72,
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadFile(pdfBlob, 'text_to_pdf.pdf');
|
downloadFile(pdfBlob, 'text_to_pdf.pdf');
|
||||||
|
|
||||||
showAlert('Success', 'Text converted to PDF successfully!', 'success', () => {
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'Text converted to PDF successfully!',
|
||||||
|
'success',
|
||||||
|
() => {
|
||||||
resetState();
|
resetState();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[TxtToPDF] Error:', e);
|
console.error('[TxtToPDF] Error:', e);
|
||||||
showAlert('Error', `Failed to convert text to PDF. ${e.message || ''}`);
|
showAlert('Error', `Failed to convert text to PDF. ${e.message || ''}`);
|
||||||
@@ -149,7 +171,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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) {
|
||||||
@@ -192,11 +216,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const handleFileSelect = (newFiles: FileList | null) => {
|
const handleFileSelect = (newFiles: FileList | null) => {
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
const validFiles = Array.from(newFiles).filter(
|
const validFiles = Array.from(newFiles).filter(
|
||||||
(file) => file.name.toLowerCase().endsWith('.txt') || file.type === 'text/plain'
|
(file) =>
|
||||||
|
file.name.toLowerCase().endsWith('.txt') || file.type === 'text/plain'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (validFiles.length < newFiles.length) {
|
if (validFiles.length < newFiles.length) {
|
||||||
showAlert('Invalid Files', 'Some files were skipped. Only text files are allowed.');
|
showAlert(
|
||||||
|
'Invalid Files',
|
||||||
|
'Some files were skipped. Only text files are allowed.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
|
|||||||
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,8 +2,9 @@ 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'];
|
||||||
@@ -34,7 +35,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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';
|
||||||
@@ -50,7 +52,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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 = () => {
|
removeBtn.onclick = () => {
|
||||||
state.files = state.files.filter((_, i) => i !== index);
|
state.files = state.files.filter((_, i) => i !== index);
|
||||||
@@ -87,14 +90,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Loading engine...');
|
showLoader('Loading 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];
|
||||||
showLoader(`Converting ${originalFile.name}...`);
|
showLoader(`Converting ${originalFile.name}...`);
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
|
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
|
||||||
|
filetype: FILETYPE,
|
||||||
|
});
|
||||||
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
|
||||||
|
|
||||||
downloadFile(pdfBlob, fileName);
|
downloadFile(pdfBlob, fileName);
|
||||||
@@ -113,9 +117,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
const file = state.files[i];
|
const file = state.files[i];
|
||||||
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
|
showLoader(
|
||||||
|
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
|
||||||
|
);
|
||||||
|
|
||||||
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
|
const pdfBlob = await pymupdf.convertToPdf(file, {
|
||||||
|
filetype: FILETYPE,
|
||||||
|
});
|
||||||
const baseName = file.name.replace(/\.[^.]+$/, '');
|
const baseName = file.name.replace(/\.[^.]+$/, '');
|
||||||
const pdfBuffer = await pdfBlob.arrayBuffer();
|
const pdfBuffer = await pdfBlob.arrayBuffer();
|
||||||
zip.file(`${baseName}.pdf`, pdfBuffer);
|
zip.file(`${baseName}.pdf`, pdfBuffer);
|
||||||
@@ -136,7 +144,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during conversion. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,13 +178,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
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) {
|
||||||
const validFiles = Array.from(files).filter(f => {
|
const validFiles = Array.from(files).filter((f) => {
|
||||||
const name = f.name.toLowerCase();
|
const name = f.name.toLowerCase();
|
||||||
return EXTENSIONS.some(ext => name.endsWith(ext));
|
return EXTENSIONS.some((ext) => name.endsWith(ext));
|
||||||
});
|
});
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
const dataTransfer = new DataTransfer();
|
const dataTransfer = new DataTransfer();
|
||||||
validFiles.forEach(f => dataTransfer.items.add(f));
|
validFiles.forEach((f) => dataTransfer.items.add(f));
|
||||||
handleFileSelect(dataTransfer.files);
|
handleFileSelect(dataTransfer.files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,14 +1,12 @@
|
|||||||
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 {
|
||||||
@@ -20,45 +18,39 @@ interface PreloadState {
|
|||||||
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;
|
||||||
|
|
||||||
|
if (!isWasmAvailable('pymupdf')) {
|
||||||
|
preloadState.pymupdf = PreloadStatus.UNAVAILABLE;
|
||||||
|
console.log('[Preloader] PyMuPDF not configured, skipping preload');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
preloadState.pymupdf = PreloadStatus.LOADING;
|
preloadState.pymupdf = PreloadStatus.LOADING;
|
||||||
console.log('[Preloader] Starting PyMuPDF preload...');
|
console.log('[Preloader] Starting PyMuPDF preload...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pymupdfBaseUrl = getWasmBaseUrl('pymupdf');
|
const pymupdfBaseUrl = getWasmBaseUrl('pymupdf')!;
|
||||||
pymupdfInstance = new PyMuPDF(pymupdfBaseUrl);
|
const gsBaseUrl = getWasmBaseUrl('ghostscript');
|
||||||
|
const normalizedUrl = pymupdfBaseUrl.endsWith('/')
|
||||||
|
? pymupdfBaseUrl
|
||||||
|
: `${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();
|
await pymupdfInstance.load();
|
||||||
preloadState.pymupdf = PreloadStatus.READY;
|
preloadState.pymupdf = PreloadStatus.READY;
|
||||||
console.log('[Preloader] PyMuPDF ready');
|
console.log('[Preloader] PyMuPDF ready');
|
||||||
@@ -71,20 +63,43 @@ async function preloadPyMuPDF(): Promise<void> {
|
|||||||
async function preloadGhostscript(): Promise<void> {
|
async function preloadGhostscript(): Promise<void> {
|
||||||
if (preloadState.ghostscript !== PreloadStatus.IDLE) return;
|
if (preloadState.ghostscript !== PreloadStatus.IDLE) return;
|
||||||
|
|
||||||
|
if (!isWasmAvailable('ghostscript')) {
|
||||||
|
preloadState.ghostscript = PreloadStatus.UNAVAILABLE;
|
||||||
|
console.log('[Preloader] Ghostscript not configured, skipping preload');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
preloadState.ghostscript = PreloadStatus.LOADING;
|
preloadState.ghostscript = PreloadStatus.LOADING;
|
||||||
console.log('[Preloader] Starting Ghostscript WASM preload...');
|
console.log('[Preloader] Starting Ghostscript WASM preload...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gsBaseUrl = getWasmBaseUrl('ghostscript');
|
const gsBaseUrl = getWasmBaseUrl('ghostscript')!;
|
||||||
|
|
||||||
|
let packageBaseUrl = gsBaseUrl;
|
||||||
|
if (packageBaseUrl.endsWith('/assets/')) {
|
||||||
|
packageBaseUrl = packageBaseUrl.slice(0, -8);
|
||||||
|
} else if (packageBaseUrl.endsWith('/assets')) {
|
||||||
|
packageBaseUrl = packageBaseUrl.slice(0, -7);
|
||||||
|
}
|
||||||
|
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({
|
const gsModule = await loadGsWASM({
|
||||||
|
baseUrl: `${normalizedUrl}assets/`,
|
||||||
locateFile: (path: string) => {
|
locateFile: (path: string) => {
|
||||||
if (path.endsWith('.wasm')) {
|
if (path.endsWith('.wasm')) {
|
||||||
return gsBaseUrl + 'gs.wasm';
|
return `${normalizedUrl}assets/gs.wasm`;
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
},
|
},
|
||||||
print: () => { },
|
print: () => {},
|
||||||
printErr: () => { },
|
printErr: () => {},
|
||||||
});
|
});
|
||||||
setCachedGsModule(gsModule as any);
|
setCachedGsModule(gsModule as any);
|
||||||
preloadState.ghostscript = PreloadStatus.READY;
|
preloadState.ghostscript = PreloadStatus.READY;
|
||||||
@@ -107,16 +122,29 @@ 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(
|
||||||
|
'[Preloader] Skipping preloads on LibreOffice page to save memory'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +154,6 @@ export function startBackgroundPreload(): void {
|
|||||||
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