feat: Add VitePress docs, EPUB to PDF tool, Phosphor icons, and licensing updates

- Set up VitePress documentation site (docs:dev, docs:build, docs:preview)
- Added Getting Started, Tools Reference, Contributing, and Commercial License pages
- Created self-hosting guides for Docker, Vercel, Netlify, Cloudflare, AWS, Hostinger, Nginx, Apache
- Updated README with documentation link, sponsors section, and docs contribution guide

- Added EPUB to PDF converter using LibreOffice WASM

- Migrated to Phosphor Icons for consistent iconography

- Added donation ribbon banner on landing page
- Removed 'Like My Work?' section (replaced by ribbon)
- Updated licensing.html with delivery model, AGPL notice, invoicing, and no-refund policy

- Added Commercial License documentation page
- Updated translations table (Chinese added, marked non-English as In Progress)

- Added sponsors.yml workflow for auto-generating sponsor avatars
This commit is contained in:
abdullahalam123
2025-12-27 19:30:31 +05:30
parent 0e888743d3
commit f30a084fce
189 changed files with 59872 additions and 3300 deletions

View File

@@ -0,0 +1,201 @@
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';
const FILETYPE = 'cbz';
const EXTENSIONS = ['.cbz', '.cbr'];
const TOOL_NAME = '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');
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 resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
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 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 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}`);
}
};
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);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});

View File

@@ -7,187 +7,94 @@ import {
} from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument } from 'pdf-lib';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
function dataUrlToBytes(dataUrl: any) {
const base64 = dataUrl.split(',')[1];
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
const CONDENSE_PRESETS = {
light: {
images: { quality: 90, dpiTarget: 150, dpiThreshold: 200 },
scrub: { metadata: false, thumbnails: true },
subsetFonts: true,
},
balanced: {
images: { quality: 75, dpiTarget: 96, dpiThreshold: 150 },
scrub: { metadata: true, thumbnails: true },
subsetFonts: true,
},
aggressive: {
images: { quality: 50, dpiTarget: 72, dpiThreshold: 100 },
scrub: { metadata: true, thumbnails: true, xmlMetadata: true },
subsetFonts: true,
},
extreme: {
images: { quality: 30, dpiTarget: 60, dpiThreshold: 96 },
scrub: { metadata: true, thumbnails: true, xmlMetadata: true },
subsetFonts: true,
},
};
const PHOTON_PRESETS = {
light: { scale: 2.0, quality: 0.85 },
balanced: { scale: 1.5, quality: 0.65 },
aggressive: { scale: 1.2, quality: 0.45 },
extreme: { scale: 1.0, quality: 0.25 },
};
async function performCondenseCompression(
fileBlob: Blob,
level: string,
customSettings?: {
imageQuality?: number;
dpiTarget?: number;
dpiThreshold?: number;
removeMetadata?: boolean;
subsetFonts?: boolean;
convertToGrayscale?: boolean;
removeThumbnails?: boolean;
}
return bytes;
}
) {
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
async function performSmartCompression(arrayBuffer: any, settings: any) {
const pdfDoc = await PDFDocument.load(arrayBuffer, {
ignoreEncryption: true,
});
const pages = pdfDoc.getPages();
const preset = CONDENSE_PRESETS[level as keyof typeof CONDENSE_PRESETS] || CONDENSE_PRESETS.balanced;
if (settings.removeMetadata) {
try {
pdfDoc.setTitle('');
pdfDoc.setAuthor('');
pdfDoc.setSubject('');
pdfDoc.setKeywords([]);
pdfDoc.setCreator('');
pdfDoc.setProducer('');
} catch (e) {
console.warn('Could not remove metadata:', e);
}
}
const dpiTarget = customSettings?.dpiTarget ?? preset.images.dpiTarget;
const userThreshold = customSettings?.dpiThreshold ?? preset.images.dpiThreshold;
const dpiThreshold = Math.max(userThreshold, dpiTarget + 10);
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const resources = page.node.Resources();
if (!resources) continue;
const xobjects = resources.lookup(PDFName.of('XObject'));
if (!(xobjects instanceof PDFDict)) continue;
for (const [key, value] of xobjects.entries()) {
const stream = pdfDoc.context.lookup(value);
if (
!(stream instanceof PDFStream) ||
stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')
)
continue;
try {
const imageBytes = stream.getContents();
if (imageBytes.length < settings.skipSize) continue;
const width =
stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
: 0;
const height =
stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
: 0;
const bitsPerComponent =
stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
? (
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
).asNumber()
: 8;
if (width > 0 && height > 0) {
let newWidth = width;
let newHeight = height;
const scaleFactor = settings.scaleFactor || 1.0;
newWidth = Math.floor(width * scaleFactor);
newHeight = Math.floor(height * scaleFactor);
if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) {
const aspectRatio = newWidth / newHeight;
if (newWidth > newHeight) {
newWidth = Math.min(newWidth, settings.maxWidth);
newHeight = newWidth / aspectRatio;
} else {
newHeight = Math.min(newHeight, settings.maxHeight);
newWidth = newHeight * aspectRatio;
}
}
const minDim = settings.minDimension || 50;
if (newWidth < minDim || newHeight < minDim) continue;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = Math.floor(newWidth);
canvas.height = Math.floor(newHeight);
const img = new Image();
const imageUrl = URL.createObjectURL(
new Blob([new Uint8Array(imageBytes)])
);
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = imageUrl;
});
ctx.imageSmoothingEnabled = settings.smoothing !== false;
ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium';
if (settings.grayscale) {
ctx.filter = 'grayscale(100%)';
} else if (settings.contrast) {
ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`;
}
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
let bestBytes = null;
let bestSize = imageBytes.length;
const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality);
const jpegBytes = dataUrlToBytes(jpegDataUrl);
if (jpegBytes.length < bestSize) {
bestBytes = jpegBytes;
bestSize = jpegBytes.length;
}
if (settings.tryWebP) {
try {
const webpDataUrl = canvas.toDataURL(
'image/webp',
settings.quality
);
const webpBytes = dataUrlToBytes(webpDataUrl);
if (webpBytes.length < bestSize) {
bestBytes = webpBytes;
bestSize = webpBytes.length;
}
} catch (e) {
/* WebP not supported */
}
}
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
(stream as any).contents = bestBytes;
stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize));
stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width));
stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height));
stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode'));
stream.dict.delete(PDFName.of('DecodeParms'));
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
if (settings.grayscale) {
stream.dict.set(
PDFName.of('ColorSpace'),
PDFName.of('DeviceGray')
);
}
}
URL.revokeObjectURL(imageUrl);
}
} catch (error) {
console.warn('Skipping an uncompressible image in smart mode:', error);
}
}
}
const saveOptions = {
useObjectStreams: settings.useObjectStreams !== false,
addDefaultPage: false,
objectsPerTick: settings.objectsPerTick || 50,
const options = {
images: {
enabled: true,
quality: customSettings?.imageQuality ?? preset.images.quality,
dpiTarget,
dpiThreshold,
convertToGray: customSettings?.convertToGrayscale ?? false,
},
scrub: {
metadata: customSettings?.removeMetadata ?? preset.scrub.metadata,
thumbnails: customSettings?.removeThumbnails ?? preset.scrub.thumbnails,
xmlMetadata: (preset.scrub as any).xmlMetadata ?? false,
},
subsetFonts: customSettings?.subsetFonts ?? preset.subsetFonts,
save: {
garbage: 4 as const,
deflate: true,
clean: true,
useObjstms: true,
},
};
return await pdfDoc.save(saveOptions);
const result = await pymupdf.compressPdf(fileBlob, options);
return result;
}
async function performLegacyCompression(arrayBuffer: any, settings: any) {
async function performPhotonCompression(arrayBuffer: ArrayBuffer, level: string) {
const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise;
const newPdfDoc = await PDFDocument.create();
const settings = PHOTON_PRESETS[level as keyof typeof PHOTON_PRESETS] || PHOTON_PRESETS.balanced;
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
const page = await pdfJsDoc.getPage(i);
@@ -197,13 +104,12 @@ async function performLegacyCompression(arrayBuffer: any, settings: any) {
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport, canvas: canvas })
.promise;
await page.render({ canvasContext: context, viewport, canvas: canvas }).promise;
const jpegBlob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', settings.quality)
const jpegBlob = await new Promise<Blob>((resolve) =>
canvas.toBlob((blob) => resolve(blob as Blob), 'image/jpeg', settings.quality)
);
const jpegBytes = await (jpegBlob as Blob).arrayBuffer();
const jpegBytes = await jpegBlob.arrayBuffer();
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(jpegImage, {
@@ -219,13 +125,19 @@ async function performLegacyCompression(arrayBuffer: any, settings: any) {
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 compressOptions = document.getElementById('compress-options');
const fileControls = document.getElementById('file-controls');
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 algorithmSelect = document.getElementById('compression-algorithm') as HTMLSelectElement;
const condenseInfo = document.getElementById('condense-info');
const photonInfo = document.getElementById('photon-info');
const toggleCustomSettings = document.getElementById('toggle-custom-settings');
const customSettingsPanel = document.getElementById('custom-settings-panel');
const customSettingsChevron = document.getElementById('custom-settings-chevron');
let useCustomSettings = false;
if (backBtn) {
backBtn.addEventListener('click', () => {
@@ -233,60 +145,79 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Toggle algorithm info
if (algorithmSelect && condenseInfo && photonInfo) {
algorithmSelect.addEventListener('change', () => {
if (algorithmSelect.value === 'condense') {
condenseInfo.classList.remove('hidden');
photonInfo.classList.add('hidden');
} else {
condenseInfo.classList.add('hidden');
photonInfo.classList.remove('hidden');
}
});
}
// Toggle custom settings panel
if (toggleCustomSettings && customSettingsPanel && customSettingsChevron) {
toggleCustomSettings.addEventListener('click', () => {
customSettingsPanel.classList.toggle('hidden');
customSettingsChevron.style.transform = customSettingsPanel.classList.contains('hidden')
? 'rotate(0deg)'
: 'rotate(180deg)';
// Mark that user wants to use custom settings
if (!customSettingsPanel.classList.contains('hidden')) {
useCustomSettings = true;
}
});
}
const updateUI = async () => {
if (!fileDisplayArea || !compressOptions || !processBtn || !fileControls) return;
if (!compressOptions) return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
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);
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`;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
}
createIcons({ icons });
fileControls.classList.remove('hidden');
createIcons({ icons });
}
compressOptions.classList.remove('hidden');
(processBtn as HTMLButtonElement).disabled = false;
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
compressOptions.classList.add('hidden');
(processBtn as HTMLButtonElement).disabled = true;
// Clear file display area
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
}
};
@@ -297,8 +228,30 @@ document.addEventListener('DOMContentLoaded', () => {
const compressionLevel = document.getElementById('compression-level') as HTMLSelectElement;
if (compressionLevel) compressionLevel.value = 'balanced';
const compressionAlgorithm = document.getElementById('compression-algorithm') as HTMLSelectElement;
if (compressionAlgorithm) compressionAlgorithm.value = 'vector';
if (algorithmSelect) algorithmSelect.value = 'condense';
useCustomSettings = false;
if (customSettingsPanel) customSettingsPanel.classList.add('hidden');
if (customSettingsChevron) customSettingsChevron.style.transform = 'rotate(0deg)';
const imageQuality = document.getElementById('image-quality') as HTMLInputElement;
const dpiTarget = document.getElementById('dpi-target') as HTMLInputElement;
const dpiThreshold = document.getElementById('dpi-threshold') as HTMLInputElement;
const removeMetadata = document.getElementById('remove-metadata') as HTMLInputElement;
const subsetFonts = document.getElementById('subset-fonts') as HTMLInputElement;
const convertToGrayscale = document.getElementById('convert-to-grayscale') as HTMLInputElement;
const removeThumbnails = document.getElementById('remove-thumbnails') as HTMLInputElement;
if (imageQuality) imageQuality.value = '75';
if (dpiTarget) dpiTarget.value = '96';
if (dpiThreshold) dpiThreshold.value = '150';
if (removeMetadata) removeMetadata.checked = true;
if (subsetFonts) subsetFonts.checked = true;
if (convertToGrayscale) convertToGrayscale.checked = false;
if (removeThumbnails) removeThumbnails.checked = true;
if (condenseInfo) condenseInfo.classList.remove('hidden');
if (photonInfo) photonInfo.classList.add('hidden');
updateUI();
};
@@ -306,52 +259,38 @@ document.addEventListener('DOMContentLoaded', () => {
const compress = async () => {
const level = (document.getElementById('compression-level') as HTMLSelectElement).value;
const algorithm = (document.getElementById('compression-algorithm') as HTMLSelectElement).value;
const convertToGrayscale = (document.getElementById('convert-to-grayscale') as HTMLInputElement)?.checked ?? false;
const settings = {
balanced: {
smart: {
quality: 0.5,
threshold: 0.95,
maxWidth: 1800,
maxHeight: 1800,
skipSize: 3000,
},
legacy: { scale: 1.5, quality: 0.6 },
},
'high-quality': {
smart: {
quality: 0.7,
threshold: 0.98,
maxWidth: 2500,
maxHeight: 2500,
skipSize: 5000,
},
legacy: { scale: 2.0, quality: 0.9 },
},
'small-size': {
smart: {
quality: 0.3,
threshold: 0.95,
maxWidth: 1200,
maxHeight: 1200,
skipSize: 2000,
},
legacy: { scale: 1.2, quality: 0.4 },
},
extreme: {
smart: {
quality: 0.1,
threshold: 0.95,
maxWidth: 1000,
maxHeight: 1000,
skipSize: 1000,
},
legacy: { scale: 1.0, quality: 0.2 },
},
};
let customSettings: {
imageQuality?: number;
dpiTarget?: number;
dpiThreshold?: number;
removeMetadata?: boolean;
subsetFonts?: boolean;
convertToGrayscale?: boolean;
removeThumbnails?: boolean;
} | undefined;
const smartSettings = { ...settings[level].smart, removeMetadata: true };
const legacySettings = settings[level].legacy;
if (useCustomSettings) {
const imageQuality = parseInt((document.getElementById('image-quality') as HTMLInputElement)?.value) || 75;
const dpiTarget = parseInt((document.getElementById('dpi-target') as HTMLInputElement)?.value) || 96;
const dpiThreshold = parseInt((document.getElementById('dpi-threshold') as HTMLInputElement)?.value) || 150;
const removeMetadata = (document.getElementById('remove-metadata') as HTMLInputElement)?.checked ?? true;
const subsetFonts = (document.getElementById('subset-fonts') as HTMLInputElement)?.checked ?? true;
const removeThumbnails = (document.getElementById('remove-thumbnails') as HTMLInputElement)?.checked ?? true;
customSettings = {
imageQuality,
dpiTarget,
dpiThreshold,
removeMetadata,
subsetFonts,
convertToGrayscale,
removeThumbnails,
};
} else {
customSettings = convertToGrayscale ? { convertToGrayscale } : undefined;
}
try {
if (state.files.length === 0) {
@@ -362,49 +301,35 @@ document.addEventListener('DOMContentLoaded', () => {
if (state.files.length === 1) {
const originalFile = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
let resultBytes;
let usedMethod;
let resultBlob: Blob;
let resultSize: number;
let usedMethod: string;
if (algorithm === 'vector') {
showLoader('Running Vector (Smart) compression...');
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
usedMethod = 'Vector';
} else if (algorithm === 'photon') {
showLoader('Running Photon (Rasterize) compression...');
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
usedMethod = 'Photon';
if (algorithm === 'condense') {
showLoader('Loading engine...');
const result = await performCondenseCompression(originalFile, level, customSettings);
resultBlob = result.blob;
resultSize = result.compressedSize;
usedMethod = 'Condense';
} else {
showLoader('Running Automatic (Vector first)...');
const vectorResultBytes = await performSmartCompression(
arrayBuffer,
smartSettings
);
if (vectorResultBytes.length < originalFile.size) {
resultBytes = vectorResultBytes;
usedMethod = 'Vector (Automatic)';
} else {
showAlert('Vector failed to reduce size. Trying Photon...', 'info');
showLoader('Running Automatic (Photon fallback)...');
resultBytes = await performLegacyCompression(
arrayBuffer,
legacySettings
);
usedMethod = 'Photon (Automatic)';
}
showLoader('Running Photon compression...');
const arrayBuffer = await readFileAsArrayBuffer(originalFile) as ArrayBuffer;
const resultBytes = await performPhotonCompression(arrayBuffer, level);
const buffer = resultBytes.buffer.slice(resultBytes.byteOffset, resultBytes.byteOffset + resultBytes.byteLength) as ArrayBuffer;
resultBlob = new Blob([buffer], { type: 'application/pdf' });
resultSize = resultBytes.length;
usedMethod = 'Photon';
}
const originalSize = formatBytes(originalFile.size);
const compressedSize = formatBytes(resultBytes.length);
const savings = originalFile.size - resultBytes.length;
const savingsPercent =
savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
const compressedSize = formatBytes(resultSize);
const savings = originalFile.size - resultSize;
const savingsPercent = savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
downloadFile(
new Blob([resultBytes], { type: 'application/pdf' }),
'compressed-final.pdf'
resultBlob,
originalFile.name.replace(/\.pdf$/i, '') + '_compressed.pdf'
);
hideLoader();
@@ -419,7 +344,7 @@ document.addEventListener('DOMContentLoaded', () => {
} else {
showAlert(
'Compression Finished',
`Method: ${usedMethod}. Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
`Method: ${usedMethod}. Could not reduce file size further. Original: ${originalSize}, New: ${compressedSize}.`,
'warning',
() => resetState()
);
@@ -434,22 +359,15 @@ document.addEventListener('DOMContentLoaded', () => {
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Compressing ${i + 1}/${state.files.length}: ${file.name}...`);
const arrayBuffer = await readFileAsArrayBuffer(file);
totalOriginalSize += file.size;
let resultBytes;
if (algorithm === 'vector') {
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
} else if (algorithm === 'photon') {
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
let resultBytes: Uint8Array;
if (algorithm === 'condense') {
const result = await performCondenseCompression(file, level, customSettings);
resultBytes = new Uint8Array(await result.blob.arrayBuffer());
} else {
const vectorResultBytes = await performSmartCompression(
arrayBuffer,
smartSettings
);
resultBytes = vectorResultBytes.length < file.size
? vectorResultBytes
: await performLegacyCompression(arrayBuffer, legacySettings);
const arrayBuffer = await readFileAsArrayBuffer(file) as ArrayBuffer;
resultBytes = await performPhotonCompression(arrayBuffer, level);
}
totalCompressedSize += resultBytes.length;
@@ -459,10 +377,9 @@ document.addEventListener('DOMContentLoaded', () => {
const zipBlob = await zip.generateAsync({ type: 'blob' });
const totalSavings = totalOriginalSize - totalCompressedSize;
const totalSavingsPercent =
totalSavings > 0
? ((totalSavings / totalOriginalSize) * 100).toFixed(1)
: 0;
const totalSavingsPercent = totalSavings > 0
? ((totalSavings / totalOriginalSize) * 100).toFixed(1)
: 0;
downloadFile(zipBlob, 'compressed-pdfs.zip');
@@ -486,6 +403,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
} catch (e: any) {
hideLoader();
console.error('[CompressPDF] Error:', e);
showAlert(
'Error',
`An error occurred during compression. Error: ${e.message}`
@@ -520,7 +438,7 @@ document.addEventListener('DOMContentLoaded', () => {
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf');
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
pdfFiles.forEach(f => dataTransfer.items.add(f));
@@ -529,7 +447,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Clear value on click to allow re-selecting the same file
fileInput.addEventListener('click', () => {
fileInput.value = '';
});

View File

@@ -0,0 +1,231 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
console.log('[CSV2PDF] Starting conversion...');
console.log('[CSV2PDF] Number of files:', state.files.length);
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one CSV file.');
hideLoader();
return;
}
const { convertCsvToPdf } = await import('../utils/csv-to-pdf.js');
if (state.files.length === 1) {
const originalFile = state.files[0];
console.log('[CSV2PDF] Converting single file:', originalFile.name, 'Size:', originalFile.size, 'bytes');
const pdfBlob = await convertCsvToPdf(originalFile, {
onProgress: (percent, message) => {
console.log(`[CSV2PDF] Progress: ${percent}% - ${message}`);
showLoader(message, percent);
}
});
console.log('[CSV2PDF] Conversion complete! PDF size:', pdfBlob.size, 'bytes');
const fileName = originalFile.name.replace(/\.csv$/i, '') + '.pdf';
downloadFile(pdfBlob, fileName);
console.log('[CSV2PDF] File downloaded:', fileName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
console.log('[CSV2PDF] Converting multiple files:', state.files.length);
showLoader('Preparing conversion...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
console.log(`[CSV2PDF] Converting file ${i + 1}/${state.files.length}:`, file.name);
const pdfBlob = await convertCsvToPdf(file, {
onProgress: (percent, message) => {
const overallPercent = ((i / state.files.length) * 100) + (percent / state.files.length);
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`, overallPercent);
}
});
console.log(`[CSV2PDF] Converted ${file.name}, PDF size:`, pdfBlob.size);
const baseName = file.name.replace(/\.csv$/i, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
console.log('[CSV2PDF] Generating ZIP file...');
showLoader('Creating ZIP archive...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
console.log('[CSV2PDF] ZIP size:', zipBlob.size);
downloadFile(zipBlob, 'csv-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} CSV file(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error('[CSV2PDF] ERROR:', e);
console.error('[CSV2PDF] Error stack:', e.stack);
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 csvFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.csv') || f.type === 'text/csv');
if (csvFiles.length > 0) {
const dataTransfer = new DataTransfer();
csvFiles.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', convertToPdf);
}
updateUI();
});

View File

@@ -1,21 +1,24 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { downloadFile, formatBytes, parsePageRanges } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
interface DividePagesState {
file: File | null;
pdfDoc: PDFLibDocument | null;
totalPages: number;
}
const pageState: DividePagesState = {
file: null,
pdfDoc: null,
totalPages: 0,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.totalPages = 0;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
@@ -28,6 +31,9 @@ function resetState() {
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
if (splitTypeSelect) splitTypeSelect.value = 'vertical';
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement;
if (pageRangeInput) pageRangeInput.value = '';
}
async function updateUI() {
@@ -73,10 +79,10 @@ async function updateUI() {
ignoreEncryption: true,
throwOnInvalidObject: false
});
pageState.totalPages = pageState.pdfDoc.getPageCount();
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.totalPages} pages`;
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
@@ -96,9 +102,25 @@ async function dividePages() {
return;
}
const pageRangeInput = document.getElementById('page-range') as HTMLInputElement;
const pageRangeValue = pageRangeInput?.value.trim().toLowerCase() || '';
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
const splitType = splitTypeSelect.value;
let pagesToDivide: Set<number>;
if (pageRangeValue === '' || pageRangeValue === 'all') {
pagesToDivide = new Set(Array.from({ length: pageState.totalPages }, (_, i) => i + 1));
} else {
const parsedIndices = parsePageRanges(pageRangeValue, pageState.totalPages);
pagesToDivide = new Set(parsedIndices.map(i => i + 1));
if (pagesToDivide.size === 0) {
showAlert('Invalid Range', 'Please enter a valid page range (e.g., 1-5, 8, 11-13).');
return;
}
}
showLoader('Splitting PDF pages...');
try {
@@ -106,27 +128,33 @@ async function dividePages() {
const pages = pageState.pdfDoc.getPages();
for (let i = 0; i < pages.length; i++) {
const pageNum = i + 1;
const originalPage = pages[i];
const { width, height } = originalPage.getSize();
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
showLoader(`Processing page ${pageNum} of ${pages.length}...`);
const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
const [page2] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
if (pagesToDivide.has(pageNum)) {
const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
const [page2] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
switch (splitType) {
case 'vertical':
page1.setCropBox(0, 0, width / 2, height);
page2.setCropBox(width / 2, 0, width / 2, height);
break;
case 'horizontal':
page1.setCropBox(0, height / 2, width, height / 2);
page2.setCropBox(0, 0, width, height / 2);
break;
switch (splitType) {
case 'vertical':
page1.setCropBox(0, 0, width / 2, height);
page2.setCropBox(width / 2, 0, width / 2, height);
break;
case 'horizontal':
page1.setCropBox(0, height / 2, width, height / 2);
page2.setCropBox(0, 0, width, height / 2);
break;
}
newPdfDoc.addPage(page1);
newPdfDoc.addPage(page2);
} else {
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
newPdfDoc.addPage(copiedPage);
}
newPdfDoc.addPage(page1);
newPdfDoc.addPage(page2);
}
const newPdfBytes = await newPdfDoc.save();

View File

@@ -4,8 +4,8 @@ import { showAlert, showLoader, hideLoader } from '../ui.js';
import { formatBytes } from '../utils/helpers.js';
const embedPdfWasmUrl = new URL(
'embedpdf-snippet/dist/pdfium.wasm',
import.meta.url
'embedpdf-snippet/dist/pdfium.wasm',
import.meta.url
).href;
let currentPdfUrl: string | null = null;
@@ -45,7 +45,6 @@ function initializePage() {
}
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
@@ -81,11 +80,7 @@ async function handleFiles(files: FileList) {
if (!pdfWrapper || !pdfContainer || !uploader || !dropZone || !fileDisplayArea) return;
// Hide uploader elements but keep the container
// Hide uploader elements but keep the container
// dropZone.classList.add('hidden');
// Show file display
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
@@ -114,7 +109,6 @@ async function handleFiles(files: FileList) {
pdfContainer.textContent = '';
pdfWrapper.classList.add('hidden');
fileDisplayArea.innerHTML = '';
// dropZone.classList.remove('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
};
@@ -123,13 +117,10 @@ async function handleFiles(files: FileList) {
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
// Clear previous content
pdfContainer.textContent = '';
if (currentPdfUrl) {
URL.revokeObjectURL(currentPdfUrl);
}
// Show editor container
pdfWrapper.classList.remove('hidden');
const fileURL = URL.createObjectURL(file);

View File

@@ -0,0 +1,205 @@
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';
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');
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 resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
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 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 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}`);
}
};
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);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});

View File

@@ -0,0 +1,216 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one Excel file.');
hideLoader();
return;
}
const converter = getLibreOfficeConverter();
// Initialize LibreOffice if not already done
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader('Processing...');
const pdfBlob = await converter.convertToPdf(originalFile);
const fileName = originalFile.name.replace(/\.(xls|xlsx|ods|csv)$/i, '') + '.pdf';
downloadFile(pdfBlob, fileName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Processing...');
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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.(xls|xlsx|ods|csv)$/i, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'excel-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} Excel file(s) to PDF.`,
'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 excelFiles = Array.from(files).filter(f => {
const name = f.name.toLowerCase();
return name.endsWith('.xls') || name.endsWith('.xlsx') || name.endsWith('.ods') || name.endsWith('.csv');
});
if (excelFiles.length > 0) {
const dataTransfer = new DataTransfer();
excelFiles.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', convertToPdf);
}
});

View File

@@ -0,0 +1,280 @@
import { showLoader, hideLoader, showAlert } from '../ui.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';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
interface ExtractedImage {
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');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
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 = '';
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)} • 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 = () => {
state.files = state.files.filter((_: File, i: number) => 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) {
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 downloadAll = async () => {
if (extractedImages.length === 0) return;
showLoader('Creating ZIP archive...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
extractedImages.forEach((img) => {
zip.file(img.name, img.data);
});
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'extracted-images.zip');
hideLoader();
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(
f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
state.files = [...state.files, ...pdfFiles];
updateUI();
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', extract);
}
if (downloadAllBtn) {
downloadAllBtn.addEventListener('click', downloadAll);
}
});

View File

@@ -0,0 +1,240 @@
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';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
let file: File | null = null;
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
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 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 = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
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');
}
async function extract() {
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;
}
});
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(/\.[^/.]+$/, '');
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');
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;
}
file = validFile;
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 (processBtn) {
processBtn.addEventListener('click', extract);
}
});

View File

@@ -0,0 +1,201 @@
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';
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');
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 resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
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 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 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}`);
}
};
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);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});

View File

@@ -1,9 +1,13 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
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;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
@@ -19,8 +23,14 @@ function initializePage() {
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 (fileInput) {
fileInput.accept = SUPPORTED_FORMATS;
fileInput.addEventListener('change', handleFileUpload);
}
@@ -43,7 +53,6 @@ function initializePage() {
}
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
@@ -78,13 +87,21 @@ function handleFileUpload(e: Event) {
}
}
function getFileExtension(filename: string): string {
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/');
}
function handleFiles(newFiles: FileList) {
const validFiles = Array.from(newFiles).filter(file =>
file.type.startsWith('image/')
);
const validFiles = Array.from(newFiles).filter(isValidImageFile);
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped. Only image files are allowed.');
showAlert('Invalid Files', 'Some files were skipped. Only supported image formats are allowed.');
}
if (validFiles.length > 0) {
@@ -146,95 +163,12 @@ function updateUI() {
}
}
function sanitizeImageAsJpeg(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(imageUrl);
return reject(new Error('Could not get canvas context'));
}
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (jpegBlob) => {
if (!jpegBlob) {
return reject(new Error('Canvas toBlob conversion failed.'));
}
const arrayBuffer = await jpegBlob.arrayBuffer();
resolve(new Uint8Array(arrayBuffer));
},
'image/jpeg',
0.9
);
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(
new Error(
'The provided file could not be loaded as an image. It may be corrupted.'
)
);
};
img.src = imageUrl;
});
}
// Special handler for SVG files - must read as text
function svgToPng(svgText: string): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const img = new Image();
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);
img.onload = () => {
const canvas = document.createElement('canvas');
const width = img.naturalWidth || img.width || 800;
const height = img.naturalHeight || img.height || 600;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(url);
return reject(new Error('Could not get canvas context'));
}
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
async (pngBlob) => {
URL.revokeObjectURL(url);
if (!pngBlob) {
return reject(new Error('Canvas toBlob conversion failed.'));
}
const arrayBuffer = await pngBlob.arrayBuffer();
resolve(new Uint8Array(arrayBuffer));
},
'image/png'
);
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load SVG image'));
};
img.src = url;
});
async function ensurePyMuPDF(): Promise<PyMuPDF> {
if (!pymupdf) {
pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
}
return pymupdf;
}
async function convertToPdf() {
@@ -243,78 +177,23 @@ async function convertToPdf() {
return;
}
showLoader('Creating PDF from images...');
showLoader('Loading PyMuPDF engine...');
try {
const pdfDoc = await PDFLibDocument.create();
const mupdf = await ensurePyMuPDF();
for (const file of files) {
try {
const isSvg = file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg');
showLoader('Converting images to PDF...');
if (isSvg) {
// Handle SVG files - read as text
const svgText = await file.text();
const pngBytes = await svgToPng(svgText);
const pngImage = await pdfDoc.embedPng(pngBytes);
const pdfBlob = await mupdf.imagesToPdf(files);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
} else if (file.type === 'image/png') {
// Handle PNG files
const originalBytes = await readFileAsArrayBuffer(file);
const pngImage = await pdfDoc.embedPng(originalBytes as Uint8Array);
downloadFile(pdfBlob, 'images_to_pdf.pdf');
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
} else {
// Handle JPG/other raster images
const originalBytes = await readFileAsArrayBuffer(file);
let jpgImage;
try {
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
} catch (e) {
// Fallback: convert to JPEG via canvas
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
}
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
page.drawImage(jpgImage, {
x: 0,
y: 0,
width: jpgImage.width,
height: jpgImage.height,
});
}
} catch (error) {
console.error(`Failed to process ${file.name}:`, error);
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_images.pdf'
);
showAlert('Success', 'PDF created successfully!', 'success', () => {
resetState();
});
} catch (e: any) {
console.error(e);
showAlert('Conversion Error', e.message);
console.error('[ImageToPDF]', e);
showAlert('Conversion Error', e.message || 'Failed to convert images to PDF.');
} finally {
hideLoader();
}

View File

@@ -1,9 +1,13 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
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;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
@@ -43,7 +47,6 @@ function initializePage() {
}
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
@@ -78,13 +81,21 @@ function handleFileUpload(e: Event) {
}
}
function getFileExtension(filename: string): string {
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);
}
function handleFiles(newFiles: FileList) {
const validFiles = Array.from(newFiles).filter(file =>
file.type === 'image/jpeg' || file.type === 'image/jpg' || file.name.toLowerCase().endsWith('.jpg') || file.name.toLowerCase().endsWith('.jpeg')
);
const validFiles = Array.from(newFiles).filter(isValidImageFile);
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped. Only JPG/JPEG images are allowed.');
showAlert('Invalid Files', 'Some files were skipped. Only JPG, JPEG, JP2, and JPX files are allowed.');
}
if (validFiles.length > 0) {
@@ -146,102 +157,37 @@ function updateUI() {
}
}
function sanitizeImageAsJpeg(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (jpegBlob) => {
if (!jpegBlob) {
return reject(new Error('Canvas toBlob conversion failed.'));
}
const arrayBuffer = await jpegBlob.arrayBuffer();
resolve(new Uint8Array(arrayBuffer));
},
'image/jpeg',
0.9
);
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(
new Error(
'The provided file could not be loaded as an image. It may be corrupted.'
)
);
};
img.src = imageUrl;
});
async function ensurePyMuPDF(): Promise<PyMuPDF> {
if (!pymupdf) {
pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
}
return pymupdf;
}
async function convertToPdf() {
if (files.length === 0) {
showAlert('No Files', 'Please select at least one JPG file.');
showAlert('No Files', 'Please select at least one JPG or JPEG2000 image.');
return;
}
showLoader('Creating PDF from JPGs...');
showLoader('Loading PyMuPDF engine...');
try {
const pdfDoc = await PDFLibDocument.create();
const mupdf = await ensurePyMuPDF();
for (const file of files) {
const originalBytes = await readFileAsArrayBuffer(file);
let jpgImage;
showLoader('Converting images to PDF...');
try {
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
} catch (e) {
showAlert(
'Warning',
`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`
);
try {
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
} catch (fallbackError) {
console.error(
`Failed to process ${file.name} after sanitization:`,
fallbackError
);
throw new Error(
`Could not process "${file.name}". The file may be corrupted.`
);
}
}
const pdfBlob = await mupdf.imagesToPdf(files);
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
page.drawImage(jpgImage, {
x: 0,
y: 0,
width: jpgImage.width,
height: jpgImage.height,
});
}
downloadFile(pdfBlob, 'from_jpgs.pdf');
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_jpgs.pdf'
);
showAlert('Success', 'PDF created successfully!', 'success', () => {
resetState();
});
} catch (e: any) {
console.error(e);
showAlert('Conversion Error', e.message);
console.error('[JpgToPdf]', e);
showAlert('Conversion Error', e.message || 'Failed to convert images to PDF.');
} finally {
hideLoader();
}

View File

@@ -0,0 +1,21 @@
import { MarkdownEditor } from '../utils/markdown-editor.js';
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('markdown-editor-container');
if (!container) {
console.error('Markdown editor container not found');
return;
}
const editor = new MarkdownEditor(container, {});
console.log('Markdown editor initialized');
const backButton = document.getElementById('back-to-tools');
if (backButton) {
backButton.addEventListener('click', () => {
window.location.href = '/';
});
}
});

View File

@@ -0,0 +1,201 @@
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';
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');
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 resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
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 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 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}`);
}
};
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);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});

