feat: implement filename deduplication utility and integrate across multiple file conversion modules

This commit is contained in:
alam00000
2026-03-24 20:36:56 +05:30
parent 850eaffc92
commit f88f872162
29 changed files with 2860 additions and 2187 deletions

View File

@@ -4,6 +4,7 @@ import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import Sortable from 'sortablejs';
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
import {
showWasmRequiredDialog,
WasmProvider,
@@ -72,19 +73,21 @@ async function updateUI() {
fileList.innerHTML = '';
try {
for (const file of pageState.files) {
for (let i = 0; i < pageState.files.length; i++) {
const file = pageState.files[i];
const fileKey = makeUniqueFileKey(i, file.name);
const arrayBuffer = await file.arrayBuffer();
pageState.pdfBytes.set(file.name, arrayBuffer);
pageState.pdfBytes.set(fileKey, arrayBuffer);
const bytesForPdfJs = arrayBuffer.slice(0);
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
pageState.pdfDocs.set(file.name, pdfjsDoc);
pageState.pdfDocs.set(fileKey, pdfjsDoc);
const pageCount = pdfjsDoc.numPages;
const li = document.createElement('li');
li.className =
'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
li.dataset.fileName = file.name;
li.dataset.fileName = fileKey;
const infoDiv = document.createElement('div');
infoDiv.className = 'flex items-center gap-2 truncate flex-1';

View File

@@ -2,6 +2,7 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { formatBytes, downloadFile } from '../utils/helpers.js';
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
const embedPdfWasmUrl = new URL(
'embedpdf-snippet/dist/pdfium.wasm',
@@ -143,10 +144,10 @@ async function handleFiles(files: FileList) {
docManagerPlugin.onDocumentOpened((data: any) => {
const docId = data?.id;
const docName = data?.name;
const docKey = data?.name;
if (!docId) return;
const pendingEntry = fileDisplayArea.querySelector(
`[data-pending-name="${CSS.escape(docName)}"]`
`[data-pending-name="${CSS.escape(docKey)}"]`
) as HTMLElement;
if (pendingEntry) {
pendingEntry.removeAttribute('data-pending-name');
@@ -166,7 +167,7 @@ async function handleFiles(files: FileList) {
docManagerPlugin.openDocumentBuffer({
buffer: firstBuffer,
name: firstFile.name,
name: makeUniqueFileKey(0, firstFile.name),
autoActivate: true,
});
@@ -174,7 +175,7 @@ async function handleFiles(files: FileList) {
const buffer = await pdfFiles[i].arrayBuffer();
docManagerPlugin.openDocumentBuffer({
buffer,
name: pdfFiles[i].name,
name: makeUniqueFileKey(i, pdfFiles[i].name),
autoActivate: false,
});
}
@@ -215,11 +216,11 @@ async function handleFiles(files: FileList) {
} else {
addFileEntries(fileDisplayArea, pdfFiles);
for (const file of pdfFiles) {
const buffer = await file.arrayBuffer();
for (let i = 0; i < pdfFiles.length; i++) {
const buffer = await pdfFiles[i].arrayBuffer();
docManagerPlugin.openDocumentBuffer({
buffer,
name: file.name,
name: makeUniqueFileKey(i, pdfFiles[i].name),
autoActivate: true,
});
}
@@ -233,11 +234,12 @@ async function handleFiles(files: FileList) {
}
function addFileEntries(fileDisplayArea: HTMLElement, files: File[]) {
for (const file of files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
fileDiv.setAttribute('data-pending-name', file.name);
fileDiv.setAttribute('data-pending-name', makeUniqueFileKey(i, file.name));
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';

View File

@@ -8,6 +8,7 @@ import { PDFDocument } from 'pdf-lib';
import { flattenAnnotations } from '../utils/flatten-annotations.js';
import { icons, createIcons } from 'lucide';
import JSZip from 'jszip';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
import { FlattenPdfState } from '@/types';
const pageState: FlattenPdfState = {
@@ -138,7 +139,7 @@ async function flattenPdf() {
const newPdfBytes = await pdfDoc.save();
downloadFile(
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`flattened_${file.name}`
);
if (loaderModal) loaderModal.classList.add('hidden');
@@ -147,6 +148,7 @@ async function flattenPdf() {
if (loaderText) loaderText.textContent = 'Flattening multiple PDFs...';
const zip = new JSZip();
const usedNames = new Set<string>();
let processedCount = 0;
for (let i = 0; i < pageState.files.length; i++) {
@@ -178,7 +180,11 @@ async function flattenPdf() {
}
const flattenedBytes = await pdfDoc.save();
zip.file(`flattened_${file.name}`, flattenedBytes);
const zipEntryName = deduplicateFileName(
`flattened_${file.name}`,
usedNames
);
zip.file(zipEntryName, flattenedBytes);
processedCount++;
} catch (e) {
console.error(`Error processing ${file.name}:`, e);

View File

@@ -5,6 +5,7 @@ import { isGhostscriptAvailable } from '../utils/ghostscript-dynamic-loader.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { icons, createIcons } from 'lucide';
import JSZip from 'jszip';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
interface FontToOutlineState {
files: File[];
@@ -128,6 +129,7 @@ async function processFiles() {
if (loaderText) loaderText.textContent = 'Processing multiple PDFs...';
const zip = new JSZip();
const usedNames = new Set<string>();
let processedCount = 0;
for (let i = 0; i < pageState.files.length; i++) {
@@ -139,7 +141,11 @@ async function processFiles() {
const resultBlob = await convertFileToOutlines(file, () => {});
const arrayBuffer = await resultBlob.arrayBuffer();
const baseName = file.name.replace(/\.pdf$/i, '');
zip.file(`${baseName}_outlined.pdf`, arrayBuffer);
const zipEntryName = deduplicateFileName(
`${baseName}_outlined.pdf`,
usedNames
);
zip.file(zipEntryName, arrayBuffer);
processedCount++;
} catch (e) {
console.error(`Error processing ${file.name}:`, e);

View File

@@ -1,237 +1,257 @@
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
import {
downloadFile,
formatBytes,
initializeQpdf,
readFileAsArrayBuffer,
} from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
import JSZip from 'jszip';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
import { LinearizePdfState } from '@/types';
const pageState: LinearizePdfState = {
files: [],
files: [],
};
function resetState() {
pageState.files = [];
pageState.files = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileControls = document.getElementById('file-controls');
if (fileControls) fileControls.classList.add('hidden');
const fileControls = document.getElementById('file-controls');
if (fileControls) fileControls.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileControls = document.getElementById('file-controls');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileControls = document.getElementById('file-controls');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.files.length > 0) {
pageState.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';
if (pageState.files.length > 0) {
pageState.files.forEach((file, index) => {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
pageState.files.splice(index, 1);
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 = function () {
pageState.files.splice(index, 1);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
if (fileControls) fileControls.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (fileControls) fileControls.classList.add('hidden');
}
if (toolOptions) toolOptions.classList.remove('hidden');
if (fileControls) fileControls.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (fileControls) fileControls.classList.add('hidden');
}
}
function 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) {
pageState.files.push(...pdfFiles);
updateUI();
}
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) {
pageState.files.push(...pdfFiles);
updateUI();
}
}
}
async function linearizePdf() {
const pdfFiles = pageState.files.filter(
(file: File) => file.type === 'application/pdf'
);
if (!pdfFiles || pdfFiles.length === 0) {
showAlert('No PDF Files', 'Please upload at least one PDF file.');
return;
}
const pdfFiles = pageState.files.filter(
(file: File) => file.type === 'application/pdf'
);
if (!pdfFiles || pdfFiles.length === 0) {
showAlert('No PDF Files', 'Please upload at least one PDF file.');
return;
}
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Optimizing PDFs for web view (linearizing)...';
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText)
loaderText.textContent = 'Optimizing PDFs for web view (linearizing)...';
const zip = new JSZip();
let qpdf: any;
let successCount = 0;
let errorCount = 0;
const zip = new JSZip();
const usedNames = new Set<string>();
let qpdf: any;
let successCount = 0;
let errorCount = 0;
try {
qpdf = await initializeQpdf();
try {
qpdf = await initializeQpdf();
for (let i = 0; i < pdfFiles.length; i++) {
const file = pdfFiles[i];
const inputPath = `/input_${i}.pdf`;
const outputPath = `/output_${i}.pdf`;
for (let i = 0; i < pdfFiles.length; i++) {
const file = pdfFiles[i];
const inputPath = `/input_${i}.pdf`;
const outputPath = `/output_${i}.pdf`;
if (loaderText) loaderText.textContent = `Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`;
if (loaderText)
loaderText.textContent = `Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`;
try {
const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
try {
const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
qpdf.FS.writeFile(inputPath, uint8Array);
const args = [inputPath, '--linearize', outputPath];
const args = [inputPath, '--linearize', outputPath];
qpdf.callMain(args);
qpdf.callMain(args);
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (!outputFile || outputFile.length === 0) {
console.error(
`Linearization resulted in an empty file for ${file.name}.`
);
throw new Error(`Processing failed for ${file.name}.`);
}
zip.file(`linearized-${file.name}`, outputFile, { binary: true });
successCount++;
} catch (fileError: any) {
errorCount++;
console.error(`Failed to linearize ${file.name}:`, fileError);
} finally {
try {
if (qpdf?.FS) {
if (qpdf.FS.analyzePath(inputPath).exists) {
qpdf.FS.unlink(inputPath);
}
if (qpdf.FS.analyzePath(outputPath).exists) {
qpdf.FS.unlink(outputPath);
}
}
} catch (cleanupError) {
console.warn(
`Failed to cleanup WASM FS for ${file.name}:`,
cleanupError
);
}
}
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (!outputFile || outputFile.length === 0) {
console.error(
`Linearization resulted in an empty file for ${file.name}.`
);
throw new Error(`Processing failed for ${file.name}.`);
}
if (successCount === 0) {
throw new Error('No PDF files could be linearized.');
}
if (loaderText) loaderText.textContent = 'Generating ZIP file...';
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'linearized-pdfs.zip');
let alertMessage = `${successCount} PDF(s) linearized successfully.`;
if (errorCount > 0) {
alertMessage += ` ${errorCount} file(s) failed.`;
}
showAlert('Processing Complete', alertMessage, 'success', () => { resetState(); });
} catch (error: any) {
console.error('Linearization process error:', error);
showAlert(
'Linearization Failed',
`An error occurred: ${error.message || 'Unknown error'}.`
const zipEntryName = deduplicateFileName(
`linearized-${file.name}`,
usedNames
);
} finally {
if (loaderModal) loaderModal.classList.add('hidden');
zip.file(zipEntryName, outputFile, { binary: true });
successCount++;
} catch (fileError: any) {
errorCount++;
console.error(`Failed to linearize ${file.name}:`, fileError);
} finally {
try {
if (qpdf?.FS) {
if (qpdf.FS.analyzePath(inputPath).exists) {
qpdf.FS.unlink(inputPath);
}
if (qpdf.FS.analyzePath(outputPath).exists) {
qpdf.FS.unlink(outputPath);
}
}
} catch (cleanupError) {
console.warn(
`Failed to cleanup WASM FS for ${file.name}:`,
cleanupError
);
}
}
}
if (successCount === 0) {
throw new Error('No PDF files could be linearized.');
}
if (loaderText) loaderText.textContent = 'Generating ZIP file...';
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'linearized-pdfs.zip');
let alertMessage = `${successCount} PDF(s) linearized successfully.`;
if (errorCount > 0) {
alertMessage += ` ${errorCount} file(s) failed.`;
}
showAlert('Processing Complete', alertMessage, 'success', () => {
resetState();
});
} catch (error: any) {
console.error('Linearization process error:', error);
showAlert(
'Linearization Failed',
`An error occurred: ${error.message || 'Unknown error'}.`
);
} finally {
if (loaderModal) loaderModal.classList.add('hidden');
}
}
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 addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const 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', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files);
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', linearizePdf);
}
if (processBtn) {
processBtn.addEventListener('click', linearizePdf);
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', function () {
fileInput.value = '';
fileInput.click();
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', function () {
fileInput.value = '';
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', function () {
resetState();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', function () {
resetState();
});
}
});

View File

@@ -131,8 +131,9 @@ async function renderPageMergeThumbnails() {
cleanupLazyRendering();
let totalPages = 0;
for (const file of state.files) {
const doc = mergeState.pdfDocs[file.name];
for (let i = 0; i < state.files.length; i++) {
const fileKey = `${i}_${state.files[i].name}`;
const doc = mergeState.pdfDocs[fileKey];
if (doc) totalPages += doc.numPages;
}
@@ -143,12 +144,13 @@ async function renderPageMergeThumbnails() {
const createWrapper = (
canvas: HTMLCanvasElement,
pageNumber: number,
fileName?: string
fileKey: string,
displayName: string
) => {
const wrapper = document.createElement('div');
wrapper.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
wrapper.dataset.fileName = fileName || '';
wrapper.dataset.fileName = fileKey;
wrapper.dataset.pageIndex = (pageNumber - 1).toString();
const imgContainer = document.createElement('div');
@@ -168,29 +170,29 @@ async function renderPageMergeThumbnails() {
const fileNamePara = document.createElement('p');
fileNamePara.className =
'text-xs text-gray-400 truncate w-full text-center';
const fullTitle = fileName
? `${fileName} (page ${pageNumber})`
const fullTitle = displayName
? `${displayName} (page ${pageNumber})`
: `Page ${pageNumber}`;
fileNamePara.title = fullTitle;
fileNamePara.textContent = fileName
? `${fileName.substring(0, 10)}... (p${pageNumber})`
fileNamePara.textContent = displayName
? `${displayName.substring(0, 10)}... (p${pageNumber})`
: `Page ${pageNumber}`;
wrapper.append(imgContainer, fileNamePara);
return wrapper;
};
// Render pages from all files progressively
for (const file of state.files) {
const pdfjsDoc = mergeState.pdfDocs[file.name];
for (let idx = 0; idx < state.files.length; idx++) {
const file = state.files[idx];
const fileKey = `${idx}_${file.name}`;
const pdfjsDoc = mergeState.pdfDocs[fileKey];
if (!pdfjsDoc) continue;
// Create a wrapper function that includes the file name
const createWrapperWithFileName = (
canvas: HTMLCanvasElement,
pageNumber: number
) => {
return createWrapper(canvas, pageNumber, file.name);
return createWrapper(canvas, pageNumber, fileKey, file.name);
};
// Render pages progressively with lazy loading
@@ -299,32 +301,27 @@ export async function merge() {
const fileList = document.getElementById('file-list');
if (!fileList) throw new Error('File list not found');
const sortedFiles = Array.from(fileList.children)
.map((li) => {
return state.files.find(
(f) => f.name === (li as HTMLElement).dataset.fileName
);
})
.filter(Boolean);
const sortedFileKeys = Array.from(fileList.children)
.map((li) => (li as HTMLElement).dataset.fileName)
.filter((key): key is string => !!key);
for (const file of sortedFiles) {
if (!file) continue;
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
for (const fileKey of sortedFileKeys) {
const safeFileName = fileKey.replace(/[^a-zA-Z0-9]/g, '_');
const rangeInput = document.getElementById(
`range-${safeFileName}`
) as HTMLInputElement;
uniqueFileNames.add(file.name);
uniqueFileNames.add(fileKey);
if (rangeInput && rangeInput.value.trim()) {
jobs.push({
fileName: file.name,
fileName: fileKey,
rangeType: 'specific',
rangeString: rangeInput.value.trim(),
});
} else {
jobs.push({
fileName: file.name,
fileName: fileKey,
rangeType: 'all',
});
}
@@ -456,13 +453,15 @@ export async function refreshMergeUI() {
mergeState.pdfDocs = {};
mergeState.pdfBytes = {};
for (const file of state.files) {
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
const fileKey = `${i}_${file.name}`;
const pdfBytes = await readFileAsArrayBuffer(file);
mergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
mergeState.pdfBytes[fileKey] = pdfBytes as ArrayBuffer;
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
mergeState.pdfDocs[file.name] = pdfjsDoc;
mergeState.pdfDocs[fileKey] = pdfjsDoc;
}
} catch (error) {
console.error('Error loading PDFs:', error);
@@ -483,14 +482,15 @@ export async function refreshMergeUI() {
fileList.textContent = ''; // Clear list safely
(state.files as File[]).forEach((f, index) => {
const doc = mergeState.pdfDocs[f.name];
const fileKey = `${index}_${f.name}`;
const doc = mergeState.pdfDocs[fileKey];
const pageCount = doc ? doc.numPages : 'N/A';
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
const safeFileName = fileKey.replace(/[^a-zA-Z0-9]/g, '_');
const li = document.createElement('li');
li.className =
'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
li.dataset.fileName = f.name;
li.dataset.fileName = fileKey;
const mainDiv = document.createElement('div');
mainDiv.className = 'flex items-center justify-between';

View File

@@ -1,188 +1,215 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
formatBytes,
} from '../utils/helpers.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';
import {
getLibreOfficeConverter,
type LoadProgress,
} from '../utils/libreoffice-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.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 = [];
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();
};
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();
const usedNames = new Set<string>();
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(/\.[^/.]+$/, '');
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, 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

@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -124,6 +125,7 @@ document.addEventListener('DOMContentLoaded', () => {
showLoader('Converting multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const usedNames = new Set<string>();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
@@ -134,7 +136,11 @@ document.addEventListener('DOMContentLoaded', () => {
const docxBlob = await pymupdf.pdfToDocx(file);
const baseName = file.name.replace(/\.pdf$/i, '');
const arrayBuffer = await docxBlob.arrayBuffer();
zip.file(`${baseName}.docx`, arrayBuffer);
const zipEntryName = deduplicateFileName(
`${baseName}.docx`,
usedNames
);
zip.file(zipEntryName, arrayBuffer);
}
showLoader('Creating ZIP archive...');

View File

@@ -4,6 +4,7 @@ import {
formatBytes,
readFileAsArrayBuffer,
} from '../utils/helpers';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
import { isCpdfAvailable } from '../utils/cpdf-helper.js';
import {
@@ -144,10 +145,12 @@ worker.onmessage = async (e: MessageEvent) => {
showStatus('Creating ZIP file...', 'info');
const zip = new JSZip();
const usedNames = new Set<string>();
jsonFiles.forEach(({ name, data }) => {
const jsonName = name.replace(/\.pdf$/i, '.json');
const uint8Array = new Uint8Array(data);
zip.file(jsonName, uint8Array);
const zipEntryName = deduplicateFileName(jsonName, usedNames);
zip.file(zipEntryName, uint8Array);
});
const zipBlob = await zip.generateAsync({ type: 'blob' });

View File

@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -130,6 +131,7 @@ document.addEventListener('DOMContentLoaded', () => {
showLoader('Converting multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const usedNames = new Set<string>();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
@@ -139,7 +141,8 @@ document.addEventListener('DOMContentLoaded', () => {
const markdown = await pymupdf.pdfToMarkdown(file, { includeImages });
const baseName = file.name.replace(/\.pdf$/i, '');
zip.file(`${baseName}.md`, markdown);
const zipEntryName = deduplicateFileName(`${baseName}.md`, usedNames);
zip.file(zipEntryName, markdown);
}
showLoader('Creating ZIP archive...');

View File

@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
import { convertFileToPdfA, type PdfALevel } from '../utils/ghostscript-loader';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -169,6 +170,7 @@ document.addEventListener('DOMContentLoaded', () => {
showLoader('Converting multiple PDFs to PDF/A...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const usedNames = new Set<string>();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
@@ -182,7 +184,11 @@ document.addEventListener('DOMContentLoaded', () => {
const baseName = file.name.replace(/\.pdf$/i, '');
const blobBuffer = await convertedBlob.arrayBuffer();
zip.file(`${baseName}_pdfa.pdf`, blobBuffer);
const zipEntryName = deduplicateFileName(
`${baseName}_pdfa.pdf`,
usedNames
);
zip.file(zipEntryName, blobBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });

View File

@@ -4,6 +4,7 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
let files: File[] = [];
let pymupdf: any = null;
@@ -196,6 +197,7 @@ async function extractText() {
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const usedNames = new Set<string>();
for (let i = 0; i < files.length; i++) {
const file = files[i];
@@ -206,7 +208,8 @@ async function extractText() {
const fullText = await mupdf.pdfToText(file);
const baseName = file.name.replace(/\.pdf$/i, '');
zip.file(`${baseName}.txt`, fullText);
const zipEntryName = deduplicateFileName(`${baseName}.txt`, usedNames);
zip.file(zipEntryName, fullText);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });

View File

@@ -2,159 +2,173 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import JSZip from 'jszip';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
interface PdfToZipState {
files: File[];
files: File[];
}
const pageState: PdfToZipState = {
files: [],
files: [],
};
function resetState() {
pageState.files = [];
pageState.files = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (pageState.files.length > 0) {
pageState.files.forEach(function (file, index) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (pageState.files.length > 0) {
pageState.files.forEach(function (file, index) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
pageState.files = pageState.files.filter(function (_, i) { return i !== index; });
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
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 () {
pageState.files = pageState.files.filter(function (_, i) {
return i !== index;
});
updateUI();
};
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function createZipArchive() {
if (pageState.files.length === 0) {
showAlert('No Files', 'Please select PDF files to create a ZIP archive.');
return;
if (pageState.files.length === 0) {
showAlert('No Files', 'Please select PDF files to create a ZIP archive.');
return;
}
showLoader('Creating ZIP archive...');
try {
const zip = new JSZip();
const usedNames = new Set<string>();
for (let i = 0; i < pageState.files.length; i++) {
const file = pageState.files[i];
showLoader(`Adding ${file.name} (${i + 1}/${pageState.files.length})...`);
const arrayBuffer = await file.arrayBuffer();
const zipEntryName = deduplicateFileName(file.name, usedNames);
zip.file(zipEntryName, arrayBuffer);
}
showLoader('Creating ZIP archive...');
showLoader('Generating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
try {
const zip = new JSZip();
downloadFile(zipBlob, 'pdfs_archive.zip');
for (let i = 0; i < pageState.files.length; i++) {
const file = pageState.files[i];
showLoader(`Adding ${file.name} (${i + 1}/${pageState.files.length})...`);
const arrayBuffer = await file.arrayBuffer();
zip.file(file.name, arrayBuffer);
}
showLoader('Generating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'pdfs_archive.zip');
showAlert('Success', 'ZIP archive created successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not create ZIP archive.');
} finally {
hideLoader();
}
showAlert(
'Success',
'ZIP archive created successfully!',
'success',
function () {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not create ZIP archive.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
pageState.files = [...pageState.files, ...pdfFiles];
updateUI();
}
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
});
if (pdfFiles.length > 0) {
pageState.files = [...pageState.files, ...pdfFiles];
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files || null);
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files || null);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', createZipArchive);
}
if (processBtn) {
processBtn.addEventListener('click', createZipArchive);
}
});

View File

@@ -1,218 +1,237 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
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';
import {
getLibreOfficeConverter,
type LoadProgress,
} from '../utils/libreoffice-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
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');
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;
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
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';
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);
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();
};
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)];
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);
}
};
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);
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();
const usedNames = new Set<string>();
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();
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, 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

@@ -8,9 +8,8 @@ import {
} from '../utils/helpers.js';
import { state } from '../state.js';
import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { loadPyMuPDF } from '../utils/pymupdf-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -132,6 +131,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const usedNames = new Set<string>();
for (const file of state.files) {
try {
@@ -142,7 +142,8 @@ document.addEventListener('DOMContentLoaded', () => {
const llamaDocs = await (pymupdf as any).pdfToLlamaIndex(file);
const outName = file.name.replace(/\.pdf$/i, '') + '_llm.json';
const jsonContent = JSON.stringify(llamaDocs, null, 2);
zip.file(outName, jsonContent);
const zipEntryName = deduplicateFileName(outName, usedNames);
zip.file(zipEntryName, jsonContent);
completed++;
} catch (error) {

View File

@@ -2,141 +2,185 @@ 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';
import {
getLibreOfficeConverter,
type LoadProgress,
} from '../utils/libreoffice-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.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 = [];
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();
};
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();
const usedNames = new Set<string>();
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(/\.[^/.]+$/, '');
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, 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

@@ -11,6 +11,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
@@ -151,6 +152,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Multiple files - create ZIP
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const usedNames = new Set<string>();
for (const file of state.files) {
try {
@@ -167,7 +169,8 @@ document.addEventListener('DOMContentLoaded', () => {
const outName =
file.name.replace(/\.pdf$/i, '') + '_rasterized.pdf';
zip.file(outName, rasterizedBlob);
const zipEntryName = deduplicateFileName(outName, usedNames);
zip.file(zipEntryName, rasterizedBlob);
completed++;
} catch (error) {

View File

@@ -1,128 +1,135 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
initializeQpdf,
readFileAsArrayBuffer,
downloadFile,
initializeQpdf,
readFileAsArrayBuffer,
} from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
export async function repairPdfFile(file: File): Promise<Uint8Array | null> {
const inputPath = '/input.pdf';
const outputPath = '/repaired_form.pdf';
let qpdf: any;
const inputPath = '/input.pdf';
const outputPath = '/repaired_form.pdf';
let qpdf: any;
try {
qpdf = await initializeQpdf();
const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
const args = [inputPath, '--decrypt', outputPath];
try {
qpdf = await initializeQpdf();
const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
const args = [inputPath, '--decrypt', outputPath];
try {
qpdf.callMain(args);
} catch (e) {
console.warn(`QPDF execution warning for ${file.name}:`, e);
}
let repairedData: Uint8Array | null = null;
try {
repairedData = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
} catch (e) {
console.warn(`Failed to read output for ${file.name}:`, e);
}
try {
try {
qpdf.FS.unlink(inputPath);
} catch (e) {
console.warn(e);
}
try {
qpdf.FS.unlink(outputPath);
} catch (e) {
console.warn(e);
}
} catch (cleanupError) {
console.warn('Cleanup error:', cleanupError);
}
return repairedData;
} catch (error) {
console.error(`Error repairing ${file.name}:`, error);
return null;
qpdf.callMain(args);
} catch (e) {
console.warn(`QPDF execution warning for ${file.name}:`, e);
}
let repairedData: Uint8Array | null = null;
try {
repairedData = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
} catch (e) {
console.warn(`Failed to read output for ${file.name}:`, e);
}
try {
try {
qpdf.FS.unlink(inputPath);
} catch (e) {
console.warn(e);
}
try {
qpdf.FS.unlink(outputPath);
} catch (e) {
console.warn(e);
}
} catch (cleanupError) {
console.warn('Cleanup error:', cleanupError);
}
return repairedData;
} catch (error) {
console.error(`Error repairing ${file.name}:`, error);
return null;
}
}
export async function repairPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select one or more PDF files.');
return;
if (state.files.length === 0) {
showAlert('No Files', 'Please select one or more PDF files.');
return;
}
const successfulRepairs: { name: string; data: Uint8Array }[] = [];
const failedRepairs: string[] = [];
try {
showLoader('Initializing repair engine...');
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Repairing ${file.name} (${i + 1}/${state.files.length})...`);
const repairedData = await repairPdfFile(file);
if (repairedData && repairedData.length > 0) {
successfulRepairs.push({
name: `repaired-${file.name}`,
data: repairedData,
});
} else {
failedRepairs.push(file.name);
}
}
const successfulRepairs: { name: string; data: Uint8Array }[] = [];
const failedRepairs: string[] = [];
hideLoader();
try {
showLoader('Initializing repair engine...');
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Repairing ${file.name} (${i + 1}/${state.files.length})...`);
const repairedData = await repairPdfFile(file);
if (repairedData && repairedData.length > 0) {
successfulRepairs.push({
name: `repaired-${file.name}`,
data: repairedData,
});
} else {
failedRepairs.push(file.name);
}
}
hideLoader();
if (successfulRepairs.length === 0) {
showAlert('Repair Failed', 'Unable to repair any of the uploaded PDF files.');
return;
}
if (failedRepairs.length > 0) {
const failedList = failedRepairs.join(', ');
showAlert(
'Partial Success',
`Repaired ${successfulRepairs.length} file(s). Failed to repair: ${failedList}`
);
}
if (successfulRepairs.length === 1) {
const file = successfulRepairs[0];
const blob = new Blob([file.data as any], { type: 'application/pdf' });
downloadFile(blob, file.name);
} else {
showLoader('Creating ZIP archive...');
const zip = new JSZip();
successfulRepairs.forEach((file) => {
zip.file(file.name, file.data);
});
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'repaired_pdfs.zip');
hideLoader();
}
if (failedRepairs.length === 0) {
showAlert('Success', 'All files repaired successfully!');
}
} catch (error: any) {
console.error('Critical error during repair:', error);
hideLoader();
showAlert('Error', 'An unexpected error occurred during the repair process.');
if (successfulRepairs.length === 0) {
showAlert(
'Repair Failed',
'Unable to repair any of the uploaded PDF files.'
);
return;
}
if (failedRepairs.length > 0) {
const failedList = failedRepairs.join(', ');
showAlert(
'Partial Success',
`Repaired ${successfulRepairs.length} file(s). Failed to repair: ${failedList}`
);
}
if (successfulRepairs.length === 1) {
const file = successfulRepairs[0];
const blob = new Blob([file.data as any], { type: 'application/pdf' });
downloadFile(blob, file.name);
} else {
showLoader('Creating ZIP archive...');
const zip = new JSZip();
const usedNames = new Set<string>();
successfulRepairs.forEach((file) => {
const zipEntryName = deduplicateFileName(file.name, usedNames);
zip.file(zipEntryName, file.data);
});
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'repaired_pdfs.zip');
hideLoader();
}
if (failedRepairs.length === 0) {
showAlert('Success', 'All files repaired successfully!');
}
} catch (error: any) {
console.error('Critical error during repair:', error);
hideLoader();
showAlert(
'Error',
'An unexpected error occurred during the repair process.'
);
}
}

View File

@@ -3,205 +3,232 @@ import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import JSZip from 'jszip';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
interface ReverseState {
files: File[];
files: File[];
}
const reverseState: ReverseState = {
files: [],
files: [],
};
function resetState() {
reverseState.files = [];
reverseState.files = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
fileDisplayArea.innerHTML = '';
if (reverseState.files.length > 0) {
reverseState.files.forEach(function (file, index) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
if (reverseState.files.length > 0) {
reverseState.files.forEach(function (file, index) {
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
reverseState.files = reverseState.files.filter(function (_, i) { return i !== index; });
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
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 () {
reverseState.files = reverseState.files.filter(function (_, i) {
return i !== index;
});
updateUI();
};
createIcons({ icons });
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function reversePages() {
if (reverseState.files.length === 0) {
showAlert('No Files', 'Please select one or more PDF files.');
return;
if (reverseState.files.length === 0) {
showAlert('No Files', 'Please select one or more PDF files.');
return;
}
showLoader('Reversing page order...');
try {
const zip = new JSZip();
const usedNames = new Set<string>();
for (let j = 0; j < reverseState.files.length; j++) {
const file = reverseState.files[j];
showLoader(
`Processing ${file.name} (${j + 1}/${reverseState.files.length})...`
);
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false,
});
const newPdf = await PDFLibDocument.create();
const pageCount = pdfDoc.getPageCount();
const reversedIndices = Array.from(
{ length: pageCount },
function (_, i) {
return pageCount - 1 - i;
}
);
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
copiedPages.forEach(function (page) {
newPdf.addPage(page);
});
const newPdfBytes = await newPdf.save();
const originalName = file.name.replace(/\.pdf$/i, '');
const fileName = `${originalName}_reversed.pdf`;
const zipEntryName = deduplicateFileName(fileName, usedNames);
zip.file(zipEntryName, newPdfBytes);
}
showLoader('Reversing page order...');
if (reverseState.files.length === 1) {
// Single file: download directly
const file = reverseState.files[0];
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false,
});
try {
const zip = new JSZip();
for (let j = 0; j < reverseState.files.length; j++) {
const file = reverseState.files[j];
showLoader(`Processing ${file.name} (${j + 1}/${reverseState.files.length})...`);
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
const newPdf = await PDFLibDocument.create();
const pageCount = pdfDoc.getPageCount();
const reversedIndices = Array.from(
{ length: pageCount },
function (_, i) { return pageCount - 1 - i; }
);
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
copiedPages.forEach(function (page) { newPdf.addPage(page); });
const newPdfBytes = await newPdf.save();
const originalName = file.name.replace(/\.pdf$/i, '');
const fileName = `${originalName}_reversed.pdf`;
zip.file(fileName, newPdfBytes);
const newPdf = await PDFLibDocument.create();
const pageCount = pdfDoc.getPageCount();
const reversedIndices = Array.from(
{ length: pageCount },
function (_, i) {
return pageCount - 1 - i;
}
);
if (reverseState.files.length === 1) {
// Single file: download directly
const file = reverseState.files[0];
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
copiedPages.forEach(function (page) {
newPdf.addPage(page);
});
const newPdf = await PDFLibDocument.create();
const pageCount = pdfDoc.getPageCount();
const reversedIndices = Array.from(
{ length: pageCount },
function (_, i) { return pageCount - 1 - i; }
);
const newPdfBytes = await newPdf.save();
const originalName = file.name.replace(/\.pdf$/i, '');
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
copiedPages.forEach(function (page) { newPdf.addPage(page); });
const newPdfBytes = await newPdf.save();
const originalName = file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_reversed.pdf`
);
} else {
// Multiple files: download as ZIP
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'reversed_pdfs.zip');
}
showAlert('Success', 'Pages have been reversed successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not reverse the PDF pages. Please check that your files are valid PDFs.');
} finally {
hideLoader();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_reversed.pdf`
);
} else {
// Multiple files: download as ZIP
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'reversed_pdfs.zip');
}
showAlert(
'Success',
'Pages have been reversed successfully!',
'success',
function () {
resetState();
}
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Could not reverse the PDF pages. Please check that your files are valid PDFs.'
);
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
reverseState.files = [...reverseState.files, ...pdfFiles];
updateUI();
}
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return (
f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf')
);
});
if (pdfFiles.length > 0) {
reverseState.files = [...reverseState.files, ...pdfFiles];
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files || null);
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files || null);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', reversePages);
}
if (processBtn) {
processBtn.addEventListener('click', reversePages);
}
});

View File

@@ -1,215 +1,234 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
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';
import {
getLibreOfficeConverter,
type LoadProgress,
} from '../utils/libreoffice-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
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');
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;
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
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';
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);
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();
};
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)];
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);
}
};
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);
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();
const usedNames = new Set<string>();
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();
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, 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

@@ -2,141 +2,185 @@ 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';
import {
getLibreOfficeConverter,
type LoadProgress,
} from '../utils/libreoffice-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.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 = [];
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();
};
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();
const usedNames = new Set<string>();
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(/\.[^/.]+$/, '');
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, 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

@@ -1,236 +1,269 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
formatBytes,
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';
import {
getLibreOfficeConverter,
type LoadProgress,
} from '../utils/libreoffice-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
document.addEventListener('DOMContentLoaded', () => {
state.files = [];
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');
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;
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!convertOptions) return;
const updateUI = async () => {
if (!convertOptions) return;
if (state.files.length > 0) {
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
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';
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);
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();
};
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)];
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);
}
};
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 = '';
});
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');
}
};
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)
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();
const usedNames = new Set<string>();
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();
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, 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

@@ -1,188 +1,215 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
formatBytes,
} from '../utils/helpers.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';
import {
getLibreOfficeConverter,
type LoadProgress,
} from '../utils/libreoffice-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.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 = [];
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();
};
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();
const usedNames = new Set<string>();
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(/\.[^/.]+$/, '');
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, 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

@@ -1,188 +1,215 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
formatBytes,
} from '../utils/helpers.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';
import {
getLibreOfficeConverter,
type LoadProgress,
} from '../utils/libreoffice-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.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 = [];
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();
};
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();
const usedNames = new Set<string>();
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(/\.[^/.]+$/, '');
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, 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

@@ -1,181 +1,205 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
formatBytes,
} from '../utils/helpers.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';
import { deduplicateFileName } from '../utils/deduplicate-filename.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');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
for (let index = 0; index < state.files.length; index++) {
const file = state.files[index];
const fileDiv = document.createElement('div');
fileDiv.className =
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className =
'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
}
createIcons({ icons });
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
} else {
fileDisplayArea.innerHTML = '';
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
}
};
const resetState = () => {
state.files = [];
updateUI();
};
const convert = async () => {
if (state.files.length === 0) {
showAlert(
'No Files',
`Please select at least one ${FILETYPE_NAME} file.`
);
return;
}
const updateUI = async () => {
if (!fileDisplayArea || !processBtn || !fileControls) return;
try {
if (state.files.length === 1) {
const file = state.files[0];
const pdfBlob = await convertXmlToPdf(file, {
onProgress: (percent, message) => {
showLoader(message, percent);
},
});
if (state.files.length > 0) {
fileDisplayArea.innerHTML = '';
const baseName = file.name.replace(/\.[^/.]+$/, '');
downloadFile(pdfBlob, `${baseName}.pdf`);
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';
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();
const usedNames = new Set<string>();
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
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 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 baseName = file.name.replace(/\.[^/.]+$/, '');
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, pdfBlob);
}
};
const resetState = () => {
state.files = [];
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();
};
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 (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', resetState);
}
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
if (processBtn) {
processBtn.addEventListener('click', convert);
}
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

@@ -5,6 +5,7 @@ import { createIcons, icons } from 'lucide';
import { isWasmAvailable, getWasmBaseUrl } from '../config/wasm-cdn-config.js';
import { showWasmRequiredDialog } from '../utils/wasm-provider.js';
import { loadPyMuPDF, isPyMuPDFAvailable } from '../utils/pymupdf-loader.js';
import { deduplicateFileName } from '../utils/deduplicate-filename.js';
const FILETYPE = 'xps';
const EXTENSIONS = ['.xps', '.oxps'];
@@ -114,6 +115,7 @@ document.addEventListener('DOMContentLoaded', () => {
showLoader('Converting files...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
const usedNames = new Set<string>();
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
@@ -126,7 +128,11 @@ document.addEventListener('DOMContentLoaded', () => {
});
const baseName = file.name.replace(/\.[^.]+$/, '');
const pdfBuffer = await pdfBlob.arrayBuffer();
zip.file(`${baseName}.pdf`, pdfBuffer);
const zipEntryName = deduplicateFileName(
`${baseName}.pdf`,
usedNames
);
zip.file(zipEntryName, pdfBuffer);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });