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

@@ -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 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;
}
export type WasmPackage = 'ghostscript' | 'pymupdf';
export function getWasmBaseUrl(packageName: WasmPackage): string {
if (USE_CDN) {
return CDN_URLS[packageName];
}
return LOCAL_PATHS[packageName];
console.warn(
`[WASM Config] No URL configured for ${packageName}. Feature unavailable.`
);
return undefined;
}
export function getWasmFallbackUrl(packageName: WasmPackage): string {
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
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);
if (!response.ok) {
throw new Error(`Failed to fetch ${fileName}: HTTP ${response.status}`);
}
return response;
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,
};
return {
packageVersions: PACKAGE_VERSIONS,
configuredProviders: WasmProvider.getAllProviders(),
};
}

View File

@@ -1,9 +1,4 @@
export const PACKAGE_VERSIONS = {
ghostscript: '0.1.0',
pymupdf: '0.1.9',
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,349 +3,399 @@ 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,
pdfDoc: null,
attachments: [],
file: null,
pdfDoc: null,
attachments: [],
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.attachments = [];
pageState.file = null;
pageState.pdfDoc = null;
pageState.attachments = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const attachmentFileList = document.getElementById('attachment-file-list');
if (attachmentFileList) attachmentFileList.innerHTML = '';
const attachmentFileList = document.getElementById('attachment-file-list');
if (attachmentFileList) attachmentFileList.innerHTML = '';
const attachmentInput = document.getElementById('attachment-files-input') as HTMLInputElement;
if (attachmentInput) attachmentInput.value = '';
const attachmentInput = document.getElementById(
'attachment-files-input'
) as HTMLInputElement;
if (attachmentInput) attachmentInput.value = '';
const attachmentLevelOptions = document.getElementById('attachment-level-options');
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
const attachmentLevelOptions = document.getElementById(
'attachment-level-options'
);
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
if (pageRangeWrapper) pageRangeWrapper.classList.add('hidden');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
if (pageRangeWrapper) pageRangeWrapper.classList.add('hidden');
const processBtn = document.getElementById('process-btn');
if (processBtn) processBtn.classList.add('hidden');
const processBtn = document.getElementById('process-btn');
if (processBtn) processBtn.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const documentRadio = document.querySelector('input[name="attachment-level"][value="document"]') as HTMLInputElement;
if (documentRadio) documentRadio.checked = true;
const documentRadio = document.querySelector(
'input[name="attachment-level"][value="document"]'
) as HTMLInputElement;
if (documentRadio) documentRadio.checked = true;
}
worker.onmessage = function (e) {
const data = e.data;
const data = e.data;
if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`${originalName}_with_attachments.pdf`
);
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 () {
resetState();
});
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
}
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.');
}
};
worker.onerror = function (error) {
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
};
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false,
});
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
const totalPagesSpan = document.getElementById('attachment-total-pages');
if (totalPagesSpan) totalPagesSpan.textContent = pageCount.toString();
const totalPagesSpan = document.getElementById('attachment-total-pages');
if (totalPagesSpan) totalPagesSpan.textContent = pageCount.toString();
hideLoader();
hideLoader();
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function updateAttachmentList() {
const attachmentFileList = document.getElementById('attachment-file-list');
const attachmentLevelOptions = document.getElementById('attachment-level-options');
const processBtn = document.getElementById('process-btn');
const attachmentFileList = document.getElementById('attachment-file-list');
const attachmentLevelOptions = document.getElementById(
'attachment-level-options'
);
const processBtn = document.getElementById('process-btn');
if (!attachmentFileList) return;
if (!attachmentFileList) return;
attachmentFileList.innerHTML = '';
attachmentFileList.innerHTML = '';
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';
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';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate text-sm';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate text-sm';
nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'text-xs text-gray-400';
sizeSpan.textContent = formatBytes(file.size);
const sizeSpan = document.createElement('span');
sizeSpan.className = 'text-xs text-gray-400';
sizeSpan.textContent = formatBytes(file.size);
div.append(nameSpan, sizeSpan);
attachmentFileList.appendChild(div);
});
div.append(nameSpan, sizeSpan);
attachmentFileList.appendChild(div);
});
if (pageState.attachments.length > 0) {
if (attachmentLevelOptions) attachmentLevelOptions.classList.remove('hidden');
if (processBtn) processBtn.classList.remove('hidden');
} else {
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
if (processBtn) processBtn.classList.add('hidden');
}
if (pageState.attachments.length > 0) {
if (attachmentLevelOptions)
attachmentLevelOptions.classList.remove('hidden');
if (processBtn) processBtn.classList.remove('hidden');
} else {
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
if (processBtn) processBtn.classList.add('hidden');
}
}
async function addAttachments() {
if (!pageState.file || !pageState.pdfDoc) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
if (!pageState.file || !pageState.pdfDoc) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
if (pageState.attachments.length === 0) {
showAlert('No Files', 'Please select at least one file to attach.');
return;
}
if (pageState.attachments.length === 0) {
showAlert('No Files', 'Please select at least one file to attach.');
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 = '';
let pageRange: string = '';
if (attachmentLevel === 'page') {
const pageRangeInput = document.getElementById('attachment-page-range') as HTMLInputElement;
pageRange = pageRangeInput?.value?.trim() || '';
if (attachmentLevel === 'page') {
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.');
return;
}
if (!pageRange) {
showAlert(
'Error',
'Please specify a page range for page-level attachments.'
);
return;
}
}
showLoader('Embedding files into PDF...');
try {
const pdfBuffer = await pageState.file.arrayBuffer();
const attachmentBuffers: ArrayBuffer[] = [];
const attachmentNames: string[] = [];
for (let i = 0; i < pageState.attachments.length; i++) {
const file = pageState.attachments[i];
showLoader(
`Reading ${file.name} (${i + 1}/${pageState.attachments.length})...`
);
const fileBuffer = await file.arrayBuffer();
attachmentBuffers.push(fileBuffer);
attachmentNames.push(file.name);
}
showLoader('Embedding files into PDF...');
showLoader('Attaching files to PDF...');
try {
const pdfBuffer = await pageState.file.arrayBuffer();
const message = {
command: 'add-attachments',
pdfBuffer: pdfBuffer,
attachmentBuffers: attachmentBuffers,
attachmentNames: attachmentNames,
attachmentLevel: attachmentLevel,
pageRange: pageRange,
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
};
const attachmentBuffers: ArrayBuffer[] = [];
const attachmentNames: string[] = [];
for (let i = 0; i < pageState.attachments.length; i++) {
const file = pageState.attachments[i];
showLoader(`Reading ${file.name} (${i + 1}/${pageState.attachments.length})...`);
const fileBuffer = await file.arrayBuffer();
attachmentBuffers.push(fileBuffer);
attachmentNames.push(file.name);
}
showLoader('Attaching files to PDF...');
const message = {
command: 'add-attachments',
pdfBuffer: pdfBuffer,
attachmentBuffers: attachmentBuffers,
attachmentNames: attachmentNames,
attachmentLevel: attachmentLevel,
pageRange: pageRange
};
const transferables = [pdfBuffer, ...attachmentBuffers];
worker.postMessage(message, transferables);
} catch (error: any) {
console.error('Error attaching files:', error);
hideLoader();
showAlert('Error', `Failed to attach files: ${error.message}`);
}
const transferables = [pdfBuffer, ...attachmentBuffers];
worker.postMessage(message, transferables);
} catch (error: any) {
console.error('Error attaching files:', error);
hideLoader();
showAlert('Error', `Failed to attach files: ${error.message}`);
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
function handleAttachmentSelect(files: FileList | null) {
if (files && files.length > 0) {
pageState.attachments = Array.from(files);
updateAttachmentList();
}
if (files && files.length > 0) {
pageState.attachments = Array.from(files);
updateAttachmentList();
}
}
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 attachmentDropZone = document.getElementById('attachment-drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
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');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (attachmentInput && attachmentDropZone) {
attachmentInput.addEventListener('change', function (e) {
handleAttachmentSelect((e.target as HTMLInputElement).files);
});
attachmentDropZone.addEventListener('dragover', function (e) {
e.preventDefault();
attachmentDropZone.classList.add('bg-gray-700');
});
attachmentDropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
attachmentDropZone.classList.remove('bg-gray-700');
});
attachmentDropZone.addEventListener('drop', function (e) {
e.preventDefault();
attachmentDropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files) {
handleAttachmentSelect(files);
}
});
attachmentInput.addEventListener('click', function () {
attachmentInput.value = '';
});
}
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
attachmentLevelRadios.forEach(function (radio) {
radio.addEventListener('change', function (e) {
const value = (e.target as HTMLInputElement).value;
if (value === 'page' && pageRangeWrapper) {
pageRangeWrapper.classList.remove('hidden');
} else if (pageRangeWrapper) {
pageRangeWrapper.classList.add('hidden');
}
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (processBtn) {
processBtn.addEventListener('click', addAttachments);
}
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (attachmentInput && attachmentDropZone) {
attachmentInput.addEventListener('change', function (e) {
handleAttachmentSelect((e.target as HTMLInputElement).files);
});
attachmentDropZone.addEventListener('dragover', function (e) {
e.preventDefault();
attachmentDropZone.classList.add('bg-gray-700');
});
attachmentDropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
attachmentDropZone.classList.remove('bg-gray-700');
});
attachmentDropZone.addEventListener('drop', function (e) {
e.preventDefault();
attachmentDropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files) {
handleAttachmentSelect(files);
}
});
attachmentInput.addEventListener('click', function () {
attachmentInput.value = '';
});
}
const attachmentLevelRadios = document.querySelectorAll(
'input[name="attachment-level"]'
);
attachmentLevelRadios.forEach(function (radio) {
radio.addEventListener('change', function (e) {
const value = (e.target as HTMLInputElement).value;
if (value === 'page' && pageRangeWrapper) {
pageRangeWrapper.classList.remove('hidden');
} else if (pageRangeWrapper) {
pageRangeWrapper.classList.add('hidden');
}
});
});
if (processBtn) {
processBtn.addEventListener('click', addAttachments);
}
});

View File

@@ -3,245 +3,278 @@ 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: [],
pdfBytes: new Map(),
pdfDocs: new Map(),
files: [],
pdfBytes: new Map(),
pdfDocs: new Map(),
};
const alternateMergeWorker = new Worker(import.meta.env.BASE_URL + 'workers/alternate-merge.worker.js');
const alternateMergeWorker = new Worker(
import.meta.env.BASE_URL + 'workers/alternate-merge.worker.js'
);
function resetState() {
pageState.files = [];
pageState.pdfBytes.clear();
pageState.pdfDocs.clear();
pageState.files = [];
pageState.pdfBytes.clear();
pageState.pdfDocs.clear();
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileList = document.getElementById('file-list');
if (fileList) fileList.innerHTML = '';
const fileList = document.getElementById('file-list');
if (fileList) fileList.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileList = document.getElementById('file-list');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileList = document.getElementById('file-list');
if (!fileDisplayArea || !fileList) return;
if (!fileDisplayArea || !fileList) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
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';
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';
const infoSpan = document.createElement('span');
infoSpan.className = 'text-gray-200';
infoSpan.textContent = `${pageState.files.length} PDF files selected`;
const infoSpan = document.createElement('span');
infoSpan.className = 'text-gray-200';
infoSpan.textContent = `${pageState.files.length} PDF files selected`;
const clearBtn = document.createElement('button');
clearBtn.className = 'text-red-400 hover:text-red-300';
clearBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
clearBtn.onclick = function () {
resetState();
};
const clearBtn = document.createElement('button');
clearBtn.className = 'text-red-400 hover:text-red-300';
clearBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
clearBtn.onclick = function () {
resetState();
};
summaryDiv.append(infoSpan, clearBtn);
fileDisplayArea.appendChild(summaryDiv);
createIcons({ icons });
summaryDiv.append(infoSpan, clearBtn);
fileDisplayArea.appendChild(summaryDiv);
createIcons({ icons });
// Load PDFs and populate list
showLoader('Loading PDF files...');
fileList.innerHTML = '';
// Load PDFs and populate list
showLoader('Loading PDF files...');
fileList.innerHTML = '';
try {
for (const file of pageState.files) {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfBytes.set(file.name, arrayBuffer);
try {
for (const file of pageState.files) {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfBytes.set(file.name, arrayBuffer);
const bytesForPdfJs = arrayBuffer.slice(0);
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
pageState.pdfDocs.set(file.name, pdfjsDoc);
const pageCount = pdfjsDoc.numPages;
const bytesForPdfJs = arrayBuffer.slice(0);
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
pageState.pdfDocs.set(file.name, pdfjsDoc);
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.dataset.fileName = file.name;
const li = document.createElement('li');
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');
infoDiv.className = 'flex items-center gap-2 truncate flex-1';
const infoDiv = document.createElement('div');
infoDiv.className = 'flex items-center gap-2 truncate flex-1';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('span');
metaSpan.className = 'text-sm text-gray-400 flex-shrink-0';
metaSpan.textContent = `${formatBytes(file.size)}${pageCount} pages`;
const metaSpan = document.createElement('span');
metaSpan.className = 'text-sm text-gray-400 flex-shrink-0';
metaSpan.textContent = `${formatBytes(file.size)}${pageCount} pages`;
infoDiv.append(nameSpan, metaSpan);
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.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>`;
const dragHandle = document.createElement('div');
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);
fileList.appendChild(li);
}
li.append(infoDiv, dragHandle);
fileList.appendChild(li);
}
Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
});
Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
});
hideLoader();
hideLoader();
if (toolOptions && pageState.files.length >= 2) {
toolOptions.classList.remove('hidden');
}
} catch (error) {
console.error('Error loading PDFs:', error);
hideLoader();
showAlert('Error', 'Failed to load one or more PDF files.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (toolOptions && pageState.files.length >= 2) {
toolOptions.classList.remove('hidden');
}
} catch (error) {
console.error('Error loading PDFs:', error);
hideLoader();
showAlert('Error', 'Failed to load one or more PDF files.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function mixPages() {
if (pageState.pdfBytes.size < 2) {
showAlert('Not Enough Files', 'Please upload at least two PDF files to alternate and mix.');
return;
if (pageState.pdfBytes.size < 2) {
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;
}
showLoader('Alternating and mixing pages...');
try {
const fileList = document.getElementById('file-list');
if (!fileList) throw new Error('File list not found');
const sortedFileNames = Array.from(fileList.children)
.map(function (li) {
return (li as HTMLElement).dataset.fileName;
})
.filter(Boolean) as string[];
interface InterleaveFile {
name: string;
data: ArrayBuffer;
}
showLoader('Alternating and mixing pages...');
try {
const fileList = document.getElementById('file-list');
if (!fileList) throw new Error('File list not found');
const sortedFileNames = Array.from(fileList.children).map(function (li) {
return (li as HTMLElement).dataset.fileName;
}).filter(Boolean) as string[];
interface InterleaveFile {
name: string;
data: ArrayBuffer;
}
const filesToMerge: InterleaveFile[] = [];
for (const name of sortedFileNames) {
const bytes = pageState.pdfBytes.get(name);
if (bytes) {
filesToMerge.push({ name, data: bytes });
}
}
if (filesToMerge.length < 2) {
showAlert('Error', 'At least two valid PDFs are required.');
hideLoader();
return;
}
const message = {
command: 'interleave',
files: filesToMerge
};
alternateMergeWorker.postMessage(message, filesToMerge.map(function (f) { return f.data; }));
alternateMergeWorker.onmessage = function (e: MessageEvent) {
hideLoader();
if (e.data.status === 'success') {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
downloadFile(blob, 'alternated-mixed.pdf');
showAlert('Success', 'PDFs have been mixed successfully!', 'success', function () {
resetState();
});
} else {
console.error('Worker interleave error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
}
};
alternateMergeWorker.onerror = function (e) {
hideLoader();
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) {
console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.');
hideLoader();
const filesToMerge: InterleaveFile[] = [];
for (const name of sortedFileNames) {
const bytes = pageState.pdfBytes.get(name);
if (bytes) {
filesToMerge.push({ name, data: bytes });
}
}
if (filesToMerge.length < 2) {
showAlert('Error', 'At least two valid PDFs are required.');
hideLoader();
return;
}
const message = {
command: 'interleave',
files: filesToMerge,
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
};
alternateMergeWorker.postMessage(
message,
filesToMerge.map(function (f) {
return f.data;
})
);
alternateMergeWorker.onmessage = function (e: MessageEvent) {
hideLoader();
if (e.data.status === 'success') {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
downloadFile(blob, 'alternated-mixed.pdf');
showAlert(
'Success',
'PDFs have been mixed successfully!',
'success',
function () {
resetState();
}
);
} else {
console.error('Worker interleave error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
}
};
alternateMergeWorker.onerror = function (e) {
hideLoader();
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) {
console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.');
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
pageState.files = pdfFiles;
updateUI();
}
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
});
if (pdfFiles.length > 0) {
pageState.files = pdfFiles;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', mixPages);
}
if (processBtn) {
processBtn.addEventListener('click', mixPages);
}
});

View File

@@ -2,336 +2,385 @@ 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],
gif: [0x47, 0x49, 0x46],
bmp: [0x42, 0x4D],
webp: [0x52, 0x49, 0x46, 0x46],
avif: [0x00, 0x00, 0x00],
jpeg: [0xff, 0xd8, 0xff],
png: [0x89, 0x50, 0x4e, 0x47],
gif: [0x47, 0x49, 0x46],
bmp: [0x42, 0x4d],
webp: [0x52, 0x49, 0x46, 0x46],
avif: [0x00, 0x00, 0x00],
};
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 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' {
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) {
return 'webp';
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
) {
return 'webp';
}
if (
data[4] === 0x66 &&
data[5] === 0x74 &&
data[6] === 0x79 &&
data[7] === 0x70
) {
const brand = String.fromCharCode(data[8], data[9], data[10], data[11]);
if (
brand === 'avif' ||
brand === 'avis' ||
brand === 'mif1' ||
brand === 'miaf'
) {
return 'avif';
}
if (data[4] === 0x66 && data[5] === 0x74 && data[6] === 0x79 && data[7] === 0x70) {
const brand = String.fromCharCode(data[8], data[9], data[10], data[11]);
if (brand === 'avif' || brand === 'avis' || brand === 'mif1' || brand === 'miaf') {
return 'avif';
}
}
return 'unknown';
}
return 'unknown';
}
function isCbzFile(filename: string): boolean {
return filename.toLowerCase().endsWith('.cbz');
return filename.toLowerCase().endsWith('.cbz');
}
async function convertImageToPng(imageData: ArrayBuffer, filename: string): Promise<Blob> {
return new Promise((resolve, reject) => {
const blob = new Blob([imageData]);
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(url);
reject(new Error('Failed to get canvas context'));
return;
}
ctx.drawImage(img, 0, 0);
canvas.toBlob((pngBlob) => {
URL.revokeObjectURL(url);
if (pngBlob) {
resolve(pngBlob);
} else {
reject(new Error(`Failed to convert ${filename} to PNG`));
}
}, 'image/png');
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error(`Failed to load image: ${filename}`));
};
img.src = url;
});
async function convertImageToPng(
imageData: ArrayBuffer,
filename: string
): Promise<Blob> {
return new Promise((resolve, reject) => {
const blob = new Blob([imageData]);
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(url);
reject(new Error('Failed to get canvas context'));
return;
}
ctx.drawImage(img, 0, 0);
canvas.toBlob((pngBlob) => {
URL.revokeObjectURL(url);
if (pngBlob) {
resolve(pngBlob);
} else {
reject(new Error(`Failed to convert ${filename} to PNG`));
}
}, 'image/png');
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error(`Failed to load image: ${filename}`));
};
img.src = url;
});
}
async function convertCbzToPdf(file: File): Promise<Blob> {
const zip = await JSZip.loadAsync(file);
const pdfDoc = await PDFDocument.create();
const zip = await JSZip.loadAsync(file);
const pdfDoc = await PDFDocument.create();
const imageFiles = Object.keys(zip.files)
.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' }));
const imageFiles = Object.keys(zip.files)
.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' })
);
for (const filename of imageFiles) {
const zipEntry = zip.files[filename];
const imageData = await zipEntry.async('arraybuffer');
const dataArray = new Uint8Array(imageData);
const actualFormat = detectImageFormat(dataArray);
for (const filename of imageFiles) {
const zipEntry = zip.files[filename];
const imageData = await zipEntry.async('arraybuffer');
const dataArray = new Uint8Array(imageData);
const actualFormat = detectImageFormat(dataArray);
let imageBytes: Uint8Array;
let embedMethod: 'png' | 'jpg';
let imageBytes: Uint8Array;
let embedMethod: 'png' | 'jpg';
if (actualFormat === 'jpeg') {
imageBytes = dataArray;
embedMethod = 'jpg';
} else if (actualFormat === 'png') {
imageBytes = dataArray;
embedMethod = 'png';
} else {
const pngBlob = await convertImageToPng(imageData, filename);
imageBytes = new Uint8Array(await pngBlob.arrayBuffer());
embedMethod = 'png';
}
const image = embedMethod === 'png'
? await pdfDoc.embedPng(imageBytes)
: await pdfDoc.embedJpg(imageBytes);
const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
if (actualFormat === 'jpeg') {
imageBytes = dataArray;
embedMethod = 'jpg';
} else if (actualFormat === 'png') {
imageBytes = dataArray;
embedMethod = 'png';
} else {
const pngBlob = await convertImageToPng(imageData, filename);
imageBytes = new Uint8Array(await pngBlob.arrayBuffer());
embedMethod = 'png';
}
const pdfBytes = await pdfDoc.save();
return new Blob([pdfBytes.buffer as ArrayBuffer], { type: 'application/pdf' });
const image =
embedMethod === 'png'
? await pdfDoc.embedPng(imageBytes)
: await pdfDoc.embedJpg(imageBytes);
const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
}
const pdfBytes = await pdfDoc.save();
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', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
for (let index = 0; index < state.files.length; index++) {
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';
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
let pdfBlob: Blob;
if (isCbzFile(originalFile.name)) {
pdfBlob = await convertCbzToPdf(originalFile);
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
pdfBlob = await convertCbrToPdf(originalFile);
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
downloadFile(pdfBlob, fileName);
hideLoader();
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const outputZip = new JSZip();
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
);
let pdfBlob: Blob;
if (isCbzFile(originalFile.name)) {
pdfBlob = await convertCbzToPdf(originalFile);
} else {
pdfBlob = await convertCbrToPdf(originalFile);
}
let pdfBlob: Blob;
if (isCbzFile(file.name)) {
pdfBlob = await convertCbzToPdf(file);
} else {
pdfBlob = await convertCbrToPdf(file);
}
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
downloadFile(pdfBlob, fileName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const outputZip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
let pdfBlob: Blob;
if (isCbzFile(file.name)) {
pdfBlob = await convertCbzToPdf(file);
} else {
pdfBlob = await convertCbrToPdf(file);
}
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
outputZip.file(`${baseName}.pdf`, pdfBuffer);
}
const zipBlob = await outputZip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'comic-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
outputZip.file(`${baseName}.pdf`, pdfBuffer);
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
const zipBlob = await outputZip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'comic-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
};
}
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});

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,352 +2,396 @@ 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,
allAttachments: [],
attachmentsToRemove: new Set(),
file: null,
allAttachments: [],
attachmentsToRemove: new Set(),
};
function resetState() {
pageState.file = null;
pageState.allAttachments = [];
pageState.attachmentsToRemove.clear();
pageState.file = null;
pageState.allAttachments = [];
pageState.attachmentsToRemove.clear();
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const attachmentsList = document.getElementById('attachments-list');
if (attachmentsList) attachmentsList.innerHTML = '';
const attachmentsList = document.getElementById('attachments-list');
if (attachmentsList) attachmentsList.innerHTML = '';
const processBtn = document.getElementById('process-btn');
if (processBtn) processBtn.classList.add('hidden');
const processBtn = document.getElementById('process-btn');
if (processBtn) processBtn.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
worker.onmessage = function (e) {
const data = e.data;
const data = e.data;
if (data.status === 'success' && data.attachments !== undefined) {
pageState.allAttachments = data.attachments.map(function (att: any) {
return {
...att,
data: new Uint8Array(att.data)
};
});
if (data.status === 'success' && data.attachments !== undefined) {
pageState.allAttachments = data.attachments.map(function (att: any) {
return {
...att,
data: new Uint8Array(att.data),
};
});
displayAttachments(data.attachments);
hideLoader();
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
displayAttachments(data.attachments);
hideLoader();
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`${originalName}_edited.pdf`
);
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 () {
resetState();
});
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
}
showAlert(
'Success',
'Attachments updated successfully!',
'success',
function () {
resetState();
}
);
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
}
};
worker.onerror = function (error) {
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
};
function displayAttachments(attachments: AttachmentInfo[]) {
const attachmentsList = document.getElementById('attachments-list');
const processBtn = document.getElementById('process-btn');
const attachmentsList = document.getElementById('attachments-list');
const processBtn = document.getElementById('process-btn');
if (!attachmentsList) return;
if (!attachmentsList) return;
attachmentsList.innerHTML = '';
attachmentsList.innerHTML = '';
if (attachments.length === 0) {
const noAttachments = document.createElement('p');
noAttachments.className = 'text-gray-400 text-center py-4';
noAttachments.textContent = 'No attachments found in this PDF.';
attachmentsList.appendChild(noAttachments);
return;
if (attachments.length === 0) {
const noAttachments = document.createElement('p');
noAttachments.className = 'text-gray-400 text-center py-4';
noAttachments.textContent = 'No attachments found in this PDF.';
attachmentsList.appendChild(noAttachments);
return;
}
// Controls container
const controlsContainer = document.createElement('div');
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
const removeAllBtn = document.createElement('button');
removeAllBtn.className =
'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
removeAllBtn.textContent = 'Remove All Attachments';
removeAllBtn.onclick = function () {
if (pageState.allAttachments.length === 0) return;
const allSelected = pageState.allAttachments.every(function (attachment) {
return pageState.attachmentsToRemove.has(attachment.index);
});
if (allSelected) {
pageState.allAttachments.forEach(function (attachment) {
pageState.attachmentsToRemove.delete(attachment.index);
const element = document.querySelector(
`[data-attachment-index="${attachment.index}"]`
);
if (element) {
element.classList.remove('opacity-50', 'line-through');
const btn = element.querySelector('button');
if (btn) {
btn.classList.remove('bg-gray-600');
btn.classList.add('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Remove All Attachments';
} else {
pageState.allAttachments.forEach(function (attachment) {
pageState.attachmentsToRemove.add(attachment.index);
const element = document.querySelector(
`[data-attachment-index="${attachment.index}"]`
);
if (element) {
element.classList.add('opacity-50', 'line-through');
const btn = element.querySelector('button');
if (btn) {
btn.classList.add('bg-gray-600');
btn.classList.remove('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Deselect All';
}
};
controlsContainer.appendChild(removeAllBtn);
attachmentsList.appendChild(controlsContainer);
// Attachment items
for (const attachment of attachments) {
const attachmentDiv = document.createElement('div');
attachmentDiv.className =
'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
const infoDiv = document.createElement('div');
infoDiv.className = 'flex-1';
const nameSpan = document.createElement('span');
nameSpan.className = 'text-white font-medium block';
nameSpan.textContent = attachment.name;
const levelSpan = document.createElement('span');
levelSpan.className = 'text-gray-400 text-sm block';
if (attachment.page === 0) {
levelSpan.textContent = 'Document-level attachment';
} else {
levelSpan.textContent = `Page ${attachment.page} attachment`;
}
// Controls container
const controlsContainer = document.createElement('div');
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
infoDiv.append(nameSpan, levelSpan);
const removeAllBtn = document.createElement('button');
removeAllBtn.className = 'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
removeAllBtn.textContent = 'Remove All Attachments';
removeAllBtn.onclick = function () {
if (pageState.allAttachments.length === 0) return;
const actionsDiv = document.createElement('div');
actionsDiv.className = 'flex items-center gap-2';
const allSelected = pageState.allAttachments.every(function (attachment) {
return pageState.attachmentsToRemove.has(attachment.index);
});
const removeBtn = document.createElement('button');
removeBtn.className = `${pageState.attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.title = 'Remove attachment';
removeBtn.onclick = function () {
if (pageState.attachmentsToRemove.has(attachment.index)) {
pageState.attachmentsToRemove.delete(attachment.index);
attachmentDiv.classList.remove('opacity-50', 'line-through');
removeBtn.classList.remove('bg-gray-600');
removeBtn.classList.add('bg-red-600');
} else {
pageState.attachmentsToRemove.add(attachment.index);
attachmentDiv.classList.add('opacity-50', 'line-through');
removeBtn.classList.add('bg-gray-600');
removeBtn.classList.remove('bg-red-600');
}
if (allSelected) {
pageState.allAttachments.forEach(function (attachment) {
pageState.attachmentsToRemove.delete(attachment.index);
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
if (element) {
element.classList.remove('opacity-50', 'line-through');
const btn = element.querySelector('button');
if (btn) {
btn.classList.remove('bg-gray-600');
btn.classList.add('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Remove All Attachments';
} else {
pageState.allAttachments.forEach(function (attachment) {
pageState.attachmentsToRemove.add(attachment.index);
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
if (element) {
element.classList.add('opacity-50', 'line-through');
const btn = element.querySelector('button');
if (btn) {
btn.classList.add('bg-gray-600');
btn.classList.remove('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Deselect All';
}
const allSelected = pageState.allAttachments.every(function (att) {
return pageState.attachmentsToRemove.has(att.index);
});
removeAllBtn.textContent = allSelected
? 'Deselect All'
: 'Remove All Attachments';
};
controlsContainer.appendChild(removeAllBtn);
attachmentsList.appendChild(controlsContainer);
actionsDiv.append(removeBtn);
attachmentDiv.append(infoDiv, actionsDiv);
attachmentsList.appendChild(attachmentDiv);
}
// Attachment items
for (const attachment of attachments) {
const attachmentDiv = document.createElement('div');
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
createIcons({ icons });
const infoDiv = document.createElement('div');
infoDiv.className = 'flex-1';
const nameSpan = document.createElement('span');
nameSpan.className = 'text-white font-medium block';
nameSpan.textContent = attachment.name;
const levelSpan = document.createElement('span');
levelSpan.className = 'text-gray-400 text-sm block';
if (attachment.page === 0) {
levelSpan.textContent = 'Document-level attachment';
} else {
levelSpan.textContent = `Page ${attachment.page} attachment`;
}
infoDiv.append(nameSpan, levelSpan);
const actionsDiv = document.createElement('div');
actionsDiv.className = 'flex items-center gap-2';
const removeBtn = document.createElement('button');
removeBtn.className = `${pageState.attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.title = 'Remove attachment';
removeBtn.onclick = function () {
if (pageState.attachmentsToRemove.has(attachment.index)) {
pageState.attachmentsToRemove.delete(attachment.index);
attachmentDiv.classList.remove('opacity-50', 'line-through');
removeBtn.classList.remove('bg-gray-600');
removeBtn.classList.add('bg-red-600');
} else {
pageState.attachmentsToRemove.add(attachment.index);
attachmentDiv.classList.add('opacity-50', 'line-through');
removeBtn.classList.add('bg-gray-600');
removeBtn.classList.remove('bg-red-600');
}
const allSelected = pageState.allAttachments.every(function (att) {
return pageState.attachmentsToRemove.has(att.index);
});
removeAllBtn.textContent = allSelected ? 'Deselect All' : 'Remove All Attachments';
};
actionsDiv.append(removeBtn);
attachmentDiv.append(infoDiv, actionsDiv);
attachmentsList.appendChild(attachmentDiv);
}
createIcons({ icons });
if (processBtn) processBtn.classList.remove('hidden');
if (processBtn) processBtn.classList.remove('hidden');
}
async function loadAttachments() {
if (!pageState.file) return;
if (!pageState.file) return;
showLoader('Loading attachments...');
showLoader('Loading attachments...');
try {
const fileBuffer = await pageState.file.arrayBuffer();
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
hideLoader();
return;
}
const message = {
command: 'get-attachments',
fileBuffer: fileBuffer,
fileName: pageState.file.name
};
try {
const fileBuffer = await pageState.file.arrayBuffer();
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error loading attachments:', error);
hideLoader();
showAlert('Error', 'Failed to load attachments from PDF.');
}
const message = {
command: 'get-attachments',
fileBuffer: fileBuffer,
fileName: pageState.file.name,
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
};
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error loading attachments:', error);
hideLoader();
showAlert('Error', 'Failed to load attachments from PDF.');
}
}
async function saveChanges() {
if (!pageState.file) {
showAlert('Error', 'No PDF file loaded.');
return;
}
if (!pageState.file) {
showAlert('Error', 'No PDF file loaded.');
return;
}
if (pageState.attachmentsToRemove.size === 0) {
showAlert('No Changes', 'No attachments selected for removal.');
return;
}
if (pageState.attachmentsToRemove.size === 0) {
showAlert('No Changes', 'No attachments selected for removal.');
return;
}
showLoader('Processing attachments...');
showLoader('Processing attachments...');
try {
const fileBuffer = await pageState.file.arrayBuffer();
// Check if CPDF is configured (double check)
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
hideLoader();
return;
}
const message = {
command: 'edit-attachments',
fileBuffer: fileBuffer,
fileName: pageState.file.name,
attachmentsToRemove: Array.from(pageState.attachmentsToRemove)
};
try {
const fileBuffer = await pageState.file.arrayBuffer();
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error editing attachments:', error);
hideLoader();
showAlert('Error', 'Failed to edit attachments.');
}
const message = {
command: 'edit-attachments',
fileBuffer: fileBuffer,
fileName: pageState.file.name,
attachmentsToRemove: Array.from(pageState.attachmentsToRemove),
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
};
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error editing attachments:', error);
hideLoader();
showAlert('Error', 'Failed to edit attachments.');
}
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
if (toolOptions) toolOptions.classList.remove('hidden');
await loadAttachments();
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
await loadAttachments();
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
});
}
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', saveChanges);
}
if (processBtn) {
processBtn.addEventListener('click', saveChanges);
}
});

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,205 +2,216 @@ 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'];
const TOOL_NAME = 'EPUB';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const convertOptions = document.getElementById('convert-options');
// ... (existing listeners)
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
if (convertOptions) convertOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
if (convertOptions) convertOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const convertOptions = document.getElementById('convert-options');
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
// ... (existing listeners)
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
showLoader('Loading engine...');
const pymupdf = await loadPyMuPDF();
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
filetype: FILETYPE,
});
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
downloadFile(pdfBlob, fileName);
hideLoader();
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
if (convertOptions) convertOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
if (convertOptions) convertOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
const pdfBlob = await pymupdf.convertToPdf(file, {
filetype: FILETYPE,
});
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
hideLoader();
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
downloadFile(pdfBlob, fileName);
hideLoader();
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
};
}
});
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});

View File

@@ -2,259 +2,297 @@ 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[];
files: File[];
}
const pageState: ExtractState = {
files: [],
files: [],
};
function resetState() {
pageState.files = [];
pageState.files = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const statusMessage = document.getElementById('status-message');
if (statusMessage) statusMessage.classList.add('hidden');
const statusMessage = document.getElementById('status-message');
if (statusMessage) statusMessage.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
}
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
const statusMessage = document.getElementById('status-message') as HTMLElement;
if (!statusMessage) return;
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'
? '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.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');
}
worker.onmessage = function (e) {
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
if (e.data.status === 'success') {
const attachments = e.data.attachments;
if (attachments.length === 0) {
showAlert(
'No Attachments',
'The PDF file(s) do not contain any attachments to extract.'
);
resetState();
return;
}
if (e.data.status === 'success') {
const attachments = e.data.attachments;
const zip = new JSZip();
let totalSize = 0;
if (attachments.length === 0) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
resetState();
return;
}
const zip = new JSZip();
let totalSize = 0;
for (const attachment of attachments) {
zip.file(attachment.name, new Uint8Array(attachment.data));
totalSize += attachment.data.byteLength;
}
zip.generateAsync({ type: 'blob' }).then(function (zipBlob) {
downloadFile(zipBlob, 'extracted-attachments.zip');
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
showStatus(
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
'success'
);
resetState();
});
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
if (errorMessage.includes('No attachments were found')) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
resetState();
} else {
showStatus(`Error: ${errorMessage}`, 'error');
}
for (const attachment of attachments) {
zip.file(attachment.name, new Uint8Array(attachment.data));
totalSize += attachment.data.byteLength;
}
zip.generateAsync({ type: 'blob' }).then(function (zipBlob) {
downloadFile(zipBlob, 'extracted-attachments.zip');
showAlert(
'Success',
`${attachments.length} attachment(s) extracted successfully!`
);
showStatus(
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
'success'
);
resetState();
});
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
if (errorMessage.includes('No attachments were found')) {
showAlert(
'No Attachments',
'The PDF file(s) do not contain any attachments to extract.'
);
resetState();
} else {
showStatus(`Error: ${errorMessage}`, 'error');
}
}
};
worker.onerror = function (error) {
console.error('Worker error:', error);
showStatus('Worker error occurred. Check console for details.', 'error');
console.error('Worker error:', error);
showStatus('Worker error occurred. Check console for details.', 'error');
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
};
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
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';
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';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const countSpan = document.createElement('div');
countSpan.className = 'font-medium text-gray-200 text-sm mb-1';
countSpan.textContent = `${pageState.files.length} PDF file(s) selected`;
const countSpan = document.createElement('div');
countSpan.className = 'font-medium text-gray-200 text-sm mb-1';
countSpan.textContent = `${pageState.files.length} PDF file(s) selected`;
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);
sizeSpan.textContent = formatBytes(totalSize);
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);
sizeSpan.textContent = formatBytes(totalSize);
infoContainer.append(countSpan, sizeSpan);
infoContainer.append(countSpan, sizeSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
summaryDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(summaryDiv);
createIcons({ icons });
summaryDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(summaryDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function extractAttachments() {
if (pageState.files.length === 0) {
showStatus('No Files', 'error');
return;
if (pageState.files.length === 0) {
showStatus('No Files', 'error');
return;
}
// Check if CPDF is configured
if (!isCpdfAvailable()) {
showWasmRequiredDialog('cpdf');
return;
}
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.add('opacity-50', 'cursor-not-allowed');
processBtn.setAttribute('disabled', 'true');
}
showStatus('Reading files...', 'info');
try {
const fileBuffers: ArrayBuffer[] = [];
const fileNames: string[] = [];
for (const file of pageState.files) {
const buffer = await file.arrayBuffer();
fileBuffers.push(buffer);
fileNames.push(file.name);
}
const processBtn = document.getElementById('process-btn');
showStatus(
`Extracting attachments from ${pageState.files.length} file(s)...`,
'info'
);
const message = {
command: 'extract-attachments',
fileBuffers,
fileNames,
cpdfUrl: WasmProvider.getUrl('cpdf')! + 'coherentpdf.browser.min.js',
};
const transferables = fileBuffers.map(function (buf) {
return buf;
});
worker.postMessage(message, transferables);
} catch (error) {
console.error('Error reading files:', error);
showStatus(
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
'error'
);
if (processBtn) {
processBtn.classList.add('opacity-50', 'cursor-not-allowed');
processBtn.setAttribute('disabled', 'true');
}
showStatus('Reading files...', 'info');
try {
const fileBuffers: ArrayBuffer[] = [];
const fileNames: string[] = [];
for (const file of pageState.files) {
const buffer = await file.arrayBuffer();
fileBuffers.push(buffer);
fileNames.push(file.name);
}
showStatus(`Extracting attachments from ${pageState.files.length} file(s)...`, 'info');
const message = {
command: 'extract-attachments',
fileBuffers,
fileNames,
};
const transferables = fileBuffers.map(function (buf) { return buf; });
worker.postMessage(message, transferables);
} catch (error) {
console.error('Error reading files:', error);
showStatus(
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
'error'
);
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
}
}
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');
});
if (pdfFiles.length > 0) {
pageState.files = pdfFiles;
updateUI();
}
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
});
if (pdfFiles.length > 0) {
pageState.files = pdfFiles;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', extractAttachments);
}
if (processBtn) {
processBtn.addEventListener('click', extractAttachments);
}
});

View File

@@ -1,281 +1,295 @@
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;
name: string;
ext: string;
data: Uint8Array;
name: string;
ext: string;
}
let extractedImages: ExtractedImage[] = [];
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const extractOptions = document.getElementById('extract-options');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const imagesContainer = document.getElementById('images-container');
const imagesGrid = document.getElementById('images-grid');
const downloadAllBtn = document.getElementById('download-all-btn');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const extractOptions = document.getElementById('extract-options');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const imagesContainer = document.getElementById('images-container');
const imagesGrid = document.getElementById('images-grid');
const downloadAllBtn = document.getElementById('download-all-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls) return;
const updateUI = async () => {
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls)
return;
// Clear extracted images when files change
extractedImages = [];
if (imagesContainer) imagesContainer.classList.add('hidden');
if (imagesGrid) imagesGrid.innerHTML = '';
// Clear extracted images when files change
extractedImages = [];
if (imagesContainer) imagesContainer.classList.add('hidden');
if (imagesGrid) imagesGrid.innerHTML = '';
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_: File, i: number) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_: File, i: number) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
}
createIcons({ icons });
fileControls.classList.remove('hidden');
extractOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
extractOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
extractedImages = [];
if (imagesContainer) imagesContainer.classList.add('hidden');
if (imagesGrid) imagesGrid.innerHTML = '';
updateUI();
};
const displayImages = () => {
if (!imagesGrid || !imagesContainer) return;
imagesGrid.innerHTML = '';
extractedImages.forEach((img, index) => {
const blob = new Blob([new Uint8Array(img.data)]);
const url = URL.createObjectURL(blob);
const card = document.createElement('div');
card.className = 'bg-gray-700 rounded-lg overflow-hidden';
const imgEl = document.createElement('img');
imgEl.src = url;
imgEl.className = 'w-full h-32 object-cover';
const info = document.createElement('div');
info.className = 'p-2 flex justify-between items-center';
const name = document.createElement('span');
name.className = 'text-xs text-gray-300 truncate';
name.textContent = img.name;
const downloadBtn = document.createElement('button');
downloadBtn.className = 'text-indigo-400 hover:text-indigo-300';
downloadBtn.innerHTML = '<i data-lucide="download" class="w-4 h-4"></i>';
downloadBtn.onclick = () => {
downloadFile(blob, img.name);
};
info.append(name, downloadBtn);
card.append(imgEl, info);
imagesGrid.appendChild(card);
});
createIcons({ icons });
imagesContainer.classList.remove('hidden');
};
const extract = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading PDF processor...');
await pymupdf.load();
extractedImages = [];
let imgCounter = 0;
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Extracting images from ${file.name}...`);
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
for (let pageIdx = 0; pageIdx < pageCount; pageIdx++) {
const page = doc.getPage(pageIdx);
const images = page.getImages();
for (const imgInfo of images) {
try {
const imgData = page.extractImage(imgInfo.xref);
if (imgData && imgData.data) {
imgCounter++;
extractedImages.push({
data: imgData.data,
name: `image_${imgCounter}.${imgData.ext || 'png'}`,
ext: imgData.ext || 'png'
});
}
} catch (e) {
console.warn('Failed to extract image:', e);
}
}
}
doc.close();
}
hideLoader();
if (extractedImages.length === 0) {
showAlert('No Images Found', 'No embedded images were found in the selected PDF(s).');
} else {
displayImages();
showAlert(
'Extraction Complete',
`Found ${extractedImages.length} image(s) in ${state.files.length} PDF(s).`,
'success'
);
}
} catch (e: any) {
hideLoader();
showAlert('Error', `An error occurred during extraction. Error: ${e.message}`);
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
};
}
const downloadAll = async () => {
if (extractedImages.length === 0) return;
createIcons({ icons });
fileControls.classList.remove('hidden');
extractOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
extractOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
showLoader('Creating ZIP archive...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const resetState = () => {
state.files = [];
state.pdfDoc = null;
extractedImages = [];
if (imagesContainer) imagesContainer.classList.add('hidden');
if (imagesGrid) imagesGrid.innerHTML = '';
updateUI();
};
extractedImages.forEach((img) => {
zip.file(img.name, img.data);
});
const displayImages = () => {
if (!imagesGrid || !imagesContainer) return;
imagesGrid.innerHTML = '';
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'extracted-images.zip');
hideLoader();
};
extractedImages.forEach((img, index) => {
const blob = new Blob([new Uint8Array(img.data)]);
const url = URL.createObjectURL(blob);
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
state.files = [...state.files, ...pdfFiles];
updateUI();
}
};
const card = document.createElement('div');
card.className = 'bg-gray-700 rounded-lg overflow-hidden';
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
const imgEl = document.createElement('img');
imgEl.src = url;
imgEl.className = 'w-full h-32 object-cover';
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
const info = document.createElement('div');
info.className = 'p-2 flex justify-between items-center';
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
const name = document.createElement('span');
name.className = 'text-xs text-gray-300 truncate';
name.textContent = img.name;
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
const downloadBtn = document.createElement('button');
downloadBtn.className = 'text-indigo-400 hover:text-indigo-300';
downloadBtn.innerHTML = '<i data-lucide="download" class="w-4 h-4"></i>';
downloadBtn.onclick = () => {
downloadFile(blob, img.name);
};
info.append(name, downloadBtn);
card.append(imgEl, info);
imagesGrid.appendChild(card);
});
createIcons({ icons });
imagesContainer.classList.remove('hidden');
};
const extract = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading PDF processor...');
const pymupdf = await loadPyMuPDF();
extractedImages = [];
let imgCounter = 0;
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Extracting images from ${file.name}...`);
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
for (let pageIdx = 0; pageIdx < pageCount; pageIdx++) {
const page = doc.getPage(pageIdx);
const images = page.getImages();
for (const imgInfo of images) {
try {
const imgData = page.extractImage(imgInfo.xref);
if (imgData && imgData.data) {
imgCounter++;
extractedImages.push({
data: imgData.data,
name: `image_${imgCounter}.${imgData.ext || 'png'}`,
ext: imgData.ext || 'png',
});
}
} catch (e) {
console.warn('Failed to extract image:', e);
}
});
}
}
doc.close();
}
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
hideLoader();
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
if (extractedImages.length === 0) {
showAlert(
'No Images Found',
'No embedded images were found in the selected PDF(s).'
);
} else {
displayImages();
showAlert(
'Extraction Complete',
`Found ${extractedImages.length} image(s) in ${state.files.length} PDF(s).`,
'success'
);
}
} catch (e: any) {
hideLoader();
showAlert(
'Error',
`An error occurred during extraction. Error: ${e.message}`
);
}
};
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
const downloadAll = async () => {
if (extractedImages.length === 0) return;
if (processBtn) {
processBtn.addEventListener('click', extract);
}
showLoader('Creating ZIP archive...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
if (downloadAllBtn) {
downloadAllBtn.addEventListener('click', downloadAll);
extractedImages.forEach((img) => {
zip.file(img.name, img.data);
});
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'extracted-images.zip');
hideLoader();
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(
(f) =>
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
state.files = [...state.files, ...pdfFiles];
updateUI();
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', extract);
}
if (downloadAllBtn) {
downloadAllBtn.addEventListener('click', downloadAll);
}
});

View File

@@ -2,240 +2,259 @@ 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 = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
if (!fileDisplayArea || !optionsPanel) return;
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (file) {
optionsPanel.classList.remove('hidden');
if (file) {
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';
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
file = null;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
file = null;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
};
function tableToCsv(rows: (string | null)[][]): string {
return rows.map(row =>
row.map(cell => {
const cellStr = cell ?? '';
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) {
return `"${cellStr.replace(/"/g, '""')}"`;
}
return cellStr;
}).join(',')
).join('\n');
return rows
.map((row) =>
row
.map((cell) => {
const cellStr = cell ?? '';
if (
cellStr.includes(',') ||
cellStr.includes('"') ||
cellStr.includes('\n')
) {
return `"${cellStr.replace(/"/g, '""')}"`;
}
return cellStr;
})
.join(',')
)
.join('\n');
}
async function extract() {
if (!file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
if (!file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
const formatRadios = document.querySelectorAll('input[name="export-format"]');
let format = 'csv';
formatRadios.forEach((radio: Element) => {
if ((radio as HTMLInputElement).checked) {
format = (radio as HTMLInputElement).value;
}
});
const formatRadios = document.querySelectorAll('input[name="export-format"]');
let format = 'csv';
formatRadios.forEach((radio: Element) => {
if ((radio as HTMLInputElement).checked) {
format = (radio as HTMLInputElement).value;
}
});
try {
showLoader('Loading Engine...');
const pymupdf = await loadPyMuPDF();
showLoader('Extracting tables...');
try {
await pymupdf.load();
showLoader('Extracting tables...');
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
const baseName = file.name.replace(/\.[^/.]+$/, '');
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
const baseName = file.name.replace(/\.[^/.]+$/, '');
interface TableData {
page: number;
tableIndex: number;
rows: (string | null)[][];
markdown: string;
rowCount: number;
colCount: number;
}
const allTables: TableData[] = [];
for (let i = 0; i < pageCount; i++) {
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
const page = doc.getPage(i);
const tables = page.findTables();
tables.forEach((table, tableIdx) => {
allTables.push({
page: i + 1,
tableIndex: tableIdx + 1,
rows: table.rows,
markdown: table.markdown,
rowCount: table.rowCount,
colCount: table.colCount
});
});
}
if (allTables.length === 0) {
showAlert('No Tables Found', 'No tables were detected in this PDF.');
return;
}
if (allTables.length === 1) {
const table = allTables[0];
let content: string;
let ext: string;
let mimeType: string;
if (format === 'csv') {
content = tableToCsv(table.rows);
ext = 'csv';
mimeType = 'text/csv';
} else if (format === 'json') {
content = JSON.stringify(table.rows, null, 2);
ext = 'json';
mimeType = 'application/json';
} else {
content = table.markdown;
ext = 'md';
mimeType = 'text/markdown';
}
const blob = new Blob([content], { type: mimeType });
downloadFile(blob, `${baseName}_table.${ext}`);
showAlert('Success', `Extracted 1 table successfully!`, 'success', resetState);
} else {
showLoader('Creating ZIP file...');
const zip = new JSZip();
allTables.forEach((table, idx) => {
const filename = `table_${idx + 1}_page${table.page}`;
let content: string;
let ext: string;
if (format === 'csv') {
content = tableToCsv(table.rows);
ext = 'csv';
} else if (format === 'json') {
content = JSON.stringify(table.rows, null, 2);
ext = 'json';
} else {
content = table.markdown;
ext = 'md';
}
zip.file(`${filename}.${ext}`, content);
});
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${baseName}_tables.zip`);
showAlert('Success', `Extracted ${allTables.length} tables successfully!`, 'success', resetState);
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
showAlert('Error', `Failed to extract tables. ${message}`);
} finally {
hideLoader();
interface TableData {
page: number;
tableIndex: number;
rows: (string | null)[][];
markdown: string;
rowCount: number;
colCount: number;
}
const allTables: TableData[] = [];
for (let i = 0; i < pageCount; i++) {
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
const page = doc.getPage(i);
const tables = page.findTables();
tables.forEach((table, tableIdx) => {
allTables.push({
page: i + 1,
tableIndex: tableIdx + 1,
rows: table.rows,
markdown: table.markdown,
rowCount: table.rowCount,
colCount: table.colCount,
});
});
}
if (allTables.length === 0) {
showAlert('No Tables Found', 'No tables were detected in this PDF.');
return;
}
if (allTables.length === 1) {
const table = allTables[0];
let content: string;
let ext: string;
let mimeType: string;
if (format === 'csv') {
content = tableToCsv(table.rows);
ext = 'csv';
mimeType = 'text/csv';
} else if (format === 'json') {
content = JSON.stringify(table.rows, null, 2);
ext = 'json';
mimeType = 'application/json';
} else {
content = table.markdown;
ext = 'md';
mimeType = 'text/markdown';
}
const blob = new Blob([content], { type: mimeType });
downloadFile(blob, `${baseName}_table.${ext}`);
showAlert(
'Success',
`Extracted 1 table successfully!`,
'success',
resetState
);
} else {
showLoader('Creating ZIP file...');
const zip = new JSZip();
allTables.forEach((table, idx) => {
const filename = `table_${idx + 1}_page${table.page}`;
let content: string;
let ext: string;
if (format === 'csv') {
content = tableToCsv(table.rows);
ext = 'csv';
} else if (format === 'json') {
content = JSON.stringify(table.rows, null, 2);
ext = 'json';
} else {
content = table.markdown;
ext = 'md';
}
zip.file(`${filename}.${ext}`, content);
});
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${baseName}_tables.zip`);
showAlert(
'Success',
`Extracted ${allTables.length} tables successfully!`,
'success',
resetState
);
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
showAlert('Error', `Failed to extract tables. ${message}`);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFile = Array.from(newFiles).find(
(f) => f.type === 'application/pdf'
);
if (!validFile) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
file = validFile;
updateUI();
};
if (!validFile) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
file = validFile;
updateUI();
};
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', extract);
}
if (processBtn) {
processBtn.addEventListener('click', extract);
}
});

View File

@@ -2,201 +2,212 @@ 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'];
const TOOL_NAME = 'FB2';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
for (let index = 0; index < state.files.length; index++) {
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';
showLoader('Loading engine...');
const pymupdf = await loadPyMuPDF();
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
filetype: FILETYPE,
});
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
downloadFile(pdfBlob, fileName);
hideLoader();
infoContainer.append(nameSpan, metaSpan);
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
const pdfBlob = await pymupdf.convertToPdf(file, {
filetype: FILETYPE,
});
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
hideLoader();
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
downloadFile(pdfBlob, fileName);
hideLoader();
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
};
}
});
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});

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,275 +1,293 @@
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);
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
initializePage();
}
function initializePage() {
createIcons({ icons });
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
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');
const formatDisplay = document.getElementById('supported-formats');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
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');
const formatDisplay = document.getElementById('supported-formats');
if (formatDisplay) {
formatDisplay.textContent = SUPPORTED_FORMATS_DISPLAY;
}
if (formatDisplay) {
formatDisplay.textContent = SUPPORTED_FORMATS_DISPLAY;
}
if (fileInput) {
fileInput.accept = SUPPORTED_FORMATS;
fileInput.addEventListener('change', handleFileUpload);
}
if (fileInput) {
fileInput.accept = SUPPORTED_FORMATS;
fileInput.addEventListener('change', handleFileUpload);
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
});
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput?.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
files = [];
updateUI();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
});
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput?.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
files = [];
updateUI();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFiles(input.files);
}
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFiles(input.files);
}
}
function getFileExtension(filename: string): string {
return '.' + filename.split('.').pop()?.toLowerCase() || '';
return '.' + filename.split('.').pop()?.toLowerCase() || '';
}
function isValidImageFile(file: File): boolean {
const ext = getFileExtension(file.name);
const validExtensions = SUPPORTED_FORMATS.split(',');
return validExtensions.includes(ext) || file.type.startsWith('image/');
const ext = getFileExtension(file.name);
const validExtensions = SUPPORTED_FORMATS.split(',');
return validExtensions.includes(ext) || file.type.startsWith('image/');
}
function handleFiles(newFiles: FileList) {
const validFiles = Array.from(newFiles).filter(isValidImageFile);
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.');
}
if (validFiles.length < newFiles.length) {
showAlert(
'Invalid Files',
'Some files were skipped. Only supported image formats are allowed.'
);
}
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
}
const resetState = () => {
files = [];
updateUI();
files = [];
updateUI();
};
function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const optionsDiv = document.getElementById('jpg-to-pdf-options');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const optionsDiv = document.getElementById('jpg-to-pdf-options');
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
fileControls.classList.remove('hidden');
optionsDiv.classList.remove('hidden');
if (files.length > 0) {
fileControls.classList.remove('hidden');
optionsDiv.classList.remove('hidden');
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';
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';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
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 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
infoContainer.append(nameSpan, sizeSpan);
infoContainer.append(nameSpan, sizeSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
fileControls.classList.add('hidden');
optionsDiv.classList.add('hidden');
}
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
fileControls.classList.add('hidden');
optionsDiv.classList.add('hidden');
}
}
async function ensurePyMuPDF(): Promise<PyMuPDF> {
if (!pymupdf) {
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
}
return pymupdf;
async function ensurePyMuPDF(): Promise<any> {
if (!pymupdf) {
pymupdf = await loadPyMuPDF();
}
return pymupdf;
}
async function preprocessFile(file: File): Promise<File> {
const ext = getFileExtension(file.name);
const ext = getFileExtension(file.name);
if (ext === '.heic' || ext === '.heif') {
try {
const conversionResult = await heic2any({
blob: file,
toType: 'image/png',
quality: 0.9,
});
if (ext === '.heic' || ext === '.heif') {
try {
const conversionResult = await heic2any({
blob: file,
toType: 'image/png',
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' });
} catch (e) {
console.error(`Failed to convert HEIC: ${file.name}`, e);
throw new Error(`Failed to process HEIC file: ${file.name}`);
}
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}`);
}
}
if (ext === '.webp') {
try {
return await new Promise<File>((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
if (ext === '.webp') {
try {
return await new Promise<File>((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(url);
reject(new Error('Canvas context failed'));
return;
}
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
URL.revokeObjectURL(url);
if (blob) {
resolve(new File([blob], file.name.replace(/\.webp$/i, '.png'), { type: 'image/png' }));
} else {
reject(new Error('Canvas toBlob failed'));
}
}, 'image/png');
};
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(url);
reject(new Error('Canvas context failed'));
return;
}
ctx.drawImage(img, 0, 0);
canvas.toBlob((blob) => {
URL.revokeObjectURL(url);
if (blob) {
resolve(
new File([blob], file.name.replace(/\.webp$/i, '.png'), {
type: 'image/png',
})
);
} else {
reject(new Error('Canvas toBlob failed'));
}
}, 'image/png');
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load WebP image'));
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load WebP image'));
};
img.src = url;
});
} catch (e) {
console.error(`Failed to convert WebP: ${file.name}`, e);
throw new Error(`Failed to process WebP file: ${file.name}`);
}
img.src = url;
});
} catch (e) {
console.error(`Failed to convert WebP: ${file.name}`, e);
throw new Error(`Failed to process WebP file: ${file.name}`);
}
}
return file;
return file;
}
async function convertToPdf() {
if (files.length === 0) {
showAlert('No Files', 'Please select at least one image file.');
return;
if (files.length === 0) {
showAlert('No Files', 'Please select at least one image file.');
return;
}
showLoader('Processing images...');
try {
const processedFiles: File[] = [];
for (const file of files) {
try {
const processed = await preprocessFile(file);
processedFiles.push(processed);
} catch (error: any) {
console.warn(error);
throw error;
}
}
showLoader('Processing images...');
showLoader('Loading engine...');
const mupdf = await ensurePyMuPDF();
try {
const processedFiles: File[] = [];
for (const file of files) {
try {
const processed = await preprocessFile(file);
processedFiles.push(processed);
} catch (error: any) {
console.warn(error);
throw error;
}
}
showLoader('Converting images to PDF...');
const pdfBlob = await mupdf.imagesToPdf(processedFiles);
showLoader('Loading engine...');
const mupdf = await ensurePyMuPDF();
downloadFile(pdfBlob, 'images_to_pdf.pdf');
showLoader('Converting images to PDF...');
const pdfBlob = await mupdf.imagesToPdf(processedFiles);
downloadFile(pdfBlob, 'images_to_pdf.pdf');
showAlert('Success', 'PDF created successfully!', 'success', () => {
resetState();
});
} catch (e: any) {
console.error('[ImageToPDF]', e);
showAlert('Conversion Error', e.message || 'Failed to convert images to PDF.');
} finally {
hideLoader();
}
showAlert('Success', 'PDF created successfully!', 'success', () => {
resetState();
});
} catch (e: any) {
console.error('[ImageToPDF]', e);
showAlert(
'Conversion Error',
e.message || 'Failed to convert images to PDF.'
);
} finally {
hideLoader();
}
}

View File

@@ -1,195 +1,205 @@
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);
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
initializePage();
}
function initializePage() {
createIcons({ icons });
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
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');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
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');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
});
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput?.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
files = [];
updateUI();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
});
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput?.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
files = [];
updateUI();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFiles(input.files);
}
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFiles(input.files);
}
}
function getFileExtension(filename: string): string {
return '.' + (filename.split('.').pop()?.toLowerCase() || '');
return '.' + (filename.split('.').pop()?.toLowerCase() || '');
}
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);
const ext = getFileExtension(file.name);
const validExtensions = SUPPORTED_FORMATS.split(',');
return (
validExtensions.includes(ext) || SUPPORTED_MIME_TYPES.includes(file.type)
);
}
function handleFiles(newFiles: FileList) {
const validFiles = Array.from(newFiles).filter(isValidImageFile);
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.');
}
if (validFiles.length < newFiles.length) {
showAlert(
'Invalid Files',
'Some files were skipped. Only JPG, JPEG, JP2, and JPX files are allowed.'
);
}
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
}
const resetState = () => {
files = [];
updateUI();
files = [];
updateUI();
};
function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const optionsDiv = document.getElementById('jpg-to-pdf-options');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const optionsDiv = document.getElementById('jpg-to-pdf-options');
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
fileControls.classList.remove('hidden');
optionsDiv.classList.remove('hidden');
if (files.length > 0) {
fileControls.classList.remove('hidden');
optionsDiv.classList.remove('hidden');
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';
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';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
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 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
infoContainer.append(nameSpan, sizeSpan);
infoContainer.append(nameSpan, sizeSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
fileControls.classList.add('hidden');
optionsDiv.classList.add('hidden');
}
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
fileControls.classList.add('hidden');
optionsDiv.classList.add('hidden');
}
}
async function ensurePyMuPDF(): Promise<PyMuPDF> {
if (!pymupdf) {
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
}
return pymupdf;
async function ensurePyMuPDF(): Promise<any> {
if (!pymupdf) {
pymupdf = await loadPyMuPDF();
}
return pymupdf;
}
async function convertToPdf() {
if (files.length === 0) {
showAlert('No Files', 'Please select at least one JPG or JPEG2000 image.');
return;
}
if (files.length === 0) {
showAlert('No Files', 'Please select at least one JPG or JPEG2000 image.');
return;
}
showLoader('Loading engine...');
showLoader('Loading engine...');
try {
const mupdf = await ensurePyMuPDF();
try {
const mupdf = await ensurePyMuPDF();
showLoader('Converting images to PDF...');
showLoader('Converting images to PDF...');
const pdfBlob = await mupdf.imagesToPdf(files);
const pdfBlob = await mupdf.imagesToPdf(files);
downloadFile(pdfBlob, 'from_jpgs.pdf');
downloadFile(pdfBlob, 'from_jpgs.pdf');
showAlert('Success', 'PDF created successfully!', 'success', () => {
resetState();
});
} catch (e: any) {
console.error('[JpgToPdf]', e);
showAlert('Conversion Error', e.message || 'Failed to convert images to PDF.');
} finally {
hideLoader();
}
showAlert('Success', 'PDF created successfully!', 'success', () => {
resetState();
});
} catch (e: any) {
console.error('[JpgToPdf]', e);
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'
? '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.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');
}
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')
worker.postMessage({
command: 'convert',
fileBuffers: fileBuffers,
fileNames: selectedFiles.map(f => f.name)
}, fileBuffers);
showStatus('Converting JSONs to PDFs...', 'info');
worker.postMessage(
{
command: 'convert',
fileBuffers: 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();

File diff suppressed because it is too large Load Diff

View File

@@ -2,201 +2,212 @@ 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'];
const TOOL_NAME = 'MOBI';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
for (let index = 0; index < state.files.length; index++) {
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';
showLoader('Loading engine...');
const pymupdf = await loadPyMuPDF();
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
filetype: FILETYPE,
});
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
downloadFile(pdfBlob, fileName);
hideLoader();
infoContainer.append(nameSpan, metaSpan);
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
const pdfBlob = await pymupdf.convertToPdf(file, {
filetype: FILETYPE,
});
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
hideLoader();
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
downloadFile(pdfBlob, fileName);
hideLoader();
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
};
}
});
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});

View File

@@ -1,21 +1,25 @@
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;
xref: number;
text: string;
on: boolean;
locked: boolean;
depth: number;
parentXref: number;
displayOrder: number;
};
number: number;
xref: number;
text: string;
on: boolean;
locked: boolean;
depth: number;
parentXref: number;
displayOrder: number;
}
let currentFile: File | null = null;
let currentDoc: any = null;
@@ -23,151 +27,170 @@ const layersMap = new Map<number, LayerData>();
let nextDisplayOrder = 0;
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const processBtnContainer = document.getElementById('process-btn-container');
const fileDisplayArea = document.getElementById('file-display-area');
const layersContainer = document.getElementById('layers-container');
const layersList = document.getElementById('layers-list');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const processBtnContainer = document.getElementById('process-btn-container');
const fileDisplayArea = document.getElementById('file-display-area');
const layersContainer = document.getElementById('layers-container');
const layersList = document.getElementById('layers-list');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !processBtnContainer || !processBtn) return;
if (currentFile) {
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = currentFile.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(currentFile.size)} • Loading pages...`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
try {
const arrayBuffer = await readFileAsArrayBuffer(currentFile);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(currentFile.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
metaSpan.textContent = `${formatBytes(currentFile.size)} • Could not load page count`;
}
createIcons({ icons });
processBtnContainer.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
processBtnContainer.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const updateUI = async () => {
if (!fileDisplayArea || !processBtnContainer || !processBtn) return;
const resetState = () => {
currentFile = null;
currentDoc = null;
layersMap.clear();
nextDisplayOrder = 0;
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';
if (dropZone) dropZone.style.display = 'flex';
if (layersContainer) layersContainer.classList.add('hidden');
updateUI();
};
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
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 confirmBtn = document.getElementById('input-confirm');
const cancelBtn = document.getElementById('input-cancel');
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = currentFile.name;
if (
!modal ||
!titleEl ||
!messageEl ||
!inputEl ||
!confirmBtn ||
!cancelBtn
) {
console.error('Input modal elements not found');
resolve(null);
return;
}
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(currentFile.size)} • Loading pages...`;
titleEl.textContent = title;
messageEl.textContent = message;
inputEl.value = defaultValue;
infoContainer.append(nameSpan, metaSpan);
const closeModal = () => {
modal.classList.add('hidden');
confirmBtn.onclick = null;
cancelBtn.onclick = null;
inputEl.onkeydown = null;
};
const removeBtn = document.createElement('button');
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();
};
const confirm = () => {
const val = inputEl.value.trim();
closeModal();
resolve(val);
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
const cancel = () => {
closeModal();
resolve(null);
};
try {
const arrayBuffer = await readFileAsArrayBuffer(currentFile);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(currentFile.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
metaSpan.textContent = `${formatBytes(currentFile.size)} • Could not load page count`;
}
confirmBtn.onclick = confirm;
cancelBtn.onclick = cancel;
createIcons({ icons });
processBtnContainer.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
processBtnContainer.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
inputEl.onkeydown = (e) => {
if (e.key === 'Enter') confirm();
if (e.key === 'Escape') cancel();
};
const resetState = () => {
currentFile = null;
currentDoc = null;
layersMap.clear();
nextDisplayOrder = 0;
modal.classList.remove('hidden');
inputEl.focus();
});
};
if (dropZone) dropZone.style.display = 'flex';
if (layersContainer) layersContainer.classList.add('hidden');
updateUI();
};
const renderLayers = () => {
if (!layersList) return;
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 confirmBtn = document.getElementById('input-confirm');
const cancelBtn = document.getElementById('input-cancel');
const layersArray = Array.from(layersMap.values());
if (!modal || !titleEl || !messageEl || !inputEl || !confirmBtn || !cancelBtn) {
console.error('Input modal elements not found');
resolve(null);
return;
}
titleEl.textContent = title;
messageEl.textContent = message;
inputEl.value = defaultValue;
const closeModal = () => {
modal.classList.add('hidden');
confirmBtn.onclick = null;
cancelBtn.onclick = null;
inputEl.onkeydown = null;
};
const confirm = () => {
const val = inputEl.value.trim();
closeModal();
resolve(val);
};
const cancel = () => {
closeModal();
resolve(null);
};
confirmBtn.onclick = confirm;
cancelBtn.onclick = cancel;
inputEl.onkeydown = (e) => {
if (e.key === 'Enter') confirm();
if (e.key === 'Escape') cancel();
};
modal.classList.remove('hidden');
inputEl.focus();
});
};
const renderLayers = () => {
if (!layersList) return;
const layersArray = Array.from(layersMap.values());
if (layersArray.length === 0) {
layersList.innerHTML = `
if (layersArray.length === 0) {
layersList.innerHTML = `
<div class="layers-empty">
<p>This PDF has no layers (OCG).</p>
<p>Add a new layer to get started!</p>
</div>
`;
return;
}
return;
}
// Sort layers by displayOrder
const sortedLayers = layersArray.sort((a, b) => a.displayOrder - b.displayOrder);
// Sort layers by 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,238 +202,261 @@ 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) => {
checkbox.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
const xref = parseInt(target.dataset.xref || '0');
const isOn = target.checked;
// Attach toggle handlers
layersList
.querySelectorAll('input[type="checkbox"]')
.forEach((checkbox) => {
checkbox.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
const xref = parseInt(target.dataset.xref || '0');
const isOn = target.checked;
try {
currentDoc.setLayerVisibility(xref, isOn);
const layer = Array.from(layersMap.values()).find(l => l.xref === xref);
if (layer) {
layer.on = isOn;
}
} catch (err) {
console.error('Failed to set layer visibility:', err);
target.checked = !isOn;
showAlert('Error', 'Failed to toggle layer visibility');
}
});
try {
currentDoc.setLayerVisibility(xref, isOn);
const layer = Array.from(layersMap.values()).find(
(l) => l.xref === xref
);
if (layer) {
layer.on = isOn;
}
} catch (err) {
console.error('Failed to set layer visibility:', err);
target.checked = !isOn;
showAlert('Error', 'Failed to toggle layer visibility');
}
});
});
// Attach delete handlers
layersList.querySelectorAll('.layer-delete').forEach((btn) => {
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);
// Attach delete handlers
layersList.querySelectorAll('.layer-delete').forEach((btn) => {
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
);
if (!layer) {
showAlert('Error', 'Layer not found');
return;
}
try {
currentDoc.deleteOCG(layer.number);
layersMap.delete(layer.number);
renderLayers();
} catch (err) {
console.error('Failed to delete layer:', err);
showAlert('Error', 'Failed to delete layer');
}
});
});
layersList.querySelectorAll('.layer-add-child').forEach((btn) => {
btn.addEventListener('click', async (e) => {
const target = e.target as HTMLButtonElement;
const parentXref = parseInt(target.dataset.xref || '0');
const parentLayer = Array.from(layersMap.values()).find(l => l.xref === parentXref);
const childName = await promptForInput('Add Child Layer', `Enter name for child layer under "${parentLayer?.text || 'Layer'}":`);
if (!childName || !childName.trim()) return;
try {
const childXref = currentDoc.addOCGWithParent(childName.trim(), parentXref);
const parentDisplayOrder = parentLayer?.displayOrder || 0;
layersMap.forEach((l) => {
if (l.displayOrder > parentDisplayOrder) {
l.displayOrder += 1;
}
});
layersMap.set(childXref, {
number: childXref,
xref: childXref,
text: childName.trim(),
on: true,
locked: false,
depth: (parentLayer?.depth || 0) + 1,
parentXref: parentXref,
displayOrder: parentDisplayOrder + 1
});
renderLayers();
} catch (err) {
console.error('Failed to add child layer:', err);
showAlert('Error', 'Failed to add child layer');
}
});
});
};
const loadLayers = async () => {
if (!currentFile) {
showAlert('No File', 'Please select a PDF file.');
return;
if (!layer) {
showAlert('Error', 'Layer not found');
return;
}
try {
showLoader('Loading engine...');
await pymupdf.load();
showLoader(`Loading layers from ${currentFile.name}...`);
currentDoc = await pymupdf.open(currentFile);
showLoader('Reading layer configuration...');
const existingLayers = currentDoc.getLayerConfig();
// Reset and populate layers map
layersMap.clear();
nextDisplayOrder = 0;
existingLayers.forEach((layer: any) => {
layersMap.set(layer.number, {
number: layer.number,
xref: layer.xref ?? layer.number,
text: layer.text,
on: layer.on,
locked: layer.locked,
depth: layer.depth ?? 0,
parentXref: layer.parentXref ?? 0,
displayOrder: layer.displayOrder ?? nextDisplayOrder++
});
if ((layer.displayOrder ?? -1) >= nextDisplayOrder) {
nextDisplayOrder = layer.displayOrder + 1;
}
});
hideLoader();
// Hide upload zone, show layers container
if (dropZone) dropZone.style.display = 'none';
if (processBtnContainer) processBtnContainer.classList.add('hidden');
if (layersContainer) layersContainer.classList.remove('hidden');
renderLayers();
setupLayerHandlers();
} catch (error: any) {
hideLoader();
showAlert('Error', error.message || 'Failed to load PDF layers');
console.error('Layers error:', error);
currentDoc.deleteOCG(layer.number);
layersMap.delete(layer.number);
renderLayers();
} catch (err) {
console.error('Failed to delete layer:', err);
showAlert('Error', 'Failed to delete layer');
}
};
});
});
const setupLayerHandlers = () => {
const addLayerBtn = document.getElementById('add-layer-btn');
const newLayerInput = document.getElementById('new-layer-name') as HTMLInputElement;
const saveLayersBtn = document.getElementById('save-layers-btn');
layersList.querySelectorAll('.layer-add-child').forEach((btn) => {
btn.addEventListener('click', async (e) => {
const target = e.target as HTMLButtonElement;
const parentXref = parseInt(target.dataset.xref || '0');
const parentLayer = Array.from(layersMap.values()).find(
(l) => l.xref === parentXref
);
if (addLayerBtn && newLayerInput) {
addLayerBtn.onclick = () => {
const name = newLayerInput.value.trim();
if (!name) {
showAlert('Invalid Name', 'Please enter a layer name');
return;
}
const childName = await promptForInput(
'Add Child Layer',
`Enter name for child layer under "${parentLayer?.text || 'Layer'}":`
);
try {
const xref = currentDoc.addOCG(name);
newLayerInput.value = '';
if (!childName || !childName.trim()) return;
const newDisplayOrder = nextDisplayOrder++;
layersMap.set(xref, {
number: xref,
xref: xref,
text: name,
on: true,
locked: false,
depth: 0,
parentXref: 0,
displayOrder: newDisplayOrder
});
renderLayers();
} catch (err: any) {
showAlert('Error', 'Failed to add layer: ' + err.message);
}
};
}
if (saveLayersBtn) {
saveLayersBtn.onclick = () => {
try {
showLoader('Saving PDF with layer changes...');
const pdfBytes = currentDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
const outName = currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
downloadFile(blob, outName);
hideLoader();
resetState();
showAlert('Success', 'PDF with layer changes saved!', 'success');
} catch (err: any) {
hideLoader();
showAlert('Error', 'Failed to save PDF: ' + err.message);
}
};
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
currentFile = file;
updateUI();
} else {
showAlert('Invalid File', 'Please select a PDF file.');
try {
const childXref = currentDoc.addOCGWithParent(
childName.trim(),
parentXref
);
const parentDisplayOrder = parentLayer?.displayOrder || 0;
layersMap.forEach((l) => {
if (l.displayOrder > parentDisplayOrder) {
l.displayOrder += 1;
}
});
layersMap.set(childXref, {
number: childXref,
xref: childXref,
text: childName.trim(),
on: true,
locked: false,
depth: (parentLayer?.depth || 0) + 1,
parentXref: parentXref,
displayOrder: parentDisplayOrder + 1,
});
renderLayers();
} catch (err) {
console.error('Failed to add child layer:', err);
showAlert('Error', 'Failed to add child layer');
}
};
});
});
};
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 = '';
});
const loadLayers = async () => {
if (!currentFile) {
showAlert('No File', 'Please select a PDF file.');
return;
}
if (processBtn) {
processBtn.addEventListener('click', loadLayers);
try {
showLoader('Loading engine...');
const pymupdf = await loadPyMuPDF();
showLoader(`Loading layers from ${currentFile.name}...`);
currentDoc = await pymupdf.open(currentFile);
showLoader('Reading layer configuration...');
const existingLayers = currentDoc.getLayerConfig();
// Reset and populate layers map
layersMap.clear();
nextDisplayOrder = 0;
existingLayers.forEach((layer: any) => {
layersMap.set(layer.number, {
number: layer.number,
xref: layer.xref ?? layer.number,
text: layer.text,
on: layer.on,
locked: layer.locked,
depth: layer.depth ?? 0,
parentXref: layer.parentXref ?? 0,
displayOrder: layer.displayOrder ?? nextDisplayOrder++,
});
if ((layer.displayOrder ?? -1) >= nextDisplayOrder) {
nextDisplayOrder = layer.displayOrder + 1;
}
});
hideLoader();
// Hide upload zone, show layers container
if (dropZone) dropZone.style.display = 'none';
if (processBtnContainer) processBtnContainer.classList.add('hidden');
if (layersContainer) layersContainer.classList.remove('hidden');
renderLayers();
setupLayerHandlers();
} catch (error: any) {
hideLoader();
showAlert('Error', error.message || 'Failed to load PDF layers');
console.error('Layers error:', error);
}
};
const setupLayerHandlers = () => {
const addLayerBtn = document.getElementById('add-layer-btn');
const newLayerInput = document.getElementById(
'new-layer-name'
) as HTMLInputElement;
const saveLayersBtn = document.getElementById('save-layers-btn');
if (addLayerBtn && newLayerInput) {
addLayerBtn.onclick = () => {
const name = newLayerInput.value.trim();
if (!name) {
showAlert('Invalid Name', 'Please enter a layer name');
return;
}
try {
const xref = currentDoc.addOCG(name);
newLayerInput.value = '';
const newDisplayOrder = nextDisplayOrder++;
layersMap.set(xref, {
number: xref,
xref: xref,
text: name,
on: true,
locked: false,
depth: 0,
parentXref: 0,
displayOrder: newDisplayOrder,
});
renderLayers();
} catch (err: any) {
showAlert('Error', 'Failed to add layer: ' + err.message);
}
};
}
if (saveLayersBtn) {
saveLayersBtn.onclick = () => {
try {
showLoader('Saving PDF with layer changes...');
const pdfBytes = currentDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)], {
type: 'application/pdf',
});
const outName =
currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
downloadFile(blob, outName);
hideLoader();
resetState();
showAlert('Success', 'PDF with layer changes saved!', 'success');
} catch (err: any) {
hideLoader();
showAlert('Error', 'Failed to save PDF: ' + err.message);
}
};
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const file = files[0];
if (
file.type === 'application/pdf' ||
file.name.toLowerCase().endsWith('.pdf')
) {
currentFile = file;
updateUI();
} else {
showAlert('Invalid File', 'Please select a PDF file.');
}
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', loadLayers);
}
});

View File

@@ -2,171 +2,186 @@ 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 = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
if (!fileDisplayArea || !optionsPanel) return;
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (file) {
optionsPanel.classList.remove('hidden');
if (file) {
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';
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
file = null;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
file = null;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
};
function tableToCsv(rows: (string | null)[][]): string {
return rows.map(row =>
row.map(cell => {
const cellStr = cell ?? '';
if (cellStr.includes(',') || cellStr.includes('"') || cellStr.includes('\n')) {
return `"${cellStr.replace(/"/g, '""')}"`;
}
return cellStr;
}).join(',')
).join('\n');
return rows
.map((row) =>
row
.map((cell) => {
const cellStr = cell ?? '';
if (
cellStr.includes(',') ||
cellStr.includes('"') ||
cellStr.includes('\n')
) {
return `"${cellStr.replace(/"/g, '""')}"`;
}
return cellStr;
})
.join(',')
)
.join('\n');
}
async function convert() {
if (!file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
if (!file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Loading Engine...');
try {
const pymupdf = await loadPyMuPDF();
showLoader('Extracting tables...');
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
const baseName = file.name.replace(/\.[^/.]+$/, '');
const allRows: (string | null)[][] = [];
for (let i = 0; i < pageCount; i++) {
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
const page = doc.getPage(i);
const tables = page.findTables();
tables.forEach((table) => {
allRows.push(...table.rows);
allRows.push([]);
});
}
showLoader('Loading Engine...');
try {
await pymupdf.load();
showLoader('Extracting tables...');
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
const baseName = file.name.replace(/\.[^/.]+$/, '');
const allRows: (string | null)[][] = [];
for (let i = 0; i < pageCount; i++) {
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
const page = doc.getPage(i);
const tables = page.findTables();
tables.forEach((table) => {
allRows.push(...table.rows);
allRows.push([]);
});
}
if (allRows.length === 0) {
showAlert('No Tables Found', 'No tables were detected in this PDF.');
return;
}
const csvContent = tableToCsv(allRows.filter(row => row.length > 0));
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
downloadFile(blob, `${baseName}.csv`);
showAlert('Success', 'PDF converted to CSV successfully!', 'success', resetState);
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
showAlert('Error', `Failed to convert PDF to CSV. ${message}`);
} finally {
hideLoader();
if (allRows.length === 0) {
showAlert('No Tables Found', 'No tables were detected in this PDF.');
return;
}
const csvContent = tableToCsv(allRows.filter((row) => row.length > 0));
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
downloadFile(blob, `${baseName}.csv`);
showAlert(
'Success',
'PDF converted to CSV successfully!',
'success',
resetState
);
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
showAlert('Error', `Failed to convert PDF to CSV. ${message}`);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFile = Array.from(newFiles).find(
(f) => f.type === 'application/pdf'
);
if (!validFile) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
file = validFile;
updateUI();
};
if (!validFile) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
file = validFile;
updateUI();
};
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -1,203 +1,216 @@
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;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const convertOptions = document.getElementById('convert-options');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const convertOptions = document.getElementById('convert-options');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls) return;
const updateUI = async () => {
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_: File, i: number) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_: File, i: number) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
}
createIcons({ icons });
fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convert = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading PDF converter...');
await pymupdf.load();
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const docxBlob = await pymupdf.pdfToDocx(file);
const outName = file.name.replace(/\.pdf$/i, '') + '.docx';
downloadFile(docxBlob, outName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${file.name} to DOCX.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
const docxBlob = await pymupdf.pdfToDocx(file);
const baseName = file.name.replace(/\.pdf$/i, '');
const arrayBuffer = await docxBlob.arrayBuffer();
zip.file(`${baseName}.docx`, arrayBuffer);
}
showLoader('Creating ZIP archive...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted-documents.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PDF(s) to DOCX.`,
'success',
() => resetState()
);
}
} catch (e: any) {
hideLoader();
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
};
}
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
state.files = [...state.files, ...pdfFiles];
updateUI();
createIcons({ icons });
fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convert = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading PDF converter...');
const pymupdf = await loadPyMuPDF();
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const docxBlob = await pymupdf.pdfToDocx(file);
const outName = file.name.replace(/\.pdf$/i, '') + '.docx';
downloadFile(docxBlob, outName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${file.name} to DOCX.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
);
const docxBlob = await pymupdf.pdfToDocx(file);
const baseName = file.name.replace(/\.pdf$/i, '');
const arrayBuffer = await docxBlob.arrayBuffer();
zip.file(`${baseName}.docx`, arrayBuffer);
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
showLoader('Creating ZIP archive...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
downloadFile(zipBlob, 'converted-documents.zip');
hideLoader();
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PDF(s) to DOCX.`,
'success',
() => resetState()
);
}
} catch (e: any) {
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(
(f) =>
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
state.files = [...state.files, ...pdfFiles];
updateUI();
}
};
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (processBtn) {
processBtn.addEventListener('click', convert);
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -1,182 +1,194 @@
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 = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
if (!fileDisplayArea || !optionsPanel) return;
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (file) {
optionsPanel.classList.remove('hidden');
if (file) {
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';
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
file = null;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
file = null;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
};
async function convert() {
if (!file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
if (!file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Loading Engine...');
try {
const pymupdf = await loadPyMuPDF();
showLoader('Extracting tables...');
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
const baseName = file.name.replace(/\.[^/.]+$/, '');
interface TableData {
page: number;
rows: (string | null)[][];
}
showLoader('Loading Engine...');
const allTables: TableData[] = [];
try {
await pymupdf.load();
showLoader('Extracting tables...');
for (let i = 0; i < pageCount; i++) {
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
const page = doc.getPage(i);
const tables = page.findTables();
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
const baseName = file.name.replace(/\.[^/.]+$/, '');
interface TableData {
page: number;
rows: (string | null)[][];
}
const allTables: TableData[] = [];
for (let i = 0; i < pageCount; i++) {
showLoader(`Scanning page ${i + 1} of ${pageCount}...`);
const page = doc.getPage(i);
const tables = page.findTables();
tables.forEach((table) => {
allTables.push({
page: i + 1,
rows: table.rows
});
});
}
if (allTables.length === 0) {
showAlert('No Tables Found', 'No tables were detected in this PDF.');
return;
}
showLoader('Creating Excel file...');
const workbook = XLSX.utils.book_new();
if (allTables.length === 1) {
const worksheet = XLSX.utils.aoa_to_sheet(allTables[0].rows);
XLSX.utils.book_append_sheet(workbook, worksheet, 'Table');
} else {
allTables.forEach((table, idx) => {
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(0, 31);
const worksheet = XLSX.utils.aoa_to_sheet(table.rows);
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
});
}
const xlsxData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([xlsxData], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
downloadFile(blob, `${baseName}.xlsx`);
showAlert('Success', `Extracted ${allTables.length} table(s) to Excel!`, 'success', resetState);
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
showAlert('Error', `Failed to convert PDF to Excel. ${message}`);
} finally {
hideLoader();
tables.forEach((table) => {
allTables.push({
page: i + 1,
rows: table.rows,
});
});
}
if (allTables.length === 0) {
showAlert('No Tables Found', 'No tables were detected in this PDF.');
return;
}
showLoader('Creating Excel file...');
const workbook = XLSX.utils.book_new();
if (allTables.length === 1) {
const worksheet = XLSX.utils.aoa_to_sheet(allTables[0].rows);
XLSX.utils.book_append_sheet(workbook, worksheet, 'Table');
} else {
allTables.forEach((table, idx) => {
const sheetName = `Table ${idx + 1} (Page ${table.page})`.substring(
0,
31
);
const worksheet = XLSX.utils.aoa_to_sheet(table.rows);
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
});
}
const xlsxData = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
const blob = new Blob([xlsxData], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
downloadFile(blob, `${baseName}.xlsx`);
showAlert(
'Success',
`Extracted ${allTables.length} table(s) to Excel!`,
'success',
resetState
);
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
showAlert('Error', `Failed to convert PDF to Excel. ${message}`);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFile = Array.from(newFiles).find(
(f) => f.type === 'application/pdf'
);
if (!validFile) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFile = Array.from(newFiles).find(f => f.type === 'application/pdf');
file = validFile;
updateUI();
};
if (!validFile) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
file = validFile;
updateUI();
};
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

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'
? '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.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');
}
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')
worker.postMessage({
command: 'convert',
fileBuffers: fileBuffers,
fileNames: selectedFiles.map(f => f.name)
}, fileBuffers);
showStatus('Converting PDFs to JSON..', 'info');
worker.postMessage(
{
command: 'convert',
fileBuffers: 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,206 +1,221 @@
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;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const convertOptions = document.getElementById('convert-options');
const fileControls = document.getElementById('file-controls');
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 fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const convertOptions = document.getElementById('convert-options');
const fileControls = document.getElementById('file-controls');
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;
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls) return;
const updateUI = async () => {
if (!fileDisplayArea || !convertOptions || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_: File, i: number) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_: File, i: number) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
}
createIcons({ icons });
fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convert = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading PDF converter...');
await pymupdf.load();
const includeImages = includeImagesCheckbox?.checked ?? false;
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
const outName = file.name.replace(/\.pdf$/i, '') + '.md';
const blob = new Blob([markdown], { type: 'text/markdown' });
downloadFile(blob, outName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${file.name} to Markdown.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
const baseName = file.name.replace(/\.pdf$/i, '');
zip.file(`${baseName}.md`, markdown);
}
showLoader('Creating ZIP archive...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'markdown-files.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PDF(s) to Markdown.`,
'success',
() => resetState()
);
}
} catch (e: any) {
hideLoader();
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
};
}
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
state.files = [...state.files, ...pdfFiles];
updateUI();
createIcons({ icons });
fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convert = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading PDF converter...');
const pymupdf = await loadPyMuPDF();
const includeImages = includeImagesCheckbox?.checked ?? false;
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
const outName = file.name.replace(/\.pdf$/i, '') + '.md';
const blob = new Blob([markdown], { type: 'text/markdown' });
downloadFile(blob, outName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${file.name} to Markdown.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
);
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
const baseName = file.name.replace(/\.pdf$/i, '');
zip.file(`${baseName}.md`, markdown);
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
showLoader('Creating ZIP archive...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
downloadFile(zipBlob, 'markdown-files.zip');
hideLoader();
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PDF(s) to Markdown.`,
'success',
() => resetState()
);
}
} catch (e: any) {
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(
(f) =>
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
state.files = [...state.files, ...pdfFiles];
updateUI();
}
};
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (processBtn) {
processBtn.addEventListener('click', convert);
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -1,228 +1,270 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
getPDFDocument,
downloadFile,
readFileAsArrayBuffer,
formatBytes,
getPDFDocument,
} from '../utils/helpers.js';
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;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const optionsContainer = document.getElementById('options-container');
const fileControls = document.getElementById('file-controls');
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 fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const optionsContainer = document.getElementById('options-container');
const fileControls = document.getElementById('file-controls');
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;
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !optionsContainer || !processBtn || !fileControls) return;
const updateUI = async () => {
if (!fileDisplayArea || !optionsContainer || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
}
createIcons({ icons });
fileControls.classList.remove('hidden');
optionsContainer.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
optionsContainer.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
if (pdfaLevelSelect) pdfaLevelSelect.value = 'PDF/A-2b';
updateUI();
};
const convertToPdfA = async () => {
const level = pdfaLevelSelect.value as PdfALevel;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
hideLoader();
return;
}
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
}
if (state.files.length === 1) {
const originalFile = state.files[0];
createIcons({ icons });
fileControls.classList.remove('hidden');
optionsContainer.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
optionsContainer.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
showLoader('Initializing Ghostscript...');
const resetState = () => {
state.files = [];
state.pdfDoc = null;
const convertedBlob = await convertFileToPdfA(
originalFile,
level,
(msg) => showLoader(msg)
);
if (pdfaLevelSelect) pdfaLevelSelect.value = 'PDF/A-2b';
const fileName = originalFile.name.replace(/\.pdf$/i, '') + '_pdfa.pdf';
updateUI();
};
downloadFile(convertedBlob, fileName);
const convertToPdfA = async () => {
const level = pdfaLevelSelect.value as PdfALevel;
hideLoader();
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
hideLoader();
return;
}
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to ${level}.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple PDFs to PDF/A...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
if (state.files.length === 1) {
const originalFile = state.files[0];
const preFlattenCheckbox = document.getElementById(
'pre-flatten'
) as HTMLInputElement;
const shouldPreFlatten = preFlattenCheckbox?.checked || false;
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
let fileToConvert = originalFile;
const convertedBlob = await convertFileToPdfA(
file,
level,
(msg) => showLoader(msg)
);
const baseName = file.name.replace(/\.pdf$/i, '');
const blobBuffer = await convertedBlob.arrayBuffer();
zip.file(`${baseName}_pdfa.pdf`, blobBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdfa-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PDF(s) to ${level}.`,
'success',
() => resetState()
);
}
} catch (e: any) {
// Pre-flatten using PyMuPDF rasterization if checkbox is checked
if (shouldPreFlatten) {
if (!isPyMuPDFAvailable()) {
showWasmRequiredDialog('pymupdf');
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
return;
}
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
showLoader('Pre-flattening PDF...');
const pymupdf = await loadPyMuPDF();
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
pdfFiles.forEach(f => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
// Rasterize PDF to images and back to PDF (300 DPI for quality)
const flattenedBlob = await (pymupdf as any).rasterizePdf(
originalFile,
{
dpi: 300,
format: 'png',
}
});
);
// Clear value on click to allow re-selecting the same file
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
fileToConvert = new File([flattenedBlob], originalFile.name, {
type: 'application/pdf',
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
showLoader('Initializing Ghostscript...');
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
const convertedBlob = await convertFileToPdfA(
fileToConvert,
level,
(msg) => showLoader(msg)
);
if (processBtn) {
processBtn.addEventListener('click', convertToPdfA);
const fileName = originalFile.name.replace(/\.pdf$/i, '') + '_pdfa.pdf';
downloadFile(convertedBlob, fileName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to ${level}.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple PDFs to PDF/A...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
);
const convertedBlob = await convertFileToPdfA(file, level, (msg) =>
showLoader(msg)
);
const baseName = file.name.replace(/\.pdf$/i, '');
const blobBuffer = await convertedBlob.arrayBuffer();
zip.file(`${baseName}_pdfa.pdf`, blobBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdfa-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PDF(s) to ${level}.`,
'success',
() => resetState()
);
}
} catch (e: any) {
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(
(f) =>
f.type === 'application/pdf' ||
f.name.toLowerCase().endsWith('.pdf')
);
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
pdfFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}
});
// Clear value on click to allow re-selecting the same file
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdfA);
}
});

View File

@@ -2,201 +2,237 @@ 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 = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const fileControls = document.getElementById('file-controls');
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const fileControls = document.getElementById('file-controls');
if (!fileDisplayArea || !optionsPanel) return;
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
optionsPanel.classList.remove('hidden');
if (fileControls) fileControls.classList.remove('hidden');
if (files.length > 0) {
optionsPanel.classList.remove('hidden');
if (fileControls) fileControls.classList.remove('hidden');
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';
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';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
if (fileControls) fileControls.classList.add('hidden');
}
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
if (fileControls) fileControls.classList.add('hidden');
}
};
const resetState = () => {
files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
};
async function convert() {
if (files.length === 0) {
showAlert('No Files', 'Please upload at least one PDF file.');
return;
if (files.length === 0) {
showAlert('No Files', 'Please upload at least one PDF file.');
return;
}
// Check if PyMuPDF is configured
if (!isPyMuPDFAvailable()) {
showWasmRequiredDialog('pymupdf');
return;
}
showLoader('Loading Engine...');
try {
// Load PyMuPDF dynamically if not already loaded
if (!pymupdf) {
pymupdf = await loadPyMuPDF();
}
showLoader('Loading Engine...');
const isSingleFile = files.length === 1;
try {
await pymupdf.load();
if (isSingleFile) {
const doc = await pymupdf.open(files[0]);
const pageCount = doc.pageCount;
const baseName = files[0].name.replace(/\.[^/.]+$/, '');
const isSingleFile = files.length === 1;
if (isSingleFile) {
const doc = await pymupdf.open(files[0]);
const pageCount = doc.pageCount;
const baseName = files[0].name.replace(/\.[^/.]+$/, '');
if (pageCount === 1) {
showLoader('Converting to SVG...');
const page = doc.getPage(0);
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());
} else {
const zip = new JSZip();
for (let i = 0; i < pageCount; i++) {
showLoader(`Converting page ${i + 1} of ${pageCount}...`);
const page = doc.getPage(i);
const svgContent = page.toSvg();
zip.file(`page_${i + 1}.svg`, svgContent);
}
showLoader('Creating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${baseName}_svg.zip`);
showAlert('Success', `Converted ${pageCount} pages to SVG!`, 'success', () => resetState());
}
} else {
const zip = new JSZip();
let totalPages = 0;
for (let f = 0; f < files.length; f++) {
const file = files[f];
showLoader(`Processing file ${f + 1} of ${files.length}...`);
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
const baseName = file.name.replace(/\.[^/.]+$/, '');
for (let i = 0; i < pageCount; i++) {
showLoader(`File ${f + 1}/${files.length}: Page ${i + 1}/${pageCount}`);
const page = doc.getPage(i);
const svgContent = page.toSvg();
const fileName = pageCount === 1 ? `${baseName}.svg` : `${baseName}_page_${i + 1}.svg`;
zip.file(fileName, svgContent);
totalPages++;
}
}
showLoader('Creating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdf_to_svg.zip');
showAlert('Success', `Converted ${files.length} files (${totalPages} pages) to SVG!`, 'success', () => resetState());
if (pageCount === 1) {
showLoader('Converting to SVG...');
const page = doc.getPage(0);
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()
);
} else {
const zip = new JSZip();
for (let i = 0; i < pageCount; i++) {
showLoader(`Converting page ${i + 1} of ${pageCount}...`);
const page = doc.getPage(i);
const svgContent = page.toSvg();
zip.file(`page_${i + 1}.svg`, svgContent);
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
showAlert('Error', `Failed to convert PDF to SVG. ${message}`);
} finally {
hideLoader();
showLoader('Creating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${baseName}_svg.zip`);
showAlert(
'Success',
`Converted ${pageCount} pages to SVG!`,
'success',
() => resetState()
);
}
} else {
const zip = new JSZip();
let totalPages = 0;
for (let f = 0; f < files.length; f++) {
const file = files[f];
showLoader(`Processing file ${f + 1} of ${files.length}...`);
const doc = await pymupdf.open(file);
const pageCount = doc.pageCount;
const baseName = file.name.replace(/\.[^/.]+$/, '');
for (let i = 0; i < pageCount; i++) {
showLoader(
`File ${f + 1}/${files.length}: Page ${i + 1}/${pageCount}`
);
const page = doc.getPage(i);
const svgContent = page.toSvg();
const fileName =
pageCount === 1
? `${baseName}.svg`
: `${baseName}_page_${i + 1}.svg`;
zip.file(fileName, svgContent);
totalPages++;
}
}
showLoader('Creating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdf_to_svg.zip');
showAlert(
'Success',
`Converted ${files.length} files (${totalPages} pages) to SVG!`,
'success',
() => resetState()
);
}
} catch (e) {
console.error(e);
const message = e instanceof Error ? e.message : 'Unknown error';
showAlert('Error', `Failed to convert PDF to SVG. ${message}`);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = (newFiles: FileList | null, replace = false) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'application/pdf'
);
if (validFiles.length === 0) {
showAlert('Invalid Files', 'Please upload PDF files.');
return;
}
const handleFileSelect = (newFiles: FileList | null, replace = false) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'application/pdf'
);
if (validFiles.length === 0) {
showAlert('Invalid Files', 'Please upload PDF files.');
return;
}
if (replace) {
files = validFiles;
} else {
files = [...files, ...validFiles];
}
updateUI();
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files, files.length === 0);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null, files.length === 0);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
if (replace) {
files = validFiles;
} else {
files = [...files, ...validFiles];
}
updateUI();
};
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput?.click());
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
if (processBtn) processBtn.addEventListener('click', convert);
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect(
(e.target as HTMLInputElement).files,
files.length === 0
);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null, files.length === 0);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn)
addMoreBtn.addEventListener('click', () => fileInput?.click());
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
if (processBtn) processBtn.addEventListener('click', convert);
});