View File

@@ -0,0 +1,189 @@
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 { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
const ACCEPTED_EXTENSIONS = ['.odg'];
const FILETYPE_NAME = 'ODG';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
const converter = getLibreOfficeConverter();
showLoader('Loading engine...');
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await converter.convertToPdf(file);
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 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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.[^/.]+$/, '');
zip.file(`${baseName}.pdf`, pdfBlob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to 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);
}
updateUI();
});

View File

@@ -0,0 +1,189 @@
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 { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
const ACCEPTED_EXTENSIONS = ['.odp'];
const FILETYPE_NAME = 'ODP';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
const converter = getLibreOfficeConverter();
showLoader('Loading engine...');
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await converter.convertToPdf(file);
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 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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.[^/.]+$/, '');
zip.file(`${baseName}.pdf`, pdfBlob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to 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);
}
updateUI();
});

View File

@@ -0,0 +1,189 @@
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 { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
const ACCEPTED_EXTENSIONS = ['.ods'];
const FILETYPE_NAME = 'ODS';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
const converter = getLibreOfficeConverter();
showLoader('Loading engine...');
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await converter.convertToPdf(file);
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 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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.[^/.]+$/, '');
zip.file(`${baseName}.pdf`, pdfBlob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to 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);
}
updateUI();
});

View File

@@ -0,0 +1,215 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one ODT file.');
hideLoader();
return;
}
const converter = getLibreOfficeConverter();
// Initialize LibreOffice if not already done
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader('Processing...');
const pdfBlob = await converter.convertToPdf(originalFile);
const fileName = originalFile.name.replace(/\.odt$/i, '') + '.pdf';
downloadFile(pdfBlob, fileName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Processing...');
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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.odt$/i, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'odt-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} ODT file(s) to PDF.`,
'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 odtFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.odt') || f.type === 'application/vnd.oasis.opendocument.text');
if (odtFiles.length > 0) {
const dataTransfer = new DataTransfer();
odtFiles.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', convertToPdf);
}
updateUI();
});

View File

@@ -0,0 +1,188 @@
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 { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
const ACCEPTED_EXTENSIONS = ['.pages'];
const FILETYPE_NAME = 'Pages';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
const converter = getLibreOfficeConverter();
showLoader('Loading engine...');
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await converter.convertToPdf(file);
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 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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.[^/.]+$/, '');
zip.file(`${baseName}.pdf`, pdfBlob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
}
} catch (e: any) {
hideLoader();
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
showAlert('Error', `An error occurred during conversion. Error: ${e.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);
}
updateUI();
});

View File

@@ -0,0 +1,517 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, degrees, PageSizes } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url
).toString();
interface BookletState {
file: File | null;
pdfDoc: PDFLibDocument | null;
pdfBytes: Uint8Array | null;
pdfjsDoc: pdfjsLib.PDFDocumentProxy | null;
}
const pageState: BookletState = {
file: null,
pdfDoc: null,
pdfBytes: null,
pdfjsDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.pdfBytes = null;
pageState.pdfjsDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
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 previewArea = document.getElementById('booklet-preview');
if (previewArea) previewArea.innerHTML = '<p class="text-gray-400 text-center py-8">Upload a PDF and click "Generate Preview" to see the booklet layout</p>';
const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement;
if (downloadBtn) downloadBtn.disabled = true;
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
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';
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 metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
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();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfBytes = new Uint8Array(arrayBuffer);
pageState.pdfDoc = await PDFLibDocument.load(pageState.pdfBytes, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
pageState.pdfjsDoc = await pdfjsLib.getDocument({ data: pageState.pdfBytes.slice() }).promise;
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
if (toolOptions) toolOptions.classList.remove('hidden');
const previewBtn = document.getElementById('preview-btn') as HTMLButtonElement;
if (previewBtn) previewBtn.disabled = false;
} 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 getGridDimensions(): { rows: number; cols: number } {
const gridMode = (document.querySelector('input[name="grid-mode"]:checked') as HTMLInputElement)?.value || '1x2';
switch (gridMode) {
case '1x2': return { rows: 1, cols: 2 };
case '2x2': return { rows: 2, cols: 2 };
case '2x4': return { rows: 2, cols: 4 };
case '4x4': return { rows: 4, cols: 4 };
default: return { rows: 1, cols: 2 };
}
}
function getOrientation(isBookletMode: boolean): 'portrait' | 'landscape' {
const orientationValue = (document.querySelector('input[name="orientation"]:checked') as HTMLInputElement)?.value || 'auto';
if (orientationValue === 'portrait') return 'portrait';
if (orientationValue === 'landscape') return 'landscape';
return isBookletMode ? 'landscape' : 'portrait';
}
function getSheetDimensions(isBookletMode: boolean): { width: number; height: number } {
const paperSizeKey = (document.getElementById('paper-size') as HTMLSelectElement).value as keyof typeof PageSizes;
const pageDims = PageSizes[paperSizeKey] || PageSizes.Letter;
const orientation = getOrientation(isBookletMode);
if (orientation === 'landscape') {
return { width: pageDims[1], height: pageDims[0] };
}
return { width: pageDims[0], height: pageDims[1] };
}
async function generatePreview() {
if (!pageState.pdfDoc || !pageState.pdfjsDoc) {
showAlert('Error', 'Please load a PDF first.');
return;
}
const previewArea = document.getElementById('booklet-preview')!;
const totalPages = pageState.pdfDoc.getPageCount();
const { rows, cols } = getGridDimensions();
const pagesPerSheet = rows * cols;
const isBookletMode = rows === 1 && cols === 2;
let numSheets: number;
if (isBookletMode) {
const sheetsNeeded = Math.ceil(totalPages / 4);
numSheets = sheetsNeeded * 2;
} else {
numSheets = Math.ceil(totalPages / pagesPerSheet);
}
const { width: sheetWidth, height: sheetHeight } = getSheetDimensions(isBookletMode);
// Get container width to make canvas fill it
const previewContainer = document.getElementById('booklet-preview')!;
const containerWidth = previewContainer.clientWidth - 32; // account for padding
const aspectRatio = sheetWidth / sheetHeight;
const canvasWidth = containerWidth;
const canvasHeight = containerWidth / aspectRatio;
previewArea.innerHTML = '<p class="text-gray-400 text-center py-4">Generating preview...</p>';
const totalRounded = isBookletMode ? Math.ceil(totalPages / 4) * 4 : totalPages;
const rotationMode = (document.querySelector('input[name="rotation"]:checked') as HTMLInputElement)?.value || 'none';
const pageThumbnails: Map<number, ImageBitmap> = new Map();
const thumbnailScale = 0.3;
for (let i = 1; i <= totalPages; i++) {
try {
const page = await pageState.pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: thumbnailScale });
const offscreen = new OffscreenCanvas(viewport.width, viewport.height);
const ctx = offscreen.getContext('2d')!;
await page.render({
canvasContext: ctx as any,
viewport: viewport,
canvas: offscreen as any,
}).promise;
const bitmap = await createImageBitmap(offscreen);
pageThumbnails.set(i, bitmap);
} catch (e) {
console.error(`Failed to render page ${i}:`, e);
}
}
previewArea.innerHTML = `<p class="text-indigo-400 text-sm mb-4 text-center">${totalPages} pages → ${numSheets} output sheets</p>`;
for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) {
const canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
canvas.className = 'border border-gray-600 rounded-lg mb-4';
const ctx = canvas.getContext('2d')!;
const isFront = sheetIndex % 2 === 0;
ctx.fillStyle = isFront ? '#1f2937' : '#1a2e1a';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.strokeStyle = '#4b5563';
ctx.lineWidth = 1;
ctx.strokeRect(0, 0, canvasWidth, canvasHeight);
const cellWidth = canvasWidth / cols;
const cellHeight = canvasHeight / rows;
const padding = 4;
ctx.strokeStyle = '#374151';
ctx.lineWidth = 1;
ctx.setLineDash([2, 2]);
for (let c = 1; c < cols; c++) {
ctx.beginPath();
ctx.moveTo(c * cellWidth, 0);
ctx.lineTo(c * cellWidth, canvasHeight);
ctx.stroke();
}
for (let r = 1; r < rows; r++) {
ctx.beginPath();
ctx.moveTo(0, r * cellHeight);
ctx.lineTo(canvasWidth, r * cellHeight);
ctx.stroke();
}
ctx.setLineDash([]);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const slotIndex = r * cols + c;
let pageNumber: number;
if (isBookletMode) {
const physicalSheet = Math.floor(sheetIndex / 2);
const isFrontSide = sheetIndex % 2 === 0;
if (isFrontSide) {
pageNumber = c === 0 ? totalRounded - 2 * physicalSheet : 2 * physicalSheet + 1;
} else {
pageNumber = c === 0 ? 2 * physicalSheet + 2 : totalRounded - 2 * physicalSheet - 1;
}
} else {
pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1;
}
const x = c * cellWidth + padding;
const y = r * cellHeight + padding;
const slotWidth = cellWidth - padding * 2;
const slotHeight = cellHeight - padding * 2;
const exists = pageNumber >= 1 && pageNumber <= totalPages;
if (exists) {
const thumbnail = pageThumbnails.get(pageNumber);
if (thumbnail) {
let rotation = 0;
if (rotationMode === '90cw') rotation = 90;
else if (rotationMode === '90ccw') rotation = -90;
else if (rotationMode === 'alternate') rotation = (pageNumber % 2 === 1) ? 90 : -90;
const isRotated = rotation !== 0;
const srcWidth = isRotated ? thumbnail.height : thumbnail.width;
const srcHeight = isRotated ? thumbnail.width : thumbnail.height;
const scale = Math.min(slotWidth / srcWidth, slotHeight / srcHeight);
const drawWidth = srcWidth * scale;
const drawHeight = srcHeight * scale;
const drawX = x + (slotWidth - drawWidth) / 2;
const drawY = y + (slotHeight - drawHeight) / 2;
ctx.save();
if (rotation !== 0) {
const centerX = drawX + drawWidth / 2;
const centerY = drawY + drawHeight / 2;
ctx.translate(centerX, centerY);
ctx.rotate((rotation * Math.PI) / 180);
ctx.drawImage(thumbnail, -drawHeight / 2, -drawWidth / 2, drawHeight, drawWidth);
} else {
ctx.drawImage(thumbnail, drawX, drawY, drawWidth, drawHeight);
}
ctx.restore();
ctx.strokeStyle = '#6b7280';
ctx.lineWidth = 1;
ctx.strokeRect(drawX, drawY, drawWidth, drawHeight);
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${pageNumber}`, x + slotWidth / 2, y + slotHeight - 4);
}
} else {
ctx.fillStyle = '#374151';
ctx.fillRect(x, y, slotWidth, slotHeight);
ctx.strokeStyle = '#4b5563';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, slotWidth, slotHeight);
ctx.fillStyle = '#6b7280';
ctx.font = '10px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('(blank)', x + slotWidth / 2, y + slotHeight / 2);
}
}
}
ctx.fillStyle = '#9ca3af';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
const sideLabel = isBookletMode ? (isFront ? 'Front' : 'Back') : '';
ctx.fillText(`Sheet ${Math.floor(sheetIndex / (isBookletMode ? 2 : 1)) + 1} ${sideLabel}`, canvasWidth - 6, 4);
previewArea.appendChild(canvas);
}
pageThumbnails.forEach(bitmap => bitmap.close());
const downloadBtn = document.getElementById('download-btn') as HTMLButtonElement;
downloadBtn.disabled = false;
}
function applyRotation(doc: PDFLibDocument, mode: string) {
const pages = doc.getPages();
pages.forEach((page, index) => {
let rotation = 0;
switch (mode) {
case '90cw': rotation = 90; break;
case '90ccw': rotation = -90; break;
case 'alternate': rotation = (index % 2 === 0) ? 90 : -90; break;
default: rotation = 0;
}
if (rotation !== 0) {
page.setRotation(degrees(page.getRotation().angle + rotation));
}
});
}
async function createBooklet() {
if (!pageState.pdfBytes) {
showAlert('Error', 'Please load a PDF first.');
return;
}
showLoader('Creating Booklet...');
try {
const sourceDoc = await PDFLibDocument.load(pageState.pdfBytes.slice());
const rotationMode = (document.querySelector('input[name="rotation"]:checked') as HTMLInputElement)?.value || 'none';
applyRotation(sourceDoc, rotationMode);
const totalPages = sourceDoc.getPageCount();
const { rows, cols } = getGridDimensions();
const pagesPerSheet = rows * cols;
const isBookletMode = rows === 1 && cols === 2;
const { width: sheetWidth, height: sheetHeight } = getSheetDimensions(isBookletMode);
const outputDoc = await PDFLibDocument.create();
let numSheets: number;
let totalRounded: number;
if (isBookletMode) {
totalRounded = Math.ceil(totalPages / 4) * 4;
numSheets = Math.ceil(totalPages / 4) * 2;
} else {
totalRounded = totalPages;
numSheets = Math.ceil(totalPages / pagesPerSheet);
}
const cellWidth = sheetWidth / cols;
const cellHeight = sheetHeight / rows;
const padding = 10;
for (let sheetIndex = 0; sheetIndex < numSheets; sheetIndex++) {
const outputPage = outputDoc.addPage([sheetWidth, sheetHeight]);
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const slotIndex = r * cols + c;
let pageNumber: number;
if (isBookletMode) {
const physicalSheet = Math.floor(sheetIndex / 2);
const isFrontSide = sheetIndex % 2 === 0;
if (isFrontSide) {
pageNumber = c === 0 ? totalRounded - 2 * physicalSheet : 2 * physicalSheet + 1;
} else {
pageNumber = c === 0 ? 2 * physicalSheet + 2 : totalRounded - 2 * physicalSheet - 1;
}
} else {
pageNumber = sheetIndex * pagesPerSheet + slotIndex + 1;
}
if (pageNumber >= 1 && pageNumber <= totalPages) {
const [embeddedPage] = await outputDoc.embedPdf(sourceDoc, [pageNumber - 1]);
const { width: srcW, height: srcH } = embeddedPage;
const availableWidth = cellWidth - padding * 2;
const availableHeight = cellHeight - padding * 2;
const scale = Math.min(availableWidth / srcW, availableHeight / srcH);
const scaledWidth = srcW * scale;
const scaledHeight = srcH * scale;
const x = c * cellWidth + padding + (availableWidth - scaledWidth) / 2;
const y = sheetHeight - (r + 1) * cellHeight + padding + (availableHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
}
}
}
}
const pdfBytes = await outputDoc.save();
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
`${originalName}_booklet.pdf`
);
showAlert('Success', `Booklet created with ${numSheets} sheets!`, 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while creating the booklet.');
} finally {
hideLoader();
}
}
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();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const previewBtn = document.getElementById('preview-btn');
const downloadBtn = document.getElementById('download-btn');
const backBtn = document.getElementById('back-to-tools');
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 (previewBtn) {
previewBtn.addEventListener('click', generatePreview);
}
if (downloadBtn) {
downloadBtn.addEventListener('click', createBooklet);
}
});

