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:
alam00000
2026-01-27 15:26:11 +05:30
parent f6d432eaa7
commit 2c85ca74e9
75 changed files with 9696 additions and 6587 deletions

View File

@@ -27,9 +27,13 @@ ARG BASE_URL
ENV BASE_URL=$BASE_URL
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 \
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
# Production stage

View File

@@ -1,5 +1,10 @@
<p align="center"><img src="public/images/favicon-no-bg.svg" width="80"></p>
<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.
@@ -85,6 +90,24 @@ BentoPDF is **dual-licensed** to fit your needs:
📖 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>
## ⭐ 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:
**Bundled Libraries:**
- **[PDFLib.js](https://pdf-lib.js.org/)** For enabling powerful client-side PDF manipulation.
- **[PDF.js](https://mozilla.github.io/pdf.js/)** For the robust PDF rendering engine in the browser.
- **[PDFKit](https://pdfkit.org/)** For creating and editing PDFs with ease.
@@ -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.
- **[Vite](https://vitejs.dev/)** For lightning-fast development and build tooling.
- **[Tailwind CSS](https://tailwindcss.com/)** For rapid, flexible, and beautiful UI styling.
- **[qpdf](https://github.com/qpdf/qpdf)** and **[qpdf-wasm](https://github.com/neslinesli93/qpdf-wasm)** A powerful command-line tool and library for inspecting, repairing, and transforming PDF file ported to wasm
- **[cpdf](https://www.coherentpdf.com/)** For content preserving pdf operations.
- **[qpdf](https://github.com/qpdf/qpdf)** and **[qpdf-wasm](https://github.com/neslinesli93/qpdf-wasm)** For inspecting, repairing, and transforming PDF files.
- **[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!

92
cloudflare/WASM-PROXY.md Normal file
View 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 |

View 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',
},
}
);
},
};

View 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" }
# ]

View File

@@ -13,7 +13,7 @@ For complete licensing information, delivery details, AGPL component notices, an
## When Do You Need a Commercial License?
| Use Case | License Required |
|----------|------------------|
| ------------------------------------------- | ---------------------- |
| Open-source project with public source code | AGPL-3.0 (Free) |
| Internal company tool (not distributed) | AGPL-3.0 (Free) |
| 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
::: warning AGPL Components
This software includes components licensed under the **GNU AGPL v3**, such as CPDF.
::: warning AGPL Components - Not Bundled
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.
- Users must comply with the AGPL v3 terms for these components.
- Source code for all AGPL components is included in the distribution.
| Component | License | Status |
| --------------- | -------- | ----------------------------- |
| **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
@@ -48,7 +68,7 @@ This software includes components licensed under the **GNU AGPL v3**, such as CP
## What's Included
| Feature | Included |
|---------|----------|
| ----------------------------- | -------------- |
| Full source code | ✅ |
| All 50+ PDF tools | ✅ |
| 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?
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?

View File

@@ -118,11 +118,47 @@ Choose your platform:
- [Kubernetes](/self-hosting/kubernetes)
- [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
| Requirement | Minimum |
| ----------- | ------------------------------- |
| Storage | ~500 MB (with all WASM modules) |
| ----------- | ----------------------------------- |
| Storage | ~100 MB (core without AGPL modules) |
| RAM | 512 MB |
| CPU | Any modern processor |

View File

@@ -209,6 +209,21 @@
</svg>
</span>
</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>
</section>
@@ -701,6 +716,28 @@
></div>
</label>
</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>

View File

@@ -989,72 +989,6 @@
</p>
</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">
<h3
class="text-xl font-semibold text-white mb-4 flex items-center gap-3"
@@ -1104,6 +1038,83 @@
</li>
</ul>
</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>
</section>

26
package-lock.json generated
View File

@@ -9,8 +9,6 @@
"version": "1.16.1",
"license": "AGPL-3.0-only",
"dependencies": {
"@bentopdf/gs-wasm": "^0.1.0",
"@bentopdf/pymupdf-wasm": "^0.11.12",
"@fontsource/cedarville-cursive": "^5.2.7",
"@fontsource/dancing-script": "^5.2.8",
"@fontsource/dm-sans": "^5.2.8",
@@ -505,24 +503,6 @@
"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": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz",
@@ -3529,12 +3509,6 @@
"dev": true,
"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": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",

View File

@@ -64,8 +64,6 @@
"vue": "^3.5.26"
},
"dependencies": {
"@bentopdf/gs-wasm": "^0.1.0",
"@bentopdf/pymupdf-wasm": "^0.11.12",
"@fontsource/cedarville-cursive": "^5.2.7",
"@fontsource/dancing-script": "^5.2.8",
"@fontsource/dm-sans": "^5.2.8",

101
public/images/badge.svg Normal file
View 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

View File

@@ -5,6 +5,7 @@ interface AddAttachmentsMessage {
pdfBuffer: ArrayBuffer;
attachmentBuffers: ArrayBuffer[];
attachmentNames: string[];
cpdfUrl?: string;
}
interface AddAttachmentsSuccessResponse {
@@ -17,4 +18,6 @@ interface AddAttachmentsErrorResponse {
message: string;
}
type AddAttachmentsResponse = AddAttachmentsSuccessResponse | AddAttachmentsErrorResponse;
type AddAttachmentsResponse =
| AddAttachmentsSuccessResponse
| AddAttachmentsErrorResponse;

View File

@@ -1,13 +1,32 @@
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
let cpdfLoaded = false;
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) {
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) {
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;
for (let i = Math.max(1, start); i <= Math.min(totalPages, end); i++) {
pages.add(i);
@@ -23,7 +42,13 @@ function parsePageRange(rangeString, totalPages) {
return Array.from(pages).sort((a, b) => a - b);
}
function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNames, attachmentLevel, pageRange) {
function addAttachmentsToPDFInWorker(
pdfBuffer,
attachmentBuffers,
attachmentNames,
attachmentLevel,
pageRange
) {
try {
const uint8Array = new Uint8Array(pdfBuffer);
@@ -33,18 +58,21 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
} catch (error) {
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('No /Root entry') ||
errorMsg.includes('PDFError')) {
errorMsg.includes('PDFError')
) {
self.postMessage({
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 {
self.postMessage({
status: 'error',
message: `Failed to load PDF: ${errorMsg}`
message: `Failed to load PDF: ${errorMsg}`,
});
}
return;
@@ -57,7 +85,7 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
if (!pageRange) {
self.postMessage({
status: 'error',
message: 'Page range is required for page-level attachments.'
message: 'Page range is required for page-level attachments.',
});
coherentpdf.deletePdf(pdf);
return;
@@ -66,7 +94,7 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
if (targetPages.length === 0) {
self.postMessage({
status: 'error',
message: 'Invalid page range specified.'
message: 'Invalid page range specified.',
});
coherentpdf.deletePdf(pdf);
return;
@@ -82,21 +110,25 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
coherentpdf.attachFileFromMemory(attachmentData, attachmentName, pdf);
} else {
for (const pageNum of targetPages) {
coherentpdf.attachFileToPageFromMemory(attachmentData, attachmentName, pdf, pageNum);
coherentpdf.attachFileToPageFromMemory(
attachmentData,
attachmentName,
pdf,
pageNum
);
}
}
} catch (error) {
console.warn(`Failed to attach file ${attachmentNames[i]}:`, error);
self.postMessage({
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);
return;
}
}
// Save the modified PDF
const modifiedBytes = coherentpdf.toMemory(pdf, false, false);
coherentpdf.deletePdf(pdf);
@@ -105,22 +137,46 @@ function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNam
modifiedBytes.byteOffset + modifiedBytes.byteLength
);
self.postMessage({
self.postMessage(
{
status: 'success',
modifiedPDF: buffer
}, [buffer]);
modifiedPDF: buffer,
},
[buffer]
);
} catch (error) {
self.postMessage({
status: 'error',
message: error instanceof Error
message:
error instanceof Error
? 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') {
addAttachmentsToPDFInWorker(
e.data.pdfBuffer,

View File

@@ -8,6 +8,7 @@ interface InterleaveFile {
interface InterleaveMessage {
command: 'interleave';
files: InterleaveFile[];
cpdfUrl?: string;
}
interface InterleaveSuccessResponse {

View File

@@ -1,8 +1,46 @@
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
let cpdfLoaded = false;
self.onmessage = function (e) {
const { command, files } = e.data;
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));
}
});
}
self.onmessage = async function (e) {
const { command, files, cpdfUrl } = e.data;
if (!cpdfUrl) {
self.postMessage({
status: 'error',
message:
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
});
return;
}
try {
await loadCpdf(cpdfUrl);
} catch (error) {
self.postMessage({
status: 'error',
message: error.message,
});
return;
}
if (command === 'interleave') {
interleavePDFs(files);
@@ -16,7 +54,7 @@ function interleavePDFs(files) {
for (const file of files) {
const uint8Array = new Uint8Array(file.data);
const pdfDoc = coherentpdf.fromMemory(uint8Array, "");
const pdfDoc = coherentpdf.fromMemory(uint8Array, '');
loadedPdfs.push(pdfDoc);
pageCounts.push(coherentpdf.pages(pdfDoc));
}
@@ -43,22 +81,29 @@ function interleavePDFs(files) {
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 buffer = mergedPdfBytes.buffer;
coherentpdf.deletePdf(mergedPdf);
loadedPdfs.forEach(pdf => coherentpdf.deletePdf(pdf));
loadedPdfs.forEach((pdf) => coherentpdf.deletePdf(pdf));
self.postMessage({
self.postMessage(
{
status: 'success',
pdfBytes: buffer
}, [buffer]);
pdfBytes: buffer,
},
[buffer]
);
} catch (error) {
self.postMessage({
status: 'error',
message: error.message || 'Unknown error during interleave merge'
message: error.message || 'Unknown error during interleave merge',
});
}
}

View File

@@ -4,6 +4,7 @@ interface GetAttachmentsMessage {
command: 'get-attachments';
fileBuffer: ArrayBuffer;
fileName: string;
cpdfUrl?: string;
}
interface EditAttachmentsMessage {
@@ -11,13 +12,21 @@ interface EditAttachmentsMessage {
fileBuffer: ArrayBuffer;
fileName: string;
attachmentsToRemove: number[];
cpdfUrl?: string;
}
type EditAttachmentsWorkerMessage = GetAttachmentsMessage | EditAttachmentsMessage;
type EditAttachmentsWorkerMessage =
| GetAttachmentsMessage
| EditAttachmentsMessage;
interface GetAttachmentsSuccessResponse {
status: 'success';
attachments: Array<{ index: number; name: string; page: number; data: ArrayBuffer }>;
attachments: Array<{
index: number;
name: string;
page: number;
data: ArrayBuffer;
}>;
fileName: string;
}
@@ -37,6 +46,12 @@ interface EditAttachmentsErrorResponse {
message: string;
}
type GetAttachmentsResponse = GetAttachmentsSuccessResponse | GetAttachmentsErrorResponse;
type EditAttachmentsResponse = EditAttachmentsSuccessResponse | EditAttachmentsErrorResponse;
type EditAttachmentsWorkerResponse = GetAttachmentsResponse | EditAttachmentsResponse;
type GetAttachmentsResponse =
| GetAttachmentsSuccessResponse
| GetAttachmentsErrorResponse;
type EditAttachmentsResponse =
| EditAttachmentsSuccessResponse
| EditAttachmentsErrorResponse;
type EditAttachmentsWorkerResponse =
| GetAttachmentsResponse
| EditAttachmentsResponse;

View File

@@ -1,5 +1,24 @@
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
let cpdfLoaded = false;
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) {
try {
@@ -11,7 +30,7 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
} catch (error) {
self.postMessage({
status: 'error',
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`,
});
return;
}
@@ -23,7 +42,7 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
self.postMessage({
status: 'success',
attachments: [],
fileName: fileName
fileName: fileName,
});
coherentpdf.deletePdf(pdf);
return;
@@ -37,13 +56,16 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
const attachmentData = coherentpdf.getAttachmentData(i);
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({
index: i,
name: String(name),
page: Number(page),
data: buffer
data: buffer,
});
} catch (error) {
console.warn(`Failed to get attachment ${i} from ${fileName}:`, error);
@@ -56,22 +78,27 @@ function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
const response = {
status: 'success',
attachments: attachments,
fileName: fileName
fileName: fileName,
};
const transferBuffers = attachments.map(att => att.data);
const transferBuffers = attachments.map((att) => att.data);
self.postMessage(response, transferBuffers);
} catch (error) {
self.postMessage({
status: 'error',
message: error instanceof Error
message:
error instanceof Error
? 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 {
const uint8Array = new Uint8Array(fileBuffer);
@@ -81,7 +108,7 @@ function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove)
} catch (error) {
self.postMessage({
status: 'error',
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`,
});
return;
}
@@ -103,7 +130,7 @@ function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove)
attachmentsToKeep.push({
name: String(name),
page: Number(page),
data: dataCopy
data: dataCopy,
});
}
}
@@ -114,9 +141,18 @@ function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove)
for (const attachment of attachmentsToKeep) {
if (attachment.page === 0) {
coherentpdf.attachFileFromMemory(attachment.data, attachment.name, pdf);
coherentpdf.attachFileFromMemory(
attachment.data,
attachment.name,
pdf
);
} 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);
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 = {
status: 'success',
modifiedPDF: buffer,
fileName: fileName
fileName: fileName,
};
self.postMessage(response, [response.modifiedPDF]);
} catch (error) {
self.postMessage({
status: 'error',
message: error instanceof Error
message:
error instanceof Error
? 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') {
getAttachmentsFromPDFInWorker(e.data.fileBuffer, e.data.fileName);
} 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
);
}
};

View File

@@ -4,6 +4,7 @@ interface ExtractAttachmentsMessage {
command: 'extract-attachments';
fileBuffers: ArrayBuffer[];
fileNames: string[];
cpdfUrl?: string;
}
interface ExtractAttachmentSuccessResponse {
@@ -16,4 +17,6 @@ interface ExtractAttachmentErrorResponse {
message: string;
}
type ExtractAttachmentResponse = ExtractAttachmentSuccessResponse | ExtractAttachmentErrorResponse;
type ExtractAttachmentResponse =
| ExtractAttachmentSuccessResponse
| ExtractAttachmentErrorResponse;

View File

@@ -1,5 +1,24 @@
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
let cpdfLoaded = false;
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) {
try {
@@ -37,7 +56,7 @@ function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
let uniqueName = attachmentName;
let counter = 1;
while (allAttachments.some(att => att.name === uniqueName)) {
while (allAttachments.some((att) => att.name === uniqueName)) {
const nameParts = attachmentName.split('.');
if (nameParts.length > 1) {
const extension = nameParts.pop();
@@ -56,10 +75,13 @@ function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
allAttachments.push({
name: uniqueName,
data: attachmentData.buffer.slice(0)
data: attachmentData.buffer.slice(0),
});
} 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) {
self.postMessage({
status: 'error',
message: 'No attachments were found in the selected PDF(s).'
message: 'No attachments were found in the selected PDF(s).',
});
return;
}
const response = {
status: 'success',
attachments: []
attachments: [],
};
const transferBuffers = [];
for (const attachment of allAttachments) {
response.attachments.push({
name: attachment.name,
data: attachment.data
data: attachment.data,
});
transferBuffers.push(attachment.data);
}
@@ -93,14 +115,36 @@ function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
} catch (error) {
self.postMessage({
status: 'error',
message: error instanceof Error
message:
error instanceof Error
? 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') {
extractAttachmentsFromPDFsInWorker(e.data.fileBuffers, e.data.fileNames);
}

View File

@@ -4,6 +4,7 @@ interface ConvertJSONToPDFMessage {
command: 'convert';
fileBuffers: ArrayBuffer[];
fileNames: string[];
cpdfUrl?: string;
}
interface JSONToPDFSuccessResponse {
@@ -17,4 +18,3 @@ interface JSONToPDFErrorResponse {
}
type JSONToPDFResponse = JSONToPDFSuccessResponse | JSONToPDFErrorResponse;

View File

@@ -1,5 +1,24 @@
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
let cpdfLoaded = false;
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) {
try {
@@ -15,9 +34,8 @@ function convertJSONsToPDFInWorker(fileBuffers, fileNames) {
try {
pdf = coherentpdf.fromJSONMemory(uint8Array);
} catch (error) {
const errorMsg = error && error.message
? error.message
: 'Unknown error';
const errorMsg =
error && error.message ? error.message : 'Unknown error';
throw new Error(
`Failed to convert "${fileName}" to PDF. ` +
`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') {
convertJSONsToPDFInWorker(e.data.fileBuffers, e.data.fileNames);
}
};

View File

@@ -18,6 +18,7 @@ interface MergeMessage {
command: 'merge';
files: MergeFile[];
jobs: MergeJob[];
cpdfUrl?: string;
}
interface MergeSuccessResponse {

View File

@@ -1,8 +1,46 @@
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
let cpdfLoaded = false;
self.onmessage = function (e) {
const { command, files, jobs } = e.data;
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));
}
});
}
self.onmessage = async function (e) {
const { command, files, jobs, cpdfUrl } = e.data;
if (!cpdfUrl) {
self.postMessage({
status: 'error',
message:
'CoherentPDF URL not provided. Please configure it in WASM Settings.',
});
return;
}
try {
await loadCpdf(cpdfUrl);
} catch (error) {
self.postMessage({
status: 'error',
message: error.message,
});
return;
}
if (command === 'merge') {
mergePDFs(files, jobs);
@@ -17,7 +55,7 @@ function mergePDFs(files, jobs) {
for (const file of files) {
const uint8Array = new Uint8Array(file.data);
const pdfDoc = coherentpdf.fromMemory(uint8Array, "");
const pdfDoc = coherentpdf.fromMemory(uint8Array, '');
loadedPdfs[file.name] = pdfDoc;
}
@@ -49,23 +87,30 @@ function mergePDFs(files, jobs) {
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 buffer = mergedPdfBytes.buffer;
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',
pdfBytes: buffer
}, [buffer]);
pdfBytes: buffer,
},
[buffer]
);
} catch (error) {
self.postMessage({
status: 'error',
message: error.message || 'Unknown error during merge'
message: error.message || 'Unknown error during merge',
});
}
}

View File

@@ -4,6 +4,7 @@ interface ConvertPDFToJSONMessage {
command: 'convert';
fileBuffers: ArrayBuffer[];
fileNames: string[];
cpdfUrl?: string;
}
interface PDFToJSONSuccessResponse {
@@ -17,4 +18,3 @@ interface PDFToJSONErrorResponse {
}
type PDFToJSONResponse = PDFToJSONSuccessResponse | PDFToJSONErrorResponse;

View File

@@ -1,5 +1,24 @@
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
let cpdfLoaded = false;
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) {
try {
@@ -12,8 +31,6 @@ function convertPDFsToJSONInWorker(fileBuffers, fileNames) {
const uint8Array = new Uint8Array(buffer);
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 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') {
convertPDFsToJSONInWorker(e.data.fileBuffers, e.data.fileNames);
}
};

View File

@@ -7,6 +7,7 @@ interface GenerateTOCMessage {
fontSize: number;
fontFamily: number;
addBookmark: boolean;
cpdfUrl?: string;
}
interface TOCSuccessResponse {

View File

@@ -1,5 +1,24 @@
const baseUrl = self.location.href.substring(0, self.location.href.lastIndexOf('/workers/') + 1);
self.importScripts(baseUrl + 'coherentpdf.browser.min.js');
let cpdfLoaded = false;
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(
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') {
generateTableOfContentsInWorker(
e.data.pdfData,

View File

@@ -1,75 +1,52 @@
/**
* WASM CDN Configuration
*
* Centralized configuration for loading WASM files from jsDelivr CDN or local paths.
* Supports environment-based toggling and automatic fallback mechanism.
*/
import { PACKAGE_VERSIONS } from '../const/cdn-version';
import { WasmProvider } from '../utils/wasm-provider';
const USE_CDN = import.meta.env.VITE_USE_CDN === 'true';
import { CDN_URLS, PACKAGE_VERSIONS } from '../const/cdn-version';
export type WasmPackage = 'ghostscript' | 'pymupdf' | 'cpdf';
const LOCAL_PATHS = {
ghostscript: import.meta.env.BASE_URL + 'ghostscript-wasm/',
pymupdf: import.meta.env.BASE_URL + 'pymupdf-wasm/',
} as const;
export type WasmPackage = 'ghostscript' | 'pymupdf';
export function getWasmBaseUrl(packageName: WasmPackage): string {
if (USE_CDN) {
return CDN_URLS[packageName];
export function getWasmBaseUrl(packageName: WasmPackage): string | undefined {
const userUrl = WasmProvider.getUrl(packageName);
if (userUrl) {
console.log(
`[WASM Config] Using configured URL for ${packageName}: ${userUrl}`
);
return userUrl;
}
return LOCAL_PATHS[packageName];
console.warn(
`[WASM Config] No URL configured for ${packageName}. Feature unavailable.`
);
return undefined;
}
export function getWasmFallbackUrl(packageName: WasmPackage): string {
return LOCAL_PATHS[packageName];
export function isWasmAvailable(packageName: WasmPackage): boolean {
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(
packageName: WasmPackage,
fileName: string
): Promise<Response> {
const cdnUrl = CDN_URLS[packageName] + fileName;
const localUrl = LOCAL_PATHS[packageName] + fileName;
const baseUrl = getWasmBaseUrl(packageName);
if (USE_CDN) {
try {
console.log(`[WASM CDN] Fetching from CDN: ${cdnUrl}`);
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...`);
}
if (!baseUrl) {
throw new Error(
`No URL configured for ${packageName}. Please configure it in WASM Settings.`
);
}
const response = await fetch(localUrl);
const url = baseUrl + fileName;
console.log(`[WASM] Fetching: ${url}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${fileName}: HTTP ${response.status}`);
}
return response;
}
// use this to debug
export function getWasmConfigInfo() {
return {
cdnEnabled: USE_CDN,
packageVersions: PACKAGE_VERSIONS,
cdnUrls: CDN_URLS,
localPaths: LOCAL_PATHS,
configuredProviders: WasmProvider.getAllProviders(),
};
}

View File

@@ -2,8 +2,3 @@ export const PACKAGE_VERSIONS = {
ghostscript: '0.1.0',
pymupdf: '0.1.9',
} as const;
export const CDN_URLS = {
ghostscript: `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@${PACKAGE_VERSIONS.ghostscript}/assets/`,
pymupdf: `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@${PACKAGE_VERSIONS.pymupdf}/assets/`,
} as const;

View File

@@ -3,8 +3,15 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
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 = {
file: null,
@@ -26,10 +33,14 @@ function resetState() {
const attachmentFileList = document.getElementById('attachment-file-list');
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 = '';
const attachmentLevelOptions = document.getElementById('attachment-level-options');
const attachmentLevelOptions = document.getElementById(
'attachment-level-options'
);
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
@@ -41,7 +52,9 @@ function resetState() {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
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;
}
@@ -51,15 +64,21 @@ worker.onmessage = function (e) {
if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
const originalName =
pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`${originalName}_with_attachments.pdf`
);
showAlert('Success', `${pageState.attachments.length} file(s) attached successfully.`, 'success', function () {
showAlert(
'Success',
`${pageState.attachments.length} file(s) attached successfully.`,
'success',
function () {
resetState();
});
}
);
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
@@ -82,7 +101,8 @@ async function updateUI() {
if (pageState.file) {
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');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -114,7 +134,7 @@ async function updateUI() {
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
throwOnInvalidObject: false,
});
const pageCount = pageState.pdfDoc.getPageCount();
@@ -139,7 +159,9 @@ async function updateUI() {
function updateAttachmentList() {
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');
if (!attachmentFileList) return;
@@ -148,7 +170,8 @@ function updateAttachmentList() {
pageState.attachments.forEach(function (file) {
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');
nameSpan.className = 'truncate text-sm';
@@ -163,7 +186,8 @@ function updateAttachmentList() {
});
if (pageState.attachments.length > 0) {
if (attachmentLevelOptions) attachmentLevelOptions.classList.remove('hidden');
if (attachmentLevelOptions)
attachmentLevelOptions.classList.remove('hidden');
if (processBtn) processBtn.classList.remove('hidden');
} else {
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
@@ -182,18 +206,32 @@ async function addAttachments() {
return;
}
const attachmentLevel = (
document.querySelector('input[name="attachment-level"]:checked') as HTMLInputElement
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
return;
}
const attachmentLevel =
(
document.querySelector(
'input[name="attachment-level"]:checked'
) as HTMLInputElement
)?.value || 'document';
let pageRange: string = '';
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() || '';
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;
}
}
@@ -208,7 +246,9 @@ async function addAttachments() {
for (let i = 0; i < pageState.attachments.length; 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();
attachmentBuffers.push(fileBuffer);
@@ -223,12 +263,12 @@ async function addAttachments() {
attachmentBuffers: attachmentBuffers,
attachmentNames: attachmentNames,
attachmentLevel: attachmentLevel,
pageRange: pageRange
pageRange: pageRange,
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
};
const transferables = [pdfBuffer, ...attachmentBuffers];
worker.postMessage(message, transferables);
} catch (error: any) {
console.error('Error attaching files:', error);
hideLoader();
@@ -239,7 +279,10 @@ async function addAttachments() {
function handleFileSelect(files: FileList | null) {
if (files && files.length > 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;
updateUI();
}
@@ -256,7 +299,9 @@ function handleAttachmentSelect(files: FileList | null) {
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
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 processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
@@ -289,7 +334,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
if (pdfFiles.length > 0) {
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) {
radio.addEventListener('change', function (e) {
const value = (e.target as HTMLInputElement).value;

View File

@@ -3,6 +3,11 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import Sortable from 'sortablejs';
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
import {
showWasmRequiredDialog,
WasmProvider,
} from '../utils/wasm-provider.js';
const pageState: AlternateMergeState = {
files: [],
@@ -10,7 +15,9 @@ const pageState: AlternateMergeState = {
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() {
pageState.files = [];
@@ -42,7 +49,8 @@ async function updateUI() {
if (pageState.files.length > 0) {
// Show file count summary
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');
infoSpan.className = 'text-gray-200';
@@ -74,7 +82,8 @@ async function updateUI() {
const pageCount = pdfjsDoc.numPages;
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;
const infoDiv = document.createElement('div');
@@ -91,7 +100,8 @@ async function updateUI() {
infoDiv.append(nameSpan, metaSpan);
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>`;
li.append(infoDiv, dragHandle);
@@ -121,7 +131,16 @@ async function updateUI() {
async function mixPages() {
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;
}
@@ -131,9 +150,11 @@ async function mixPages() {
const fileList = document.getElementById('file-list');
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;
}).filter(Boolean) as string[];
})
.filter(Boolean) as string[];
interface InterleaveFile {
name: string;
@@ -156,19 +177,30 @@ async function mixPages() {
const message = {
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) {
hideLoader();
if (e.data.status === 'success') {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
downloadFile(blob, 'alternated-mixed.pdf');
showAlert('Success', 'PDFs have been mixed successfully!', 'success', function () {
showAlert(
'Success',
'PDFs have been mixed successfully!',
'success',
function () {
resetState();
});
}
);
} else {
console.error('Worker interleave error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
@@ -180,7 +212,6 @@ async function mixPages() {
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) {
console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.');
@@ -191,7 +222,9 @@ async function mixPages() {
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
return (
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
});
if (pdfFiles.length > 0) {
pageState.files = pdfFiles;

View File

@@ -2,44 +2,79 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
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 JSZip from 'jszip';
import { PDFDocument } from 'pdf-lib';
const EXTENSIONS = ['.cbz', '.cbr'];
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 = {
jpeg: [0xFF, 0xD8, 0xFF],
png: [0x89, 0x50, 0x4E, 0x47],
jpeg: [0xff, 0xd8, 0xff],
png: [0x89, 0x50, 0x4e, 0x47],
gif: [0x47, 0x49, 0x46],
bmp: [0x42, 0x4D],
bmp: [0x42, 0x4d],
webp: [0x52, 0x49, 0x46, 0x46],
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++) {
if (data[offset + i] !== signature[i]) return false;
}
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 (matchesSignature(data, IMAGE_SIGNATURES.jpeg)) return 'jpeg';
if (matchesSignature(data, IMAGE_SIGNATURES.png)) return 'png';
if (matchesSignature(data, IMAGE_SIGNATURES.gif)) return 'gif';
if (matchesSignature(data, IMAGE_SIGNATURES.bmp)) return 'bmp';
if (matchesSignature(data, IMAGE_SIGNATURES.webp) &&
data[8] === 0x57 && data[9] === 0x45 && data[10] === 0x42 && data[11] === 0x50) {
if (
matchesSignature(data, IMAGE_SIGNATURES.webp) &&
data[8] === 0x57 &&
data[9] === 0x45 &&
data[10] === 0x42 &&
data[11] === 0x50
) {
return 'webp';
}
if (data[4] === 0x66 && data[5] === 0x74 && data[6] === 0x79 && data[7] === 0x70) {
if (
data[4] === 0x66 &&
data[5] === 0x74 &&
data[6] === 0x79 &&
data[7] === 0x70
) {
const brand = String.fromCharCode(data[8], data[9], data[10], data[11]);
if (brand === 'avif' || brand === 'avis' || brand === 'mif1' || brand === 'miaf') {
if (
brand === 'avif' ||
brand === 'avis' ||
brand === 'mif1' ||
brand === 'miaf'
) {
return 'avif';
}
}
@@ -50,7 +85,10 @@ function isCbzFile(filename: string): boolean {
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) => {
const blob = new Blob([imageData]);
const url = URL.createObjectURL(blob);
@@ -88,12 +126,14 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
const pdfDoc = await PDFDocument.create();
const imageFiles = Object.keys(zip.files)
.filter(name => {
.filter((name) => {
if (zip.files[name].dir) return false;
const ext = name.toLowerCase().substring(name.lastIndexOf('.'));
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) {
const zipEntry = zip.files[filename];
@@ -116,7 +156,8 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
embedMethod = 'png';
}
const image = embedMethod === 'png'
const image =
embedMethod === 'png'
? await pdfDoc.embedPng(imageBytes)
: await pdfDoc.embedJpg(imageBytes);
const page = pdfDoc.addPage([image.width, image.height]);
@@ -129,13 +170,14 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
}
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> {
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
return await pymupdf.convertToPdf(file, { filetype: 'cbz' });
const pymupdf = await loadPyMuPDF();
return await (pymupdf as any).convertToPdf(file, { filetype: 'cbz' });
}
document.addEventListener('DOMContentLoaded', () => {
@@ -163,7 +205,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -179,7 +222,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
@@ -242,7 +286,9 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < state.files.length; 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;
if (isCbzFile(file.name)) {
@@ -271,7 +317,10 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
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');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}

View File

@@ -8,8 +8,9 @@ import {
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument } from 'pdf-lib';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
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 * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@@ -60,8 +61,8 @@ async function performCondenseCompression(
removeThumbnails?: boolean;
}
) {
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
// Load PyMuPDF dynamically from user-provided URL
const pymupdf = await loadPyMuPDF();
const preset =
CONDENSE_PRESETS[level as keyof typeof CONDENSE_PRESETS] ||
@@ -390,6 +391,15 @@ document.addEventListener('DOMContentLoaded', () => {
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) {
const originalFile = state.files[0];

View File

@@ -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 { downloadFile } from '../utils/helpers';
@@ -10,13 +12,11 @@ interface DeskewResult {
}
let selectedFiles: File[] = [];
let pymupdf: PyMuPDF | null = null;
let pymupdf: any = null;
function initPyMuPDF(): PyMuPDF {
async function initPyMuPDF(): Promise<any> {
if (!pymupdf) {
pymupdf = new PyMuPDF({
assetPath: import.meta.env.BASE_URL + 'pymupdf-wasm/',
});
pymupdf = await loadPyMuPDF();
}
return pymupdf;
}
@@ -137,6 +137,12 @@ async function processDeskew(): Promise<void> {
return;
}
// Check if PyMuPDF is configured
if (!isWasmAvailable('pymupdf')) {
showWasmRequiredDialog('pymupdf');
return;
}
const thresholdSelect = document.getElementById(
'deskew-threshold'
) as HTMLSelectElement;
@@ -148,7 +154,7 @@ async function processDeskew(): Promise<void> {
showLoader('Initializing PyMuPDF...');
try {
const pdf = initPyMuPDF();
const pdf = await initPyMuPDF();
await pdf.load();
for (const file of selectedFiles) {

View File

@@ -2,8 +2,15 @@ import { EditAttachmentState, AttachmentInfo } from '@/types';
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
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 = {
file: null,
@@ -39,7 +46,7 @@ worker.onmessage = function (e) {
pageState.allAttachments = data.attachments.map(function (att: any) {
return {
...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) {
hideLoader();
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
const originalName =
pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`${originalName}_edited.pdf`
);
showAlert('Success', 'Attachments updated successfully!', 'success', function () {
showAlert(
'Success',
'Attachments updated successfully!',
'success',
function () {
resetState();
});
}
);
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
@@ -90,7 +103,8 @@ function displayAttachments(attachments: AttachmentInfo[]) {
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
const removeAllBtn = document.createElement('button');
removeAllBtn.className = 'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
removeAllBtn.className =
'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
removeAllBtn.textContent = 'Remove All Attachments';
removeAllBtn.onclick = function () {
if (pageState.allAttachments.length === 0) return;
@@ -102,7 +116,9 @@ function displayAttachments(attachments: AttachmentInfo[]) {
if (allSelected) {
pageState.allAttachments.forEach(function (attachment) {
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) {
element.classList.remove('opacity-50', 'line-through');
const btn = element.querySelector('button');
@@ -116,7 +132,9 @@ function displayAttachments(attachments: AttachmentInfo[]) {
} else {
pageState.allAttachments.forEach(function (attachment) {
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) {
element.classList.add('opacity-50', 'line-through');
const btn = element.querySelector('button');
@@ -136,7 +154,8 @@ function displayAttachments(attachments: AttachmentInfo[]) {
// Attachment items
for (const attachment of attachments) {
const attachmentDiv = document.createElement('div');
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
attachmentDiv.className =
'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
const infoDiv = document.createElement('div');
@@ -179,7 +198,9 @@ function displayAttachments(attachments: AttachmentInfo[]) {
const allSelected = pageState.allAttachments.every(function (att) {
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);
@@ -197,13 +218,21 @@ async function loadAttachments() {
showLoader('Loading attachments...');
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
hideLoader();
return;
}
try {
const fileBuffer = await pageState.file.arrayBuffer();
const message = {
command: 'get-attachments',
fileBuffer: fileBuffer,
fileName: pageState.file.name
fileName: pageState.file.name,
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
};
worker.postMessage(message, [fileBuffer]);
@@ -227,6 +256,13 @@ async function saveChanges() {
showLoader('Processing attachments...');
// Check if CPDF is configured (double check)
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
hideLoader();
return;
}
try {
const fileBuffer = await pageState.file.arrayBuffer();
@@ -234,7 +270,8 @@ async function saveChanges() {
command: 'edit-attachments',
fileBuffer: fileBuffer,
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]);
@@ -255,7 +292,8 @@ async function updateUI() {
if (pageState.file) {
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');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -292,7 +330,10 @@ async function updateUI() {
function handleFileSelect(files: FileList | null) {
if (files && files.length > 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;
updateUI();
}
@@ -332,7 +373,10 @@ document.addEventListener('DOMContentLoaded', function () {
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();

View File

@@ -3,8 +3,9 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { parseEmailFile, renderEmailToHtml } from './email-to-pdf.js';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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 TOOL_NAME = 'Email';
@@ -109,8 +110,7 @@ document.addEventListener('DOMContentLoaded', () => {
const includeAttachments = includeAttachmentsCheckbox?.checked ?? true;
showLoader('Loading PDF engine...');
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
if (state.files.length === 1) {
const originalFile = state.files[0];

View File

@@ -2,8 +2,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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 EXTENSIONS = ['.epub'];
@@ -38,7 +39,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -54,7 +56,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
@@ -91,14 +94,15 @@ document.addEventListener('DOMContentLoaded', () => {
}
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
if (state.files.length === 1) {
const originalFile = state.files[0];
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';
downloadFile(pdfBlob, fileName);
@@ -117,9 +121,13 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < state.files.length; 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 pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
@@ -140,7 +148,10 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
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');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}

View File

@@ -2,8 +2,15 @@ import { showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
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 {
files: File[];
@@ -35,12 +42,18 @@ function resetState() {
}
}
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
const statusMessage = document.getElementById('status-message') as HTMLElement;
function showStatus(
message: string,
type: 'success' | 'error' | 'info' = 'info'
) {
const statusMessage = document.getElementById(
'status-message'
) as HTMLElement;
if (!statusMessage) return;
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'
: type === 'error'
? 'bg-red-900 text-red-200'
@@ -60,7 +73,10 @@ worker.onmessage = function (e) {
const attachments = e.data.attachments;
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();
return;
}
@@ -76,7 +92,10 @@ worker.onmessage = function (e) {
zip.generateAsync({ type: 'blob' }).then(function (zipBlob) {
downloadFile(zipBlob, 'extracted-attachments.zip');
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
showAlert(
'Success',
`${attachments.length} attachment(s) extracted successfully!`
);
showStatus(
`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);
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();
} else {
showStatus(`Error: ${errorMessage}`, 'error');
@@ -119,7 +141,8 @@ async function updateUI() {
if (pageState.files.length > 0) {
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');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -130,7 +153,9 @@ async function updateUI() {
const sizeSpan = document.createElement('div');
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);
infoContainer.append(countSpan, sizeSpan);
@@ -158,6 +183,12 @@ async function extractAttachments() {
return;
}
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
return;
}
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.add('opacity-50', 'cursor-not-allowed');
@@ -176,17 +207,22 @@ async function extractAttachments() {
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 = {
command: 'extract-attachments',
fileBuffers,
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);
} catch (error) {
console.error('Error reading files:', error);
showStatus(
@@ -204,7 +240,9 @@ async function extractAttachments() {
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
return (
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
});
if (pdfFiles.length > 0) {
pageState.files = pdfFiles;

View File

@@ -1,11 +1,15 @@
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 { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
interface ExtractedImage {
data: Uint8Array;
@@ -36,7 +40,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
const updateUI = async () => {
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls) return;
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls)
return;
// Clear extracted images when files change
extractedImages = [];
@@ -49,7 +54,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -65,7 +71,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_: File, i: number) => i !== index);
@@ -151,7 +158,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
showLoader('Loading PDF processor...');
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
extractedImages = [];
let imgCounter = 0;
@@ -175,7 +182,7 @@ document.addEventListener('DOMContentLoaded', () => {
extractedImages.push({
data: imgData.data,
name: `image_${imgCounter}.${imgData.ext || 'png'}`,
ext: imgData.ext || 'png'
ext: imgData.ext || 'png',
});
}
} catch (e) {
@@ -189,7 +196,10 @@ document.addEventListener('DOMContentLoaded', () => {
hideLoader();
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 {
displayImages();
showAlert(
@@ -200,7 +210,10 @@ document.addEventListener('DOMContentLoaded', () => {
}
} catch (e: any) {
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) => {
if (files && files.length > 0) {
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];
updateUI();

View File

@@ -2,10 +2,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import JSZip from 'jszip';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
let file: File | null = null;
const updateUI = () => {
@@ -20,7 +19,8 @@ const updateUI = () => {
optionsPanel.classList.remove('hidden');
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');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -57,15 +57,23 @@ const resetState = () => {
};
function tableToCsv(rows: (string | null)[][]): string {
return rows.map(row =>
row.map(cell => {
return rows
.map((row) =>
row
.map((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;
}).join(',')
).join('\n');
})
.join(',')
)
.join('\n');
}
async function extract() {
@@ -82,10 +90,9 @@ async function extract() {
}
});
showLoader('Loading Engine...');
try {
await pymupdf.load();
showLoader('Loading Engine...');
const pymupdf = await loadPyMuPDF();
showLoader('Extracting tables...');
const doc = await pymupdf.open(file);
@@ -115,7 +122,7 @@ async function extract() {
rows: table.rows,
markdown: table.markdown,
rowCount: table.rowCount,
colCount: table.colCount
colCount: table.colCount,
});
});
}
@@ -147,7 +154,12 @@ async function extract() {
const blob = new Blob([content], { type: mimeType });
downloadFile(blob, `${baseName}_table.${ext}`);
showAlert('Success', `Extracted 1 table successfully!`, 'success', resetState);
showAlert(
'Success',
`Extracted 1 table successfully!`,
'success',
resetState
);
} else {
showLoader('Creating ZIP file...');
const zip = new JSZip();
@@ -173,7 +185,12 @@ async function extract() {
const zipBlob = await zip.generateAsync({ type: 'blob' });
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) {
console.error(e);
@@ -198,7 +215,9 @@ document.addEventListener('DOMContentLoaded', () => {
const handleFileSelect = (newFiles: FileList | null) => {
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) {
showAlert('Invalid File', 'Please upload a PDF file.');

View File

@@ -2,8 +2,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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 EXTENSIONS = ['.fb2'];
@@ -34,7 +35,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -50,7 +52,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
@@ -87,14 +90,15 @@ document.addEventListener('DOMContentLoaded', () => {
}
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
if (state.files.length === 1) {
const originalFile = state.files[0];
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';
downloadFile(pdfBlob, fileName);
@@ -113,9 +117,13 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < state.files.length; 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 pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
@@ -136,7 +144,10 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
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');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}

View File

@@ -1,6 +1,8 @@
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.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 JSZip from 'jszip';
@@ -98,6 +100,12 @@ async function processFiles() {
return;
}
// Check if Ghostscript is configured
if (!isGhostscriptAvailable()) {
showWasmRequiredDialog('ghostscript');
return;
}
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');

View File

@@ -1,15 +1,18 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
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 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_DISPLAY = 'JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP';
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_DISPLAY =
'JPG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX, JP2, PSD, SVG, HEIC, WebP';
let files: File[] = [];
let pymupdf: PyMuPDF | null = null;
let pymupdf: any = null;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
@@ -103,7 +106,10 @@ function handleFiles(newFiles: FileList) {
const validFiles = Array.from(newFiles).filter(isValidImageFile);
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) {
@@ -132,7 +138,8 @@ function updateUI() {
files.forEach((file, index) => {
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');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
@@ -148,7 +155,8 @@ function updateUI() {
infoContainer.append(nameSpan, sizeSpan);
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.onclick = () => {
files = files.filter((_, i) => i !== index);
@@ -165,10 +173,9 @@ function updateUI() {
}
}
async function ensurePyMuPDF(): Promise<PyMuPDF> {
async function ensurePyMuPDF(): Promise<any> {
if (!pymupdf) {
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
pymupdf = await loadPyMuPDF();
}
return pymupdf;
}
@@ -184,8 +191,12 @@ async function preprocessFile(file: File): Promise<File> {
quality: 0.9,
});
const blob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult;
return new File([blob], file.name.replace(/\.(heic|heif)$/i, '.png'), { type: 'image/png' });
const blob = Array.isArray(conversionResult)
? conversionResult[0]
: conversionResult;
return new File([blob], file.name.replace(/\.(heic|heif)$/i, '.png'), {
type: 'image/png',
});
} catch (e) {
console.error(`Failed to convert HEIC: ${file.name}`, e);
throw new Error(`Failed to process HEIC file: ${file.name}`);
@@ -212,7 +223,11 @@ async function preprocessFile(file: File): Promise<File> {
canvas.toBlob((blob) => {
URL.revokeObjectURL(url);
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 {
reject(new Error('Canvas toBlob failed'));
}
@@ -268,7 +283,10 @@ async function convertToPdf() {
});
} catch (e: any) {
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 {
hideLoader();
}

View File

@@ -1,14 +1,15 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/jp2'];
let files: File[] = [];
let pymupdf: PyMuPDF | null = null;
let pymupdf: any = null;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
@@ -89,14 +90,19 @@ function getFileExtension(filename: string): string {
function isValidImageFile(file: File): boolean {
const ext = getFileExtension(file.name);
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) {
const validFiles = Array.from(newFiles).filter(isValidImageFile);
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) {
@@ -125,7 +131,8 @@ function updateUI() {
files.forEach((file, index) => {
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');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
@@ -141,7 +148,8 @@ function updateUI() {
infoContainer.append(nameSpan, sizeSpan);
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.onclick = () => {
files = files.filter((_, i) => i !== index);
@@ -158,10 +166,9 @@ function updateUI() {
}
}
async function ensurePyMuPDF(): Promise<PyMuPDF> {
async function ensurePyMuPDF(): Promise<any> {
if (!pymupdf) {
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
pymupdf = await loadPyMuPDF();
}
return pymupdf;
}
@@ -188,7 +195,10 @@ async function convertToPdf() {
});
} catch (e: any) {
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 {
hideLoader();
}

View File

@@ -1,101 +1,133 @@
import JSZip from 'jszip'
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers';
import JSZip from 'jszip';
import {
downloadFile,
formatBytes,
readFileAsArrayBuffer,
} from '../utils/helpers';
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 convertBtn = document.getElementById('convertBtn') as HTMLButtonElement
const statusMessage = document.getElementById('status-message') as HTMLDivElement
const fileListDiv = document.getElementById('fileList') as HTMLDivElement
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement
const jsonFilesInput = document.getElementById('jsonFiles') as HTMLInputElement;
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement;
const statusMessage = document.getElementById(
'status-message'
) as HTMLDivElement;
const fileListDiv = document.getElementById('fileList') as HTMLDivElement;
const backToToolsBtn = document.getElementById(
'back-to-tools'
) as HTMLButtonElement;
function showStatus(
message: string,
type: 'success' | 'error' | 'info' = 'info'
) {
statusMessage.textContent = message
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
statusMessage.textContent = message;
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
type === 'success'
? 'bg-green-900 text-green-200'
: type === 'error'
? 'bg-red-900 text-red-200'
: 'bg-blue-900 text-blue-200'
}`
statusMessage.classList.remove('hidden')
}`;
statusMessage.classList.remove('hidden');
}
function hideStatus() {
statusMessage.classList.add('hidden')
statusMessage.classList.add('hidden');
}
function updateFileList() {
fileListDiv.innerHTML = ''
fileListDiv.innerHTML = '';
if (selectedFiles.length === 0) {
fileListDiv.classList.add('hidden')
return
fileListDiv.classList.add('hidden');
return;
}
fileListDiv.classList.remove('hidden')
fileListDiv.classList.remove('hidden');
selectedFiles.forEach((file) => {
const fileDiv = document.createElement('div')
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2'
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2';
const nameSpan = document.createElement('span')
nameSpan.className = 'truncate font-medium text-gray-200'
nameSpan.textContent = file.name
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-gray-200';
nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span')
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'
sizeSpan.textContent = formatBytes(file.size)
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
sizeSpan.textContent = formatBytes(file.size);
fileDiv.append(nameSpan, sizeSpan)
fileListDiv.appendChild(fileDiv)
})
fileDiv.append(nameSpan, sizeSpan);
fileListDiv.appendChild(fileDiv);
});
}
jsonFilesInput.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement
const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
selectedFiles = Array.from(target.files)
convertBtn.disabled = selectedFiles.length === 0
updateFileList()
selectedFiles = Array.from(target.files);
convertBtn.disabled = selectedFiles.length === 0;
updateFileList();
if (selectedFiles.length === 0) {
showStatus('Please select at least 1 JSON file', 'info')
showStatus('Please select at least 1 JSON file', 'info');
} else {
showStatus(`${selectedFiles.length} file(s) selected. Ready to convert!`, 'info')
showStatus(
`${selectedFiles.length} file(s) selected. Ready to convert!`,
'info'
);
}
}
})
});
async function convertJSONsToPDF() {
if (selectedFiles.length === 0) {
showStatus('Please select at least 1 JSON file', 'error')
return
showStatus('Please select at least 1 JSON file', 'error');
return;
}
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
return;
}
try {
convertBtn.disabled = true
showStatus('Reading files (Main Thread)...', 'info')
convertBtn.disabled = true;
showStatus('Reading files (Main Thread)...', 'info');
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',
fileBuffers: fileBuffers,
fileNames: selectedFiles.map(f => f.name)
}, fileBuffers);
fileNames: selectedFiles.map((f) => f.name),
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
},
fileBuffers
);
} catch (error) {
console.error('Error reading files:', error)
showStatus(`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
convertBtn.disabled = false
console.error('Error reading files:', error);
showStatus(
`❌ 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;
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 {
showStatus('Creating ZIP file...', 'info')
showStatus('Creating ZIP file...', 'info');
const zip = new JSZip()
const zip = new JSZip();
pdfFiles.forEach(({ name, data }) => {
const pdfName = name.replace(/\.json$/i, '.pdf')
const uint8Array = new Uint8Array(data)
zip.file(pdfName, uint8Array)
})
const pdfName = name.replace(/\.json$/i, '.pdf');
const uint8Array = new Uint8Array(data);
zip.file(pdfName, uint8Array);
});
const zipBlob = await zip.generateAsync({ type: 'blob' })
const url = URL.createObjectURL(zipBlob)
const a = document.createElement('a')
a.href = url
a.download = 'jsons-to-pdf.zip'
downloadFile(zipBlob, 'jsons-to-pdf.zip')
const zipBlob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = url;
a.download = '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 = []
jsonFilesInput.value = ''
fileListDiv.innerHTML = ''
fileListDiv.classList.add('hidden')
convertBtn.disabled = true
selectedFiles = [];
jsonFilesInput.value = '';
fileListDiv.innerHTML = '';
fileListDiv.classList.add('hidden');
convertBtn.disabled = true;
setTimeout(() => {
hideStatus()
}, 3000)
hideStatus();
}, 3000);
} catch (error) {
console.error('Error creating ZIP:', error)
showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
console.error('Error creating ZIP:', error);
showStatus(
`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`,
'error'
);
}
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
@@ -148,12 +187,12 @@ worker.onmessage = async (e: MessageEvent) => {
if (backToToolsBtn) {
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
showStatus('Select JSON files to get started', 'info')
initializeGlobalShortcuts()
showStatus('Select JSON files to get started', 'info');
initializeGlobalShortcuts();

View File

@@ -1,14 +1,29 @@
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 { 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 * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs';
// @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 {
pdfDocs: Record<string, any>;
@@ -35,7 +50,9 @@ const mergeState: MergeState = {
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() {
const fileList = document.getElementById('file-list');
@@ -122,7 +139,11 @@ async function renderPageMergeThumbnails() {
let currentPageNumber = 0;
// 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');
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';
@@ -146,7 +167,9 @@ async function renderPageMergeThumbnails() {
const fileNamePara = document.createElement('p');
fileNamePara.className =
'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.textContent = fileName
? `${fileName.substring(0, 10)}... (p${pageNumber})`
@@ -162,7 +185,10 @@ async function renderPageMergeThumbnails() {
if (!pdfjsDoc) continue;
// 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);
};
@@ -177,13 +203,11 @@ async function renderPageMergeThumbnails() {
lazyLoadMargin: '300px',
onProgress: (current, total) => {
currentPageNumber++;
showLoader(
`Rendering page previews...`
);
showLoader(`Rendering page previews...`);
},
onBatchComplete: () => {
createIcons({ icons });
}
},
}
);
}
@@ -253,8 +277,13 @@ const resetState = async () => {
await updateUI();
};
export async function merge() {
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
return;
}
showLoader('Merging PDFs...');
try {
// @ts-ignore
@@ -269,14 +298,18 @@ export async function merge() {
const sortedFiles = Array.from(fileList.children)
.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);
for (const file of sortedFiles) {
if (!file) continue;
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);
@@ -284,12 +317,12 @@ export async function merge() {
jobs.push({
fileName: file.name,
rangeType: 'specific',
rangeString: rangeInput.value.trim()
rangeString: rangeInput.value.trim(),
});
} else {
jobs.push({
fileName: file.name,
rangeType: 'all'
rangeType: 'all',
});
}
}
@@ -330,7 +363,7 @@ export async function merge() {
jobs.push({
fileName: current.fileName,
rangeType: 'single',
pageIndex: current.pageIndex
pageIndex: current.pageIndex,
});
} else {
// Range of pages
@@ -338,7 +371,7 @@ export async function merge() {
fileName: current.fileName,
rangeType: 'range',
startPage: current.pageIndex + 1,
endPage: endPage + 1
endPage: endPage + 1,
});
}
}
@@ -361,10 +394,14 @@ export async function merge() {
const message: MergeMessage = {
command: 'merge',
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
mergeWorker.onmessage = (e: MessageEvent<MergeResponse>) => {
@@ -373,9 +410,14 @@ export async function merge() {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
downloadFile(blob, 'merged.pdf');
mergeState.mergeSuccess = true;
showAlert('Success', 'PDFs merged successfully!', 'success', async () => {
showAlert(
'Success',
'PDFs merged successfully!',
'success',
async () => {
await resetState();
});
}
);
} else {
console.error('Worker merge error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to merge PDFs.');
@@ -387,7 +429,6 @@ export async function merge() {
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) {
console.error('Merge error:', e);
showAlert(
@@ -400,7 +441,9 @@ export async function merge() {
export async function refreshMergeUI() {
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;
const wasInPageMode = mergeState.activeMode === 'page';
@@ -432,7 +475,8 @@ export async function refreshMergeUI() {
const pagePanel = document.getElementById('page-mode-panel');
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
(state.files as File[]).forEach((f, index) => {
@@ -481,7 +525,8 @@ export async function refreshMergeUI() {
inputWrapper.append(label, input);
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.title = 'Remove file';
deleteBtn.onclick = (e) => {
@@ -565,8 +610,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', async (e) => {
const files = (e.target as HTMLInputElement).files;
@@ -591,7 +634,11 @@ document.addEventListener('DOMContentLoaded', () => {
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
const pdfFiles = Array.from(files).filter(
(f) =>
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
if (pdfFiles.length > 0) {
state.files = [...state.files, ...pdfFiles];
await updateUI();
@@ -599,7 +646,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
@@ -624,6 +670,4 @@ document.addEventListener('DOMContentLoaded', () => {
await merge();
});
}
});

View File

@@ -2,8 +2,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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 EXTENSIONS = ['.mobi'];
@@ -34,7 +35,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -50,7 +52,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
@@ -87,14 +90,15 @@ document.addEventListener('DOMContentLoaded', () => {
}
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
if (state.files.length === 1) {
const originalFile = state.files[0];
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';
downloadFile(pdfBlob, fileName);
@@ -113,9 +117,13 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < state.files.length; 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 pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
@@ -136,7 +144,10 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
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');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}

View File

@@ -1,10 +1,14 @@
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 { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
interface LayerData {
number: number;
@@ -15,7 +19,7 @@ interface LayerData {
depth: number;
parentXref: number;
displayOrder: number;
};
}
let currentFile: File | null = null;
let currentDoc: any = null;
@@ -44,7 +48,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (currentFile) {
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -60,7 +65,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
resetState();
@@ -99,16 +105,29 @@ document.addEventListener('DOMContentLoaded', () => {
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) => {
const modal = document.getElementById('input-modal');
const titleEl = document.getElementById('input-title');
const messageEl = document.getElementById('input-message');
const inputEl = document.getElementById('input-value') as HTMLInputElement;
const inputEl = document.getElementById(
'input-value'
) as HTMLInputElement;
const confirmBtn = document.getElementById('input-confirm');
const cancelBtn = document.getElementById('input-cancel');
if (!modal || !titleEl || !messageEl || !inputEl || !confirmBtn || !cancelBtn) {
if (
!modal ||
!titleEl ||
!messageEl ||
!inputEl ||
!confirmBtn ||
!cancelBtn
) {
console.error('Input modal elements not found');
resolve(null);
return;
@@ -165,9 +184,13 @@ document.addEventListener('DOMContentLoaded', () => {
}
// 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;">
<label class="layer-toggle">
<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>` : ''}
</div>
</div>
`).join('');
`
)
.join('');
// Attach toggle handlers
layersList.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
layersList
.querySelectorAll('input[type="checkbox"]')
.forEach((checkbox) => {
checkbox.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
const xref = parseInt(target.dataset.xref || '0');
@@ -190,7 +217,9 @@ document.addEventListener('DOMContentLoaded', () => {
try {
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) {
layer.on = isOn;
}
@@ -207,7 +236,9 @@ document.addEventListener('DOMContentLoaded', () => {
btn.addEventListener('click', (e) => {
const target = e.target as HTMLButtonElement;
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) {
showAlert('Error', 'Layer not found');
@@ -229,14 +260,22 @@ document.addEventListener('DOMContentLoaded', () => {
btn.addEventListener('click', async (e) => {
const target = e.target as HTMLButtonElement;
const parentXref = parseInt(target.dataset.xref || '0');
const parentLayer = Array.from(layersMap.values()).find(l => l.xref === parentXref);
const 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;
try {
const childXref = currentDoc.addOCGWithParent(childName.trim(), parentXref);
const childXref = currentDoc.addOCGWithParent(
childName.trim(),
parentXref
);
const parentDisplayOrder = parentLayer?.displayOrder || 0;
layersMap.forEach((l) => {
if (l.displayOrder > parentDisplayOrder) {
@@ -252,7 +291,7 @@ document.addEventListener('DOMContentLoaded', () => {
locked: false,
depth: (parentLayer?.depth || 0) + 1,
parentXref: parentXref,
displayOrder: parentDisplayOrder + 1
displayOrder: parentDisplayOrder + 1,
});
renderLayers();
@@ -272,7 +311,7 @@ document.addEventListener('DOMContentLoaded', () => {
try {
showLoader('Loading engine...');
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
showLoader(`Loading layers from ${currentFile.name}...`);
currentDoc = await pymupdf.open(currentFile);
@@ -293,7 +332,7 @@ document.addEventListener('DOMContentLoaded', () => {
locked: layer.locked,
depth: layer.depth ?? 0,
parentXref: layer.parentXref ?? 0,
displayOrder: layer.displayOrder ?? nextDisplayOrder++
displayOrder: layer.displayOrder ?? nextDisplayOrder++,
});
if ((layer.displayOrder ?? -1) >= nextDisplayOrder) {
nextDisplayOrder = layer.displayOrder + 1;
@@ -309,7 +348,6 @@ document.addEventListener('DOMContentLoaded', () => {
renderLayers();
setupLayerHandlers();
} catch (error: any) {
hideLoader();
showAlert('Error', error.message || 'Failed to load PDF layers');
@@ -319,7 +357,9 @@ document.addEventListener('DOMContentLoaded', () => {
const setupLayerHandlers = () => {
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');
if (addLayerBtn && newLayerInput) {
@@ -343,7 +383,7 @@ document.addEventListener('DOMContentLoaded', () => {
locked: false,
depth: 0,
parentXref: 0,
displayOrder: newDisplayOrder
displayOrder: newDisplayOrder,
});
renderLayers();
@@ -358,8 +398,11 @@ document.addEventListener('DOMContentLoaded', () => {
try {
showLoader('Saving PDF with layer changes...');
const pdfBytes = currentDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
const outName = currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
const blob = new Blob([new Uint8Array(pdfBytes)], {
type: 'application/pdf',
});
const outName =
currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
downloadFile(blob, outName);
hideLoader();
resetState();
@@ -375,7 +418,10 @@ document.addEventListener('DOMContentLoaded', () => {
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 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;
updateUI();
} else {

View File

@@ -2,10 +2,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import JSZip from 'jszip';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
let file: File | null = null;
const updateUI = () => {
@@ -20,7 +19,8 @@ const updateUI = () => {
optionsPanel.classList.remove('hidden');
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');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -57,15 +57,23 @@ const resetState = () => {
};
function tableToCsv(rows: (string | null)[][]): string {
return rows.map(row =>
row.map(cell => {
return rows
.map((row) =>
row
.map((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;
}).join(',')
).join('\n');
})
.join(',')
)
.join('\n');
}
async function convert() {
@@ -77,7 +85,7 @@ async function convert() {
showLoader('Loading Engine...');
try {
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
showLoader('Extracting tables...');
const doc = await pymupdf.open(file);
@@ -102,10 +110,15 @@ async function convert() {
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;' });
downloadFile(blob, `${baseName}.csv`);
showAlert('Success', 'PDF converted to CSV successfully!', 'success', resetState);
showAlert(
'Success',
'PDF converted to CSV successfully!',
'success',
resetState
);
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
@@ -129,7 +142,9 @@ document.addEventListener('DOMContentLoaded', () => {
const handleFileSelect = (newFiles: FileList | null) => {
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) {
showAlert('Invalid File', 'Please upload a PDF file.');

View File

@@ -1,11 +1,15 @@
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 { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -25,7 +29,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
const updateUI = async () => {
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls) return;
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
@@ -33,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -49,7 +55,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_: File, i: number) => i !== index);
@@ -94,7 +101,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
showLoader('Loading PDF converter...');
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
if (state.files.length === 1) {
const file = state.files[0];
@@ -119,7 +126,9 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < state.files.length; 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 baseName = file.name.replace(/\.pdf$/i, '');
@@ -142,14 +151,18 @@ document.addEventListener('DOMContentLoaded', () => {
}
} catch (e: any) {
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) => {
if (files && files.length > 0) {
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];
updateUI();

View File

@@ -1,11 +1,10 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
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 * as XLSX from 'xlsx';
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
let file: File | null = null;
const updateUI = () => {
@@ -20,7 +19,8 @@ const updateUI = () => {
optionsPanel.classList.remove('hidden');
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');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -65,7 +65,7 @@ async function convert() {
showLoader('Loading Engine...');
try {
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
showLoader('Extracting tables...');
const doc = await pymupdf.open(file);
@@ -87,7 +87,7 @@ async function convert() {
tables.forEach((table) => {
allTables.push({
page: i + 1,
rows: table.rows
rows: table.rows,
});
});
}
@@ -106,16 +106,26 @@ async function convert() {
XLSX.utils.book_append_sheet(workbook, worksheet, 'Table');
} else {
allTables.forEach((table, idx) => {
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(0, 31);
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(
0,
31
);
const worksheet = XLSX.utils.aoa_to_sheet(table.rows);
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
});
}
const xlsxData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([xlsxData], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const blob = new Blob([xlsxData], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
downloadFile(blob, `${baseName}.xlsx`);
showAlert('Success', `Extracted ${allTables.length} table(s) to Excel!`, 'success', resetState);
showAlert(
'Success',
`Extracted ${allTables.length} table(s) to Excel!`,
'success',
resetState
);
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
@@ -139,7 +149,9 @@ document.addEventListener('DOMContentLoaded', () => {
const handleFileSelect = (newFiles: FileList | null) => {
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) {
showAlert('Invalid File', 'Please upload a PDF file.');

View File

@@ -1,101 +1,133 @@
import JSZip from 'jszip'
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers';
import JSZip from 'jszip';
import {
downloadFile,
formatBytes,
readFileAsArrayBuffer,
} from '../utils/helpers';
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 convertBtn = document.getElementById('convertBtn') as HTMLButtonElement
const statusMessage = document.getElementById('status-message') as HTMLDivElement
const fileListDiv = document.getElementById('fileList') as HTMLDivElement
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement
const pdfFilesInput = document.getElementById('pdfFiles') as HTMLInputElement;
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement;
const statusMessage = document.getElementById(
'status-message'
) as HTMLDivElement;
const fileListDiv = document.getElementById('fileList') as HTMLDivElement;
const backToToolsBtn = document.getElementById(
'back-to-tools'
) as HTMLButtonElement;
function showStatus(
message: string,
type: 'success' | 'error' | 'info' = 'info'
) {
statusMessage.textContent = message
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
statusMessage.textContent = message;
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
type === 'success'
? 'bg-green-900 text-green-200'
: type === 'error'
? 'bg-red-900 text-red-200'
: 'bg-blue-900 text-blue-200'
}`
statusMessage.classList.remove('hidden')
}`;
statusMessage.classList.remove('hidden');
}
function hideStatus() {
statusMessage.classList.add('hidden')
statusMessage.classList.add('hidden');
}
function updateFileList() {
fileListDiv.innerHTML = ''
fileListDiv.innerHTML = '';
if (selectedFiles.length === 0) {
fileListDiv.classList.add('hidden')
return
fileListDiv.classList.add('hidden');
return;
}
fileListDiv.classList.remove('hidden')
fileListDiv.classList.remove('hidden');
selectedFiles.forEach((file) => {
const fileDiv = document.createElement('div')
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2'
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2';
const nameSpan = document.createElement('span')
nameSpan.className = 'truncate font-medium text-gray-200'
nameSpan.textContent = file.name
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-gray-200';
nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span')
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'
sizeSpan.textContent = formatBytes(file.size)
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400';
sizeSpan.textContent = formatBytes(file.size);
fileDiv.append(nameSpan, sizeSpan)
fileListDiv.appendChild(fileDiv)
})
fileDiv.append(nameSpan, sizeSpan);
fileListDiv.appendChild(fileDiv);
});
}
pdfFilesInput.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement
const target = e.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
selectedFiles = Array.from(target.files)
convertBtn.disabled = selectedFiles.length === 0
updateFileList()
selectedFiles = Array.from(target.files);
convertBtn.disabled = selectedFiles.length === 0;
updateFileList();
if (selectedFiles.length === 0) {
showStatus('Please select at least 1 PDF file', 'info')
showStatus('Please select at least 1 PDF file', 'info');
} else {
showStatus(`${selectedFiles.length} file(s) selected. Ready to convert!`, 'info')
showStatus(
`${selectedFiles.length} file(s) selected. Ready to convert!`,
'info'
);
}
}
})
});
async function convertPDFsToJSON() {
if (selectedFiles.length === 0) {
showStatus('Please select at least 1 PDF file', 'error')
return
showStatus('Please select at least 1 PDF file', 'error');
return;
}
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
return;
}
try {
convertBtn.disabled = true
showStatus('Reading files (Main Thread)...', 'info')
convertBtn.disabled = true;
showStatus('Reading files (Main Thread)...', 'info');
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',
fileBuffers: fileBuffers,
fileNames: selectedFiles.map(f => f.name)
}, fileBuffers);
fileNames: selectedFiles.map((f) => f.name),
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
},
fileBuffers
);
} catch (error) {
console.error('Error reading files:', error)
showStatus(`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
convertBtn.disabled = false
console.error('Error reading files:', error);
showStatus(
`❌ 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;
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 {
showStatus('Creating ZIP file...', 'info')
showStatus('Creating ZIP file...', 'info');
const zip = new JSZip()
const zip = new JSZip();
jsonFiles.forEach(({ name, data }) => {
const jsonName = name.replace(/\.pdf$/i, '.json')
const uint8Array = new Uint8Array(data)
zip.file(jsonName, uint8Array)
})
const jsonName = name.replace(/\.pdf$/i, '.json');
const uint8Array = new Uint8Array(data);
zip.file(jsonName, uint8Array);
});
const zipBlob = await zip.generateAsync({ type: 'blob' })
downloadFile(zipBlob, 'pdfs-to-json.zip')
const zipBlob = await zip.generateAsync({ type: 'blob' });
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 = []
pdfFilesInput.value = ''
fileListDiv.innerHTML = ''
fileListDiv.classList.add('hidden')
convertBtn.disabled = true
selectedFiles = [];
pdfFilesInput.value = '';
fileListDiv.innerHTML = '';
fileListDiv.classList.add('hidden');
convertBtn.disabled = true;
setTimeout(() => {
hideStatus()
}, 3000)
hideStatus();
}, 3000);
} catch (error) {
console.error('Error creating ZIP:', error)
showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
console.error('Error creating ZIP:', error);
showStatus(
`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`,
'error'
);
}
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
@@ -144,11 +183,11 @@ worker.onmessage = async (e: MessageEvent) => {
if (backToToolsBtn) {
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')
initializeGlobalShortcuts()
showStatus('Select PDF files to get started', 'info');
initializeGlobalShortcuts();

View File

@@ -1,11 +1,15 @@
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 { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -17,7 +21,9 @@ document.addEventListener('DOMContentLoaded', () => {
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
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) {
backBtn.addEventListener('click', () => {
@@ -26,7 +32,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
const updateUI = async () => {
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls) return;
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
@@ -34,7 +41,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -50,7 +58,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_: File, i: number) => i !== index);
@@ -95,7 +104,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
showLoader('Loading PDF converter...');
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
const includeImages = includeImagesCheckbox?.checked ?? false;
@@ -123,7 +132,9 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < state.files.length; 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 baseName = file.name.replace(/\.pdf$/i, '');
@@ -145,14 +156,18 @@ document.addEventListener('DOMContentLoaded', () => {
}
} catch (e: any) {
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) => {
if (files && files.length > 0) {
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];
updateUI();

View File

@@ -8,6 +8,8 @@ import {
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
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', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -19,7 +21,9 @@ document.addEventListener('DOMContentLoaded', () => {
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
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) {
backBtn.addEventListener('click', () => {
@@ -28,7 +32,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
const updateUI = async () => {
if (!fileDisplayArea || !optionsContainer || !processBtn || !fileControls) return;
if (!fileDisplayArea || !optionsContainer || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
@@ -36,7 +41,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -52,7 +58,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
@@ -105,11 +112,42 @@ document.addEventListener('DOMContentLoaded', () => {
if (state.files.length === 1) {
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...');
const convertedBlob = await convertFileToPdfA(
originalFile,
fileToConvert,
level,
(msg) => showLoader(msg)
);
@@ -133,12 +171,12 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < state.files.length; 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(
file,
level,
(msg) => showLoader(msg)
const convertedBlob = await convertFileToPdfA(file, level, (msg) =>
showLoader(msg)
);
const baseName = file.name.replace(/\.pdf$/i, '');
@@ -195,10 +233,14 @@ document.addEventListener('DOMContentLoaded', () => {
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
const pdfFiles = Array.from(files).filter(
(f) =>
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
pdfFiles.forEach(f => dataTransfer.items.add(f));
pdfFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}

View File

@@ -2,10 +2,11 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import JSZip from 'jszip';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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[] = [];
const updateUI = () => {
@@ -23,7 +24,8 @@ const updateUI = () => {
files.forEach((file, index) => {
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');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -39,7 +41,8 @@ const updateUI = () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
files = files.filter((_, i) => i !== index);
@@ -70,10 +73,19 @@ async function convert() {
return;
}
// Check if PyMuPDF is configured
if (!isPyMuPDFAvailable()) {
showWasmRequiredDialog('pymupdf');
return;
}
showLoader('Loading Engine...');
try {
await pymupdf.load();
// Load PyMuPDF dynamically if not already loaded
if (!pymupdf) {
pymupdf = await loadPyMuPDF();
}
const isSingleFile = files.length === 1;
@@ -88,7 +100,12 @@ async function convert() {
const svgContent = page.toSvg();
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
downloadFile(svgBlob, `${baseName}.svg`);
showAlert('Success', 'PDF converted to SVG successfully!', 'success', () => resetState());
showAlert(
'Success',
'PDF converted to SVG successfully!',
'success',
() => resetState()
);
} else {
const zip = new JSZip();
for (let i = 0; i < pageCount; i++) {
@@ -100,7 +117,12 @@ async function convert() {
showLoader('Creating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${baseName}_svg.zip`);
showAlert('Success', `Converted ${pageCount} pages to SVG!`, 'success', () => resetState());
showAlert(
'Success',
`Converted ${pageCount} pages to SVG!`,
'success',
() => resetState()
);
}
} else {
const zip = new JSZip();
@@ -114,10 +136,15 @@ async function convert() {
const baseName = file.name.replace(/\.[^/.]+$/, '');
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 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);
totalPages++;
}
@@ -126,7 +153,12 @@ async function convert() {
showLoader('Creating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdf_to_svg.zip');
showAlert('Success', `Converted ${files.length} files (${totalPages} pages) to SVG!`, 'success', () => resetState());
showAlert(
'Success',
`Converted ${files.length} files (${totalPages} pages) to SVG!`,
'success',
() => resetState()
);
}
} catch (e) {
console.error(e);
@@ -172,7 +204,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (fileInput && dropZone) {
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) => {
@@ -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 (processBtn) processBtn.addEventListener('click', convert);
});

View File

@@ -1,11 +1,12 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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 pymupdf: PyMuPDF | null = null;
let pymupdf: any = null;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
@@ -20,7 +21,9 @@ function initializePage() {
const dropZone = document.getElementById('drop-zone');
const addMoreBtn = document.getElementById('add-more-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) {
fileInput.addEventListener('change', handleFileUpload);
@@ -80,12 +83,17 @@ function handleFileUpload(e: Event) {
}
function handleFiles(newFiles: FileList) {
const validFiles = Array.from(newFiles).filter(file =>
file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')
const validFiles = Array.from(newFiles).filter(
(file) =>
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
);
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped. Only PDF files are allowed.');
showAlert(
'Invalid Files',
'Some files were skipped. Only PDF files are allowed.'
);
}
if (validFiles.length > 0) {
@@ -114,7 +122,8 @@ function updateUI() {
files.forEach((file, index) => {
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');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
@@ -130,7 +139,8 @@ function updateUI() {
infoContainer.append(nameSpan, sizeSpan);
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.onclick = () => {
files = files.filter((_, i) => i !== index);
@@ -147,10 +157,9 @@ function updateUI() {
}
}
async function ensurePyMuPDF(): Promise<PyMuPDF> {
async function ensurePyMuPDF(): Promise<any> {
if (!pymupdf) {
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
pymupdf = await loadPyMuPDF();
}
return pymupdf;
}
@@ -173,7 +182,9 @@ async function extractText() {
const fullText = await mupdf.pdfToText(file);
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`);
hideLoader();
@@ -188,7 +199,9 @@ async function extractText() {
for (let i = 0; i < files.length; 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);
@@ -200,13 +213,21 @@ async function extractText() {
downloadFile(zipBlob, 'pdf-to-text.zip');
hideLoader();
showAlert('Success', `Extracted text from ${files.length} PDF files!`, 'success', () => {
showAlert(
'Success',
`Extracted text from ${files.length} PDF files!`,
'success',
() => {
resetState();
});
}
);
}
} catch (e: any) {
console.error('[PDFToText]', e);
hideLoader();
showAlert('Extraction Error', e.message || 'Failed to extract text from PDF.');
showAlert(
'Extraction Error',
e.message || 'Failed to extract text from PDF.'
);
}
}

View File

@@ -1,11 +1,15 @@
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 { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -25,7 +29,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
const updateUI = async () => {
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls) return;
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
@@ -33,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -49,7 +55,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
@@ -95,7 +102,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
showLoader('Loading engine...');
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
const total = state.files.length;
let completed = 0;
@@ -108,10 +115,18 @@ document.addEventListener('DOMContentLoaded', () => {
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
const jsonContent = JSON.stringify(llamaDocs, null, 2);
downloadFile(new Blob([jsonContent], { type: 'application/json' }), outName);
downloadFile(
new Blob([jsonContent], { type: 'application/json' }),
outName
);
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 {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default;
@@ -119,7 +134,9 @@ document.addEventListener('DOMContentLoaded', () => {
for (const file of state.files) {
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 outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
@@ -141,20 +158,36 @@ document.addEventListener('DOMContentLoaded', () => {
hideLoader();
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 {
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) {
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) => {
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) {
state.files = [...state.files, ...pdfFiles];
updateUI();

View File

@@ -2,18 +2,18 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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 FILETYPE_NAME = 'PSD';
let pymupdf: PyMuPDF | null = null;
let pymupdf: any = null;
async function ensurePyMuPDF(): Promise<PyMuPDF> {
async function ensurePyMuPDF(): Promise<any> {
if (!pymupdf) {
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
pymupdf = await loadPyMuPDF();
}
return pymupdf;
}
@@ -41,7 +41,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
@@ -52,7 +53,8 @@ document.addEventListener('DOMContentLoaded', () => {
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
@@ -78,7 +80,10 @@ document.addEventListener('DOMContentLoaded', () => {
const convert = async () => {
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;
}
try {
@@ -92,25 +97,38 @@ document.addEventListener('DOMContentLoaded', () => {
const baseName = file.name.replace(/\.[^/.]+$/, '');
downloadFile(pdfBlob, `${baseName}.pdf`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
showAlert(
'Conversion Complete',
`Successfully converted ${file.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple files...');
const pdfBlob = await mupdf.imagesToPdf(state.files);
downloadFile(pdfBlob, 'psd_to_pdf.pdf');
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} PSD files to a single PDF.`, 'success', () => resetState());
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PSD files to a single PDF.`,
'success',
() => resetState()
);
}
} catch (err) {
hideLoader();
const message = err instanceof Error ? err.message : 'Unknown error';
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
showAlert(
'Error',
`An error occurred during conversion. Error: ${message}`
);
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(file => {
const validFiles = Array.from(files).filter((file) => {
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
return ACCEPTED_EXTENSIONS.includes(ext);
});
@@ -122,11 +140,25 @@ document.addEventListener('DOMContentLoaded', () => {
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => handleFileSelect((e.target as HTMLInputElement).files));
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); });
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); handleFileSelect(e.dataTransfer?.files ?? null); });
fileInput.addEventListener('click', () => { fileInput.value = ''; });
fileInput.addEventListener('change', (e) =>
handleFileSelect((e.target as HTMLInputElement).files)
);
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput.click());
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);

View File

@@ -1,11 +1,15 @@
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 { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -25,7 +29,8 @@ document.addEventListener('DOMContentLoaded', () => {
}
const updateUI = async () => {
if (!fileDisplayArea || !rasterizeOptions || !processBtn || !fileControls) return;
if (!fileDisplayArea || !rasterizeOptions || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
@@ -33,7 +38,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -49,7 +55,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
@@ -94,13 +101,25 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
if (!isPyMuPDFAvailable()) {
showWasmRequiredDialog('pymupdf');
return;
}
showLoader('Loading engine...');
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
// Get options from UI
const dpi = parseInt((document.getElementById('rasterize-dpi') as HTMLSelectElement).value) || 150;
const format = (document.getElementById('rasterize-format') as HTMLSelectElement).value as 'png' | 'jpeg';
const grayscale = (document.getElementById('rasterize-grayscale') as HTMLInputElement).checked;
const dpi =
parseInt(
(document.getElementById('rasterize-dpi') as HTMLSelectElement).value
) || 150;
const format = (
document.getElementById('rasterize-format') as HTMLSelectElement
).value as 'png' | 'jpeg';
const grayscale = (
document.getElementById('rasterize-grayscale') as HTMLInputElement
).checked;
const total = state.files.length;
let completed = 0;
@@ -114,14 +133,19 @@ document.addEventListener('DOMContentLoaded', () => {
dpi,
format,
grayscale,
quality: 95
quality: 95,
});
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
downloadFile(rasterizedBlob, outName);
hideLoader();
showAlert('Rasterization Complete', `Successfully rasterized PDF at ${dpi} DPI.`, 'success', () => resetState());
showAlert(
'Rasterization Complete',
`Successfully rasterized PDF at ${dpi} DPI.`,
'success',
() => resetState()
);
} else {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default;
@@ -129,16 +153,19 @@ document.addEventListener('DOMContentLoaded', () => {
for (const file of state.files) {
try {
showLoader(`Rasterizing ${file.name} (${completed + 1}/${total})...`);
showLoader(
`Rasterizing ${file.name} (${completed + 1}/${total})...`
);
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
dpi,
format,
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);
completed++;
@@ -156,20 +183,36 @@ document.addEventListener('DOMContentLoaded', () => {
hideLoader();
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 {
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) {
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) => {
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) {
state.files = [...state.files, ...pdfFiles];
updateUI();

View File

@@ -1,14 +1,27 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { createIcons, icons } from 'lucide';
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 { 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 { PDFDocument as PDFLibDocument } from 'pdf-lib';
// @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', () => {
let visualSelectorRendered = false;
@@ -21,7 +34,9 @@ document.addEventListener('DOMContentLoaded', () => {
const backBtn = document.getElementById('back-to-tools');
// 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 visualPanel = document.getElementById('visual-select-panel');
const evenOddPanel = document.getElementById('even-odd-panel');
@@ -43,7 +58,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
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');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -60,7 +76,8 @@ document.addEventListener('DOMContentLoaded', () => {
// Add remove 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.onclick = () => {
state.files = [];
@@ -76,7 +93,9 @@ document.addEventListener('DOMContentLoaded', () => {
try {
if (!state.pdfDoc) {
showLoader('Loading PDF...');
const arrayBuffer = await readFileAsArrayBuffer(file) as ArrayBuffer;
const arrayBuffer = (await readFileAsArrayBuffer(
file
)) as ArrayBuffer;
state.pdfDoc = await PDFLibDocument.load(arrayBuffer);
hideLoader();
}
@@ -92,7 +111,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (splitOptions) splitOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
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 (state.files.length > 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);
} else {
throw new Error('No PDF document loaded');
@@ -172,11 +192,7 @@ document.addEventListener('DOMContentLoaded', () => {
};
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdf,
container,
createWrapper,
{
await renderPagesProgressively(pdf, container, createWrapper, {
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '400px',
@@ -185,9 +201,8 @@ document.addEventListener('DOMContentLoaded', () => {
},
onBatchComplete: () => {
createIcons({ icons });
}
}
);
},
});
} catch (error) {
console.error('Error rendering visual selector:', error);
showAlert('Error', 'Failed to render page previews.');
@@ -203,7 +218,9 @@ document.addEventListener('DOMContentLoaded', () => {
state.pdfDoc = null;
// 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.add('border-transparent');
});
@@ -212,14 +229,20 @@ document.addEventListener('DOMContentLoaded', () => {
if (container) container.innerHTML = '';
// Reset inputs
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement;
const pageRangeInput = document.getElementById(
'page-range'
) as HTMLInputElement;
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';
// 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) {
rangeRadio.checked = true;
rangeRadio.dispatchEvent(new Event('change'));
@@ -237,8 +260,8 @@ document.addEventListener('DOMContentLoaded', () => {
const split = async () => {
const splitMode = splitModeSelect.value;
const downloadAsZip =
(document.getElementById('download-as-zip') as HTMLInputElement)?.checked ||
false;
(document.getElementById('download-as-zip') as HTMLInputElement)
?.checked || false;
showLoader('Splitting PDF...');
@@ -250,7 +273,9 @@ document.addEventListener('DOMContentLoaded', () => {
switch (splitMode) {
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.');
const ranges = pageRangeInput.split(',');
@@ -273,7 +298,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = start; i <= end; i++) groupIndices.push(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages)
continue;
groupIndices.push(pageNum - 1);
}
@@ -296,7 +322,8 @@ document.addEventListener('DOMContentLoaded', () => {
const minPage = Math.min(...group) + 1;
const maxPage = Math.max(...group) + 1;
const filename = minPage === maxPage
const filename =
minPage === maxPage
? `page-${minPage}.pdf`
: `pages-${minPage}-${maxPage}.pdf`;
zip.file(filename, pdfBytes);
@@ -305,9 +332,14 @@ document.addEventListener('DOMContentLoaded', () => {
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'split-pages.zip');
hideLoader();
showAlert('Success', `PDF split into ${rangeGroups.length} files successfully!`, 'success', () => {
showAlert(
'Success',
`PDF split into ${rangeGroups.length} files successfully!`,
'success',
() => {
resetState();
});
}
);
return;
}
break;
@@ -316,10 +348,12 @@ document.addEventListener('DOMContentLoaded', () => {
const choiceElement = document.querySelector(
'input[name="even-odd-choice"]:checked'
) 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;
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);
}
break;
@@ -329,10 +363,15 @@ document.addEventListener('DOMContentLoaded', () => {
case 'visual':
indicesToExtract = Array.from(
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;
case 'bookmarks':
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
hideLoader();
return;
}
const { getCpdf } = await import('../utils/cpdf-helper.js');
const cpdf = await getCpdf();
const pdfBytes = await state.pdfDoc.save();
@@ -340,7 +379,9 @@ document.addEventListener('DOMContentLoaded', () => {
cpdf.startGetBookmarkInfo(pdf);
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[] = [];
for (let i = 0; i < bookmarkCount; i++) {
@@ -365,11 +406,20 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < splitPages.length; 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 pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, idx) => startPage + idx);
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
const pageIndices = Array.from(
{ length: endPage - startPage + 1 },
(_, idx) => startPage + idx
);
const copiedPages = await newPdf.copyPages(
state.pdfDoc,
pageIndices
);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const pdfBytes2 = await newPdf.save();
zip.file(`split-${i + 1}.pdf`, pdfBytes2);
@@ -384,7 +434,10 @@ document.addEventListener('DOMContentLoaded', () => {
return;
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.');
const zip2 = new JSZip();
@@ -393,10 +446,16 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < numSplits; i++) {
const startPage = i * nValue;
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 copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
const copiedPages = await newPdf.copyPages(
state.pdfDoc,
pageIndices
);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const pdfBytes3 = await newPdf.save();
zip2.file(`split-${i + 1}.pdf`, pdfBytes3);
@@ -412,7 +471,11 @@ document.addEventListener('DOMContentLoaded', () => {
}
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.');
}
@@ -455,7 +518,6 @@ document.addEventListener('DOMContentLoaded', () => {
showAlert('Success', 'PDF split successfully!', 'success', () => {
resetState();
});
} catch (e: any) {
console.error(e);
showAlert(
@@ -495,7 +557,11 @@ document.addEventListener('DOMContentLoaded', () => {
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.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) {
// Take only the first PDF
const dataTransfer = new DataTransfer();
@@ -551,7 +617,10 @@ document.addEventListener('DOMContentLoaded', () => {
const updateWarning = () => {
if (!state.pdfDoc) return;
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;
if (remainder !== 0 && nTimesWarning) {
nTimesWarning.classList.remove('hidden');
@@ -565,7 +634,9 @@ document.addEventListener('DOMContentLoaded', () => {
};
updateWarning();
document.getElementById('split-n-value')?.addEventListener('input', updateWarning);
document
.getElementById('split-n-value')
?.addEventListener('input', updateWarning);
}
});
}

View File

@@ -1,8 +1,14 @@
import { downloadFile, formatBytes } from "../utils/helpers";
import { initializeGlobalShortcuts } from "../utils/shortcuts-init.js";
import { downloadFile, formatBytes } from '../utils/helpers';
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/table-of-contents.worker.js');
const worker = new Worker(
import.meta.env.BASE_URL + 'workers/table-of-contents.worker.js'
);
let pdfFile: File | null = null;
@@ -55,7 +61,8 @@ function showStatus(
type: 'success' | 'error' | 'info' = 'info'
) {
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'
: type === 'error'
? 'bg-red-900 text-red-200'
@@ -130,6 +137,12 @@ async function generateTableOfContents() {
return;
}
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
return;
}
try {
generateBtn.disabled = true;
showStatus('Reading file (Main Thread)...', 'info');
@@ -143,13 +156,14 @@ async function generateTableOfContents() {
const fontFamily = parseInt(fontFamilySelect.value, 10);
const addBookmark = addBookmarkCheckbox.checked;
const message: GenerateTOCMessage = {
const message = {
command: 'generate-toc',
pdfData: arrayBuffer,
title,
fontSize,
fontFamily,
addBookmark,
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
};
worker.postMessage(message, [arrayBuffer]);
@@ -171,7 +185,10 @@ worker.onmessage = (e: MessageEvent<TOCWorkerResponse>) => {
const pdfBytes = new Uint8Array(pdfBytesBuffer);
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(
'Table of contents generated successfully! Download started.',

View File

@@ -1,14 +1,16 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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 currentMode: 'upload' | 'text' = 'upload';
// 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 {
return RTL_PATTERN.test(text);
@@ -29,7 +31,8 @@ const updateUI = () => {
files.forEach((file, index) => {
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');
infoSpan.className = 'truncate font-medium text-gray-200';
@@ -60,17 +63,28 @@ const updateUI = () => {
const resetState = () => {
files = [];
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 (textInput) textInput.value = '';
updateUI();
};
async function convert() {
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement).value) || 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';
const fontSize =
parseInt(
(document.getElementById('font-size') as HTMLInputElement).value
) || 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) {
showAlert('No Files', 'Please select at least one text file.');
@@ -78,7 +92,9 @@ async function convert() {
}
if (currentMode === 'text') {
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
const textInput = document.getElementById(
'text-input'
) as HTMLTextAreaElement;
if (!textInput.value.trim()) {
showAlert('No Text', 'Please enter some text to convert.');
return;
@@ -88,8 +104,7 @@ async function convert() {
showLoader('Loading engine...');
try {
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
let textContent = '';
@@ -99,7 +114,9 @@ async function convert() {
textContent += text + '\n\n';
}
} else {
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
const textInput = document.getElementById(
'text-input'
) as HTMLTextAreaElement;
textContent = textInput.value;
}
@@ -110,14 +127,19 @@ async function convert() {
pageSize: pageSizeKey as 'a4' | 'letter' | 'legal' | 'a3' | 'a5',
fontName: fontName as 'helv' | 'tiro' | 'cour' | 'times',
textColor,
margins: 72
margins: 72,
});
downloadFile(pdfBlob, 'text_to_pdf.pdf');
showAlert('Success', 'Text converted to PDF successfully!', 'success', () => {
showAlert(
'Success',
'Text converted to PDF successfully!',
'success',
() => {
resetState();
});
}
);
} catch (e: any) {
console.error('[TxtToPDF] Error:', e);
showAlert('Error', `Failed to convert text to PDF. ${e.message || ''}`);
@@ -149,7 +171,9 @@ document.addEventListener('DOMContentLoaded', () => {
const textModeBtn = document.getElementById('txt-mode-text-btn');
const uploadPanel = document.getElementById('txt-upload-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
if (backBtn) {
@@ -192,11 +216,15 @@ document.addEventListener('DOMContentLoaded', () => {
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.name.toLowerCase().endsWith('.txt') || file.type === 'text/plain'
(file) =>
file.name.toLowerCase().endsWith('.txt') || file.type === 'text/plain'
);
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped. Only text files are allowed.');
showAlert(
'Invalid Files',
'Some files were skipped. Only text files are allowed.'
);
}
if (validFiles.length > 0) {

View 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'
);
});
}

View File

@@ -2,8 +2,9 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, 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 EXTENSIONS = ['.xps', '.oxps'];
@@ -34,7 +35,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
@@ -50,7 +52,8 @@ document.addEventListener('DOMContentLoaded', () => {
infoContainer.append(nameSpan, metaSpan);
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.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
@@ -87,14 +90,15 @@ document.addEventListener('DOMContentLoaded', () => {
}
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
const pymupdf = await loadPyMuPDF();
if (state.files.length === 1) {
const originalFile = state.files[0];
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';
downloadFile(pdfBlob, fileName);
@@ -113,9 +117,13 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < state.files.length; 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 pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
@@ -136,7 +144,10 @@ document.addEventListener('DOMContentLoaded', () => {
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
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');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}

View File

@@ -1,15 +1,35 @@
import { WasmProvider } from './wasm-provider';
let cpdfLoaded = false;
let cpdfLoadPromise: Promise<void> | null = null;
//TODO: @ALAM,is it better to use a worker to load the cpdf library?
// or just use the browser version?
export async function ensureCpdfLoaded(): Promise<void> {
function getCpdfUrl(): string | undefined {
const userUrl = WasmProvider.getUrl('cpdf');
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 (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) => {
if (typeof (window as any).coherentpdf !== 'undefined') {
cpdfLoaded = true;
@@ -18,13 +38,14 @@ export async function ensureCpdfLoaded(): Promise<void> {
}
const script = document.createElement('script');
script.src = import.meta.env.BASE_URL + 'coherentpdf.browser.min.js';
script.src = cpdfUrl;
script.onload = () => {
cpdfLoaded = true;
console.log('[CPDF] Loaded from:', script.src);
resolve();
};
script.onerror = () => {
reject(new Error('Failed to load CoherentPDF library'));
reject(new Error('Failed to load CoherentPDF library from: ' + cpdfUrl));
};
document.head.appendChild(script);
});
@@ -32,11 +53,7 @@ export async function ensureCpdfLoaded(): Promise<void> {
return cpdfLoadPromise;
}
/**
* Gets the cpdf instance, ensuring it's loaded first
*/
export async function getCpdf(): Promise<any> {
await ensureCpdfLoaded();
await isCpdfLoaded();
return (window as any).coherentpdf;
}

View 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;
}

View File

@@ -1,10 +1,14 @@
/**
* 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 { getWasmBaseUrl, fetchWasmFile } from '../config/wasm-cdn-config.js';
import {
getWasmBaseUrl,
fetchWasmFile,
isWasmAvailable,
} from '../config/wasm-cdn-config.js';
import { PDFDocument, PDFDict, PDFName, PDFArray } from 'pdf-lib';
interface GhostscriptModule {
@@ -34,6 +38,12 @@ export async function convertToPdfA(
level: PdfALevel = 'PDF/A-2b',
onProgress?: (msg: string) => void
): Promise<Uint8Array> {
if (!isWasmAvailable('ghostscript')) {
throw new Error(
'Ghostscript is not configured. Please configure it in WASM Settings.'
);
}
onProgress?.('Loading Ghostscript...');
let gs: GhostscriptModule;
@@ -41,11 +51,16 @@ export async function convertToPdfA(
if (cachedGsModule) {
gs = cachedGsModule;
} 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({
baseUrl: `${gsBaseUrl}assets/`,
locateFile: (path: string) => {
if (path.endsWith('.wasm')) {
return gsBaseUrl + 'gs.wasm';
return gsBaseUrl + 'assets/gs.wasm';
}
return path;
},
@@ -73,11 +88,12 @@ export async function convertToPdfA(
try {
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) {
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,
onProgress?: (msg: string) => void
): Promise<Uint8Array> {
if (!isWasmAvailable('ghostscript')) {
throw new Error(
'Ghostscript is not configured. Please configure it in WASM Settings.'
);
}
onProgress?.('Loading Ghostscript...');
let gs: GhostscriptModule;
@@ -369,11 +391,16 @@ export async function convertFontsToOutlines(
if (cachedGsModule) {
gs = cachedGsModule;
} 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({
baseUrl: `${gsBaseUrl}assets/`,
locateFile: (path: string) => {
if (path.endsWith('.wasm')) {
return gsBaseUrl + 'gs.wasm';
return gsBaseUrl + 'assets/gs.wasm';
}
return path;
},

View 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;
}

View File

@@ -1,14 +1,12 @@
import { getLibreOfficeConverter } from './libreoffice-loader.js';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import loadGsWASM from '@bentopdf/gs-wasm';
import { setCachedGsModule } from './ghostscript-loader.js';
import { getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
export enum PreloadStatus {
IDLE = 'idle',
LOADING = 'loading',
READY = 'ready',
ERROR = 'error'
ERROR = 'error',
UNAVAILABLE = 'unavailable',
}
interface PreloadState {
@@ -20,45 +18,39 @@ interface PreloadState {
const preloadState: PreloadState = {
libreoffice: PreloadStatus.IDLE,
pymupdf: PreloadStatus.IDLE,
ghostscript: PreloadStatus.IDLE
ghostscript: PreloadStatus.IDLE,
};
let pymupdfInstance: PyMuPDF | null = null;
export function getPreloadStatus(): Readonly<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> {
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;
console.log('[Preloader] Starting PyMuPDF preload...');
try {
const pymupdfBaseUrl = getWasmBaseUrl('pymupdf');
pymupdfInstance = new PyMuPDF(pymupdfBaseUrl);
const pymupdfBaseUrl = getWasmBaseUrl('pymupdf')!;
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();
preloadState.pymupdf = PreloadStatus.READY;
console.log('[Preloader] PyMuPDF ready');
@@ -71,20 +63,43 @@ async function preloadPyMuPDF(): Promise<void> {
async function preloadGhostscript(): Promise<void> {
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;
console.log('[Preloader] Starting Ghostscript WASM preload...');
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({
baseUrl: `${normalizedUrl}assets/`,
locateFile: (path: string) => {
if (path.endsWith('.wasm')) {
return gsBaseUrl + 'gs.wasm';
return `${normalizedUrl}assets/gs.wasm`;
}
return path;
},
print: () => { },
printErr: () => { },
print: () => {},
printErr: () => {},
});
setCachedGsModule(gsModule as any);
preloadState.ghostscript = PreloadStatus.READY;
@@ -107,16 +122,29 @@ export function startBackgroundPreload(): void {
console.log('[Preloader] Scheduling background WASM preloads...');
const libreOfficePages = [
'word-to-pdf', 'excel-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'
'word-to-pdf',
'excel-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 isLibreOfficePage = libreOfficePages.some(page => currentPath.includes(page));
const isLibreOfficePage = libreOfficePages.some((page) =>
currentPath.includes(page)
);
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;
}
@@ -126,7 +154,6 @@ export function startBackgroundPreload(): void {
await preloadPyMuPDF();
await preloadGhostscript();
console.log('[Preloader] Sequential preloads complete (LibreOffice skipped - loaded on demand)');
console.log('[Preloader] Sequential preloads complete');
});
}

View 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;
}

View File

@@ -191,6 +191,29 @@
</select>
</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">
Convert to PDF/A
</button>

View 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>

View File

@@ -81,6 +81,11 @@
>Privacy Policy</a
>
</li>
<li>
<a href="wasm-settings.html" class="hover:text-indigo-400"
>Advanced Settings</a
>
</li>
</ul>
</div>

View File

@@ -274,34 +274,6 @@ export default defineConfig(() => {
}
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',
dest: 'embedpdf',