View File

@@ -1,212 +1,233 @@
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);
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
initializePage();
}
function initializePage() {
createIcons({ icons });
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
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 fileInput = document.getElementById('file-input') as HTMLInputElement;
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;
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-600');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-600');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-600');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
});
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput?.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
files = [];
updateUI();
});
}
if (processBtn) {
processBtn.addEventListener('click', extractText);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-600');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-600');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-600');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
});
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput?.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
files = [];
updateUI();
});
}
if (processBtn) {
processBtn.addEventListener('click', extractText);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFiles(input.files);
}
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFiles(input.files);
}
}
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.'
);
}
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped. Only PDF files are allowed.');
}
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
}
const resetState = () => {
files = [];
updateUI();
files = [];
updateUI();
};
function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const extractOptions = document.getElementById('extract-options');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const extractOptions = document.getElementById('extract-options');
if (!fileDisplayArea || !fileControls || !extractOptions) return;
if (!fileDisplayArea || !fileControls || !extractOptions) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
fileControls.classList.remove('hidden');
extractOptions.classList.remove('hidden');
if (files.length > 0) {
fileControls.classList.remove('hidden');
extractOptions.classList.remove('hidden');
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';
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';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
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 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
infoContainer.append(nameSpan, sizeSpan);
infoContainer.append(nameSpan, sizeSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
fileControls.classList.add('hidden');
extractOptions.classList.add('hidden');
}
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
fileControls.classList.add('hidden');
extractOptions.classList.add('hidden');
}
}
async function ensurePyMuPDF(): Promise<PyMuPDF> {
if (!pymupdf) {
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
}
return pymupdf;
async function ensurePyMuPDF(): Promise<any> {
if (!pymupdf) {
pymupdf = await loadPyMuPDF();
}
return pymupdf;
}
async function extractText() {
if (files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
if (files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading engine...');
showLoader('Loading engine...');
try {
const mupdf = await ensurePyMuPDF();
try {
const mupdf = await ensurePyMuPDF();
if (files.length === 1) {
const file = files[0];
showLoader(`Extracting text from ${file.name}...`);
if (files.length === 1) {
const file = files[0];
showLoader(`Extracting text from ${file.name}...`);
const fullText = await mupdf.pdfToText(file);
const fullText = await mupdf.pdfToText(file);
const baseName = file.name.replace(/\.pdf$/i, '');
const textBlob = new Blob([fullText], { type: 'text/plain;charset=utf-8' });
downloadFile(textBlob, `${baseName}.txt`);
const baseName = file.name.replace(/\.pdf$/i, '');
const textBlob = new Blob([fullText], {
type: 'text/plain;charset=utf-8',
});
downloadFile(textBlob, `${baseName}.txt`);
hideLoader();
showAlert('Success', 'Text extracted successfully!', 'success', () => {
resetState();
});
} else {
showLoader('Extracting text from multiple files...');
hideLoader();
showAlert('Success', 'Text extracted successfully!', 'success', () => {
resetState();
});
} else {
showLoader('Extracting text from multiple files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < files.length; i++) {
const file = files[i];
showLoader(`Extracting text from file ${i + 1}/${files.length}: ${file.name}...`);
for (let i = 0; i < files.length; i++) {
const file = files[i];
showLoader(
`Extracting text from file ${i + 1}/${files.length}: ${file.name}...`
);
const fullText = await mupdf.pdfToText(file);
const fullText = await mupdf.pdfToText(file);
const baseName = file.name.replace(/\.pdf$/i, '');
zip.file(`${baseName}.txt`, fullText);
}
const baseName = file.name.replace(/\.pdf$/i, '');
zip.file(`${baseName}.txt`, fullText);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdf-to-text.zip');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdf-to-text.zip');
hideLoader();
showAlert('Success', `Extracted text from ${files.length} PDF files!`, 'success', () => {
resetState();
});
hideLoader();
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.');
);
}
} catch (e: any) {
console.error('[PDFToText]', e);
hideLoader();
showAlert(
'Extraction Error',
e.message || 'Failed to extract text from PDF.'
);
}
}

View File

@@ -1,204 +1,237 @@
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;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const extractOptions = document.getElementById('extract-options');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const extractOptions = document.getElementById('extract-options');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls) return;
const updateUI = async () => {
if (!fileDisplayArea || !extractOptions || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
}
createIcons({ icons });
fileControls.classList.remove('hidden');
extractOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
extractOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const extractForAI = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading engine...');
await pymupdf.load();
const total = state.files.length;
let completed = 0;
let failed = 0;
if (total === 1) {
const file = state.files[0];
showLoader(`Extracting ${file.name} for AI...`);
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
const jsonContent = JSON.stringify(llamaDocs, null, 2);
downloadFile(new Blob([jsonContent], { type: 'application/json' }), outName);
hideLoader();
showAlert('Extraction Complete', `Successfully extracted PDF for AI/LLM use.`, 'success', () => resetState());
} else {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (const file of state.files) {
try {
showLoader(`Extracting ${file.name} for AI (${completed + 1}/${total})...`);
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
const jsonContent = JSON.stringify(llamaDocs, null, 2);
zip.file(outName, jsonContent);
completed++;
} catch (error) {
console.error(`Failed to extract ${file.name}:`, error);
failed++;
}
}
showLoader('Creating ZIP archive...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdf-for-ai.zip');
hideLoader();
if (failed === 0) {
showAlert('Extraction Complete', `Successfully extracted ${completed} PDF(s) for AI/LLM use.`, 'success', () => resetState());
} else {
showAlert('Extraction Partial', `Extracted ${completed} PDF(s), failed ${failed}.`, 'warning', () => resetState());
}
}
} catch (e: any) {
hideLoader();
showAlert('Error', `An error occurred during extraction. Error: ${e.message}`);
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
};
}
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'));
if (pdfFiles.length > 0) {
state.files = [...state.files, ...pdfFiles];
updateUI();
}
createIcons({ icons });
fileControls.classList.remove('hidden');
extractOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
extractOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const extractForAI = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading engine...');
const pymupdf = await loadPyMuPDF();
const total = state.files.length;
let completed = 0;
let failed = 0;
if (total === 1) {
const file = state.files[0];
showLoader(`Extracting ${file.name} for AI...`);
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
const jsonContent = JSON.stringify(llamaDocs, null, 2);
downloadFile(
new Blob([jsonContent], { type: 'application/json' }),
outName
);
hideLoader();
showAlert(
'Extraction Complete',
`Successfully extracted PDF for AI/LLM use.`,
'success',
() => resetState()
);
} else {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (const file of state.files) {
try {
showLoader(
`Extracting ${file.name} for AI (${completed + 1}/${total})...`
);
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
const jsonContent = JSON.stringify(llamaDocs, null, 2);
zip.file(outName, jsonContent);
completed++;
} catch (error) {
console.error(`Failed to extract ${file.name}:`, error);
failed++;
}
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
showLoader('Creating ZIP archive...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
downloadFile(zipBlob, 'pdf-for-ai.zip');
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
hideLoader();
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
if (failed === 0) {
showAlert(
'Extraction Complete',
`Successfully extracted ${completed} PDF(s) for AI/LLM use.`,
'success',
() => resetState()
);
} else {
showAlert(
'Extraction Partial',
`Extracted ${completed} PDF(s), failed ${failed}.`,
'warning',
() => resetState()
);
}
}
} catch (e: any) {
hideLoader();
showAlert(
'Error',
`An error occurred during extraction. Error: ${e.message}`
);
}
};
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
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')
);
if (pdfFiles.length > 0) {
state.files = [...state.files, ...pdfFiles];
updateUI();
}
}
};
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', resetState);
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (processBtn) {
processBtn.addEventListener('click', extractForAI);
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', resetState);
}
if (processBtn) {
processBtn.addEventListener('click', extractForAI);
}
});