View File

@@ -0,0 +1,415 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
interface LayerData {
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;
let 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');
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 resetState = () => {
currentFile = null;
currentDoc = null;
layersMap.clear();
nextDisplayOrder = 0;
if (dropZone) dropZone.style.display = 'flex';
if (layersContainer) layersContainer.classList.add('hidden');
updateUI();
};
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');
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 = `
<div class="layers-empty">
<p>This PDF has no layers (OCG).</p>
<p>Add a new layer to get started!</p>
</div>
`;
return;
}
// Sort layers by displayOrder
const sortedLayers = layersArray.sort((a, b) => a.displayOrder - b.displayOrder);
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}" />
<span class="layer-name">${layer.depth > 0 ? '└ ' : ''}${layer.text || `Layer ${layer.number}`}</span>
${layer.locked ? '<span class="layer-locked">🔒</span>' : ''}
</label>
<div class="layer-actions">
${!layer.locked ? `<button class="layer-add-child" data-xref="${layer.xref}" title="Add child layer">+</button>` : ''}
${!layer.locked ? `<button class="layer-delete" data-xref="${layer.xref}" title="Delete layer">✕</button>` : ''}
</div>
</div>
`).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;
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);
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;
}
try {
showLoader('Loading PyMuPDF...');
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);
}
};
const setupLayerHandlers = () => {
const addLayerBtn = document.getElementById('add-layer-btn');
const newLayerInput = document.getElementById('new-layer-name') as HTMLInputElement;
const saveLayersBtn = document.getElementById('save-layers-btn');
if (addLayerBtn && newLayerInput) {
addLayerBtn.onclick = () => {
const name = newLayerInput.value.trim();
if (!name) {
showAlert('Invalid Name', 'Please enter a layer name');
return;
}
try {
const xref = currentDoc.addOCG(name);
newLayerInput.value = '';
const newDisplayOrder = nextDisplayOrder++;
layersMap.set(xref, {
number: xref,
xref: xref,
text: name,
on: true,
locked: false,
depth: 0,
parentXref: 0,
displayOrder: newDisplayOrder
});
renderLayers();
} catch (err: any) {
showAlert('Error', 'Failed to add layer: ' + err.message);
}
};
}
if (saveLayersBtn) {
saveLayersBtn.onclick = () => {
try {
showLoader('Saving PDF with layer changes...');
const pdfBytes = currentDoc.save();
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
const outName = currentFile!.name.replace(/\.pdf$/i, '') + '_layers.pdf';
downloadFile(blob, outName);
hideLoader();
resetState();
showAlert('Success', 'PDF with layer changes saved!', 'success');
} catch (err: any) {
hideLoader();
showAlert('Error', 'Failed to save PDF: ' + err.message);
}
};
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
currentFile = file;
updateUI();
} else {
showAlert('Invalid File', 'Please select a PDF file.');
}
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', loadLayers);
}
});

View File

@@ -0,0 +1,171 @@
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';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
let file: File | null = null;
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
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 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 = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
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');
}
async function convert() {
if (!file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
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();
}
}
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');
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;
}
file = validFile;
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 (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -0,0 +1,202 @@
import { showLoader, hideLoader, showAlert } from '../ui.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';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
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');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !convertOptions || !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)} • 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 = () => {
state.files = state.files.filter((_: File, i: number) => 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) {
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 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', convert);
}
});

View File

@@ -0,0 +1,181 @@
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 * as XLSX from 'xlsx';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
let file: File | null = null;
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
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 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 = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
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;
}
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(/\.[^/.]+$/, '');
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();
}
}
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');
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;
}
file = validFile;
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 (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -0,0 +1,205 @@
import { showLoader, hideLoader, showAlert } from '../ui.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';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
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;
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !convertOptions || !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)} • 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 = () => {
state.files = state.files.filter((_: File, i: number) => 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) {
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 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', convert);
}
});

View File

@@ -0,0 +1,228 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
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';
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;
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !optionsContainer || !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)} • 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 = () => {
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;
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
hideLoader();
return;
}
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader('Initializing Ghostscript...');
const convertedBlob = await convertFileToPdfA(
originalFile,
level,
(msg) => showLoader(msg)
);
const fileName = originalFile.name.replace(/\.pdf$/i, '') + '_pdfa.pdf';
downloadFile(convertedBlob, fileName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to ${level}.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple PDFs to PDF/A...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
const convertedBlob = await convertFileToPdfA(
file,
level,
(msg) => showLoader(msg)
);
const baseName = file.name.replace(/\.pdf$/i, '');
const blobBuffer = await convertedBlob.arrayBuffer();
zip.file(`${baseName}_pdfa.pdf`, blobBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdfa-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PDF(s) to ${level}.`,
'success',
() => resetState()
);
}
} catch (e: any) {
hideLoader();
showAlert(
'Error',
`An error occurred during conversion. Error: ${e.message}`
);
}
};
const handleFileSelect = (files: FileList | null) => {
if (files && files.length > 0) {
state.files = [...state.files, ...Array.from(files)];
updateUI();
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
pdfFiles.forEach(f => dataTransfer.items.add(f));
handleFileSelect(dataTransfer.files);
}
}
});
// Clear value on click to allow re-selecting the same file
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdfA);
}
});

View File

@@ -0,0 +1,201 @@
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';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
let files: File[] = [];
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const fileControls = document.getElementById('file-controls');
if (!fileDisplayArea || !optionsPanel) return;
fileDisplayArea.innerHTML = '';
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';
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 = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
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();
};
async function convert() {
if (files.length === 0) {
showAlert('No Files', 'Please upload at least one PDF file.');
return;
}
showLoader('Loading Engine...');
try {
await pymupdf.load();
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());
}
} 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');
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;
}
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 (addMoreBtn) addMoreBtn.addEventListener('click', () => fileInput?.click());
if (clearFilesBtn) clearFilesBtn.addEventListener('click', resetState);
if (processBtn) processBtn.addEventListener('click', convert);
});

View File

@@ -0,0 +1,211 @@
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';
let files: File[] = [];
let pymupdf: PyMuPDF | null = null;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() {
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;
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;
});
}
function handleFileUpload(e: Event) {
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')
);
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();
}
}
const resetState = () => {
files = [];
updateUI();
};
function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const extractOptions = document.getElementById('extract-options');
if (!fileDisplayArea || !fileControls || !extractOptions) return;
fileDisplayArea.innerHTML = '';
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';
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 sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
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();
};
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(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
}
return pymupdf;
}
async function extractText() {
if (files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
showLoader('Loading engine...');
try {
const mupdf = await ensurePyMuPDF();
if (files.length === 1) {
const file = files[0];
showLoader(`Extracting text from ${file.name}...`);
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`);
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();
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 baseName = file.name.replace(/\.pdf$/i, '');
zip.file(`${baseName}.txt`, fullText);
}
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();
});
}
} catch (e: any) {
console.error('[PDFToText]', e);
hideLoader();
showAlert('Extraction Error', e.message || 'Failed to extract text from PDF.');
}
}

View File

@@ -0,0 +1,218 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PowerPoint file.');
hideLoader();
return;
}
const converter = getLibreOfficeConverter();
// Initialize LibreOffice if not already done
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader('Processing...');
const pdfBlob = await converter.convertToPdf(originalFile);
const fileName = originalFile.name.replace(/\.(ppt|pptx|odp)$/i, '') + '.pdf';
downloadFile(pdfBlob, fileName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Processing...');
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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.(ppt|pptx|odp)$/i, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'powerpoint-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} PowerPoint file(s) to PDF.`,
'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 pptFiles = Array.from(files).filter(f => {
const name = f.name.toLowerCase();
return name.endsWith('.ppt') || name.endsWith('.pptx') || name.endsWith('.odp');
});
if (pptFiles.length > 0) {
const dataTransfer = new DataTransfer();
pptFiles.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', convertToPdf);
}
updateUI();
});

View File

@@ -0,0 +1,203 @@
import { showLoader, hideLoader, showAlert } from '../ui.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';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
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');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !extractOptions || !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)} • 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 = () => {
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');
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 PyMuPDF...');
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 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 (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', extractForAI);
}
});

View File

@@ -0,0 +1,133 @@
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';
const ACCEPTED_EXTENSIONS = ['.psd'];
const FILETYPE_NAME = 'PSD';
let pymupdf: PyMuPDF | null = null;
async function ensurePyMuPDF(): Promise<PyMuPDF> {
if (!pymupdf) {
pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
}
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');
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 resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
showLoader('Loading PyMuPDF 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);
});

View File

@@ -0,0 +1,142 @@
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 { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
const ACCEPTED_EXTENSIONS = ['.pub'];
const FILETYPE_NAME = 'PUB';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
const converter = getLibreOfficeConverter();
showLoader('Loading engine...');
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await converter.convertToPdf(file);
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 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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.[^/.]+$/, '');
zip.file(`${baseName}.pdf`, pdfBlob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to 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);
updateUI();
});

View File

@@ -0,0 +1,218 @@
import { showLoader, hideLoader, showAlert } from '../ui.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';
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
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');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !rasterizeOptions || !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)} • 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 = () => {
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');
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 PyMuPDF...');
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 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 (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', rasterize);
}
});

View File

@@ -0,0 +1,386 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, degrees } from 'pdf-lib';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface RotateState {
file: File | null;
pdfDoc: PDFLibDocument | null;
pdfJsDoc: pdfjsLib.PDFDocumentProxy | null;
rotations: number[];
}
const pageState: RotateState = {
file: null,
pdfDoc: null,
pdfJsDoc: null,
rotations: [],
};
function resetState() {
cleanupLazyRendering();
pageState.file = null;
pageState.pdfDoc = null;
pageState.pdfJsDoc = null;
pageState.rotations = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const pageThumbnails = document.getElementById('page-thumbnails');
if (pageThumbnails) pageThumbnails.innerHTML = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const batchAngle = document.getElementById('batch-custom-angle') as HTMLInputElement;
if (batchAngle) batchAngle.value = '0';
}
function updateAllRotationDisplays() {
for (let i = 0; i < pageState.rotations.length; i++) {
const input = document.getElementById(`page-angle-${i}`) as HTMLInputElement;
if (input) input.value = pageState.rotations[i].toString();
const container = document.querySelector(`[data-page-index="${i}"]`);
if (container) {
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
if (wrapper) wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`;
}
}
}
function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLElement {
const pageIndex = pageNumber - 1;
const container = document.createElement('div');
container.className = 'page-thumbnail relative bg-gray-700 rounded-lg overflow-hidden';
container.dataset.pageIndex = pageIndex.toString();
container.dataset.pageNumber = pageNumber.toString();
const canvasWrapper = document.createElement('div');
canvasWrapper.className = 'thumbnail-wrapper flex items-center justify-center p-2 h-36';
canvasWrapper.style.transition = 'transform 0.3s ease';
// Apply initial rotation if it exists (negated for canvas display)
const initialRotation = pageState.rotations[pageIndex] || 0;
canvasWrapper.style.transform = `rotate(${-initialRotation}deg)`;
canvas.className = 'max-w-full max-h-full object-contain';
canvasWrapper.appendChild(canvas);
const pageLabel = document.createElement('div');
pageLabel.className = 'absolute top-1 left-1 bg-black bg-opacity-60 text-white text-xs px-2 py-1 rounded';
pageLabel.textContent = `${pageNumber}`;
container.appendChild(canvasWrapper);
container.appendChild(pageLabel);
// Per-page rotation controls - Custom angle input
const controls = document.createElement('div');
controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800';
const decrementBtn = document.createElement('button');
decrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
decrementBtn.textContent = '-';
decrementBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
const current = parseInt(input.value) || 0;
input.value = (current - 1).toString();
};
const angleInput = document.createElement('input');
angleInput.type = 'number';
angleInput.id = `page-angle-${pageIndex}`;
angleInput.value = pageState.rotations[pageIndex]?.toString() || '0';
angleInput.className = 'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs';
const incrementBtn = document.createElement('button');
incrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
incrementBtn.textContent = '+';
incrementBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
const current = parseInt(input.value) || 0;
input.value = (current + 1).toString();
};
const applyBtn = document.createElement('button');
applyBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600';
applyBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
applyBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
const angle = parseInt(input.value) || 0;
pageState.rotations[pageIndex] = angle;
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`;
};
controls.append(decrementBtn, angleInput, incrementBtn, applyBtn);
container.appendChild(controls);
// Re-create icons for the new element
setTimeout(function () {
createIcons({ icons });
}, 0);
return container;
}
async function renderThumbnails() {
const pageThumbnails = document.getElementById('page-thumbnails');
if (!pageThumbnails || !pageState.pdfJsDoc) return;
pageThumbnails.innerHTML = '';
await renderPagesProgressively(
pageState.pdfJsDoc,
pageThumbnails,
createPageWrapper,
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '200px',
eagerLoadBatches: 2,
onBatchComplete: function () {
createIcons({ icons });
}
}
);
createIcons({ icons });
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
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';
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 metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
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();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer.slice(0), {
ignoreEncryption: true,
throwOnInvalidObject: false
});
pageState.pdfJsDoc = await getPDFDocument({ data: arrayBuffer.slice(0) }).promise;
const pageCount = pageState.pdfDoc.getPageCount();
pageState.rotations = new Array(pageCount).fill(0);
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
await renderThumbnails();
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');
}
}
async function applyRotations() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
showLoader('Applying rotations...');
try {
const pageCount = pageState.pdfDoc.getPageCount();
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < pageCount; i++) {
const rotation = pageState.rotations[i] || 0;
const originalPage = pageState.pdfDoc.getPage(i);
const currentRotation = originalPage.getRotation().angle;
const totalRotation = currentRotation + rotation;
console.log(`Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}`);
if (totalRotation % 90 === 0) {
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
copiedPage.setRotation(degrees(totalRotation));
newPdfDoc.addPage(copiedPage);
} else {
const embeddedPage = await newPdfDoc.embedPage(originalPage);
const { width, height } = embeddedPage.scale(1);
const angleRad = (totalRotation * Math.PI) / 180;
const absCos = Math.abs(Math.cos(angleRad));
const absSin = Math.abs(Math.sin(angleRad));
const newWidth = width * absCos + height * absSin;
const newHeight = width * absSin + height * absCos;
const newPage = newPdfDoc.addPage([newWidth, newHeight]);
const x = newWidth / 2 - (width / 2 * Math.cos(angleRad) - height / 2 * Math.sin(angleRad));
const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad));
newPage.drawPage(embeddedPage, {
x,
y,
width,
height,
rotate: degrees(totalRotation),
});
}
}
const rotatedPdfBytes = await newPdfDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(rotatedPdfBytes)], { type: 'application/pdf' }),
`${originalName}_rotated.pdf`
);
showAlert('Success', 'Rotations applied successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not apply rotations.');
} finally {
hideLoader();
}
}
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();
}
}
}
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 batchDecrement = document.getElementById('batch-decrement');
const batchIncrement = document.getElementById('batch-increment');
const batchApply = document.getElementById('batch-apply');
const batchAngleInput = document.getElementById('batch-custom-angle') as HTMLInputElement;
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (batchDecrement && batchAngleInput) {
batchDecrement.addEventListener('click', function () {
const current = parseInt(batchAngleInput.value) || 0;
batchAngleInput.value = (current - 1).toString();
});
}
if (batchIncrement && batchAngleInput) {
batchIncrement.addEventListener('click', function () {
const current = parseInt(batchAngleInput.value) || 0;
batchAngleInput.value = (current + 1).toString();
});
}
if (batchApply && batchAngleInput) {
batchApply.addEventListener('click', function () {
const angle = parseInt(batchAngleInput.value) || 0;
for (let i = 0; i < pageState.rotations.length; i++) {
pageState.rotations[i] = angle;
}
updateAllRotationDisplays();
});
}
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 (processBtn) {
processBtn.addEventListener('click', applyRotations);
}
});

View File

