Last active
June 12, 2025 16:57
-
-
Save pfelipm/31a0cb1b37b3687f638a9be5b42946be to your computer and use it in GitHub Desktop.
Herramienta web para unir y dividir PDF. Funciona totalmente en local.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="es"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Herramientas PDF - Unir y Dividir</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://unpkg.com/[email protected]/dist/pdf-lib.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script> | |
<style> | |
/* Estilos personalizados para la apariencia */ | |
body { | |
font-family: 'Inter', sans-serif; | |
} | |
.drag-over { | |
border-color: #3b82f6; /* Azul brillante al arrastrar */ | |
background-color: #eff6ff; /* Azul muy claro */ | |
} | |
.progress-bar-fill { | |
transition: width 0.3s ease-in-out; | |
} | |
/* Clase que SortableJS aplica al elemento fantasma mientras se arrastra */ | |
.dragging { | |
opacity: 0.5; | |
background-color: #dbeafe; | |
} | |
/* Estilos para las pestañas */ | |
.tab-btn { | |
transition: background-color 0.2s, color 0.2s, border-color 0.2s; | |
border-bottom: 2px solid transparent; | |
} | |
.tab-btn.active { | |
border-bottom-color: #3b82f6; /* Azul */ | |
color: #3b82f6; | |
font-weight: 600; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 flex items-center justify-center min-h-screen p-4"> | |
<div class="w-full max-w-2xl bg-white rounded-2xl shadow-xl p-6 md:p-8"> | |
<div class="mb-6 border-b border-gray-200"> | |
<nav class="-mb-px flex gap-6" aria-label="Tabs"> | |
<button id="tab-merge-btn" class="tab-btn active py-4 px-1 text-gray-500 hover:text-blue-600">Unir PDFs</button> | |
<button id="tab-split-btn" class="tab-btn py-4 px-1 text-gray-500 hover:text-blue-600">Dividir PDF</button> | |
</nav> | |
</div> | |
<div id="merge-content"> | |
<div class="text-center mb-6"> | |
<h1 class="text-3xl font-bold text-gray-800">Unir Archivos PDF</h1> | |
<p class="text-gray-500 mt-2">Arrastra y suelta archivos, reordénalos y únelos en un solo PDF.</p> | |
</div> | |
<div id="merge-drop-zone" class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:border-blue-500 transition-colors"> | |
<input type="file" id="merge-file-input" multiple accept=".pdf" class="hidden"> | |
<div class="text-gray-500"> | |
<!-- Icono Actualizado --> | |
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg> | |
<p class="mt-2">Arrastra y suelta tus archivos PDF aquí</p> | |
<p class="text-xs text-gray-400 mt-1">o</p> | |
<button type="button" id="merge-browse-btn" class="mt-2 font-semibold text-blue-600 hover:text-blue-800">Selecciona los archivos</button> | |
</div> | |
</div> | |
<div id="merge-file-list-area" class="mt-6"> | |
<div id="merge-file-list-container"></div> | |
<div class="text-center mt-4"><button id="merge-clear-all-btn" class="text-sm font-semibold text-red-600 hover:text-red-800 transition-colors hidden">Eliminar Todos</button></div> | |
</div> | |
<div id="merge-progress-container" class="w-full bg-gray-200 rounded-full h-2.5 my-4 hidden"><div id="merge-progress-bar" class="bg-blue-600 h-2.5 rounded-full progress-bar-fill" style="width: 0%"></div></div> | |
<p id="merge-progress-text" class="text-center text-sm text-gray-600 mb-4 hidden"></p> | |
<div id="merge-error-message" class="mt-4 text-center text-red-600 font-medium hidden"></div> | |
<div class="mt-8 flex flex-col md:flex-row gap-4"> | |
<button id="merge-btn" class="w-full bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed">Unir PDFs</button> | |
<a id="merge-download-btn" href="#" download="unido.pdf" class="w-full text-center bg-green-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-green-700 transition-colors hidden">Descargar PDF Resultante</a> | |
</div> | |
</div> | |
<div id="split-content" class="hidden"> | |
<div class="text-center mb-6"> | |
<h1 class="text-3xl font-bold text-gray-800">Dividir Archivo PDF</h1> | |
<p class="text-gray-500 mt-2">Extrae páginas o rangos de un PDF en múltiples archivos.</p> | |
</div> | |
<div id="split-file-area"> | |
<div id="split-drop-zone" class="border-2 border-dashed border-gray-300 rounded-xl p-8 text-center cursor-pointer hover:border-blue-500 transition-colors"> | |
<input type="file" id="split-file-input" accept=".pdf" class="hidden"> | |
<div class="text-gray-500"> | |
<!-- Icono Actualizado --> | |
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg> | |
<p class="mt-2">Arrastra y suelta tu archivo PDF aquí</p> | |
<p class="text-xs text-gray-400 mt-1">o</p> | |
<button type="button" id="split-browse-btn" class="mt-2 font-semibold text-blue-600 hover:text-blue-800">Selecciona un archivo</button> | |
</div> | |
</div> | |
<div id="split-options-area" class="mt-6 hidden"> | |
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200"> | |
<div class="flex justify-between items-center gap-4"> | |
<div class="min-w-0"> | |
<p class="font-medium text-gray-800 truncate" id="split-file-name"></p> | |
<p class="text-sm text-gray-500" id="split-file-pages"></p> | |
</div> | |
<button id="split-clear-btn" class="text-sm font-semibold text-red-600 hover:text-red-800 flex-shrink-0">Cambiar archivo</button> | |
</div> | |
</div> | |
<div class="mt-6"> | |
<h3 class="text-lg font-medium text-gray-900 mb-4">Opciones de División</h3> | |
<div class="space-y-4"> | |
<div class="flex items-start"> | |
<input id="split-mode-range" name="split-mode" type="radio" class="h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500 mt-1" checked> | |
<div class="ml-3 text-sm"> | |
<label for="split-mode-range" class="font-medium text-gray-700">Extraer rangos</label> | |
<input type="text" id="split-range-input" class="mt-2 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" placeholder="Ej: 1-5, 8, 10-"> | |
</div> | |
</div> | |
<div class="flex items-start"> | |
<input id="split-mode-all" name="split-mode" type="radio" class="h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500 mt-1"> | |
<label for="split-mode-all" class="ml-3 block text-sm font-medium text-gray-700">Dividir todas las páginas</label> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="split-progress-container" class="w-full bg-gray-200 rounded-full h-2.5 my-4 hidden"><div id="split-progress-bar" class="bg-blue-600 h-2.5 rounded-full progress-bar-fill" style="width: 0%"></div></div> | |
<p id="split-progress-text" class="text-center text-sm text-gray-600 mb-4 hidden"></p> | |
<div id="split-error-message" class="mt-4 text-center text-red-600 font-medium hidden"></div> | |
<div id="split-action-area" class="mt-8"> | |
<button id="split-btn" class="w-full bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"> | |
Dividir PDF | |
</button> | |
</div> | |
<div id="split-results-area" class="mt-6 hidden"> | |
<h3 class="text-lg font-medium text-gray-900 mb-4">Archivos Generados</h3> | |
<div id="split-downloads-list" class="space-y-2"></div> | |
</div> | |
</div> | |
<div class="text-right text-xs text-gray-400 mt-8 pt-4 border-t border-gray-200"> | |
Versión 1 (junio 2025) | |
</div> | |
</div> | |
<script> | |
// --- INICIALIZACIÓN Y VARIABLES GLOBALES --- | |
const { PDFDocument } = PDFLib; | |
// Elementos de Pestañas | |
const tabMergeBtn = document.getElementById('tab-merge-btn'); | |
const tabSplitBtn = document.getElementById('tab-split-btn'); | |
const mergeContent = document.getElementById('merge-content'); | |
const splitContent = document.getElementById('split-content'); | |
// --- LÓGICA DE PESTAÑAS --- | |
tabMergeBtn.addEventListener('click', () => { | |
tabMergeBtn.classList.add('active'); | |
tabSplitBtn.classList.remove('active'); | |
mergeContent.classList.remove('hidden'); | |
splitContent.classList.add('hidden'); | |
}); | |
tabSplitBtn.addEventListener('click', () => { | |
tabSplitBtn.classList.add('active'); | |
tabMergeBtn.classList.remove('active'); | |
splitContent.classList.remove('hidden'); | |
mergeContent.classList.add('hidden'); | |
}); | |
// ======================================================================== | |
// --- SECCIÓN DE UNIR PDF --- | |
// ======================================================================== | |
{ | |
const dropZone = document.getElementById('merge-drop-zone'); | |
const fileInput = document.getElementById('merge-file-input'); | |
const browseBtn = document.getElementById('merge-browse-btn'); | |
const mergeBtn = document.getElementById('merge-btn'); | |
const downloadBtn = document.getElementById('merge-download-btn'); | |
const fileListContainer = document.getElementById('merge-file-list-container'); | |
const clearAllBtn = document.getElementById('merge-clear-all-btn'); | |
const progressContainer = document.getElementById('merge-progress-container'); | |
const progressBar = document.getElementById('merge-progress-bar'); | |
const progressText = document.getElementById('merge-progress-text'); | |
const errorMessage = document.getElementById('merge-error-message'); | |
let selectedFiles = []; | |
browseBtn.addEventListener('click', () => fileInput.click()); | |
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); | |
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); }); | |
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); handleFiles(e.dataTransfer.files); }); | |
fileInput.addEventListener('change', (e) => handleFiles(e.target.files)); | |
async function handleFiles(files) { | |
hideError(); | |
const pdfFiles = Array.from(files).filter(file => file.type === 'application/pdf'); | |
if (pdfFiles.length === 0 && files.length > 0) { | |
showError("Por favor, selecciona solo archivos PDF."); | |
return; | |
} | |
// Deshabilitar la adición de más archivos mientras se procesan los actuales para evitar conflictos | |
dropZone.style.pointerEvents = 'none'; | |
browseBtn.disabled = true; | |
const newFilesData = []; | |
// Procesar archivos secuencialmente para evitar sobrecargar el navegador | |
for (const file of pdfFiles) { | |
try { | |
const fileArrayBuffer = await file.arrayBuffer(); | |
const pdfDoc = await PDFDocument.load(fileArrayBuffer, { ignoreEncryption: true }); | |
newFilesData.push({ file: file, id: crypto.randomUUID(), pageCount: pdfDoc.getPageCount() }); | |
} catch (e) { | |
console.error("Error al leer el PDF:", file.name, e); | |
showError(`No se pudo leer "${file.name}". Puede que esté dañado o protegido.`); | |
// La ejecución continúa con el siguiente archivo | |
} | |
} | |
selectedFiles.push(...newFilesData); | |
updateFileListUI(); | |
// Rehabilitar la adición de archivos | |
dropZone.style.pointerEvents = 'auto'; | |
browseBtn.disabled = false; | |
} | |
function updateFileListUI() { | |
fileListContainer.innerHTML = ''; | |
if (selectedFiles.length > 0) { | |
const list = document.createElement('ul'); | |
list.className = 'flex flex-col gap-2'; | |
selectedFiles.forEach((fileWrapper) => { | |
const listItem = document.createElement('li'); | |
listItem.setAttribute('data-id', fileWrapper.id); | |
listItem.className = 'file-item-container flex items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-200 cursor-grab active:cursor-grabbing'; | |
listItem.innerHTML = ` | |
<div class="flex items-center overflow-hidden pointer-events-none"> | |
<svg class="w-5 h-5 text-gray-400 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg> | |
<span class="font-medium text-gray-700 truncate">${fileWrapper.file.name}</span> | |
<span class="ml-2 text-sm text-gray-500 flex-shrink-0">(${fileWrapper.pageCount} ${fileWrapper.pageCount === 1 ? 'pág' : 'págs'})</span> | |
</div> | |
<button class="remove-btn text-gray-400 hover:text-red-600 transition-colors"> | |
<svg class="w-5 h-5 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg> | |
</button> | |
`; | |
list.appendChild(listItem); | |
}); | |
fileListContainer.appendChild(list); | |
document.querySelectorAll('.remove-btn').forEach(button => { | |
button.addEventListener('click', (e) => { | |
const idToRemove = e.currentTarget.parentElement.getAttribute('data-id'); | |
selectedFiles = selectedFiles.filter(f => f.id !== idToRemove); | |
updateFileListUI(); | |
}); | |
}); | |
new Sortable(list, { | |
animation: 150, ghostClass: 'dragging', | |
onEnd: function (evt) { | |
const movedItem = selectedFiles.splice(evt.oldIndex, 1)[0]; | |
selectedFiles.splice(evt.newIndex, 0, movedItem); | |
} | |
}); | |
} | |
clearAllBtn.classList.toggle('hidden', selectedFiles.length === 0); | |
mergeBtn.disabled = selectedFiles.length < 2; | |
downloadBtn.classList.add('hidden'); | |
} | |
clearAllBtn.addEventListener('click', () => { | |
selectedFiles = []; | |
// Restablecer el valor del input de archivo es crucial para permitir volver a seleccionar los mismos archivos | |
// y solucionar el problema de bloqueo de la interfaz de carga. | |
fileInput.value = null; | |
updateFileListUI(); | |
// Como medida de seguridad, nos aseguramos de que los controles de carga estén habilitados. | |
dropZone.style.pointerEvents = 'auto'; | |
browseBtn.disabled = false; | |
}); | |
function showError(message) { errorMessage.textContent = message; errorMessage.classList.remove('hidden'); } | |
function hideError() { errorMessage.classList.add('hidden'); } | |
mergeBtn.addEventListener('click', async () => { | |
if (selectedFiles.length < 2) { showError("Necesitas al menos 2 archivos PDF para unir."); return; } | |
setProcessingState(true); hideError(); | |
try { | |
const mergedPdf = await PDFDocument.create(); | |
let filesProcessed = 0; | |
for (const fileWrapper of selectedFiles) { | |
const fileArrayBuffer = await fileWrapper.file.arrayBuffer(); | |
const pdfToMerge = await PDFDocument.load(fileArrayBuffer, { ignoreEncryption: true }); | |
const copiedPages = await mergedPdf.copyPages(pdfToMerge, pdfToMerge.getPageIndices()); | |
copiedPages.forEach((page) => mergedPdf.addPage(page)); | |
filesProcessed++; | |
updateProgress(filesProcessed, selectedFiles.length); | |
} | |
const mergedPdfBytes = await mergedPdf.save(); | |
const blob = new Blob([mergedPdfBytes], { type: 'application/pdf' }); | |
const url = URL.createObjectURL(blob); | |
const totalPages = mergedPdf.getPageCount(); | |
downloadBtn.textContent = `Descargar PDF (${totalPages} ${totalPages === 1 ? 'página' : 'páginas'})`; | |
downloadBtn.href = url; | |
downloadBtn.classList.remove('hidden'); | |
} catch (error) { | |
console.error('Error al unir los PDFs:', error); | |
showError("Ocurrió un error al procesar los archivos."); | |
} finally { | |
setProcessingState(false); | |
progressText.textContent = "¡Proceso completado!"; | |
} | |
}); | |
function setProcessingState(isProcessing) { | |
mergeBtn.disabled = isProcessing; browseBtn.disabled = isProcessing; | |
dropZone.style.pointerEvents = isProcessing ? 'none' : 'auto'; | |
const sortableInstance = fileListContainer.querySelector('ul')?.Sortable; | |
if (sortableInstance) { sortableInstance.option('disabled', isProcessing); } | |
if (isProcessing) { | |
progressContainer.classList.remove('hidden'); progressText.classList.remove('hidden'); | |
downloadBtn.classList.add('hidden'); | |
document.querySelectorAll('.remove-btn').forEach(btn => btn.disabled = true); | |
clearAllBtn.disabled = true; | |
} else { | |
document.querySelectorAll('.remove-btn').forEach(btn => btn.disabled = false); | |
clearAllBtn.disabled = false; | |
} | |
} | |
function updateProgress(current, total) { | |
const percentage = Math.round((current / total) * 100); | |
progressBar.style.width = `${percentage}%`; | |
progressText.textContent = `Procesando archivo ${current} de ${total}... (${percentage}%)`; | |
} | |
updateFileListUI(); | |
} | |
// ======================================================================== | |
// --- SECCIÓN DE DIVIDIR PDF --- | |
// ======================================================================== | |
{ | |
const dropZone = document.getElementById('split-drop-zone'); | |
const fileInput = document.getElementById('split-file-input'); | |
const browseBtn = document.getElementById('split-browse-btn'); | |
const splitBtn = document.getElementById('split-btn'); | |
const clearBtn = document.getElementById('split-clear-btn'); | |
const fileArea = document.getElementById('split-file-area'); | |
const optionsArea = document.getElementById('split-options-area'); | |
const actionArea = document.getElementById('split-action-area'); | |
const resultsArea = document.getElementById('split-results-area'); | |
const downloadsList = document.getElementById('split-downloads-list'); | |
const fileNameEl = document.getElementById('split-file-name'); | |
const filePagesEl = document.getElementById('split-file-pages'); | |
const rangeInput = document.getElementById('split-range-input'); | |
const progressContainer = document.getElementById('split-progress-container'); | |
const progressBar = document.getElementById('split-progress-bar'); | |
const progressText = document.getElementById('split-progress-text'); | |
const errorMessage = document.getElementById('split-error-message'); | |
let sourcePdf = { file: null, doc: null, totalPages: 0 }; | |
function resetUI() { | |
sourcePdf = { file: null, doc: null, totalPages: 0 }; | |
fileInput.value = ''; | |
rangeInput.value = ''; // Limpiar el campo de rangos | |
dropZone.classList.remove('hidden'); | |
optionsArea.classList.add('hidden'); | |
actionArea.classList.add('hidden'); | |
resultsArea.classList.add('hidden'); | |
downloadsList.innerHTML = ''; | |
splitBtn.disabled = true; | |
hideError(); | |
} | |
browseBtn.addEventListener('click', () => fileInput.click()); | |
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); | |
dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); }); | |
dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); handleFile(e.dataTransfer.files); }); | |
fileInput.addEventListener('change', (e) => handleFile(e.target.files)); | |
clearBtn.addEventListener('click', resetUI); | |
document.getElementById('split-mode-range').addEventListener('change', () => rangeInput.disabled = false); | |
document.getElementById('split-mode-all').addEventListener('change', () => rangeInput.disabled = true); | |
async function handleFile(files) { | |
if (files.length === 0) return; | |
const file = files[0]; | |
if (file.type !== 'application/pdf') { | |
showError("Por favor, selecciona un archivo PDF."); | |
return; | |
} | |
hideError(); | |
setProcessingState(true, 'Cargando archivo...'); | |
try { | |
const fileArrayBuffer = await file.arrayBuffer(); | |
const pdfDoc = await PDFDocument.load(fileArrayBuffer, { ignoreEncryption: true }); | |
sourcePdf = { file: file, doc: pdfDoc, totalPages: pdfDoc.getPageCount() }; | |
fileNameEl.textContent = file.name; | |
filePagesEl.textContent = `${sourcePdf.totalPages} ${sourcePdf.totalPages === 1 ? 'página' : 'páginas'} en total`; | |
dropZone.classList.add('hidden'); | |
optionsArea.classList.remove('hidden'); | |
actionArea.classList.remove('hidden'); | |
resultsArea.classList.add('hidden'); | |
splitBtn.disabled = false; | |
} catch (e) { | |
console.error("Error al leer el PDF:", file.name, e); | |
showError(`No se pudo leer "${file.name}". Puede que esté dañado.`); | |
resetUI(); | |
} finally { | |
setProcessingState(false); | |
} | |
} | |
function parsePageRanges(rangeStr, totalPages) { | |
const ranges = []; | |
const parts = rangeStr.split(',').filter(p => p.trim() !== ''); | |
if (parts.length === 0) throw new Error("La lista de páginas está vacía."); | |
for (const part of parts) { | |
const trimmedPart = part.trim(); | |
if (trimmedPart.includes('-')) { | |
let [start, end] = trimmedPart.split('-').map(n => n.trim()); | |
start = start === '' ? 1 : parseInt(start, 10); | |
end = end === '' ? totalPages : parseInt(end, 10); | |
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) { | |
throw new Error(`Rango inválido: "${trimmedPart}"`); | |
} | |
ranges.push(Array.from({length: end - start + 1}, (_, i) => start + i)); | |
} else { | |
const page = parseInt(trimmedPart, 10); | |
if (isNaN(page) || page < 1 || page > totalPages) { | |
throw new Error(`Página inválida: "${trimmedPart}"`); | |
} | |
ranges.push([page]); | |
} | |
} | |
return ranges; | |
} | |
splitBtn.addEventListener('click', async () => { | |
hideError(); | |
let pageGroups; | |
const splitModeAll = document.getElementById('split-mode-all').checked; | |
try { | |
if (splitModeAll) { | |
pageGroups = Array.from({ length: sourcePdf.totalPages }, (_, i) => [i + 1]); | |
} else { | |
pageGroups = parsePageRanges(rangeInput.value, sourcePdf.totalPages); | |
} | |
} catch(e) { | |
showError(e.message); | |
return; | |
} | |
setProcessingState(true); | |
downloadsList.innerHTML = ''; | |
try { | |
const originalFileName = sourcePdf.file.name.replace(/\.pdf$/i, ''); | |
let filesProcessed = 0; | |
for (const pageGroup of pageGroups) { | |
const newPdfDoc = await PDFDocument.create(); | |
const pageIndices = pageGroup.map(p => p - 1); // a 0-based | |
const copiedPages = await newPdfDoc.copyPages(sourcePdf.doc, pageIndices); | |
copiedPages.forEach(page => newPdfDoc.addPage(page)); | |
const pdfBytes = await newPdfDoc.save(); | |
const blob = new Blob([pdfBytes], { type: 'application/pdf' }); | |
const url = URL.createObjectURL(blob); | |
let newFileName; | |
if (pageGroup.length === 1) { | |
newFileName = `${originalFileName}_pagina_${pageGroup[0]}.pdf`; | |
} else { | |
newFileName = `${originalFileName}_paginas_${pageGroup[0]}-${pageGroup[pageGroup.length - 1]}.pdf`; | |
} | |
const link = document.createElement('a'); | |
link.href = url; | |
link.download = newFileName; | |
link.className = "block bg-gray-50 p-3 rounded-lg border border-gray-200 text-blue-600 hover:bg-blue-50 hover:border-blue-300 transition-colors"; | |
link.textContent = newFileName; | |
downloadsList.appendChild(link); | |
filesProcessed++; | |
updateProgress(filesProcessed, pageGroups.length); | |
} | |
resultsArea.classList.remove('hidden'); | |
} catch (e) { | |
console.error('Error al dividir el PDF:', e); | |
showError("Ocurrió un error al dividir el archivo."); | |
} finally { | |
setProcessingState(false, "¡Proceso completado!"); | |
} | |
}); | |
function setProcessingState(isProcessing, text = '') { | |
splitBtn.disabled = isProcessing; | |
clearBtn.disabled = isProcessing; | |
browseBtn.disabled = isProcessing; | |
dropZone.style.pointerEvents = isProcessing ? 'none' : 'auto'; | |
if (isProcessing) { | |
progressContainer.classList.remove('hidden'); | |
progressText.classList.remove('hidden'); | |
progressText.textContent = text; | |
progressBar.style.width = '0%'; | |
} else { | |
// Cuando el procesamiento termina... | |
if (text) { | |
// Si hay un mensaje final (ej. "¡Proceso completado!"), lo muestra. | |
// El contenedor de progreso se mantiene visible para mostrar este texto. | |
progressText.textContent = text; | |
} else { | |
// Si no hay mensaje final (ej. después de cargar un archivo), oculta los elementos de progreso. | |
progressContainer.classList.add('hidden'); | |
progressText.classList.add('hidden'); | |
} | |
} | |
} | |
function updateProgress(current, total) { | |
const percentage = Math.round((current / total) * 100); | |
progressBar.style.width = `${percentage}%`; | |
progressText.textContent = `Creando archivo ${current} de ${total}... (${percentage}%)`; | |
} | |
function showError(message) { errorMessage.textContent = message; errorMessage.classList.remove('hidden'); } | |
function hideError() { errorMessage.classList.add('hidden'); } | |
resetUI(); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment