2025-12-04 14:02:20 +05:30
import { showLoader , hideLoader , showAlert } from '../ui.js' ;
import { createIcons , icons } from 'lucide' ;
import * as pdfjsLib from 'pdfjs-dist' ;
import { downloadFile , getPDFDocument , readFileAsArrayBuffer , formatBytes } from '../utils/helpers.js' ;
import { state } from '../state.js' ;
import { renderPagesProgressively , cleanupLazyRendering } from '../utils/render-utils.js' ;
import JSZip from 'jszip' ;
import { PDFDocument as PDFLibDocument } from 'pdf-lib' ;
// @ts-ignore
pdfjsLib . GlobalWorkerOptions . workerSrc = new URL ( 'pdfjs-dist/build/pdf.worker.min.mjs' , import . meta . url ) . toString ( ) ;
document . addEventListener ( 'DOMContentLoaded' , ( ) = > {
let visualSelectorRendered = false ;
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 splitOptions = document . getElementById ( 'split-options' ) ;
const backBtn = document . getElementById ( 'back-to-tools' ) ;
// Split Mode Elements
const splitModeSelect = document . getElementById ( 'split-mode' ) as HTMLSelectElement ;
const rangePanel = document . getElementById ( 'range-panel' ) ;
const visualPanel = document . getElementById ( 'visual-select-panel' ) ;
const evenOddPanel = document . getElementById ( 'even-odd-panel' ) ;
const zipOptionWrapper = document . getElementById ( 'zip-option-wrapper' ) ;
const allPagesPanel = document . getElementById ( 'all-pages-panel' ) ;
const bookmarksPanel = document . getElementById ( 'bookmarks-panel' ) ;
const nTimesPanel = document . getElementById ( 'n-times-panel' ) ;
const nTimesWarning = document . getElementById ( 'n-times-warning' ) ;
if ( backBtn ) {
backBtn . addEventListener ( 'click' , ( ) = > {
2025-12-04 23:53:00 +05:30
window . location . href = import . meta . env . BASE_URL ;
2025-12-04 14:02:20 +05:30
} ) ;
}
const updateUI = async ( ) = > {
if ( state . files . length > 0 ) {
const file = state . files [ 0 ] ;
if ( fileDisplayArea ) {
fileDisplayArea . innerHTML = '' ;
const fileDiv = document . createElement ( 'div' ) ;
fileDiv . className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm' ;
const infoContainer = document . createElement ( 'div' ) ;
infoContainer . className = 'flex flex-col overflow-hidden' ;
2025-12-11 19:34:14 +05:30
const nameSpan = document . createElement ( 'div' ) ;
nameSpan . className = 'truncate font-medium text-gray-200 text-sm mb-1' ;
2025-12-04 14:02:20 +05:30
nameSpan . textContent = file . name ;
2025-12-11 19:34:14 +05:30
const metaSpan = document . createElement ( 'div' ) ;
metaSpan . className = 'text-xs text-gray-400' ;
metaSpan . textContent = ` ${ formatBytes ( file . size ) } • Loading pages... ` ; // Placeholder
2025-12-04 14:02:20 +05:30
2025-12-11 19:34:14 +05:30
infoContainer . append ( nameSpan , metaSpan ) ;
2025-12-04 14:02:20 +05:30
// Add remove button
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 . pdfDoc = null ;
updateUI ( ) ;
} ;
fileDiv . append ( infoContainer , removeBtn ) ;
fileDisplayArea . appendChild ( fileDiv ) ;
createIcons ( { icons } ) ;
// Load PDF Document
try {
if ( ! state . pdfDoc ) {
showLoader ( 'Loading PDF...' ) ;
const arrayBuffer = await readFileAsArrayBuffer ( file ) as ArrayBuffer ;
state . pdfDoc = await PDFLibDocument . load ( arrayBuffer ) ;
hideLoader ( ) ;
}
// Update page count
2025-12-11 19:34:14 +05:30
metaSpan . textContent = ` ${ formatBytes ( file . size ) } • ${ state . pdfDoc . getPageCount ( ) } pages ` ;
2025-12-04 14:02:20 +05:30
} catch ( error ) {
console . error ( 'Error loading PDF:' , error ) ;
showAlert ( 'Error' , 'Failed to load PDF file.' ) ;
state . files = [ ] ;
updateUI ( ) ;
return ;
}
}
if ( splitOptions ) splitOptions . classList . remove ( 'hidden' ) ;
} else {
if ( fileDisplayArea ) fileDisplayArea . innerHTML = '' ;
if ( splitOptions ) splitOptions . classList . add ( 'hidden' ) ;
state . pdfDoc = null ;
}
} ;
const renderVisualSelector = async ( ) = > {
if ( visualSelectorRendered ) return ;
const container = document . getElementById ( 'page-selector-grid' ) ;
if ( ! container ) return ;
visualSelectorRendered = true ;
container . textContent = '' ;
// Cleanup any previous lazy loading observers
cleanupLazyRendering ( ) ;
showLoader ( 'Rendering page previews...' ) ;
try {
if ( ! state . pdfDoc ) {
// If pdfDoc is not loaded yet (e.g. page refresh), try to load it from the first file
if ( state . files . length > 0 ) {
const file = state . files [ 0 ] ;
const arrayBuffer = await readFileAsArrayBuffer ( file ) as ArrayBuffer ;
state . pdfDoc = await PDFLibDocument . load ( arrayBuffer ) ;
} else {
throw new Error ( 'No PDF document loaded' ) ;
}
}
const pdfData = await state . pdfDoc . save ( ) ;
const pdf = await getPDFDocument ( { data : pdfData } ) . promise ;
// Function to create wrapper element for each page
const createWrapper = ( canvas : HTMLCanvasElement , pageNumber : number ) = > {
const wrapper = document . createElement ( 'div' ) ;
wrapper . className =
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500 relative' ;
wrapper . dataset . pageIndex = ( pageNumber - 1 ) . toString ( ) ;
const img = document . createElement ( 'img' ) ;
img . src = canvas . toDataURL ( ) ;
img . className = 'rounded-md w-full h-auto' ;
const p = document . createElement ( 'p' ) ;
p . className = 'text-center text-xs mt-1 text-gray-300' ;
p . textContent = ` Page ${ pageNumber } ` ;
wrapper . append ( img , p ) ;
const handleSelection = ( e : any ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
const isSelected = wrapper . classList . contains ( 'selected' ) ;
if ( isSelected ) {
wrapper . classList . remove ( 'selected' , 'border-indigo-500' ) ;
wrapper . classList . add ( 'border-transparent' ) ;
} else {
wrapper . classList . add ( 'selected' , 'border-indigo-500' ) ;
wrapper . classList . remove ( 'border-transparent' ) ;
}
} ;
wrapper . addEventListener ( 'click' , handleSelection ) ;
wrapper . addEventListener ( 'touchend' , handleSelection ) ;
wrapper . addEventListener ( 'touchstart' , ( e ) = > {
e . preventDefault ( ) ;
} ) ;
return wrapper ;
} ;
// Render pages progressively with lazy loading
await renderPagesProgressively (
pdf ,
container ,
createWrapper ,
{
batchSize : 8 ,
useLazyLoading : true ,
lazyLoadMargin : '400px' ,
onProgress : ( current , total ) = > {
showLoader ( ` Rendering page previews: ${ current } / ${ total } ` ) ;
} ,
onBatchComplete : ( ) = > {
createIcons ( { icons } ) ;
}
}
) ;
} catch ( error ) {
console . error ( 'Error rendering visual selector:' , error ) ;
showAlert ( 'Error' , 'Failed to render page previews.' ) ;
// Reset the flag on error so the user can try again.
visualSelectorRendered = false ;
} finally {
hideLoader ( ) ;
}
} ;
const resetState = ( ) = > {
state . files = [ ] ;
state . pdfDoc = null ;
// Reset visual selection
document . querySelectorAll ( '.page-thumbnail-wrapper.selected' ) . forEach ( el = > {
el . classList . remove ( 'selected' , 'border-indigo-500' ) ;
el . classList . add ( 'border-transparent' ) ;
} ) ;
visualSelectorRendered = false ;
const container = document . getElementById ( 'page-selector-grid' ) ;
if ( container ) container . innerHTML = '' ;
// Reset inputs
const pageRangeInput = document . getElementById ( 'page-range' ) as HTMLInputElement ;
if ( pageRangeInput ) pageRangeInput . value = '' ;
const nValueInput = document . getElementById ( 'split-n-value' ) as HTMLInputElement ;
if ( nValueInput ) nValueInput . value = '5' ;
// Reset radio buttons to default (range)
const rangeRadio = document . querySelector ( 'input[name="split-mode"][value="range"]' ) as HTMLInputElement ;
if ( rangeRadio ) {
rangeRadio . checked = true ;
rangeRadio . dispatchEvent ( new Event ( 'change' ) ) ;
}
// Reset split mode select
if ( splitModeSelect ) {
splitModeSelect . value = 'range' ;
splitModeSelect . dispatchEvent ( new Event ( 'change' ) ) ;
}
updateUI ( ) ;
} ;
const split = async ( ) = > {
const splitMode = splitModeSelect . value ;
const downloadAsZip =
( document . getElementById ( 'download-as-zip' ) as HTMLInputElement ) ? . checked ||
false ;
showLoader ( 'Splitting PDF...' ) ;
try {
if ( ! state . pdfDoc ) throw new Error ( 'No PDF document loaded.' ) ;
const totalPages = state . pdfDoc . getPageCount ( ) ;
let indicesToExtract : number [ ] = [ ] ;
switch ( splitMode ) {
case 'range' :
const pageRangeInput = ( document . getElementById ( 'page-range' ) as HTMLInputElement ) . value ;
if ( ! pageRangeInput ) throw new Error ( 'Choose a valid page range.' ) ;
const ranges = pageRangeInput . split ( ',' ) ;
for ( const range of ranges ) {
const trimmedRange = range . trim ( ) ;
if ( trimmedRange . includes ( '-' ) ) {
const [ start , end ] = trimmedRange . split ( '-' ) . map ( Number ) ;
if (
isNaN ( start ) ||
isNaN ( end ) ||
start < 1 ||
end > totalPages ||
start > end
)
continue ;
for ( let i = start ; i <= end ; i ++ ) indicesToExtract . push ( i - 1 ) ;
} else {
const pageNum = Number ( trimmedRange ) ;
if ( isNaN ( pageNum ) || pageNum < 1 || pageNum > totalPages ) continue ;
indicesToExtract . push ( pageNum - 1 ) ;
}
}
break ;
case 'even-odd' :
const choiceElement = document . querySelector (
'input[name="even-odd-choice"]:checked'
) as HTMLInputElement ;
if ( ! choiceElement ) throw new Error ( 'Please select even or odd pages.' ) ;
const choice = choiceElement . value ;
for ( let i = 0 ; i < totalPages ; i ++ ) {
if ( choice === 'even' && ( i + 1 ) % 2 === 0 ) indicesToExtract . push ( i ) ;
if ( choice === 'odd' && ( i + 1 ) % 2 !== 0 ) indicesToExtract . push ( i ) ;
}
break ;
case 'all' :
indicesToExtract = Array . from ( { length : totalPages } , ( _ , i ) = > i ) ;
break ;
case 'visual' :
indicesToExtract = Array . from (
document . querySelectorAll ( '.page-thumbnail-wrapper.selected' )
)
. map ( ( el ) = > parseInt ( ( el as HTMLElement ) . dataset . pageIndex || '0' ) ) ;
break ;
case 'bookmarks' :
const { getCpdf } = await import ( '../utils/cpdf-helper.js' ) ;
const cpdf = await getCpdf ( ) ;
const pdfBytes = await state . pdfDoc . save ( ) ;
const pdf = cpdf . fromMemory ( new Uint8Array ( pdfBytes ) , '' ) ;
cpdf . startGetBookmarkInfo ( pdf ) ;
const bookmarkCount = cpdf . numberBookmarks ( ) ;
const bookmarkLevel = ( document . getElementById ( 'bookmark-level' ) as HTMLSelectElement ) ? . value ;
const splitPages : number [ ] = [ ] ;
for ( let i = 0 ; i < bookmarkCount ; i ++ ) {
const level = cpdf . getBookmarkLevel ( i ) ;
const page = cpdf . getBookmarkPage ( pdf , i ) ;
if ( bookmarkLevel === 'all' || level === parseInt ( bookmarkLevel ) ) {
if ( page > 1 && ! splitPages . includes ( page - 1 ) ) {
splitPages . push ( page - 1 ) ; // Convert to 0-based index
}
}
}
cpdf . endGetBookmarkInfo ( ) ;
cpdf . deletePdf ( pdf ) ;
if ( splitPages . length === 0 ) {
throw new Error ( 'No bookmarks found at the selected level.' ) ;
}
splitPages . sort ( ( a , b ) = > a - b ) ;
const zip = new JSZip ( ) ;
for ( let i = 0 ; i < splitPages . length ; i ++ ) {
const startPage = i === 0 ? 0 : splitPages [ i ] ;
const endPage = i < splitPages . length - 1 ? splitPages [ i + 1 ] - 1 : totalPages - 1 ;
const newPdf = await PDFLibDocument . create ( ) ;
const pageIndices = Array . from ( { length : endPage - startPage + 1 } , ( _ , idx ) = > startPage + idx ) ;
const copiedPages = await newPdf . copyPages ( state . pdfDoc , pageIndices ) ;
copiedPages . forEach ( ( page : any ) = > newPdf . addPage ( page ) ) ;
const pdfBytes2 = await newPdf . save ( ) ;
zip . file ( ` split- ${ i + 1 } .pdf ` , pdfBytes2 ) ;
}
const zipBlob = await zip . generateAsync ( { type : 'blob' } ) ;
downloadFile ( zipBlob , 'split-by-bookmarks.zip' ) ;
hideLoader ( ) ;
showAlert ( 'Success' , 'PDF split successfully!' , 'success' , ( ) = > {
resetState ( ) ;
} ) ;
return ;
case 'n-times' :
const nValue = parseInt ( ( document . getElementById ( 'split-n-value' ) as HTMLInputElement ) ? . value || '5' ) ;
if ( nValue < 1 ) throw new Error ( 'N must be at least 1.' ) ;
const zip2 = new JSZip ( ) ;
const numSplits = Math . ceil ( totalPages / nValue ) ;
for ( let i = 0 ; i < numSplits ; i ++ ) {
const startPage = i * nValue ;
const endPage = Math . min ( startPage + nValue - 1 , totalPages - 1 ) ;
const pageIndices = Array . from ( { length : endPage - startPage + 1 } , ( _ , idx ) = > startPage + idx ) ;
const newPdf = await PDFLibDocument . create ( ) ;
const copiedPages = await newPdf . copyPages ( state . pdfDoc , pageIndices ) ;
copiedPages . forEach ( ( page : any ) = > newPdf . addPage ( page ) ) ;
const pdfBytes3 = await newPdf . save ( ) ;
zip2 . file ( ` split- ${ i + 1 } .pdf ` , pdfBytes3 ) ;
}
const zipBlob2 = await zip2 . generateAsync ( { type : 'blob' } ) ;
downloadFile ( zipBlob2 , 'split-n-times.zip' ) ;
hideLoader ( ) ;
showAlert ( 'Success' , 'PDF split successfully!' , 'success' , ( ) = > {
resetState ( ) ;
} ) ;
return ;
}
const uniqueIndices = [ . . . new Set ( indicesToExtract ) ] ;
if ( uniqueIndices . length === 0 && splitMode !== 'bookmarks' && splitMode !== 'n-times' ) {
throw new Error ( 'No pages were selected for splitting.' ) ;
}
if (
splitMode === 'all' ||
( [ 'range' , 'visual' ] . includes ( splitMode ) && downloadAsZip )
) {
showLoader ( 'Creating ZIP file...' ) ;
const zip = new JSZip ( ) ;
for ( const index of uniqueIndices ) {
const newPdf = await PDFLibDocument . create ( ) ;
const [ copiedPage ] = await newPdf . copyPages ( state . pdfDoc , [
index as number ,
] ) ;
newPdf . addPage ( copiedPage ) ;
const pdfBytes = await newPdf . save ( ) ;
// @ts-ignore
zip . file ( ` page- ${ index + 1 } .pdf ` , pdfBytes ) ;
}
const zipBlob = await zip . generateAsync ( { type : 'blob' } ) ;
downloadFile ( zipBlob , 'split-pages.zip' ) ;
} else {
const newPdf = await PDFLibDocument . create ( ) ;
const copiedPages = await newPdf . copyPages (
state . pdfDoc ,
uniqueIndices as number [ ]
) ;
copiedPages . forEach ( ( page : any ) = > newPdf . addPage ( page ) ) ;
const pdfBytes = await newPdf . save ( ) ;
downloadFile (
new Blob ( [ new Uint8Array ( pdfBytes ) ] , { type : 'application/pdf' } ) ,
'split-document.pdf'
) ;
}
if ( splitMode === 'visual' ) {
visualSelectorRendered = false ;
}
showAlert ( 'Success' , 'PDF split successfully!' , 'success' , ( ) = > {
resetState ( ) ;
} ) ;
} catch ( e : any ) {
console . error ( e ) ;
showAlert (
'Error' ,
e . message || 'Failed to split PDF. Please check your selection.'
) ;
} finally {
hideLoader ( ) ;
}
} ;
const handleFileSelect = async ( files : FileList | null ) = > {
if ( files && files . length > 0 ) {
// Split tool only supports one file at a time
state . files = [ files [ 0 ] ] ;
await 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 ) {
const pdfFiles = Array . from ( files ) . filter ( f = > f . type === 'application/pdf' || f . name . toLowerCase ( ) . endsWith ( '.pdf' ) ) ;
if ( pdfFiles . length > 0 ) {
// Take only the first PDF
const dataTransfer = new DataTransfer ( ) ;
dataTransfer . items . add ( pdfFiles [ 0 ] ) ;
handleFileSelect ( dataTransfer . files ) ;
}
}
} ) ;
2025-12-11 19:34:14 +05:30
// Clear value on click to allow re-selecting the same file
fileInput . addEventListener ( 'click' , ( ) = > {
fileInput . value = '' ;
2025-12-04 14:02:20 +05:30
} ) ;
}
if ( splitModeSelect ) {
splitModeSelect . addEventListener ( 'change' , ( e ) = > {
const mode = ( e . target as HTMLSelectElement ) . value ;
if ( mode !== 'visual' ) {
visualSelectorRendered = false ;
const container = document . getElementById ( 'page-selector-grid' ) ;
if ( container ) container . innerHTML = '' ;
}
rangePanel ? . classList . add ( 'hidden' ) ;
visualPanel ? . classList . add ( 'hidden' ) ;
evenOddPanel ? . classList . add ( 'hidden' ) ;
allPagesPanel ? . classList . add ( 'hidden' ) ;
bookmarksPanel ? . classList . add ( 'hidden' ) ;
nTimesPanel ? . classList . add ( 'hidden' ) ;
zipOptionWrapper ? . classList . add ( 'hidden' ) ;
if ( nTimesWarning ) nTimesWarning . classList . add ( 'hidden' ) ;
if ( mode === 'range' ) {
rangePanel ? . classList . remove ( 'hidden' ) ;
zipOptionWrapper ? . classList . remove ( 'hidden' ) ;
} else if ( mode === 'visual' ) {
visualPanel ? . classList . remove ( 'hidden' ) ;
zipOptionWrapper ? . classList . remove ( 'hidden' ) ;
renderVisualSelector ( ) ;
} else if ( mode === 'even-odd' ) {
evenOddPanel ? . classList . remove ( 'hidden' ) ;
} else if ( mode === 'all' ) {
allPagesPanel ? . classList . remove ( 'hidden' ) ;
} else if ( mode === 'bookmarks' ) {
bookmarksPanel ? . classList . remove ( 'hidden' ) ;
zipOptionWrapper ? . classList . remove ( 'hidden' ) ;
} else if ( mode === 'n-times' ) {
nTimesPanel ? . classList . remove ( 'hidden' ) ;
zipOptionWrapper ? . classList . remove ( 'hidden' ) ;
const updateWarning = ( ) = > {
if ( ! state . pdfDoc ) return ;
const totalPages = state . pdfDoc . getPageCount ( ) ;
const nValue = parseInt ( ( document . getElementById ( 'split-n-value' ) as HTMLInputElement ) ? . value || '5' ) ;
const remainder = totalPages % nValue ;
if ( remainder !== 0 && nTimesWarning ) {
nTimesWarning . classList . remove ( 'hidden' ) ;
const warningText = document . getElementById ( 'n-times-warning-text' ) ;
if ( warningText ) {
warningText . textContent = ` The PDF has ${ totalPages } pages, which is not evenly divisible by ${ nValue } . The last PDF will contain ${ remainder } page(s). ` ;
}
} else if ( nTimesWarning ) {
nTimesWarning . classList . add ( 'hidden' ) ;
}
} ;
updateWarning ( ) ;
document . getElementById ( 'split-n-value' ) ? . addEventListener ( 'input' , updateWarning ) ;
}
} ) ;
}
if ( processBtn ) {
processBtn . addEventListener ( 'click' , split ) ;
}
} ) ;