@@ -39,19 +39,14 @@ function resetState() {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const batchAngle = document.getElementById('batch-custom-angle') as HTMLInputElement;
if (batchAngle) batchAngle.value = '0';
}
function updateAllRotationDisplays() {
for (let i = 0; i < pageState.rotations.length; i++) {
const input = document.getElementById(`page-angle-${i}`) as HTMLInputElement;
if (input) input.value = pageState.rotations[i].toString();
const container = document.querySelector(`[data-page-index="${i}"]`);
if (container) {
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
if (wrapper) wrapper.style.transform = `rotate(${-pageState.rotations[i]}deg)`;
if (wrapper) wrapper.style.transform = `rotate(${pageState.rotations[i]}deg)`;
}
}
}
@@ -67,6 +62,9 @@ function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLE
const canvasWrapper = document.createElement('div');
canvasWrapper.className = 'thumbnail-wrapper flex items-center justify-center p-2 h-36';
canvasWrapper.style.transition = 'transform 0.3s ease';
// Apply initial rotation if it exists
const initialRotation = pageState.rotations[pageIndex] || 0;
canvasWrapper.style.transform = `rotate(${initialRotation}deg)`;
canvas.className = 'max-w-full max-h-full object-contain';
canvasWrapper.appendChild(canvas);
@@ -78,49 +76,31 @@ function createPageWrapper(canvas: HTMLCanvasElement, pageNumber: number): HTMLE
container.appendChild(canvasWrapper);
container.appendChild(pageLabel);
// Per-page rotation controls
// Per-page rotation controls - Left and Right buttons only
const controls = document.createElement('div');
controls.className = 'flex items-center justify-center gap-1 p-2 bg-gray-800';
controls.className = 'flex items-center justify-center gap-2 p-2 bg-gray-800';
const decrementBtn = document.createElement('button');
decrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
decrementBtn.textContent = '';
decrementBtn.onclick = function (e) {
const rotateLeftBtn = document.createElement('button');
rotateLeftBtn.className = 'flex items-center gap-1 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-xs';
rotateLeftBtn.innerHTML = '<i data-lucide="rotate-ccw" class="w-3 h-3"></i>';
rotateLeftBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
const current = parseInt(input.value) || 0;
input.value = (current - 1).toString();
};
const angleInput = document.createElement('input');
angleInput.type = 'number';
angleInput.id = `page-angle-${pageIndex}`;
angleInput.value = pageState.rotations[pageIndex]?.toString() || '0';
angleInput.className = 'w-12 h-8 text-center bg-gray-700 border border-gray-600 text-white rounded text-xs';
const incrementBtn = document.createElement('button');
incrementBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-sm';
incrementBtn.textContent = '+';
incrementBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
const current = parseInt(input.value) || 0;
input.value = (current + 1).toString();
};
const applyBtn = document.createElement('button');
applyBtn.className = 'w-8 h-8 flex items-center justify-center bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600';
applyBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
applyBtn.onclick = function (e) {
e.stopPropagation();
const input = document.getElementById(`page-angle-${pageIndex}`) as HTMLInputElement;
const angle = parseInt(input.value) || 0;
pageState.rotations[pageIndex] = angle;
pageState.rotations[pageIndex] = pageState.rotations[pageIndex] - 90;
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
if (wrapper) wrapper.style.transform = `rotate(${-angle}deg)`;
if (wrapper) wrapper.style.transform = `rotate(${pageState.rotations[pageIndex]}deg)`;
};
controls.append(decrementBtn, angleInput, incrementBtn, applyBtn);
const rotateRightBtn = document.createElement('button');
rotateRightBtn.className = 'flex items-center gap-1 px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded border border-gray-600 text-xs';
rotateRightBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-3 h-3"></i>';
rotateRightBtn.onclick = function (e) {
e.stopPropagation();
pageState.rotations[pageIndex] = pageState.rotations[pageIndex] + 90;
const wrapper = container.querySelector('.thumbnail-wrapper') as HTMLElement;
if (wrapper) wrapper.style.transform = `rotate(${pageState.rotations[pageIndex]}deg)`;
};
controls.append(rotateLeftBtn, rotateRightBtn);
container.appendChild(controls);
// Re-create icons for the new element
@@ -240,6 +220,8 @@ async function applyRotations() {
const currentRotation = originalPage.getRotation().angle;
const totalRotation = currentRotation + rotation;
console.log(`Page ${i}: rotation=${rotation}, currentRotation=${currentRotation}, totalRotation=${totalRotation}, applying=${-totalRotation}`);
if (totalRotation % 90 === 0) {
const [copiedPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
copiedPage.setRotation(degrees(totalRotation));
@@ -306,10 +288,6 @@ document.addEventListener('DOMContentLoaded', function () {
const backBtn = document.getElementById('back-to-tools');
const rotateAllLeft = document.getElementById('rotate-all-left');
const rotateAllRight = document.getElementById('rotate-all-right');
const batchDecrement = document.getElementById('batch-decrement');
const batchIncrement = document.getElementById('batch-increment');
const batchApply = document.getElementById('batch-apply');
const batchAngleInput = document.getElementById('batch-custom-angle') as HTMLInputElement;
if (backBtn) {
backBtn.addEventListener('click', function () {
@@ -320,7 +298,7 @@ document.addEventListener('DOMContentLoaded', function () {
if (rotateAllLeft) {
rotateAllLeft.addEventListener('click', function () {
for (let i = 0; i < pageState.rotations.length; i++) {
pageState.rotations[i] = pageState.rotations[i] + 90;
pageState.rotations[i] = pageState.rotations[i] - 90;
}
updateAllRotationDisplays();
});
@@ -329,38 +307,12 @@ document.addEventListener('DOMContentLoaded', function () {
if (rotateAllRight) {
rotateAllRight.addEventListener('click', function () {
for (let i = 0; i < pageState.rotations.length; i++) {
pageState.rotations[i] = pageState.rotations[i] - 90;
pageState.rotations[i] = pageState.rotations[i] + 90;
}
updateAllRotationDisplays();
});
}
if (batchDecrement && batchAngleInput) {
batchDecrement.addEventListener('click', function () {
const current = parseInt(batchAngleInput.value) || 0;
batchAngleInput.value = (current - 1).toString();
});
}
if (batchIncrement && batchAngleInput) {
batchIncrement.addEventListener('click', function () {
const current = parseInt(batchAngleInput.value) || 0;
batchAngleInput.value = (current + 1).toString();
});
}
if (batchApply && batchAngleInput) {
batchApply.addEventListener('click', function () {
const angle = parseInt(batchAngleInput.value) || 0;
if (angle !== 0) {
for (let i = 0; i < pageState.rotations.length; i++) {
pageState.rotations[i] = pageState.rotations[i] + angle;
}
updateAllRotationDisplays();
}
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);

View File

@@ -0,0 +1,215 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one RTF file.');
hideLoader();
return;
}
const converter = getLibreOfficeConverter();
// Initialize LibreOffice if not already done
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader('Processing...');
const pdfBlob = await converter.convertToPdf(originalFile);
const fileName = originalFile.name.replace(/\.rtf$/i, '') + '.pdf';
downloadFile(pdfBlob, fileName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
showLoader('Converting multiple RTF files to PDF...');
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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.rtf$/i, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'rtf-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} RTF file(s) to PDF.`,
'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 rtfFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.rtf') || f.type === 'text/rtf' || f.type === 'application/rtf');
if (rtfFiles.length > 0) {
const dataTransfer = new DataTransfer();
rtfFiles.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', convertToPdf);
}
updateUI();
});

View File

@@ -1,25 +1,17 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument, rgb, StandardFonts, PageSizes } from 'pdf-lib';
import { getFontForLanguage, getLanguageForChar } from '../utils/font-loader.js';
import { languageToFontFamily } from '../config/font-mappings.js';
import fontkit from '@pdf-lib/fontkit';
import { PyMuPDF } from '@bentopdf/pymupdf-wasm';
let files: File[] = [];
let currentMode: 'upload' | 'text' = 'upload';
let selectedLanguages: string[] = ['eng'];
const allLanguages = Object.keys(languageToFontFamily).sort().map(code => {
let name = code;
try {
const displayNames = new Intl.DisplayNames(['en'], { type: 'language' });
name = displayNames.of(code) || code;
} catch (e) {
console.warn(`Failed to get language name for ${code}`, e);
}
return { code, name: `${name} (${code})` };
});
// 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]/;
function hasRtlCharacters(text: string): boolean {
return RTL_PATTERN.test(text);
}
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
@@ -73,140 +65,11 @@ const resetState = () => {
updateUI();
};
async function createPdfFromText(
text: string,
fontSize: number,
pageSizeKey: string,
colorHex: string,
orientation: string,
customWidth?: number,
customHeight?: number
): Promise<Uint8Array> {
const pdfDoc = await PDFDocument.create();
pdfDoc.registerFontkit(fontkit);
const fontMap = new Map<string, any>();
const fallbackFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
if (!selectedLanguages.includes('eng')) {
selectedLanguages.push('eng');
}
for (const lang of selectedLanguages) {
try {
const fontBytes = await getFontForLanguage(lang);
const font = await pdfDoc.embedFont(fontBytes, { subset: false });
fontMap.set(lang, font);
} catch (e) {
console.warn(`Failed to load font for ${lang}, using fallback`, e);
fontMap.set(lang, fallbackFont);
}
}
let pageSize = pageSizeKey === 'Custom'
? [customWidth || 595, customHeight || 842] as [number, number]
: (PageSizes as any)[pageSizeKey];
if (orientation === 'landscape') {
pageSize = [pageSize[1], pageSize[0]] as [number, number];
}
const margin = 72;
const textColor = hexToRgb(colorHex);
let page = pdfDoc.addPage(pageSize);
let { width, height } = page.getSize();
const textWidth = width - margin * 2;
const lineHeight = fontSize * 1.3;
let y = height - margin;
const paragraphs = text.split('\n');
for (const paragraph of paragraphs) {
if (paragraph.trim() === '') {
y -= lineHeight;
if (y < margin) {
page = pdfDoc.addPage(pageSize);
y = page.getHeight() - margin;
}
continue;
}
const words = paragraph.split(' ');
let currentLineWords: { text: string; font: any }[] = [];
let currentLineWidth = 0;
for (const word of words) {
let wordLang = 'eng';
for (const char of word) {
const charLang = getLanguageForChar(char);
if (selectedLanguages.includes(charLang)) {
wordLang = charLang;
break;
}
}
const font = fontMap.get(wordLang) || fontMap.get('eng') || fallbackFont;
const wordWidth = font.widthOfTextAtSize(word + ' ', fontSize);
if (currentLineWidth + wordWidth > textWidth && currentLineWords.length > 0) {
currentLineWords.forEach((item, idx) => {
const x = margin + (currentLineWidth * idx / currentLineWords.length);
page.drawText(item.text, {
x: margin + (currentLineWidth - textWidth) / 2,
y: y,
size: fontSize,
font: item.font,
color: rgb(textColor.r / 255, textColor.g / 255, textColor.b / 255),
});
});
currentLineWords = [];
currentLineWidth = 0;
y -= lineHeight;
if (y < margin) {
page = pdfDoc.addPage(pageSize);
y = page.getHeight() - margin;
}
}
currentLineWords.push({ text: word + ' ', font });
currentLineWidth += wordWidth;
}
if (currentLineWords.length > 0) {
let x = margin;
currentLineWords.forEach((item) => {
page.drawText(item.text, {
x: x,
y: y,
size: fontSize,
font: item.font,
color: rgb(textColor.r / 255, textColor.g / 255, textColor.b / 255),
});
x += item.font.widthOfTextAtSize(item.text, fontSize);
});
y -= lineHeight;
if (y < margin) {
page = pdfDoc.addPage(pageSize);
y = page.getHeight() - margin;
}
}
}
return await pdfDoc.save();
}
async function convert() {
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement).value) || 12;
const pageSizeKey = (document.getElementById('page-size') as HTMLSelectElement).value;
const colorHex = (document.getElementById('text-color') as HTMLInputElement).value;
const orientation = (document.getElementById('page-orientation') as HTMLSelectElement).value;
const customWidth = parseInt((document.getElementById('custom-width') as HTMLInputElement)?.value);
const customHeight = parseInt((document.getElementById('custom-height') as HTMLInputElement)?.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.');
@@ -221,58 +84,59 @@ async function convert() {
}
}
showLoader('Creating PDF...');
showLoader('Loading engine...');
try {
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
let textContent = '';
if (currentMode === 'upload') {
let combinedText = '';
for (const file of files) {
const text = await file.text();
combinedText += text + '\n\n';
textContent += text + '\n\n';
}
const pdfBytes = await createPdfFromText(
combinedText,
fontSize,
pageSizeKey,
colorHex,
orientation,
customWidth,
customHeight
);
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_text.pdf'
);
} else {
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
const pdfBytes = await createPdfFromText(
textInput.value,
fontSize,
pageSizeKey,
colorHex,
orientation,
customWidth,
customHeight
);
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_text.pdf'
);
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) {
console.error(e);
showAlert('Error', 'Failed to convert text to PDF.');
} 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';
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
@@ -284,12 +148,7 @@ document.addEventListener('DOMContentLoaded', () => {
const textModeBtn = document.getElementById('txt-mode-text-btn');
const uploadPanel = document.getElementById('txt-upload-panel');
const textPanel = document.getElementById('txt-text-panel');
const pageSizeSelect = document.getElementById('page-size') as HTMLSelectElement;
const customSizeContainer = document.getElementById('custom-size-container');
const langDropdownBtn = document.getElementById('lang-dropdown-btn');
const langDropdownContent = document.getElementById('lang-dropdown-content');
const langSearch = document.getElementById('lang-search') as HTMLInputElement;
const langContainer = document.getElementById('language-list-container');
const textInput = document.getElementById('text-input') as HTMLTextAreaElement;
// Back to Tools
if (backBtn) {
@@ -321,86 +180,13 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Custom page size toggle
if (pageSizeSelect && customSizeContainer) {
pageSizeSelect.addEventListener('change', () => {
if (pageSizeSelect.value === 'Custom') {
customSizeContainer.classList.remove('hidden');
} else {
customSizeContainer.classList.add('hidden');
}
// RTL auto-detection for textarea
if (textInput) {
textInput.addEventListener('input', () => {
updateTextareaDirection(textInput);
});
}
// Language dropdown
if (langDropdownBtn && langDropdownContent && langContainer) {
// Populate language list
allLanguages.forEach(lang => {
const label = document.createElement('label');
label.className = 'flex items-center gap-2 p-2 hover:bg-gray-700 rounded cursor-pointer';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = lang.code;
checkbox.className = 'w-4 h-4';
checkbox.checked = lang.code === 'eng';
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
if (!selectedLanguages.includes(lang.code)) {
selectedLanguages.push(lang.code);
}
} else {
selectedLanguages = selectedLanguages.filter(l => l !== lang.code);
}
updateLanguageDisplay();
});
const span = document.createElement('span');
span.textContent = lang.name;
span.className = 'text-sm text-gray-300';
label.append(checkbox, span);
langContainer.appendChild(label);
});
langDropdownBtn.addEventListener('click', () => {
langDropdownContent.classList.toggle('hidden');
});
document.addEventListener('click', (e) => {
if (!langDropdownBtn.contains(e.target as Node) && !langDropdownContent.contains(e.target as Node)) {
langDropdownContent.classList.add('hidden');
}
});
if (langSearch) {
langSearch.addEventListener('input', () => {
const searchTerm = langSearch.value.toLowerCase();
const labels = langContainer.querySelectorAll('label');
labels.forEach(label => {
const text = label.textContent?.toLowerCase() || '';
if (text.includes(searchTerm)) {
(label as HTMLElement).style.display = 'flex';
} else {
(label as HTMLElement).style.display = 'none';
}
});
});
}
}
function updateLanguageDisplay() {
const langDropdownText = document.getElementById('lang-dropdown-text');
if (langDropdownText) {
const selectedNames = selectedLanguages.map(code => {
const lang = allLanguages.find(l => l.code === code);
return lang?.name || code;
});
langDropdownText.textContent = selectedNames.length > 0 ? selectedNames.join(', ') : 'Select Languages';
}
}
// File handling
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;

View File

@@ -0,0 +1,142 @@
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 { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
const ACCEPTED_EXTENSIONS = ['.vsd', '.vsdx'];
const FILETYPE_NAME = 'VSD';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
const converter = getLibreOfficeConverter();
showLoader('Loading engine...');
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await converter.convertToPdf(file);
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 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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.[^/.]+$/, '');
zip.file(`${baseName}.pdf`, pdfBlob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to 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);
updateUI();
});

View File

@@ -0,0 +1,236 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
console.log('[Word2PDF] Starting conversion...');
console.log('[Word2PDF] Number of files:', state.files.length);
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one Word document.');
hideLoader();
return;
}
const converter = getLibreOfficeConverter();
console.log('[Word2PDF] Got converter instance');
// Initialize LibreOffice if not already done
console.log('[Word2PDF] Initializing LibreOffice...');
await converter.initialize((progress: LoadProgress) => {
console.log('[Word2PDF] Init progress:', progress.percent + '%', progress.message);
showLoader(progress.message, progress.percent);
});
console.log('[Word2PDF] LibreOffice initialized successfully!');
if (state.files.length === 1) {
const originalFile = state.files[0];
console.log('[Word2PDF] Converting single file:', originalFile.name);
showLoader('Processing...');
const pdfBlob = await converter.convertToPdf(originalFile);
console.log('[Word2PDF] Conversion complete! PDF size:', pdfBlob.size);
const fileName = originalFile.name.replace(/\.(doc|docx|odt|rtf)$/i, '') + '.pdf';
downloadFile(pdfBlob, fileName);
console.log('[Word2PDF] File downloaded:', fileName);
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${originalFile.name} to PDF.`,
'success',
() => resetState()
);
} else {
console.log('[Word2PDF] Converting multiple files:', state.files.length);
showLoader('Processing...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
console.log(`[Word2PDF] Converting file ${i + 1}/${state.files.length}:`, file.name);
showLoader(`Converting ${i + 1}/${state.files.length}: ${file.name}...`);
const pdfBlob = await converter.convertToPdf(file);
console.log(`[Word2PDF] Converted ${file.name}, PDF size:`, pdfBlob.size);
const baseName = file.name.replace(/\.(doc|docx|odt|rtf)$/i, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
}
console.log('[Word2PDF] Generating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
console.log('[Word2PDF] ZIP size:', zipBlob.size);
downloadFile(zipBlob, 'word-converted.zip');
hideLoader();
showAlert(
'Conversion Complete',
`Successfully converted ${state.files.length} Word document(s) to PDF.`,
'success',
() => resetState()
);
}
} catch (e: any) {
console.error('[Word2PDF] ERROR:', e);
console.error('[Word2PDF] Error stack:', e.stack);
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 wordFiles = Array.from(files).filter(f => {
const name = f.name.toLowerCase();
return name.endsWith('.doc') || name.endsWith('.docx') || name.endsWith('.odt') || name.endsWith('.rtf');
});
if (wordFiles.length > 0) {
const dataTransfer = new DataTransfer();
wordFiles.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', convertToPdf);
}
// Initialize UI state (ensures button is hidden when no files)
updateUI();
});

View File

@@ -0,0 +1,188 @@
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 { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
const ACCEPTED_EXTENSIONS = ['.wpd'];
const FILETYPE_NAME = 'WPD';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
const converter = getLibreOfficeConverter();
showLoader('Loading engine...');
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await converter.convertToPdf(file);
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 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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.[^/.]+$/, '');
zip.file(`${baseName}.pdf`, pdfBlob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
}
} catch (e: any) {
hideLoader();
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
showAlert('Error', `An error occurred during conversion. Error: ${e.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);
}
updateUI();
});

View File

@@ -0,0 +1,188 @@
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 { getLibreOfficeConverter, type LoadProgress } from '../utils/libreoffice-loader.js';
const ACCEPTED_EXTENSIONS = ['.wps'];
const FILETYPE_NAME = 'WPS';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const convertOptions = document.getElementById('convert-options');
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 processBtn = document.getElementById('process-btn');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
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 });
}
if (fileControls) fileControls.classList.remove('hidden');
convertOptions.classList.remove('hidden');
} else {
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
if (fileControls) fileControls.classList.add('hidden');
convertOptions.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
const converter = getLibreOfficeConverter();
showLoader('Loading engine...');
await converter.initialize((progress: LoadProgress) => {
showLoader(progress.message, progress.percent);
});
if (state.files.length === 1) {
const file = state.files[0];
showLoader(`Converting ${file.name}...`);
const pdfBlob = await converter.convertToPdf(file);
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 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 pdfBlob = await converter.convertToPdf(file);
const baseName = file.name.replace(/\.[^/.]+$/, '');
zip.file(`${baseName}.pdf`, pdfBlob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to PDF.`, 'success', () => resetState());
}
} catch (e: any) {
hideLoader();
console.error(`[${FILETYPE_NAME}ToPDF] Error:`, e);
showAlert('Error', `An error occurred during conversion. Error: ${e.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);
}
updateUI();
});

View File

@@ -0,0 +1,181 @@
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 { convertXmlToPdf } from '../utils/xml-to-pdf.js';
const ACCEPTED_EXTENSIONS = ['.xml'];
const FILETYPE_NAME = 'XML';
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');
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 resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${FILETYPE_NAME} file.`);
return;
}
try {
if (state.files.length === 1) {
const file = state.files[0];
const pdfBlob = await convertXmlToPdf(file, {
onProgress: (percent, message) => {
showLoader(message, percent);
}
});
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 JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
const pdfBlob = await convertXmlToPdf(file, {
onProgress: (percent, message) => {
showLoader(`File ${i + 1}/${state.files.length}: ${message}`, percent);
}
});
const baseName = file.name.replace(/\.[^/.]+$/, '');
zip.file(`${baseName}.pdf`, pdfBlob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${FILETYPE_NAME.toLowerCase()}-to-pdf.zip`);
hideLoader();
showAlert('Conversion Complete', `Successfully converted ${state.files.length} files to 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);
}
});

View File

@@ -0,0 +1,201 @@
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';
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');
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 resetState = () => {
state.files = [];
state.pdfDoc = null;
updateUI();
};
const convertToPdf = async () => {
try {
if (state.files.length === 0) {
showAlert('No Files', `Please select at least one ${TOOL_NAME} file.`);
return;
}
showLoader('Loading engine...');
const pymupdf = new PyMuPDF(import.meta.env.BASE_URL + 'pymupdf-wasm/');
await pymupdf.load();
if (state.files.length === 1) {
const originalFile = state.files[0];
showLoader(`Converting ${originalFile.name}...`);
const pdfBlob = await pymupdf.convertToPdf(originalFile, { filetype: FILETYPE });
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 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 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}`);
}
};
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);
}
}
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
});