View File

@@ -2,133 +2,165 @@ 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> {
if (!pymupdf) {
pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
}
return pymupdf;
async function ensurePyMuPDF(): Promise<any> {
if (!pymupdf) {
pymupdf = await loadPyMuPDF();
}
return pymupdf;
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
}
};
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const resetState = () => {
state.files = [];
const convert = async () => {
if (state.files.length === 0) {
showAlert(
'No Files',
`Please select at least one ${FILETYPE_NAME} file.`
);
return;
}
try {
showLoader('Loading engine...');
const mupdf = await ensurePyMuPDF();
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await mupdf.imageToPdf(file, { imageType: 'psd' });
const baseName = file.name.replace(/\.[^/.]+$/, '');
downloadFile(pdfBlob, `${baseName}.pdf`);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${file.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple files...');
const pdfBlob = await mupdf.imagesToPdf(state.files);
downloadFile(pdfBlob, 'psd_to_pdf.pdf');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PSD files to a single PDF.`,
'success',
() => resetState()
);
}
} catch (err) {
hideLoader();
const message = err instanceof Error ? err.message : 'Unknown error';
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
showAlert(
'Error',
`An error occurred during conversion. Error: ${message}`
);
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const validFiles = Array.from(files).filter((file) => {
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
return ACCEPTED_EXTENSIONS.includes(ext);
});
if (validFiles.length > 0) {
state.files = [...state.files, ...validFiles];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
showLoader('Loading engine...');
const mupdf = await ensurePyMuPDF();
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await mupdf.imageToPdf(file, { imageType: 'psd' });
const baseName = file.name.replace(/\.[^/.]+$/, '');
downloadFile(pdfBlob, `${baseName}.pdf`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${file.name} to PDF.`, 'success', () => resetState());
} else {
showLoader('Converting multiple files...');
const pdfBlob = await mupdf.imagesToPdf(state.files);
downloadFile(pdfBlob, 'psd_to_pdf.pdf');
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} PSD files to a single PDF.`, 'success', () => resetState());
}
} catch (err) {
hideLoader();
const message = err instanceof Error ? err.message : 'Unknown error';
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, err);
showAlert('Error', `An error occurred during conversion. Error: ${message}`);
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(file => {
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
return ACCEPTED_EXTENSIONS.includes(ext);
});
if (validFiles.length > 0) {
state.files = [...state.files, ...validFiles];
updateUI();
}
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => handleFileSelect((e.target as HTMLInputElement).files));
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); });
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('bg-gray-700'); handleFileSelect(e.dataTransfer?.files ?? null); });
fileInput.addEventListener('click', () => { fileInput.value = ''; });
}
}
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput.click());
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
if (processBtn) processBtn.addEventListener('click', convert);
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) =>
handleFileSelect((e.target as HTMLInputElement).files)
);
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput.click());
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
if (processBtn) processBtn.addEventListener('click', convert);
});

View File

@@ -1,219 +1,262 @@
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;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const rasterizeOptions = document.getElementById('rasterize-options');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const rasterizeOptions = document.getElementById('rasterize-options');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !rasterizeOptions || !processBtn || !fileControls) return;
const updateUI = async () => {
if (!fileDisplayArea || !rasterizeOptions || !processBtn || !fileControls)
return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
}
createIcons({ icons });
fileControls.classList.remove('hidden');
rasterizeOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
rasterizeOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const rasterize = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading engine...');
await pymupdf.load();
// Get options from UI
const dpi = parseInt((document.getElementById('rasterize-dpi') as HTMLSelectElement).value) || 150;
const format = (document.getElementById('rasterize-format') as HTMLSelectElement).value as 'png' | 'jpeg';
const grayscale = (document.getElementById('rasterize-grayscale') as HTMLInputElement).checked;
const total = state.files.length;
let completed = 0;
let failed = 0;
if (total === 1) {
const file = state.files[0];
showLoader(`Rasterizing ${file.name}...`);
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
dpi,
format,
grayscale,
quality: 95
});
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
downloadFile(rasterizedBlob, outName);
hideLoader();
showAlert('Rasterization Complete', `Successfully rasterized PDF at ${dpi} DPI.`, 'success', () => resetState());
} else {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (const file of state.files) {
try {
showLoader(`Rasterizing ${file.name} (${completed + 1}/${total})...`);
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
dpi,
format,
grayscale,
quality: 95
});
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
zip.file(outName, rasterizedBlob);
completed++;
} catch (error) {
console.error(`Failed to rasterize ${file.name}:`, error);
failed++;
}
}
showLoader('Creating ZIP archive...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'rasterized-pdfs.zip');
hideLoader();
if (failed === 0) {
showAlert('Rasterization Complete', `Successfully rasterized ${completed} PDF(s) at ${dpi} DPI.`, 'success', () => resetState());
} else {
showAlert('Rasterization Partial', `Rasterized ${completed} PDF(s), failed ${failed}.`, 'warning', () => resetState());
}
}
} catch (e: any) {
hideLoader();
showAlert('Error', `An error occurred during rasterization. Error: ${e.message}`);
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
}
};
}
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'));
if (pdfFiles.length > 0) {
state.files = [...state.files, ...pdfFiles];
updateUI();
}
createIcons({ icons });
fileControls.classList.remove('hidden');
rasterizeOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
rasterizeOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const rasterize = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
if (!isPyMuPDFAvailable()) {
showWasmRequiredDialog('pymupdf');
return;
}
showLoader('Loading engine...');
const pymupdf = await loadPyMuPDF();
// Get options from UI
const dpi =
parseInt(
(document.getElementById('rasterize-dpi') as HTMLSelectElement).value
) || 150;
const format = (
document.getElementById('rasterize-format') as HTMLSelectElement
).value as 'png' | 'jpeg';
const grayscale = (
document.getElementById('rasterize-grayscale') as HTMLInputElement
).checked;
const total = state.files.length;
let completed = 0;
let failed = 0;
if (total === 1) {
const file = state.files[0];
showLoader(`Rasterizing ${file.name}...`);
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
dpi,
format,
grayscale,
quality: 95,
});
const outName = file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
downloadFile(rasterizedBlob, outName);
hideLoader();
showAlert(
'Rasterization Complete',
`Successfully rasterized PDF at ${dpi} DPI.`,
'success',
() => resetState()
);
} else {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (const file of state.files) {
try {
showLoader(
`Rasterizing ${file.name} (${completed + 1}/${total})...`
);
const rasterizedBlob = await (pymupdf as any).rasterizePdf(file, {
dpi,
format,
grayscale,
quality: 95,
});
const outName =
file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
zip.file(outName, rasterizedBlob);
completed++;
} catch (error) {
console.error(`Failed to rasterize ${file.name}:`, error);
failed++;
}
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
showLoader('Creating ZIP archive...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
downloadFile(zipBlob, 'rasterized-pdfs.zip');
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
hideLoader();
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
if (failed === 0) {
showAlert(
'Rasterization Complete',
`Successfully rasterized ${completed} PDF(s) at ${dpi} DPI.`,
'success',
() => resetState()
);
} else {
showAlert(
'Rasterization Partial',
`Rasterized ${completed} PDF(s), failed ${failed}.`,
'warning',
() => resetState()
);
}
}
} catch (e: any) {
hideLoader();
showAlert(
'Error',
`An error occurred during rasterization. Error: ${e.message}`
);
}
};
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
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')
);
if (pdfFiles.length > 0) {
state.files = [...state.files, ...pdfFiles];
updateUI();
}
}
};
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', resetState);
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (processBtn) {
processBtn.addEventListener('click', rasterize);
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', resetState);
}
if (processBtn) {
processBtn.addEventListener('click', rasterize);
}
});

File diff suppressed because it is too large Load Diff

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,12 +61,13 @@ function showStatus(
type: 'success' | 'error' | 'info' = 'info'
) {
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.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');
}
@@ -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,252 +1,280 @@
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);
return RTL_PATTERN.test(text);
}
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const dropZone = document.getElementById('drop-zone');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const dropZone = document.getElementById('drop-zone');
if (!fileDisplayArea || !fileControls || !dropZone) return;
if (!fileDisplayArea || !fileControls || !dropZone) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (files.length > 0 && currentMode === 'upload') {
dropZone.classList.add('hidden');
fileControls.classList.remove('hidden');
if (files.length > 0 && currentMode === 'upload') {
dropZone.classList.add('hidden');
fileControls.classList.remove('hidden');
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';
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';
const infoSpan = document.createElement('span');
infoSpan.className = 'truncate font-medium text-gray-200';
infoSpan.textContent = file.name;
const infoSpan = document.createElement('span');
infoSpan.className = 'truncate font-medium text-gray-200';
infoSpan.textContent = file.name;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'text-gray-400 text-xs ml-2';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'text-gray-400 text-xs ml-2';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoSpan, sizeSpan, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
dropZone.classList.remove('hidden');
fileControls.classList.add('hidden');
}
fileDiv.append(infoSpan, sizeSpan, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
dropZone.classList.remove('hidden');
fileControls.classList.add('hidden');
}
};
const resetState = () => {
files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
if (fileInput) fileInput.value = '';
if (textInput) textInput.value = '';
updateUI();
files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
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.');
return;
if (currentMode === 'upload' && files.length === 0) {
showAlert('No Files', 'Please select at least one text file.');
return;
}
if (currentMode === 'text') {
const textInput = document.getElementById(
'text-input'
) as HTMLTextAreaElement;
if (!textInput.value.trim()) {
showAlert('No Text', 'Please enter some text to convert.');
return;
}
}
showLoader('Loading engine...');
try {
const pymupdf = await loadPyMuPDF();
let textContent = '';
if (currentMode === 'upload') {
for (const file of files) {
const text = await file.text();
textContent += text + '\n\n';
}
} else {
const textInput = document.getElementById(
'text-input'
) as HTMLTextAreaElement;
textContent = textInput.value;
}
if (currentMode === 'text') {
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
if (!textInput.value.trim()) {
showAlert('No Text', 'Please enter some text to convert.');
return;
}
}
showLoader('Creating PDF...');
showLoader('Loading engine...');
const pdfBlob = await pymupdf.textToPdf(textContent, {
fontSize,
pageSize: pageSizeKey as 'a4' | 'letter' | 'legal' | 'a3' | 'a5',
fontName: fontName as 'helv' | 'tiro' | 'cour' | 'times',
textColor,
margins: 72,
});
try {
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
downloadFile(pdfBlob, 'text_to_pdf.pdf');
let textContent = '';
if (currentMode === 'upload') {
for (const file of files) {
const text = await file.text();
textContent += text + '\n\n';
}
} else {
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
textContent = textInput.value;
}
showLoader('Creating PDF...');
const pdfBlob = await pymupdf.textToPdf(textContent, {
fontSize,
pageSize: pageSizeKey as 'a4' | 'letter' | 'legal' | 'a3' | 'a5',
fontName: fontName as 'helv' | 'tiro' | 'cour' | 'times',
textColor,
margins: 72
});
downloadFile(pdfBlob, 'text_to_pdf.pdf');
showAlert('Success', 'Text converted to PDF successfully!', 'success', () => {
resetState();
});
} catch (e: any) {
console.error('[TxtToPDF] Error:', e);
showAlert('Error', `Failed to convert text to PDF. ${e.message || ''}`);
} finally {
hideLoader();
}
showAlert(
'Success',
'Text converted to PDF successfully!',
'success',
() => {
resetState();
}
);
} catch (e: any) {
console.error('[TxtToPDF] Error:', e);
showAlert('Error', `Failed to convert text to PDF. ${e.message || ''}`);
} finally {
hideLoader();
}
}
// Update textarea direction based on RTL detection
function updateTextareaDirection(textarea: HTMLTextAreaElement) {
const text = textarea.value;
if (hasRtlCharacters(text)) {
textarea.style.direction = 'rtl';
textarea.style.textAlign = 'right';
} else {
textarea.style.direction = 'ltr';
textarea.style.textAlign = 'left';
}
const text = textarea.value;
if (hasRtlCharacters(text)) {
textarea.style.direction = 'rtl';
textarea.style.textAlign = 'right';
} else {
textarea.style.direction = 'ltr';
textarea.style.textAlign = 'left';
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
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');
const backBtn = document.getElementById('back-to-tools');
const uploadModeBtn = document.getElementById('txt-mode-upload-btn');
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 fileInput = document.getElementById('file-input') as HTMLInputElement;
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');
const backBtn = document.getElementById('back-to-tools');
const uploadModeBtn = document.getElementById('txt-mode-upload-btn');
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;
// Back to Tools
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
// Back to Tools
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
// Mode switching
if (uploadModeBtn && textModeBtn && uploadPanel && textPanel) {
uploadModeBtn.addEventListener('click', () => {
currentMode = 'upload';
uploadModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
uploadModeBtn.classList.add('bg-indigo-600', 'text-white');
textModeBtn.classList.remove('bg-indigo-600', 'text-white');
textModeBtn.classList.add('bg-gray-700', 'text-gray-300');
uploadPanel.classList.remove('hidden');
textPanel.classList.add('hidden');
});
textModeBtn.addEventListener('click', () => {
currentMode = 'text';
textModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
textModeBtn.classList.add('bg-indigo-600', 'text-white');
uploadModeBtn.classList.remove('bg-indigo-600', 'text-white');
uploadModeBtn.classList.add('bg-gray-700', 'text-gray-300');
textPanel.classList.remove('hidden');
uploadPanel.classList.add('hidden');
});
}
// RTL auto-detection for textarea
if (textInput) {
textInput.addEventListener('input', () => {
updateTextareaDirection(textInput);
});
}
// File handling
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) =>
file.name.toLowerCase().endsWith('.txt') || file.type === 'text/plain'
);
if (validFiles.length < newFiles.length) {
showAlert(
'Invalid Files',
'Some files were skipped. Only text files are allowed.'
);
}
// Mode switching
if (uploadModeBtn && textModeBtn && uploadPanel && textPanel) {
uploadModeBtn.addEventListener('click', () => {
currentMode = 'upload';
uploadModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
uploadModeBtn.classList.add('bg-indigo-600', 'text-white');
textModeBtn.classList.remove('bg-indigo-600', 'text-white');
textModeBtn.classList.add('bg-gray-700', 'text-gray-300');
uploadPanel.classList.remove('hidden');
textPanel.classList.add('hidden');
});
textModeBtn.addEventListener('click', () => {
currentMode = 'text';
textModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
textModeBtn.classList.add('bg-indigo-600', 'text-white');
uploadModeBtn.classList.remove('bg-indigo-600', 'text-white');
uploadModeBtn.classList.add('bg-gray-700', 'text-gray-300');
textPanel.classList.remove('hidden');
uploadPanel.classList.add('hidden');
});
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
};
// RTL auto-detection for textarea
if (textInput) {
textInput.addEventListener('input', () => {
updateTextareaDirection(textInput);
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
// File handling
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.name.toLowerCase().endsWith('.txt') || file.type === 'text/plain'
);
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped. Only text files are allowed.');
}
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
};
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (addMoreBtn && fileInput) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
files = [];
updateUI();
});
}
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
if (processBtn) {
processBtn.addEventListener('click', convert);
}
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn && fileInput) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
files = [];
updateUI();
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
createIcons({ icons });
createIcons({ icons });
});

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,201 +2,212 @@ 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'];
const TOOL_NAME = 'XPS';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
}
};
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
for (let index = 0; index < state.files.length; index++) {
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';
showLoader('Loading engine...');
const pymupdf = await loadPyMuPDF();
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const pdfBlob = await pymupdf.convertToPdf(originalFile, {
filetype: FILETYPE,
});
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
downloadFile(pdfBlob, fileName);
hideLoader();
infoContainer.append(nameSpan, metaSpan);
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(
`Converting ${i + 1}/${state.files.length}: ${file.name}...`
);
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
const pdfBlob = await pymupdf.convertToPdf(file, {
filetype: FILETYPE,
});
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
hideLoader();
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(getWasmBaseUrl('pymupdf'));
await pymupdf.load();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
const fileName = originalFile.name.replace(/\.[^.]+$/, '') + '.pdf';
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
downloadFile(pdfBlob, fileName);
hideLoader();
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
const pdfBlob = await pymupdf.convertToPdf(file, { filetype: FILETYPE });
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE}-converted.zip`);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ${TOOL_NAME} file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error(`[${TOOL_NAME}2PDF] ERROR:`, e);
hideLoader();
showAlert('Error', `An error occurred during conversion. Error: ${e.message}`);
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter((f) => {
const name = f.name.toLowerCase();
return EXTENSIONS.some((ext) => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach((f) => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
};
}
});
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const validFiles = Array.from(files).filter(f => {
const name = f.name.toLowerCase();
return EXTENSIONS.some(ext => name.endsWith(ext));
});
if (validFiles.length > 0) {
const dataTransfer = new DataTransfer();
validFiles.forEach(f => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});

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,132 +1,159 @@
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'
IDLE = 'idle',
LOADING = 'loading',
READY = 'ready',
ERROR = 'error',
UNAVAILABLE = 'unavailable',
}
interface PreloadState {
libreoffice: PreloadStatus;
pymupdf: PreloadStatus;
ghostscript: PreloadStatus;
libreoffice: PreloadStatus;
pymupdf: PreloadStatus;
ghostscript: PreloadStatus;
}
const preloadState: PreloadState = {
libreoffice: PreloadStatus.IDLE,
pymupdf: PreloadStatus.IDLE,
ghostscript: PreloadStatus.IDLE
libreoffice: PreloadStatus.IDLE,
pymupdf: 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);
}
return { ...preloadState };
}
async function preloadPyMuPDF(): Promise<void> {
if (preloadState.pymupdf !== PreloadStatus.IDLE) return;
if (preloadState.pymupdf !== PreloadStatus.IDLE) return;
preloadState.pymupdf = PreloadStatus.LOADING;
console.log('[Preloader] Starting PyMuPDF preload...');
if (!isWasmAvailable('pymupdf')) {
preloadState.pymupdf = PreloadStatus.UNAVAILABLE;
console.log('[Preloader] PyMuPDF not configured, skipping preload');
return;
}
try {
const pymupdfBaseUrl = getWasmBaseUrl('pymupdf');
pymupdfInstance = new PyMuPDF(pymupdfBaseUrl);
await pymupdfInstance.load();
preloadState.pymupdf = PreloadStatus.READY;
console.log('[Preloader] PyMuPDF ready');
} catch (e) {
preloadState.pymupdf = PreloadStatus.ERROR;
console.warn('[Preloader] PyMuPDF preload failed:', e);
}
preloadState.pymupdf = PreloadStatus.LOADING;
console.log('[Preloader] Starting PyMuPDF preload...');
try {
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');
} catch (e) {
preloadState.pymupdf = PreloadStatus.ERROR;
console.warn('[Preloader] PyMuPDF preload failed:', e);
}
}
async function preloadGhostscript(): Promise<void> {
if (preloadState.ghostscript !== PreloadStatus.IDLE) return;
if (preloadState.ghostscript !== PreloadStatus.IDLE) return;
preloadState.ghostscript = PreloadStatus.LOADING;
console.log('[Preloader] Starting Ghostscript WASM preload...');
if (!isWasmAvailable('ghostscript')) {
preloadState.ghostscript = PreloadStatus.UNAVAILABLE;
console.log('[Preloader] Ghostscript not configured, skipping preload');
return;
}
try {
const gsBaseUrl = getWasmBaseUrl('ghostscript');
const gsModule = await loadGsWASM({
locateFile: (path: string) => {
if (path.endsWith('.wasm')) {
return gsBaseUrl + 'gs.wasm';
}
return path;
},
print: () => { },
printErr: () => { },
});
setCachedGsModule(gsModule as any);
preloadState.ghostscript = PreloadStatus.READY;
console.log('[Preloader] Ghostscript WASM ready');
} catch (e) {
preloadState.ghostscript = PreloadStatus.ERROR;
console.warn('[Preloader] Ghostscript preload failed:', e);
preloadState.ghostscript = PreloadStatus.LOADING;
console.log('[Preloader] Starting Ghostscript WASM preload...');
try {
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 `${normalizedUrl}assets/gs.wasm`;
}
return path;
},
print: () => {},
printErr: () => {},
});
setCachedGsModule(gsModule as any);
preloadState.ghostscript = PreloadStatus.READY;
console.log('[Preloader] Ghostscript WASM ready');
} catch (e) {
preloadState.ghostscript = PreloadStatus.ERROR;
console.warn('[Preloader] Ghostscript preload failed:', e);
}
}
function scheduleIdleTask(task: () => Promise<void>): void {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => task(), { timeout: 5000 });
} else {
setTimeout(() => task(), 1000);
}
if ('requestIdleCallback' in window) {
requestIdleCallback(() => task(), { timeout: 5000 });
} else {
setTimeout(() => task(), 1000);
}
}
export function startBackgroundPreload(): void {
console.log('[Preloader] Scheduling background WASM preloads...');
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'
];
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',
];
const currentPath = window.location.pathname;
const isLibreOfficePage = libreOfficePages.some(page => currentPath.includes(page));
const currentPath = window.location.pathname;
const isLibreOfficePage = libreOfficePages.some((page) =>
currentPath.includes(page)
);
if (isLibreOfficePage) {
console.log('[Preloader] Skipping preloads on LibreOffice page to save memory');
return;
}
if (isLibreOfficePage) {
console.log(
'[Preloader] Skipping preloads on LibreOffice page to save memory'
);
return;
}
scheduleIdleTask(async () => {
console.log('[Preloader] Starting sequential WASM preloads...');
scheduleIdleTask(async () => {
console.log('[Preloader] Starting sequential WASM preloads...');
await preloadPyMuPDF();
await preloadGhostscript();
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>