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

@@ -7,13 +7,18 @@ import {
renderFileDisplay,
switchView,
} from '../ui.js';
import { formatIsoDate, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import {
formatIsoDate,
readFileAsArrayBuffer,
getPDFDocument,
} from '../utils/helpers.js';
import { setupCanvasEditor } from '../canvasEditor.js';
import { toolLogic } from '../logic/index.js';
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
import Sortable from 'sortablejs';
import { makeUniqueFileKey } from '../utils/deduplicate-filename.js';
import {
multiFileTools,
simpleTools,
@@ -21,16 +26,23 @@ import {
} from '../config/pdf-tools.js';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
// Re-export rotation state utilities
export { getRotationState, updateRotationState, resetRotationState, initializeRotationState } from '../utils/rotation-state.js';
export {
getRotationState,
updateRotationState,
resetRotationState,
initializeRotationState,
} from '../utils/rotation-state.js';
const rotationState: number[] = [];
let imageSortableInstance: Sortable | null = null;
const activeImageUrls = new Map<File, string>();
async function handleSinglePdfUpload(toolId, file) {
showLoader('Loading PDF...');
try {
@@ -107,7 +119,11 @@ async function handleSinglePdfUpload(toolId, file) {
.toString();
}
if (toolId === 'organize' || toolId === 'rotate' || toolId === 'delete-pages') {
if (
toolId === 'organize' ||
toolId === 'rotate' ||
toolId === 'delete-pages'
) {
await renderPageThumbnails(toolId, state.pdfDoc);
if (toolId === 'rotate') {
@@ -124,11 +140,18 @@ async function handleSinglePdfUpload(toolId, file) {
const rotateAllRightBtn = document.getElementById(
'rotate-all-right-btn'
);
const rotateAllCustomBtn = document.getElementById('rotate-all-custom-btn');
const rotateAllCustomInput = document.getElementById('custom-rotate-all-input') as HTMLInputElement;
const rotateAllDecrementBtn = document.getElementById('rotate-all-decrement-btn');
const rotateAllIncrementBtn = document.getElementById('rotate-all-increment-btn');
const rotateAllCustomBtn = document.getElementById(
'rotate-all-custom-btn'
);
const rotateAllCustomInput = document.getElementById(
'custom-rotate-all-input'
) as HTMLInputElement;
const rotateAllDecrementBtn = document.getElementById(
'rotate-all-decrement-btn'
);
const rotateAllIncrementBtn = document.getElementById(
'rotate-all-increment-btn'
);
rotateAllControls.classList.remove('hidden');
createIcons({ icons });
@@ -136,12 +159,14 @@ async function handleSinglePdfUpload(toolId, file) {
const rotateAll = (angle: number) => {
// Update rotation state for ALL pages (including unrendered ones)
for (let i = 0; i < rotationState.length; i++) {
rotationState[i] = (rotationState[i] + angle);
rotationState[i] = rotationState[i] + angle;
}
// Update DOM for currently rendered pages
document.querySelectorAll('.page-rotator-item').forEach((item) => {
const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0');
const pageIndex = parseInt(
(item as HTMLElement).dataset.pageIndex || '0'
);
const newRotation = rotationState[pageIndex];
(item as HTMLElement).dataset.rotation = newRotation.toString();
@@ -463,7 +488,8 @@ async function handleSinglePdfUpload(toolId, file) {
addBtn.onclick = () => {
const fieldWrapper = document.createElement('div');
fieldWrapper.className = 'flex flex-col sm:flex-row items-stretch sm:items-center gap-2 custom-field-wrapper';
fieldWrapper.className =
'flex flex-col sm:flex-row items-stretch sm:items-center gap-2 custom-field-wrapper';
const keyInput = document.createElement('input');
keyInput.type = 'text';
@@ -505,7 +531,9 @@ async function handleSinglePdfUpload(toolId, file) {
// Setup quality sliders for image conversion tools
if (toolId === 'pdf-to-jpg') {
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
const qualitySlider = document.getElementById(
'jpg-quality'
) as HTMLInputElement;
const qualityValue = document.getElementById('jpg-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
@@ -517,7 +545,9 @@ async function handleSinglePdfUpload(toolId, file) {
}
if (toolId === 'pdf-to-png') {
const qualitySlider = document.getElementById('png-quality') as HTMLInputElement;
const qualitySlider = document.getElementById(
'png-quality'
) as HTMLInputElement;
const qualityValue = document.getElementById('png-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
@@ -529,7 +559,9 @@ async function handleSinglePdfUpload(toolId, file) {
}
if (toolId === 'pdf-to-webp') {
const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement;
const qualitySlider = document.getElementById(
'webp-quality'
) as HTMLInputElement;
const qualityValue = document.getElementById('webp-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
@@ -624,16 +656,20 @@ async function handleMultiFileUpload(toolId) {
const imageList = document.getElementById('image-list');
const renderedFiles = new Set(
Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName)
Array.from(imageList.querySelectorAll('li')).map(
(li) => li.dataset.fileKey
)
);
state.files.forEach((file) => {
state.files.forEach((file, index) => {
if (!file) {
console.error('Invalid file encountered in state.files');
return;
}
if (renderedFiles.has(file.name)) {
const fileKey = makeUniqueFileKey(index, file.name);
if (renderedFiles.has(fileKey)) {
return;
}
@@ -645,10 +681,12 @@ async function handleMultiFileUpload(toolId) {
const li = document.createElement('li');
li.className = 'relative group cursor-move';
li.dataset.fileName = file.name;
li.dataset.fileKey = fileKey;
li.dataset.fileIndex = String(index);
const wrapper = document.createElement('div');
wrapper.className = 'w-full h-36 sm:h-40 md:h-44 bg-gray-900 rounded-md border-2 border-gray-600 flex items-center justify-center overflow-hidden';
wrapper.className =
'w-full h-36 sm:h-40 md:h-44 bg-gray-900 rounded-md border-2 border-gray-600 flex items-center justify-center overflow-hidden';
const img = document.createElement('img');
img.src = url;
@@ -665,11 +703,14 @@ async function handleMultiFileUpload(toolId) {
});
const syncStateWithDOM = () => {
const domOrder = Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName);
state.files.sort((a, b) => {
const aIndex = domOrder.indexOf(a.name);
const bIndex = domOrder.indexOf(b.name);
return aIndex - bIndex;
const domOrder = Array.from(imageList.querySelectorAll('li')).map((li) =>
Number(li.dataset.fileIndex)
);
const reordered = domOrder.map((i) => state.files[i]);
state.files.length = 0;
reordered.forEach((f) => state.files.push(f));
Array.from(imageList.querySelectorAll('li')).forEach((li, i) => {
(li as HTMLElement).dataset.fileIndex = String(i);
});
};
@@ -678,7 +719,7 @@ async function handleMultiFileUpload(toolId) {
animation: 150,
onEnd: () => {
syncStateWithDOM();
}
},
});
}
@@ -687,10 +728,13 @@ async function handleMultiFileUpload(toolId) {
const opts = document.getElementById('image-to-pdf-options');
if (opts && opts.classList.contains('hidden')) {
opts.classList.remove('hidden');
const slider = document.getElementById('image-pdf-quality') as HTMLInputElement;
const slider = document.getElementById(
'image-pdf-quality'
) as HTMLInputElement;
const value = document.getElementById('image-pdf-quality-value');
if (slider && value) {
const update = () => (value.textContent = `${Math.round(parseFloat(slider.value) * 100)}%`);
const update = () =>
(value.textContent = `${Math.round(parseFloat(slider.value) * 100)}%`);
slider.addEventListener('input', update);
update();
}
@@ -698,7 +742,9 @@ async function handleMultiFileUpload(toolId) {
}
if (toolId === 'pdf-to-jpg') {
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
const qualitySlider = document.getElementById(
'jpg-quality'
) as HTMLInputElement;
const qualityValue = document.getElementById('jpg-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
@@ -710,7 +756,9 @@ async function handleMultiFileUpload(toolId) {
}
if (toolId === 'pdf-to-png') {
const qualitySlider = document.getElementById('png-quality') as HTMLInputElement;
const qualitySlider = document.getElementById(
'png-quality'
) as HTMLInputElement;
const qualityValue = document.getElementById('png-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
@@ -722,7 +770,9 @@ async function handleMultiFileUpload(toolId) {
}
if (toolId === 'pdf-to-webp') {
const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement;
const qualitySlider = document.getElementById(
'webp-quality'
) as HTMLInputElement;
const qualityValue = document.getElementById('webp-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
@@ -750,11 +800,23 @@ export function setupFileInputHandler(toolId) {
if (newFiles.length === 0) return;
if (toolId === 'image-to-pdf') {
const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff'];
const validFiles = newFiles.filter(file => validTypes.includes(file.type));
const validTypes = [
'image/jpeg',
'image/png',
'image/webp',
'image/gif',
'image/bmp',
'image/tiff',
];
const validFiles = newFiles.filter((file) =>
validTypes.includes(file.type)
);
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped because they are not supported images.');
showAlert(
'Invalid Files',
'Some files were skipped because they are not supported images.'
);
}
newFiles = validFiles;
@@ -780,7 +842,12 @@ export function setupFileInputHandler(toolId) {
}
if (isMultiFileTool) {
if (toolId === 'txt-to-pdf' || toolId === 'compress' || toolId === 'extract-attachments' || toolId === 'flatten') {
if (
toolId === 'txt-to-pdf' ||
toolId === 'compress' ||
toolId === 'extract-attachments' ||
toolId === 'flatten'
) {
const processBtn = document.getElementById('process-btn');
if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false;
@@ -839,7 +906,7 @@ export function setupFileInputHandler(toolId) {
const clearBtn = document.getElementById('clear-files-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
activeImageUrls.forEach(url => URL.revokeObjectURL(url));
activeImageUrls.forEach((url) => URL.revokeObjectURL(url));
activeImageUrls.clear();
state.files = [];

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

View File

@@ -0,0 +1,28 @@
export function deduplicateFileName(
name: string,
usedNames: Set<string>
): string {
if (!usedNames.has(name)) {
usedNames.add(name);
return name;
}
const dotIndex = name.lastIndexOf('.');
const hasExtension = dotIndex > 0;
const baseName = hasExtension ? name.slice(0, dotIndex) : name;
const extension = hasExtension ? name.slice(dotIndex) : '';
let counter = 1;
let candidate = `${baseName} (${counter})${extension}`;
while (usedNames.has(candidate)) {
counter++;
candidate = `${baseName} (${counter})${extension}`;
}
usedNames.add(candidate);
return candidate;
}
export function makeUniqueFileKey(index: number, name: string): string {
return `${index}_${name}`;
}

View File

@@ -0,0 +1,198 @@
import { describe, it, expect } from 'vitest';
import {
deduplicateFileName,
makeUniqueFileKey,
} from '@/js/utils/deduplicate-filename';
describe('deduplicateFileName', () => {
it('should return the original name when no duplicates exist', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('report.pdf', usedNames)).toBe('report.pdf');
});
it('should add the name to the Set after returning', () => {
const usedNames = new Set<string>();
deduplicateFileName('report.pdf', usedNames);
expect(usedNames.has('report.pdf')).toBe(true);
});
it('should append (1) for the first duplicate', () => {
const usedNames = new Set<string>();
deduplicateFileName('report.pdf', usedNames);
expect(deduplicateFileName('report.pdf', usedNames)).toBe('report (1).pdf');
});
it('should increment counter for multiple duplicates', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file.pdf');
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (1).pdf');
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (2).pdf');
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (3).pdf');
});
it('should track deduplicated names to avoid collisions with them', () => {
const usedNames = new Set<string>();
deduplicateFileName('file.pdf', usedNames);
deduplicateFileName('file (1).pdf', usedNames);
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (2).pdf');
});
it('should handle files with no extension', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('README', usedNames)).toBe('README');
expect(deduplicateFileName('README', usedNames)).toBe('README (1)');
expect(deduplicateFileName('README', usedNames)).toBe('README (2)');
});
it('should handle files with multiple dots in the name', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('my.report.2024.pdf', usedNames)).toBe(
'my.report.2024.pdf'
);
expect(deduplicateFileName('my.report.2024.pdf', usedNames)).toBe(
'my.report.2024 (1).pdf'
);
});
it('should handle dotfiles (name starts with dot)', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('.gitignore', usedNames)).toBe('.gitignore');
expect(deduplicateFileName('.gitignore', usedNames)).toBe('.gitignore (1)');
});
it('should handle dotfiles with extension', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('.env.local', usedNames)).toBe('.env.local');
expect(deduplicateFileName('.env.local', usedNames)).toBe('.env (1).local');
});
it('should handle empty string', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('', usedNames)).toBe('');
expect(deduplicateFileName('', usedNames)).toBe(' (1)');
});
it('should handle extension-only name', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('.pdf', usedNames)).toBe('.pdf');
expect(deduplicateFileName('.pdf', usedNames)).toBe('.pdf (1)');
});
it('should preserve the original extension exactly', () => {
const usedNames = new Set<string>();
deduplicateFileName('photo.JPEG', usedNames);
const result = deduplicateFileName('photo.JPEG', usedNames);
expect(result).toBe('photo (1).JPEG');
expect(result.endsWith('.JPEG')).toBe(true);
});
it('should handle very long filenames', () => {
const usedNames = new Set<string>();
const longName = 'a'.repeat(200) + '.pdf';
expect(deduplicateFileName(longName, usedNames)).toBe(longName);
expect(deduplicateFileName(longName, usedNames)).toBe(
'a'.repeat(200) + ' (1).pdf'
);
});
it('should handle names with special characters', () => {
const usedNames = new Set<string>();
const name = 'report (final) [v2].pdf';
expect(deduplicateFileName(name, usedNames)).toBe(name);
expect(deduplicateFileName(name, usedNames)).toBe(
'report (final) [v2] (1).pdf'
);
});
it('should handle names with unicode characters', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('レポート.pdf', usedNames)).toBe('レポート.pdf');
expect(deduplicateFileName('レポート.pdf', usedNames)).toBe(
'レポート (1).pdf'
);
});
it('should handle names with spaces', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('my report.pdf', usedNames)).toBe(
'my report.pdf'
);
expect(deduplicateFileName('my report.pdf', usedNames)).toBe(
'my report (1).pdf'
);
});
it('should not confuse different extensions as duplicates', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file.pdf');
expect(deduplicateFileName('file.txt', usedNames)).toBe('file.txt');
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (1).pdf');
expect(deduplicateFileName('file.txt', usedNames)).toBe('file (1).txt');
});
it('should work with a fresh Set for each batch', () => {
const batch1 = new Set<string>();
deduplicateFileName('file.pdf', batch1);
deduplicateFileName('file.pdf', batch1);
const batch2 = new Set<string>();
expect(deduplicateFileName('file.pdf', batch2)).toBe('file.pdf');
});
it('should handle many duplicates without infinite loop', () => {
const usedNames = new Set<string>();
for (let i = 0; i < 100; i++) {
deduplicateFileName('test.pdf', usedNames);
}
expect(usedNames.size).toBe(100);
expect(usedNames.has('test.pdf')).toBe(true);
expect(usedNames.has('test (1).pdf')).toBe(true);
expect(usedNames.has('test (99).pdf')).toBe(true);
});
it('should handle pre-populated Set', () => {
const usedNames = new Set<string>(['file.pdf', 'file (1).pdf']);
expect(deduplicateFileName('file.pdf', usedNames)).toBe('file (2).pdf');
});
it('should handle name that is just a dot', () => {
const usedNames = new Set<string>();
expect(deduplicateFileName('.', usedNames)).toBe('.');
expect(deduplicateFileName('.', usedNames)).toBe('. (1)');
});
});
describe('makeUniqueFileKey', () => {
it('should combine index and name', () => {
expect(makeUniqueFileKey(0, 'file.pdf')).toBe('0_file.pdf');
expect(makeUniqueFileKey(1, 'file.pdf')).toBe('1_file.pdf');
});
it('should produce different keys for same name at different indices', () => {
const key1 = makeUniqueFileKey(0, 'report.pdf');
const key2 = makeUniqueFileKey(1, 'report.pdf');
expect(key1).not.toBe(key2);
});
it('should produce same key for same index and name', () => {
expect(makeUniqueFileKey(5, 'test.pdf')).toBe(
makeUniqueFileKey(5, 'test.pdf')
);
});
it('should handle empty name', () => {
expect(makeUniqueFileKey(0, '')).toBe('0_');
});
it('should handle large indices', () => {
expect(makeUniqueFileKey(9999, 'file.pdf')).toBe('9999_file.pdf');
});
it('should handle names with underscores', () => {
expect(makeUniqueFileKey(0, 'my_file.pdf')).toBe('0_my_file.pdf');
});
it('should handle names with special characters', () => {
expect(makeUniqueFileKey(0, 'file (1).pdf')).toBe('0_file (1).pdf');
});
});