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