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:
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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.',
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
219
src/js/logic/wasm-settings-page.ts
Normal file
219
src/js/logic/wasm-settings-page.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { WasmProvider, type WasmPackage } from '../utils/wasm-provider.js';
|
||||
import { clearPyMuPDFCache } from '../utils/pymupdf-loader.js';
|
||||
import { clearGhostscriptCache } from '../utils/ghostscript-dynamic-loader.js';
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
document.querySelectorAll('.copy-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const url = btn.getAttribute('data-copy');
|
||||
if (url) {
|
||||
await navigator.clipboard.writeText(url);
|
||||
const svg = btn.querySelector('svg');
|
||||
if (svg) {
|
||||
const checkIcon = document.createElement('i');
|
||||
checkIcon.setAttribute('data-lucide', 'check');
|
||||
checkIcon.className = 'w-3.5 h-3.5';
|
||||
svg.replaceWith(checkIcon);
|
||||
createIcons({ icons });
|
||||
|
||||
setTimeout(() => {
|
||||
const newSvg = btn.querySelector('svg');
|
||||
if (newSvg) {
|
||||
const copyIcon = document.createElement('i');
|
||||
copyIcon.setAttribute('data-lucide', 'copy');
|
||||
copyIcon.className = 'w-3.5 h-3.5';
|
||||
newSvg.replaceWith(copyIcon);
|
||||
createIcons({ icons });
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const pymupdfUrl = document.getElementById('pymupdf-url') as HTMLInputElement;
|
||||
const pymupdfTest = document.getElementById(
|
||||
'pymupdf-test'
|
||||
) as HTMLButtonElement;
|
||||
const pymupdfStatus = document.getElementById(
|
||||
'pymupdf-status'
|
||||
) as HTMLSpanElement;
|
||||
|
||||
const ghostscriptUrl = document.getElementById(
|
||||
'ghostscript-url'
|
||||
) as HTMLInputElement;
|
||||
const ghostscriptTest = document.getElementById(
|
||||
'ghostscript-test'
|
||||
) as HTMLButtonElement;
|
||||
const ghostscriptStatus = document.getElementById(
|
||||
'ghostscript-status'
|
||||
) as HTMLSpanElement;
|
||||
|
||||
const cpdfUrl = document.getElementById('cpdf-url') as HTMLInputElement;
|
||||
const cpdfTest = document.getElementById('cpdf-test') as HTMLButtonElement;
|
||||
const cpdfStatus = document.getElementById('cpdf-status') as HTMLSpanElement;
|
||||
|
||||
const saveBtn = document.getElementById('save-btn') as HTMLButtonElement;
|
||||
const clearBtn = document.getElementById('clear-btn') as HTMLButtonElement;
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
backBtn?.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
|
||||
loadConfiguration();
|
||||
|
||||
function loadConfiguration() {
|
||||
const config = WasmProvider.getAllProviders();
|
||||
|
||||
if (config.pymupdf) {
|
||||
pymupdfUrl.value = config.pymupdf;
|
||||
updateStatus('pymupdf', true);
|
||||
}
|
||||
|
||||
if (config.ghostscript) {
|
||||
ghostscriptUrl.value = config.ghostscript;
|
||||
updateStatus('ghostscript', true);
|
||||
}
|
||||
|
||||
if (config.cpdf) {
|
||||
cpdfUrl.value = config.cpdf;
|
||||
updateStatus('cpdf', true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(
|
||||
packageName: WasmPackage,
|
||||
configured: boolean,
|
||||
testing = false
|
||||
) {
|
||||
const statusMap: Record<WasmPackage, HTMLSpanElement> = {
|
||||
pymupdf: pymupdfStatus,
|
||||
ghostscript: ghostscriptStatus,
|
||||
cpdf: cpdfStatus,
|
||||
};
|
||||
|
||||
const statusEl = statusMap[packageName];
|
||||
if (!statusEl) return;
|
||||
|
||||
if (testing) {
|
||||
statusEl.textContent = 'Testing...';
|
||||
statusEl.className =
|
||||
'text-xs px-2 py-1 rounded-full bg-yellow-600/30 text-yellow-300';
|
||||
} else if (configured) {
|
||||
statusEl.textContent = 'Configured';
|
||||
statusEl.className =
|
||||
'text-xs px-2 py-1 rounded-full bg-green-600/30 text-green-300';
|
||||
} else {
|
||||
statusEl.textContent = 'Not Configured';
|
||||
statusEl.className =
|
||||
'text-xs px-2 py-1 rounded-full bg-gray-600 text-gray-300';
|
||||
}
|
||||
}
|
||||
|
||||
async function testConnection(packageName: WasmPackage, url: string) {
|
||||
if (!url.trim()) {
|
||||
showAlert('Empty URL', 'Please enter a URL to test.');
|
||||
return;
|
||||
}
|
||||
|
||||
updateStatus(packageName, false, true);
|
||||
|
||||
const result = await WasmProvider.validateUrl(packageName, url);
|
||||
|
||||
if (result.valid) {
|
||||
updateStatus(packageName, true);
|
||||
showAlert(
|
||||
'Success',
|
||||
`Connection to ${WasmProvider.getPackageDisplayName(packageName)} successful!`,
|
||||
'success'
|
||||
);
|
||||
} else {
|
||||
updateStatus(packageName, false);
|
||||
showAlert(
|
||||
'Connection Failed',
|
||||
result.error || 'Could not connect to the URL.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pymupdfTest?.addEventListener('click', () => {
|
||||
testConnection('pymupdf', pymupdfUrl.value);
|
||||
});
|
||||
|
||||
ghostscriptTest?.addEventListener('click', () => {
|
||||
testConnection('ghostscript', ghostscriptUrl.value);
|
||||
});
|
||||
|
||||
cpdfTest?.addEventListener('click', () => {
|
||||
testConnection('cpdf', cpdfUrl.value);
|
||||
});
|
||||
|
||||
saveBtn?.addEventListener('click', async () => {
|
||||
showLoader('Saving configuration...');
|
||||
|
||||
try {
|
||||
if (pymupdfUrl.value.trim()) {
|
||||
WasmProvider.setUrl('pymupdf', pymupdfUrl.value.trim());
|
||||
updateStatus('pymupdf', true);
|
||||
} else {
|
||||
WasmProvider.removeUrl('pymupdf');
|
||||
updateStatus('pymupdf', false);
|
||||
}
|
||||
|
||||
if (ghostscriptUrl.value.trim()) {
|
||||
WasmProvider.setUrl('ghostscript', ghostscriptUrl.value.trim());
|
||||
updateStatus('ghostscript', true);
|
||||
} else {
|
||||
WasmProvider.removeUrl('ghostscript');
|
||||
updateStatus('ghostscript', false);
|
||||
}
|
||||
|
||||
if (cpdfUrl.value.trim()) {
|
||||
WasmProvider.setUrl('cpdf', cpdfUrl.value.trim());
|
||||
updateStatus('cpdf', true);
|
||||
} else {
|
||||
WasmProvider.removeUrl('cpdf');
|
||||
updateStatus('cpdf', false);
|
||||
}
|
||||
|
||||
hideLoader();
|
||||
showAlert('Saved', 'Configuration saved successfully!', 'success');
|
||||
} catch (e: unknown) {
|
||||
hideLoader();
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||
showAlert('Error', `Failed to save configuration: ${errorMessage}`);
|
||||
}
|
||||
});
|
||||
|
||||
clearBtn?.addEventListener('click', () => {
|
||||
WasmProvider.clearAll();
|
||||
|
||||
clearPyMuPDFCache();
|
||||
clearGhostscriptCache();
|
||||
|
||||
pymupdfUrl.value = '';
|
||||
ghostscriptUrl.value = '';
|
||||
cpdfUrl.value = '';
|
||||
|
||||
updateStatus('pymupdf', false);
|
||||
updateStatus('ghostscript', false);
|
||||
updateStatus('cpdf', false);
|
||||
|
||||
showAlert(
|
||||
'Cleared',
|
||||
'All configurations and cached modules have been cleared.',
|
||||
'success'
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -2,201 +2,212 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { 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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
89
src/js/utils/ghostscript-dynamic-loader.ts
Normal file
89
src/js/utils/ghostscript-dynamic-loader.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { WasmProvider } from './wasm-provider.js';
|
||||
|
||||
let cachedGS: any = null;
|
||||
let loadPromise: Promise<any> | null = null;
|
||||
|
||||
export interface GhostscriptInterface {
|
||||
convertToPDFA(pdfBuffer: ArrayBuffer, profile: string): Promise<ArrayBuffer>;
|
||||
fontToOutline(pdfBuffer: ArrayBuffer): Promise<ArrayBuffer>;
|
||||
}
|
||||
|
||||
export async function loadGhostscript(): Promise<GhostscriptInterface> {
|
||||
if (cachedGS) {
|
||||
return cachedGS;
|
||||
}
|
||||
|
||||
if (loadPromise) {
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
loadPromise = (async () => {
|
||||
const baseUrl = WasmProvider.getUrl('ghostscript');
|
||||
if (!baseUrl) {
|
||||
throw new Error(
|
||||
'Ghostscript is not configured. Please configure it in Advanced Settings.'
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
||||
|
||||
try {
|
||||
const wrapperUrl = `${normalizedUrl}gs.js`;
|
||||
|
||||
await loadScript(wrapperUrl);
|
||||
|
||||
const globalScope =
|
||||
typeof globalThis !== 'undefined' ? globalThis : window;
|
||||
|
||||
if (typeof (globalScope as any).loadGS === 'function') {
|
||||
cachedGS = await (globalScope as any).loadGS({
|
||||
baseUrl: normalizedUrl,
|
||||
});
|
||||
} else if (typeof (globalScope as any).GhostscriptWASM === 'function') {
|
||||
cachedGS = new (globalScope as any).GhostscriptWASM(normalizedUrl);
|
||||
await cachedGS.init?.();
|
||||
} else {
|
||||
throw new Error(
|
||||
'Ghostscript wrapper did not expose expected interface. Expected loadGS() or GhostscriptWASM class.'
|
||||
);
|
||||
}
|
||||
|
||||
return cachedGS;
|
||||
} catch (error: any) {
|
||||
loadPromise = null;
|
||||
throw new Error(
|
||||
`Failed to load Ghostscript from ${normalizedUrl}: ${error.message}`
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
function loadScript(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (document.querySelector(`script[src="${url}"]`)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.type = 'text/javascript';
|
||||
script.async = true;
|
||||
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
export function isGhostscriptAvailable(): boolean {
|
||||
return WasmProvider.isConfigured('ghostscript');
|
||||
}
|
||||
|
||||
export function clearGhostscriptCache(): void {
|
||||
cachedGS = null;
|
||||
loadPromise = null;
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
/**
|
||||
* PDF/A Conversion using Ghostscript WASM
|
||||
* * 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;
|
||||
},
|
||||
|
||||
87
src/js/utils/pymupdf-loader.ts
Normal file
87
src/js/utils/pymupdf-loader.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { WasmProvider } from './wasm-provider.js';
|
||||
|
||||
let cachedPyMuPDF: any = null;
|
||||
let loadPromise: Promise<any> | null = null;
|
||||
|
||||
export interface PyMuPDFInterface {
|
||||
load(): Promise<void>;
|
||||
compressPdf(
|
||||
file: Blob,
|
||||
options: any
|
||||
): Promise<{ blob: Blob; compressedSize: number }>;
|
||||
convertToPdf(file: Blob, ext: string): Promise<Blob>;
|
||||
extractText(file: Blob, options?: any): Promise<string>;
|
||||
extractImages(file: Blob): Promise<Array<{ data: Uint8Array; ext: string }>>;
|
||||
extractTables(file: Blob): Promise<any[]>;
|
||||
toSvg(file: Blob, pageNum: number): Promise<string>;
|
||||
renderPageToImage(file: Blob, pageNum: number, scale: number): Promise<Blob>;
|
||||
getPageCount(file: Blob): Promise<number>;
|
||||
rasterizePdf(file: Blob | File, options: any): Promise<Blob>;
|
||||
}
|
||||
|
||||
export async function loadPyMuPDF(): Promise<any> {
|
||||
if (cachedPyMuPDF) {
|
||||
return cachedPyMuPDF;
|
||||
}
|
||||
|
||||
if (loadPromise) {
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
loadPromise = (async () => {
|
||||
if (!WasmProvider.isConfigured('pymupdf')) {
|
||||
throw new Error(
|
||||
'PyMuPDF is not configured. Please configure it in Advanced Settings.'
|
||||
);
|
||||
}
|
||||
if (!WasmProvider.isConfigured('ghostscript')) {
|
||||
throw new Error(
|
||||
'Ghostscript is not configured. PyMuPDF requires Ghostscript for some operations. Please configure both in Advanced Settings.'
|
||||
);
|
||||
}
|
||||
|
||||
const pymupdfUrl = WasmProvider.getUrl('pymupdf')!;
|
||||
const gsUrl = WasmProvider.getUrl('ghostscript')!;
|
||||
const normalizedPymupdf = pymupdfUrl.endsWith('/')
|
||||
? pymupdfUrl
|
||||
: `${pymupdfUrl}/`;
|
||||
|
||||
try {
|
||||
const wrapperUrl = `${normalizedPymupdf}dist/index.js`;
|
||||
const module = await import(/* @vite-ignore */ wrapperUrl);
|
||||
|
||||
if (typeof module.PyMuPDF !== 'function') {
|
||||
throw new Error(
|
||||
'PyMuPDF module did not export expected PyMuPDF class.'
|
||||
);
|
||||
}
|
||||
|
||||
cachedPyMuPDF = new module.PyMuPDF({
|
||||
assetPath: `${normalizedPymupdf}assets/`,
|
||||
ghostscriptUrl: gsUrl,
|
||||
});
|
||||
|
||||
await cachedPyMuPDF.load();
|
||||
|
||||
console.log('[PyMuPDF Loader] Successfully loaded from CDN');
|
||||
return cachedPyMuPDF;
|
||||
} catch (error: any) {
|
||||
loadPromise = null;
|
||||
throw new Error(`Failed to load PyMuPDF from CDN: ${error.message}`);
|
||||
}
|
||||
})();
|
||||
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
export function isPyMuPDFAvailable(): boolean {
|
||||
return (
|
||||
WasmProvider.isConfigured('pymupdf') &&
|
||||
WasmProvider.isConfigured('ghostscript')
|
||||
);
|
||||
}
|
||||
|
||||
export function clearPyMuPDFCache(): void {
|
||||
cachedPyMuPDF = null;
|
||||
loadPromise = null;
|
||||
}
|
||||
@@ -1,132 +1,159 @@
|
||||
import { getLibreOfficeConverter } from './libreoffice-loader.js';
|
||||
import { 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');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
328
src/js/utils/wasm-provider.ts
Normal file
328
src/js/utils/wasm-provider.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
export type WasmPackage = 'pymupdf' | 'ghostscript' | 'cpdf';
|
||||
|
||||
interface WasmProviderConfig {
|
||||
pymupdf?: string;
|
||||
ghostscript?: string;
|
||||
cpdf?: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'bentopdf:wasm-providers';
|
||||
|
||||
class WasmProviderManager {
|
||||
private config: WasmProviderConfig;
|
||||
private validationCache: Map<WasmPackage, boolean> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.config = this.loadConfig();
|
||||
}
|
||||
|
||||
private loadConfig(): WasmProviderConfig {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'[WasmProvider] Failed to load config from localStorage:',
|
||||
e
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private saveConfig(): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.config));
|
||||
} catch (e) {
|
||||
console.error('[WasmProvider] Failed to save config to localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
getUrl(packageName: WasmPackage): string | undefined {
|
||||
return this.config[packageName];
|
||||
}
|
||||
|
||||
setUrl(packageName: WasmPackage, url: string): void {
|
||||
const normalizedUrl = url.endsWith('/') ? url : `${url}/`;
|
||||
this.config[packageName] = normalizedUrl;
|
||||
this.validationCache.delete(packageName);
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
removeUrl(packageName: WasmPackage): void {
|
||||
delete this.config[packageName];
|
||||
this.validationCache.delete(packageName);
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
isConfigured(packageName: WasmPackage): boolean {
|
||||
return !!this.config[packageName];
|
||||
}
|
||||
|
||||
hasAnyProvider(): boolean {
|
||||
return Object.keys(this.config).length > 0;
|
||||
}
|
||||
|
||||
async validateUrl(
|
||||
packageName: WasmPackage,
|
||||
url?: string
|
||||
): Promise<{ valid: boolean; error?: string }> {
|
||||
const testUrl = url || this.config[packageName];
|
||||
if (!testUrl) {
|
||||
return { valid: false, error: 'No URL configured' };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(testUrl);
|
||||
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'URL must start with http:// or https://',
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
'Invalid URL format. Please enter a valid URL (e.g., https://example.com/wasm/)',
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedUrl = testUrl.endsWith('/') ? testUrl : `${testUrl}/`;
|
||||
|
||||
try {
|
||||
const testFiles: Record<WasmPackage, string> = {
|
||||
pymupdf: 'dist/index.js',
|
||||
ghostscript: 'gs.js',
|
||||
cpdf: 'coherentpdf.browser.min.js',
|
||||
};
|
||||
|
||||
const testFile = testFiles[packageName];
|
||||
const fullUrl = `${normalizedUrl}${testFile}`;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s
|
||||
|
||||
const response = await fetch(fullUrl, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Could not find ${testFile} at the specified URL (HTTP ${response.status}). Make sure the file exists.`,
|
||||
};
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (reader) {
|
||||
try {
|
||||
await reader.read();
|
||||
reader.cancel();
|
||||
} catch {
|
||||
return {
|
||||
valid: false,
|
||||
error: `File exists but could not be read. Check CORS configuration.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
contentType &&
|
||||
!contentType.includes('javascript') &&
|
||||
!contentType.includes('application/octet-stream') &&
|
||||
!contentType.includes('text/')
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `The URL returned unexpected content type: ${contentType}. Expected a JavaScript file.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!url || url === this.config[packageName]) {
|
||||
this.validationCache.set(packageName, true);
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch (e: unknown) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error';
|
||||
|
||||
if (
|
||||
errorMessage.includes('Failed to fetch') ||
|
||||
errorMessage.includes('NetworkError')
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
'Network error: Could not connect to the URL. Check that the URL is correct and the server allows CORS requests.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
error: `Network error: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getAllProviders(): WasmProviderConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
clearAll(): void {
|
||||
this.config = {};
|
||||
this.validationCache.clear();
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
getPackageDisplayName(packageName: WasmPackage): string {
|
||||
const names: Record<WasmPackage, string> = {
|
||||
pymupdf: 'PyMuPDF (Document Processing)',
|
||||
ghostscript: 'Ghostscript (PDF/A Conversion)',
|
||||
cpdf: 'CoherentPDF (Bookmarks & Metadata)',
|
||||
};
|
||||
return names[packageName];
|
||||
}
|
||||
|
||||
getPackageFeatures(packageName: WasmPackage): string[] {
|
||||
const features: Record<WasmPackage, string[]> = {
|
||||
pymupdf: [
|
||||
'PDF to Text',
|
||||
'PDF to Markdown',
|
||||
'PDF to SVG',
|
||||
'PDF to Images (High Quality)',
|
||||
'PDF to DOCX',
|
||||
'PDF to Excel/CSV',
|
||||
'Extract Images',
|
||||
'Extract Tables',
|
||||
'EPUB/MOBI/FB2/XPS/CBZ to PDF',
|
||||
'Image Compression',
|
||||
'Deskew PDF',
|
||||
'PDF Layers',
|
||||
],
|
||||
ghostscript: ['PDF/A Conversion', 'Font to Outline'],
|
||||
cpdf: [
|
||||
'Merge PDF',
|
||||
'Alternate Merge',
|
||||
'Split by Bookmarks',
|
||||
'Table of Contents',
|
||||
'PDF to JSON',
|
||||
'JSON to PDF',
|
||||
'Add/Edit/Extract Attachments',
|
||||
'Edit Bookmarks',
|
||||
'PDF Metadata',
|
||||
],
|
||||
};
|
||||
return features[packageName];
|
||||
}
|
||||
}
|
||||
|
||||
export const WasmProvider = new WasmProviderManager();
|
||||
|
||||
export function showWasmRequiredDialog(
|
||||
packageName: WasmPackage,
|
||||
onConfigure?: () => void
|
||||
): void {
|
||||
const displayName = WasmProvider.getPackageDisplayName(packageName);
|
||||
const features = WasmProvider.getPackageFeatures(packageName);
|
||||
|
||||
// Create modal
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className =
|
||||
'fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4';
|
||||
overlay.id = 'wasm-required-modal';
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className =
|
||||
'bg-gray-800 rounded-2xl max-w-md w-full shadow-2xl border border-gray-700';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-12 h-12 rounded-full bg-amber-500/20 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white">Advanced Feature Required</h3>
|
||||
<p class="text-sm text-gray-400">External processing module needed</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-300 mb-4">
|
||||
This feature requires <strong class="text-white">${displayName}</strong> to be configured.
|
||||
</p>
|
||||
|
||||
<div class="bg-gray-700/50 rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-gray-400 mb-2">Features enabled by this module:</p>
|
||||
<ul class="text-sm text-gray-300 space-y-1">
|
||||
${features
|
||||
.slice(0, 4)
|
||||
.map(
|
||||
(f) =>
|
||||
`<li class="flex items-center gap-2"><span class="text-green-400">✓</span> ${f}</li>`
|
||||
)
|
||||
.join('')}
|
||||
${features.length > 4 ? `<li class="text-gray-500">+ ${features.length - 4} more...</li>` : ''}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 mb-4">
|
||||
This module is licensed under AGPL-3.0. By configuring it, you agree to its license terms.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-700 p-4 flex gap-3">
|
||||
<button id="wasm-modal-cancel" class="flex-1 px-4 py-2.5 rounded-lg bg-gray-700 text-gray-300 hover:bg-gray-600 transition-colors font-medium">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="wasm-modal-configure" class="flex-1 px-4 py-2.5 rounded-lg bg-gradient-to-r from-blue-600 to-blue-500 text-white hover:from-blue-500 hover:to-blue-400 transition-all font-medium">
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const cancelBtn = modal.querySelector('#wasm-modal-cancel');
|
||||
const configureBtn = modal.querySelector('#wasm-modal-configure');
|
||||
|
||||
const closeModal = () => {
|
||||
overlay.remove();
|
||||
};
|
||||
|
||||
cancelBtn?.addEventListener('click', closeModal);
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) closeModal();
|
||||
});
|
||||
|
||||
configureBtn?.addEventListener('click', () => {
|
||||
closeModal();
|
||||
if (onConfigure) {
|
||||
onConfigure();
|
||||
} else {
|
||||
window.location.href = `${import.meta.env.BASE_URL}wasm-settings.html`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function requireWasm(
|
||||
packageName: WasmPackage,
|
||||
onAvailable?: () => void
|
||||
): boolean {
|
||||
if (WasmProvider.isConfigured(packageName)) {
|
||||
onAvailable?.();
|
||||
return true;
|
||||
}
|
||||
|
||||
showWasmRequiredDialog(packageName);
|
||||
return false;
|
||||
}
|
||||
@@ -191,6 +191,29 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center gap-3 bg-gray-700/50 p-4 rounded-lg border border-gray-600"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="pre-flatten"
|
||||
class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500 focus:ring-2"
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
for="pre-flatten"
|
||||
class="text-sm font-medium text-gray-200 cursor-pointer"
|
||||
>
|
||||
Pre-flatten PDF (recommended for complex files)
|
||||
</label>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Converts the PDF to images first, ensuring better PDF/A
|
||||
compliance. Recommended if validation fails on the normal
|
||||
conversion.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="process-btn" class="btn-gradient w-full mt-4">
|
||||
Convert to PDF/A
|
||||
</button>
|
||||
|
||||
332
src/pages/wasm-settings.html
Normal file
332
src/pages/wasm-settings.html
Normal file
@@ -0,0 +1,332 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>
|
||||
Advanced Features Settings - Configure WASM Modules | BentoPDF
|
||||
</title>
|
||||
<meta
|
||||
name="title"
|
||||
content="Advanced Features Settings - Configure WASM Modules | BentoPDF"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Configure advanced PDF processing modules for BentoPDF. Enable features like PDF/A conversion, document extraction, and more."
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="wasm settings, pdf processing, advanced features"
|
||||
/>
|
||||
<meta name="author" content="BentoPDF" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
|
||||
<!-- Canonical URL -->
|
||||
<link rel="canonical" href="https://www.bentopdf.com/wasm-settings.html" />
|
||||
|
||||
<!-- Mobile Web App -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="WASM Settings" />
|
||||
|
||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||
|
||||
<!-- Web App Manifest -->
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="192x192"
|
||||
href="/images/favicon-192x192.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="512x512"
|
||||
href="/images/favicon-512x512.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/images/apple-touch-icon.png"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" sizes="32x32" />
|
||||
</head>
|
||||
|
||||
<body class="antialiased bg-gray-900">
|
||||
{{> navbar }}
|
||||
|
||||
<div
|
||||
id="uploader"
|
||||
class="min-h-screen flex flex-col items-center justify-start py-12 p-4 bg-gray-900"
|
||||
>
|
||||
<div
|
||||
id="tool-uploader"
|
||||
class="bg-gray-800 rounded-xl shadow-xl px-4 py-8 md:p-8 max-w-2xl w-full text-gray-200 border border-gray-700"
|
||||
>
|
||||
<button
|
||||
id="back-to-tools"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
|
||||
>
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer" data-i18n="tools.backToTools">
|
||||
Back to Tools
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">
|
||||
Advanced Features Settings
|
||||
</h1>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Configure external processing modules to enable advanced PDF features.
|
||||
These modules are optional and licensed separately.
|
||||
</p>
|
||||
|
||||
<!-- Info Banner -->
|
||||
<div
|
||||
class="bg-amber-900/30 border border-amber-600/50 rounded-lg p-4 mb-6"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<i
|
||||
data-lucide="info"
|
||||
class="w-5 h-5 text-amber-400 flex-shrink-0 mt-0.5"
|
||||
></i>
|
||||
<div>
|
||||
<p class="text-amber-200 text-sm">
|
||||
<strong>Why is this needed?</strong> Some advanced features
|
||||
require external processing modules that are licensed under
|
||||
AGPL-3.0. By providing your own module URLs, you can enable
|
||||
these features.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PyMuPDF Section -->
|
||||
<div class="space-y-6">
|
||||
<div class="bg-gray-700/50 rounded-lg p-4 border border-gray-600">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-white">PyMuPDF</h3>
|
||||
<p class="text-xs text-gray-400">Document Processing Engine</p>
|
||||
</div>
|
||||
<span
|
||||
id="pymupdf-status"
|
||||
class="text-xs px-2 py-1 rounded-full bg-gray-600 text-gray-300"
|
||||
>
|
||||
Not Configured
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mb-3">
|
||||
Enables: PDF to Text, Markdown, SVG, DOCX, Excel • Extract
|
||||
Images/Tables • Format Conversion
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="pymupdf-url"
|
||||
placeholder="https://your-cdn.com/pymupdf-wasm/"
|
||||
class="flex-1 bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 text-sm"
|
||||
/>
|
||||
<button
|
||||
id="pymupdf-test"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<span class="text-xs text-gray-500">Recommended:</span>
|
||||
<code
|
||||
class="text-xs text-indigo-400 bg-gray-800 px-2 py-1 rounded flex-1 truncate"
|
||||
>https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.1.9/</code
|
||||
>
|
||||
<button
|
||||
data-copy="https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.1.9/"
|
||||
class="copy-btn p-1.5 bg-gray-600 hover:bg-gray-500 rounded text-gray-300 hover:text-white transition-colors"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ghostscript Section -->
|
||||
<div class="bg-gray-700/50 rounded-lg p-4 border border-gray-600">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-white">Ghostscript</h3>
|
||||
<p class="text-xs text-gray-400">PDF/A Conversion Engine</p>
|
||||
</div>
|
||||
<span
|
||||
id="ghostscript-status"
|
||||
class="text-xs px-2 py-1 rounded-full bg-gray-600 text-gray-300"
|
||||
>
|
||||
Not Configured
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mb-3">
|
||||
Enables: PDF/A-1b, PDF/A-2b, PDF/A-3b Conversion • Font to Outline
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="ghostscript-url"
|
||||
placeholder="https://your-cdn.com/ghostscript-wasm/"
|
||||
class="flex-1 bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 text-sm"
|
||||
/>
|
||||
<button
|
||||
id="ghostscript-test"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<span class="text-xs text-gray-500">Recommended:</span>
|
||||
<code
|
||||
class="text-xs text-indigo-400 bg-gray-800 px-2 py-1 rounded flex-1 truncate"
|
||||
>https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/</code
|
||||
>
|
||||
<button
|
||||
data-copy="https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/"
|
||||
class="copy-btn p-1.5 bg-gray-600 hover:bg-gray-500 rounded text-gray-300 hover:text-white transition-colors"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPDF Section -->
|
||||
<div class="bg-gray-700/50 rounded-lg p-4 border border-gray-600">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h3 class="font-semibold text-white">CoherentPDF</h3>
|
||||
<p class="text-xs text-gray-400">Bookmarks & Metadata Engine</p>
|
||||
</div>
|
||||
<span
|
||||
id="cpdf-status"
|
||||
class="text-xs px-2 py-1 rounded-full bg-gray-600 text-gray-300"
|
||||
>
|
||||
Not Configured
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-400 mb-3">
|
||||
Enables: Split by Bookmarks • Edit Bookmarks • PDF Metadata
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="cpdf-url"
|
||||
placeholder="https://your-cdn.com/cpdf/"
|
||||
class="flex-1 bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 text-sm"
|
||||
/>
|
||||
<button
|
||||
id="cpdf-test"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-500 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<span class="text-xs text-gray-500">Recommended:</span>
|
||||
<code
|
||||
class="text-xs text-indigo-400 bg-gray-800 px-2 py-1 rounded flex-1 truncate"
|
||||
>https://cdn.jsdelivr.net/npm/coherentpdf/dist/</code
|
||||
>
|
||||
<button
|
||||
data-copy="https://cdn.jsdelivr.net/npm/coherentpdf/dist/"
|
||||
class="copy-btn p-1.5 bg-gray-600 hover:bg-gray-500 rounded text-gray-300 hover:text-white transition-colors"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button id="save-btn" class="btn-gradient flex-1">
|
||||
Save Configuration
|
||||
</button>
|
||||
<button
|
||||
id="clear-btn"
|
||||
class="px-6 py-2.5 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg font-medium transition-colors border border-red-600/50"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- License Notice -->
|
||||
<div
|
||||
class="mt-6 p-4 bg-gray-700/30 rounded-lg border border-gray-600/50"
|
||||
>
|
||||
<p class="text-xs text-gray-500">
|
||||
<strong class="text-gray-400">License Notice:</strong> The external
|
||||
modules (PyMuPDF, Ghostscript, CoherentPDF) are licensed under
|
||||
AGPL-3.0 or similar copyleft licenses. By configuring and using
|
||||
these modules, you agree to their respective license terms. BentoPDF
|
||||
is compatible with any Ghostscript WASM and PyMuPDF WASM
|
||||
implementation that follows the expected interface.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loader Modal -->
|
||||
<div
|
||||
id="loader-modal"
|
||||
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl"
|
||||
>
|
||||
<div class="solid-spinner"></div>
|
||||
<p
|
||||
id="loader-text"
|
||||
class="text-white text-lg font-medium"
|
||||
data-i18n="loader.processing"
|
||||
>
|
||||
Processing...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Modal -->
|
||||
<div
|
||||
id="alert-modal"
|
||||
class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700"
|
||||
>
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">
|
||||
Alert
|
||||
</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6"></p>
|
||||
<button
|
||||
id="alert-ok"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200"
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{> footer }}
|
||||
<script type="module" src="/src/js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="/src/js/utils/full-width.ts"></script>
|
||||
<script type="module" src="/src/js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="/src/version.ts"></script>
|
||||
<script type="module" src="/src/js/logic/wasm-settings-page.ts"></script>
|
||||
<script type="module" src="/src/js/mobileMenu.ts"></script>
|
||||
<script type="module" src="/src/js/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -81,6 +81,11 @@
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="wasm-settings.html" class="hover:text-indigo-400"
|
||||
>Advanced Settings</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user