Merge remote-tracking branch 'origin/main' into pdf-to-image-direct-image
This commit is contained in:
356
src/js/logic/add-attachments-page.ts
Normal file
356
src/js/logic/add-attachments-page.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/add-attachments.worker.js');
|
||||
|
||||
interface AddAttachmentState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
attachments: File[];
|
||||
}
|
||||
|
||||
const pageState: AddAttachmentState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
pageState.attachments = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const attachmentFileList = document.getElementById('attachment-file-list');
|
||||
if (attachmentFileList) attachmentFileList.innerHTML = '';
|
||||
|
||||
const attachmentInput = document.getElementById('attachment-files-input') as HTMLInputElement;
|
||||
if (attachmentInput) attachmentInput.value = '';
|
||||
|
||||
const attachmentLevelOptions = document.getElementById('attachment-level-options');
|
||||
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
||||
|
||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||
if (pageRangeWrapper) pageRangeWrapper.classList.add('hidden');
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) processBtn.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const documentRadio = document.querySelector('input[name="attachment-level"][value="document"]') as HTMLInputElement;
|
||||
if (documentRadio) documentRadio.checked = true;
|
||||
}
|
||||
|
||||
worker.onmessage = function (e) {
|
||||
const data = e.data;
|
||||
|
||||
if (data.status === 'success' && data.modifiedPDF !== undefined) {
|
||||
hideLoader();
|
||||
|
||||
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
||||
`${originalName}_with_attachments.pdf`
|
||||
);
|
||||
|
||||
showAlert('Success', `${pageState.attachments.length} file(s) attached successfully.`, 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} else if (data.status === 'error') {
|
||||
hideLoader();
|
||||
showAlert('Error', data.message || 'Unknown error occurred.');
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = function (error) {
|
||||
hideLoader();
|
||||
console.error('Worker error:', error);
|
||||
showAlert('Error', 'Worker error occurred. Check console for details.');
|
||||
};
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
showLoader('Loading PDF...');
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
|
||||
const totalPagesSpan = document.getElementById('attachment-total-pages');
|
||||
if (totalPagesSpan) totalPagesSpan.textContent = pageCount.toString();
|
||||
|
||||
hideLoader();
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
resetState();
|
||||
}
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function updateAttachmentList() {
|
||||
const attachmentFileList = document.getElementById('attachment-file-list');
|
||||
const attachmentLevelOptions = document.getElementById('attachment-level-options');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!attachmentFileList) return;
|
||||
|
||||
attachmentFileList.innerHTML = '';
|
||||
|
||||
pageState.attachments.forEach(function (file) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex justify-between items-center p-2 bg-gray-800 rounded-md text-white';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate text-sm';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'text-xs text-gray-400';
|
||||
sizeSpan.textContent = formatBytes(file.size);
|
||||
|
||||
div.append(nameSpan, sizeSpan);
|
||||
attachmentFileList.appendChild(div);
|
||||
});
|
||||
|
||||
if (pageState.attachments.length > 0) {
|
||||
if (attachmentLevelOptions) attachmentLevelOptions.classList.remove('hidden');
|
||||
if (processBtn) processBtn.classList.remove('hidden');
|
||||
} else {
|
||||
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
|
||||
if (processBtn) processBtn.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function addAttachments() {
|
||||
if (!pageState.file || !pageState.pdfDoc) {
|
||||
showAlert('Error', 'Please upload a PDF first.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageState.attachments.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one file to attach.');
|
||||
return;
|
||||
}
|
||||
|
||||
const attachmentLevel = (
|
||||
document.querySelector('input[name="attachment-level"]:checked') as HTMLInputElement
|
||||
)?.value || 'document';
|
||||
|
||||
let pageRange: string = '';
|
||||
|
||||
if (attachmentLevel === 'page') {
|
||||
const pageRangeInput = document.getElementById('attachment-page-range') as HTMLInputElement;
|
||||
pageRange = pageRangeInput?.value?.trim() || '';
|
||||
|
||||
if (!pageRange) {
|
||||
showAlert('Error', 'Please specify a page range for page-level attachments.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showLoader('Embedding files into PDF...');
|
||||
|
||||
try {
|
||||
const pdfBuffer = await pageState.file.arrayBuffer();
|
||||
|
||||
const attachmentBuffers: ArrayBuffer[] = [];
|
||||
const attachmentNames: string[] = [];
|
||||
|
||||
for (let i = 0; i < pageState.attachments.length; i++) {
|
||||
const file = pageState.attachments[i];
|
||||
showLoader(`Reading ${file.name} (${i + 1}/${pageState.attachments.length})...`);
|
||||
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
attachmentBuffers.push(fileBuffer);
|
||||
attachmentNames.push(file.name);
|
||||
}
|
||||
|
||||
showLoader('Attaching files to PDF...');
|
||||
|
||||
const message = {
|
||||
command: 'add-attachments',
|
||||
pdfBuffer: pdfBuffer,
|
||||
attachmentBuffers: attachmentBuffers,
|
||||
attachmentNames: attachmentNames,
|
||||
attachmentLevel: attachmentLevel,
|
||||
pageRange: pageRange
|
||||
};
|
||||
|
||||
const transferables = [pdfBuffer, ...attachmentBuffers];
|
||||
worker.postMessage(message, transferables);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error attaching files:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleAttachmentSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
pageState.attachments = Array.from(files);
|
||||
updateAttachmentList();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const attachmentInput = document.getElementById('attachment-files-input') as HTMLInputElement;
|
||||
const attachmentDropZone = document.getElementById('attachment-drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (attachmentInput && attachmentDropZone) {
|
||||
attachmentInput.addEventListener('change', function (e) {
|
||||
handleAttachmentSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
attachmentDropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
attachmentDropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
attachmentDropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
attachmentDropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
attachmentDropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
attachmentDropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files) {
|
||||
handleAttachmentSelect(files);
|
||||
}
|
||||
});
|
||||
|
||||
attachmentInput.addEventListener('click', function () {
|
||||
attachmentInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
|
||||
attachmentLevelRadios.forEach(function (radio) {
|
||||
radio.addEventListener('change', function (e) {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
if (value === 'page' && pageRangeWrapper) {
|
||||
pageRangeWrapper.classList.remove('hidden');
|
||||
} else if (pageRangeWrapper) {
|
||||
pageRangeWrapper.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', addAttachments);
|
||||
}
|
||||
});
|
||||
@@ -1,212 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
|
||||
import { state } from '../state';
|
||||
|
||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/add-attachments.worker.js');
|
||||
|
||||
let attachments: File[] = [];
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
const data = e.data;
|
||||
|
||||
if (data.status === 'success' && data.modifiedPDF !== undefined) {
|
||||
hideLoader();
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
||||
`attached-${state.files[0].name}`
|
||||
);
|
||||
|
||||
showAlert('Success', `${attachments.length} file(s) attached successfully.`);
|
||||
clearAttachments();
|
||||
} else if (data.status === 'error') {
|
||||
hideLoader();
|
||||
showAlert('Error', data.message || 'Unknown error occurred.');
|
||||
clearAttachments();
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (error) => {
|
||||
hideLoader();
|
||||
console.error('Worker error:', error);
|
||||
showAlert('Error', 'Worker error occurred. Check console for details.');
|
||||
clearAttachments();
|
||||
};
|
||||
|
||||
export async function addAttachments() {
|
||||
if (!state.files || state.files.length === 0) {
|
||||
showAlert('Error', 'Main PDF is not loaded.');
|
||||
return;
|
||||
}
|
||||
if (attachments.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one file to attach.');
|
||||
return;
|
||||
}
|
||||
|
||||
const attachmentLevel = (
|
||||
document.querySelector('input[name="attachment-level"]:checked') as HTMLInputElement
|
||||
)?.value || 'document';
|
||||
|
||||
let pageRange: string = '';
|
||||
|
||||
if (attachmentLevel === 'page') {
|
||||
const pageRangeInput = document.getElementById('attachment-page-range') as HTMLInputElement;
|
||||
pageRange = pageRangeInput?.value?.trim() || '';
|
||||
|
||||
if (!pageRange) {
|
||||
showAlert('Error', 'Please specify a page range for page-level attachments.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
showLoader('Embedding files into PDF...');
|
||||
try {
|
||||
const pdfFile = state.files[0];
|
||||
const pdfBuffer = (await readFileAsArrayBuffer(pdfFile)) as ArrayBuffer;
|
||||
|
||||
const attachmentBuffers: ArrayBuffer[] = [];
|
||||
const attachmentNames: string[] = [];
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
const file = attachments[i];
|
||||
showLoader(`Reading ${file.name} (${i + 1}/${attachments.length})...`);
|
||||
|
||||
const fileBuffer = (await readFileAsArrayBuffer(file)) as ArrayBuffer;
|
||||
attachmentBuffers.push(fileBuffer);
|
||||
attachmentNames.push(file.name);
|
||||
}
|
||||
|
||||
showLoader('Attaching files to PDF...');
|
||||
|
||||
const message = {
|
||||
command: 'add-attachments',
|
||||
pdfBuffer: pdfBuffer,
|
||||
attachmentBuffers: attachmentBuffers,
|
||||
attachmentNames: attachmentNames,
|
||||
attachmentLevel: attachmentLevel,
|
||||
pageRange: pageRange
|
||||
};
|
||||
|
||||
const transferables = [pdfBuffer, ...attachmentBuffers];
|
||||
worker.postMessage(message, transferables);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Error attaching files:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||
clearAttachments();
|
||||
}
|
||||
}
|
||||
|
||||
function clearAttachments() {
|
||||
attachments = [];
|
||||
const fileListDiv = document.getElementById('attachment-file-list');
|
||||
const attachmentInput = document.getElementById(
|
||||
'attachment-files-input'
|
||||
) as HTMLInputElement;
|
||||
const processBtn = document.getElementById(
|
||||
'process-btn'
|
||||
) as HTMLButtonElement;
|
||||
const attachmentLevelOptions = document.getElementById('attachment-level-options');
|
||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||
|
||||
if (fileListDiv) fileListDiv.innerHTML = '';
|
||||
if (attachmentInput) attachmentInput.value = '';
|
||||
if (processBtn) {
|
||||
processBtn.disabled = true;
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
if (attachmentLevelOptions) {
|
||||
attachmentLevelOptions.classList.add('hidden');
|
||||
}
|
||||
if (pageRangeWrapper) {
|
||||
pageRangeWrapper.classList.add('hidden');
|
||||
}
|
||||
|
||||
const documentRadio = document.querySelector('input[name="attachment-level"][value="document"]') as HTMLInputElement;
|
||||
if (documentRadio) {
|
||||
documentRadio.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function setupAddAttachmentsTool() {
|
||||
const optionsDiv = document.getElementById('attachment-options');
|
||||
const attachmentInput = document.getElementById(
|
||||
'attachment-files-input'
|
||||
) as HTMLInputElement;
|
||||
const fileListDiv = document.getElementById('attachment-file-list');
|
||||
const processBtn = document.getElementById(
|
||||
'process-btn'
|
||||
) as HTMLButtonElement;
|
||||
const attachmentLevelOptions = document.getElementById('attachment-level-options');
|
||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||
const totalPagesSpan = document.getElementById('attachment-total-pages');
|
||||
|
||||
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
|
||||
console.error('Attachment tool UI elements not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.files || state.files.length === 0) {
|
||||
console.error('No PDF file loaded for adding attachments.');
|
||||
return;
|
||||
}
|
||||
|
||||
optionsDiv.classList.remove('hidden');
|
||||
|
||||
if (totalPagesSpan && state.pdfDoc) {
|
||||
totalPagesSpan.textContent = state.pdfDoc.getPageCount().toString();
|
||||
}
|
||||
|
||||
if (attachmentInput.dataset.listenerAttached) return;
|
||||
attachmentInput.dataset.listenerAttached = 'true';
|
||||
|
||||
attachmentInput.addEventListener('change', (e) => {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files && files.length > 0) {
|
||||
attachments = Array.from(files);
|
||||
|
||||
fileListDiv.innerHTML = '';
|
||||
attachments.forEach((file) => {
|
||||
const div = document.createElement('div');
|
||||
div.className =
|
||||
'flex justify-between items-center p-2 bg-gray-800 rounded-md text-white';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate text-sm';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'text-xs text-gray-400';
|
||||
sizeSpan.textContent = `${Math.round(file.size / 1024)} KB`;
|
||||
|
||||
div.appendChild(nameSpan);
|
||||
div.appendChild(sizeSpan);
|
||||
fileListDiv.appendChild(div);
|
||||
});
|
||||
|
||||
if (attachmentLevelOptions) {
|
||||
attachmentLevelOptions.classList.remove('hidden');
|
||||
}
|
||||
|
||||
processBtn.disabled = false;
|
||||
processBtn.classList.remove('hidden');
|
||||
} else {
|
||||
clearAttachments();
|
||||
}
|
||||
});
|
||||
|
||||
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
|
||||
attachmentLevelRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
if (value === 'page' && pageRangeWrapper) {
|
||||
pageRangeWrapper.classList.remove('hidden');
|
||||
} else if (pageRangeWrapper) {
|
||||
pageRangeWrapper.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
processBtn.onclick = addAttachments;
|
||||
}
|
||||
234
src/js/logic/add-blank-page-page.ts
Normal file
234
src/js/logic/add-blank-page-page.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
interface AddBlankPageState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
}
|
||||
|
||||
const pageState: AddBlankPageState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
|
||||
if (pagePositionInput) pagePositionInput.value = '0';
|
||||
|
||||
const pageCountInput = document.getElementById('page-count') as HTMLInputElement;
|
||||
if (pageCountInput) pageCountInput.value = '1';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const pagePositionHint = document.getElementById('page-position-hint');
|
||||
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
// Load PDF document
|
||||
try {
|
||||
showLoader('Loading PDF...');
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
|
||||
if (pagePositionHint) {
|
||||
pagePositionHint.textContent = `Enter 0 to insert at the beginning, or ${pageCount} to insert at the end.`;
|
||||
}
|
||||
if (pagePositionInput) {
|
||||
pagePositionInput.max = pageCount.toString();
|
||||
}
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
resetState();
|
||||
}
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function addBlankPages() {
|
||||
if (!pageState.pdfDoc || !pageState.file) {
|
||||
showAlert('Error', 'Please upload a PDF first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
|
||||
const pageCountInput = document.getElementById('page-count') as HTMLInputElement;
|
||||
|
||||
const position = parseInt(pagePositionInput.value);
|
||||
const insertCount = parseInt(pageCountInput.value);
|
||||
const totalPages = pageState.pdfDoc.getPageCount();
|
||||
|
||||
if (isNaN(position) || position < 0 || position > totalPages) {
|
||||
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(insertCount) || insertCount < 1) {
|
||||
showAlert('Invalid Input', 'Please enter a valid number of pages (1 or more).');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader(`Adding ${insertCount} blank page${insertCount > 1 ? 's' : ''}...`);
|
||||
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const { width, height } = pageState.pdfDoc.getPage(0).getSize();
|
||||
const allIndices = Array.from({ length: totalPages }, function (_, i) { return i; });
|
||||
|
||||
const indicesBefore = allIndices.slice(0, position);
|
||||
const indicesAfter = allIndices.slice(position);
|
||||
|
||||
if (indicesBefore.length > 0) {
|
||||
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesBefore);
|
||||
copied.forEach(function (p) { newPdf.addPage(p); });
|
||||
}
|
||||
|
||||
// Add the specified number of blank pages
|
||||
for (let i = 0; i < insertCount; i++) {
|
||||
newPdf.addPage([width, height]);
|
||||
}
|
||||
|
||||
if (indicesAfter.length > 0) {
|
||||
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter);
|
||||
copied.forEach(function (p) { newPdf.addPage(p); });
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`${originalName}_blank-pages-added.pdf`
|
||||
);
|
||||
|
||||
showAlert('Success', `Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`, 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', `Could not add blank page${insertCount > 1 ? 's' : ''}.`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', addBlankPages);
|
||||
}
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function addBlankPage() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageNumberInput = document.getElementById('page-number').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageCountInput = document.getElementById('page-count').value;
|
||||
|
||||
if (pageNumberInput.trim() === '') {
|
||||
showAlert('Invalid Input', 'Please enter a page number.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageCountInput.trim() === '') {
|
||||
showAlert('Invalid Input', 'Please enter the number of pages to insert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const position = parseInt(pageNumberInput);
|
||||
const pageCount = parseInt(pageCountInput);
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
if (isNaN(position) || position < 0 || position > totalPages) {
|
||||
showAlert(
|
||||
'Invalid Input',
|
||||
`Please enter a number between 0 and ${totalPages}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(pageCount) || pageCount < 1) {
|
||||
showAlert(
|
||||
'Invalid Input',
|
||||
'Please enter a valid number of pages (1 or more).'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader(`Adding ${pageCount} blank page${pageCount > 1 ? 's' : ''}...`);
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
||||
const allIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||
|
||||
const indicesBefore = allIndices.slice(0, position);
|
||||
const indicesAfter = allIndices.slice(position);
|
||||
|
||||
if (indicesBefore.length > 0) {
|
||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesBefore);
|
||||
copied.forEach((p: any) => newPdf.addPage(p));
|
||||
}
|
||||
|
||||
// Add the specified number of blank pages
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
newPdf.addPage([width, height]);
|
||||
}
|
||||
|
||||
if (indicesAfter.length > 0) {
|
||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
||||
copied.forEach((p: any) => newPdf.addPage(p));
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`blank-page${pageCount > 1 ? 's' : ''}-added.pdf`
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', `Could not add blank page${pageCount > 1 ? 's' : ''}.`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, parsePageRanges } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||
|
||||
export function setupHeaderFooterUI() {
|
||||
const totalPagesSpan = document.getElementById('total-pages');
|
||||
if (totalPagesSpan && state.pdfDoc) {
|
||||
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
|
||||
}
|
||||
}
|
||||
|
||||
export async function addHeaderFooter() {
|
||||
showLoader('Adding header & footer...');
|
||||
try {
|
||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const allPages = state.pdfDoc.getPages();
|
||||
const totalPages = allPages.length;
|
||||
const margin = 40;
|
||||
|
||||
// --- 1. Get new formatting options from the UI ---
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('font-color').value;
|
||||
const fontColor = hexToRgb(colorHex);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageRangeInput = document.getElementById('page-range').value;
|
||||
|
||||
// --- 2. Get text values ---
|
||||
const texts = {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerLeft: document.getElementById('header-left').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerCenter: document.getElementById('header-center').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerRight: document.getElementById('header-right').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerLeft: document.getElementById('footer-left').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerCenter: document.getElementById('footer-center').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerRight: document.getElementById('footer-right').value,
|
||||
};
|
||||
|
||||
// --- 3. Parse page range to determine which pages to modify ---
|
||||
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
if (indicesToProcess.length === 0) {
|
||||
throw new Error(
|
||||
"Invalid page range specified. Please check your input (e.g., '1-3, 5')."
|
||||
);
|
||||
}
|
||||
|
||||
// --- 4. Define drawing options with new values ---
|
||||
const drawOptions = {
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
color: rgb(fontColor.r, fontColor.g, fontColor.b),
|
||||
};
|
||||
|
||||
// --- 5. Loop over only the selected pages ---
|
||||
for (const pageIndex of indicesToProcess) {
|
||||
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
||||
const page = allPages[pageIndex];
|
||||
const { width, height } = page.getSize();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
const pageNumber = pageIndex + 1; // For dynamic text
|
||||
|
||||
// Helper to replace placeholders like {page} and {total}
|
||||
const processText = (text: any) =>
|
||||
text.replace(/{page}/g, pageNumber).replace(/{total}/g, totalPages);
|
||||
|
||||
// Get processed text for the current page
|
||||
const processedTexts = {
|
||||
headerLeft: processText(texts.headerLeft),
|
||||
headerCenter: processText(texts.headerCenter),
|
||||
headerRight: processText(texts.headerRight),
|
||||
footerLeft: processText(texts.footerLeft),
|
||||
footerCenter: processText(texts.footerCenter),
|
||||
footerRight: processText(texts.footerRight),
|
||||
};
|
||||
|
||||
if (processedTexts.headerLeft)
|
||||
page.drawText(processedTexts.headerLeft, {
|
||||
...drawOptions,
|
||||
x: margin,
|
||||
y: height - margin,
|
||||
});
|
||||
if (processedTexts.headerCenter)
|
||||
page.drawText(processedTexts.headerCenter, {
|
||||
...drawOptions,
|
||||
x:
|
||||
width / 2 -
|
||||
helveticaFont.widthOfTextAtSize(
|
||||
processedTexts.headerCenter,
|
||||
fontSize
|
||||
) /
|
||||
2,
|
||||
y: height - margin,
|
||||
});
|
||||
if (processedTexts.headerRight)
|
||||
page.drawText(processedTexts.headerRight, {
|
||||
...drawOptions,
|
||||
x:
|
||||
width -
|
||||
margin -
|
||||
helveticaFont.widthOfTextAtSize(
|
||||
processedTexts.headerRight,
|
||||
fontSize
|
||||
),
|
||||
y: height - margin,
|
||||
});
|
||||
if (processedTexts.footerLeft)
|
||||
page.drawText(processedTexts.footerLeft, {
|
||||
...drawOptions,
|
||||
x: margin,
|
||||
y: margin,
|
||||
});
|
||||
if (processedTexts.footerCenter)
|
||||
page.drawText(processedTexts.footerCenter, {
|
||||
...drawOptions,
|
||||
x:
|
||||
width / 2 -
|
||||
helveticaFont.widthOfTextAtSize(
|
||||
processedTexts.footerCenter,
|
||||
fontSize
|
||||
) /
|
||||
2,
|
||||
y: margin,
|
||||
});
|
||||
if (processedTexts.footerRight)
|
||||
page.drawText(processedTexts.footerRight, {
|
||||
...drawOptions,
|
||||
x:
|
||||
width -
|
||||
margin -
|
||||
helveticaFont.widthOfTextAtSize(
|
||||
processedTexts.footerRight,
|
||||
fontSize
|
||||
),
|
||||
y: margin,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'header-footer-added.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not add header or footer.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { rgb, StandardFonts } from 'pdf-lib';
|
||||
|
||||
export async function addPageNumbers() {
|
||||
showLoader('Adding page numbers...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const position = document.getElementById('position').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const format = document.getElementById('number-format').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
|
||||
const pages = state.pdfDoc.getPages();
|
||||
const totalPages = pages.length;
|
||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const page = pages[i];
|
||||
|
||||
const mediaBox = page.getMediaBox();
|
||||
const cropBox = page.getCropBox();
|
||||
const bounds = cropBox || mediaBox;
|
||||
const width = bounds.width;
|
||||
const height = bounds.height;
|
||||
const xOffset = bounds.x || 0;
|
||||
const yOffset = bounds.y || 0;
|
||||
|
||||
let pageNumText =
|
||||
format === 'page_x_of_y' ? `${i + 1} / ${totalPages}` : `${i + 1}`;
|
||||
|
||||
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
|
||||
const textHeight = fontSize;
|
||||
|
||||
const minMargin = 8;
|
||||
const maxMargin = 40;
|
||||
const marginPercentage = 0.04;
|
||||
|
||||
const horizontalMargin = Math.max(
|
||||
minMargin,
|
||||
Math.min(maxMargin, width * marginPercentage)
|
||||
);
|
||||
const verticalMargin = Math.max(
|
||||
minMargin,
|
||||
Math.min(maxMargin, height * marginPercentage)
|
||||
);
|
||||
|
||||
// Ensure text doesn't go outside visible page boundaries
|
||||
const safeHorizontalMargin = Math.max(
|
||||
horizontalMargin,
|
||||
textWidth / 2 + 3
|
||||
);
|
||||
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
||||
|
||||
let x, y;
|
||||
|
||||
switch (position) {
|
||||
case 'bottom-center':
|
||||
x =
|
||||
Math.max(
|
||||
safeHorizontalMargin,
|
||||
Math.min(
|
||||
width - safeHorizontalMargin - textWidth,
|
||||
(width - textWidth) / 2
|
||||
)
|
||||
) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x =
|
||||
Math.max(
|
||||
safeHorizontalMargin,
|
||||
width - safeHorizontalMargin - textWidth
|
||||
) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'top-center':
|
||||
x =
|
||||
Math.max(
|
||||
safeHorizontalMargin,
|
||||
Math.min(
|
||||
width - safeHorizontalMargin - textWidth,
|
||||
(width - textWidth) / 2
|
||||
)
|
||||
) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-right':
|
||||
x =
|
||||
Math.max(
|
||||
safeHorizontalMargin,
|
||||
width - safeHorizontalMargin - textWidth
|
||||
) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
}
|
||||
|
||||
// Final safety check to ensure coordinates are within visible page bounds
|
||||
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
|
||||
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
|
||||
|
||||
page.drawText(pageNumText, {
|
||||
x,
|
||||
y,
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'paginated.pdf'
|
||||
);
|
||||
showAlert('Success', 'Page numbers added successfully!');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not add page numbers.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { formatBytes, readFileAsArrayBuffer } from '../utils/helpers'
|
||||
import { formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers'
|
||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'
|
||||
import { createIcons, icons } from 'lucide'
|
||||
|
||||
let selectedFile: File | null = null
|
||||
let viewerIframe: HTMLIFrameElement | null = null
|
||||
@@ -12,6 +13,38 @@ const viewerContainer = document.getElementById('stamp-viewer-container') as HTM
|
||||
const viewerCard = document.getElementById('viewer-card') as HTMLDivElement | null
|
||||
const saveStampedBtn = document.getElementById('save-stamped-btn') as HTMLButtonElement
|
||||
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement | null
|
||||
const toolUploader = document.getElementById('tool-uploader') as HTMLDivElement | null
|
||||
const usernameInput = document.getElementById('stamp-username') as HTMLInputElement | null
|
||||
|
||||
function resetState() {
|
||||
selectedFile = null
|
||||
if (currentBlobUrl) {
|
||||
URL.revokeObjectURL(currentBlobUrl)
|
||||
currentBlobUrl = null
|
||||
}
|
||||
if (viewerIframe && viewerContainer && viewerIframe.parentElement === viewerContainer) {
|
||||
viewerContainer.removeChild(viewerIframe)
|
||||
}
|
||||
viewerIframe = null
|
||||
viewerReady = false
|
||||
if (viewerCard) viewerCard.classList.add('hidden')
|
||||
if (saveStampedBtn) saveStampedBtn.classList.add('hidden')
|
||||
|
||||
if (viewerContainer) {
|
||||
viewerContainer.style.height = ''
|
||||
viewerContainer.style.aspectRatio = ''
|
||||
}
|
||||
|
||||
// Revert container width only if NOT in full width mode
|
||||
const isFullWidth = localStorage.getItem('fullWidthMode') === 'true'
|
||||
if (toolUploader && !isFullWidth) {
|
||||
toolUploader.classList.remove('max-w-6xl')
|
||||
toolUploader.classList.add('max-w-2xl')
|
||||
}
|
||||
|
||||
updateFileList()
|
||||
if (pdfInput) pdfInput.value = ''
|
||||
}
|
||||
|
||||
function updateFileList() {
|
||||
if (!selectedFile) {
|
||||
@@ -23,19 +56,67 @@ function updateFileList() {
|
||||
fileListDiv.classList.remove('hidden')
|
||||
fileListDiv.innerHTML = ''
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg mb-2'
|
||||
// Expand container width for viewer if NOT in full width mode
|
||||
const isFullWidth = localStorage.getItem('fullWidthMode') === 'true'
|
||||
if (toolUploader && !isFullWidth) {
|
||||
toolUploader.classList.remove('max-w-2xl')
|
||||
toolUploader.classList.add('max-w-6xl')
|
||||
}
|
||||
|
||||
const nameSpan = document.createElement('span')
|
||||
nameSpan.className = 'truncate font-medium text-gray-200'
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors'
|
||||
|
||||
const innerDiv = document.createElement('div')
|
||||
innerDiv.className = 'flex items-center justify-between'
|
||||
|
||||
const infoDiv = document.createElement('div')
|
||||
infoDiv.className = 'flex-1 min-w-0'
|
||||
|
||||
const nameSpan = document.createElement('p')
|
||||
nameSpan.className = 'truncate font-medium text-white'
|
||||
nameSpan.textContent = selectedFile.name
|
||||
|
||||
const sizeSpan = document.createElement('span')
|
||||
sizeSpan.className = 'ml-3 text-gray-400 text-xs flex-shrink-0'
|
||||
const sizeSpan = document.createElement('p')
|
||||
sizeSpan.className = 'text-gray-400 text-sm'
|
||||
sizeSpan.textContent = formatBytes(selectedFile.size)
|
||||
|
||||
wrapper.append(nameSpan, sizeSpan)
|
||||
infoDiv.append(nameSpan, sizeSpan)
|
||||
|
||||
const deleteBtn = document.createElement('button')
|
||||
deleteBtn.className = 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2'
|
||||
deleteBtn.title = 'Remove file'
|
||||
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>'
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.stopPropagation()
|
||||
resetState()
|
||||
}
|
||||
|
||||
innerDiv.append(infoDiv, deleteBtn)
|
||||
wrapper.appendChild(innerDiv)
|
||||
fileListDiv.appendChild(wrapper)
|
||||
|
||||
createIcons({ icons })
|
||||
}
|
||||
|
||||
async function adjustViewerHeight(file: File) {
|
||||
if (!viewerContainer) return
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const loadingTask = getPDFDocument({ data: arrayBuffer })
|
||||
const pdf = await loadingTask.promise
|
||||
const page = await pdf.getPage(1)
|
||||
const viewport = page.getViewport({ scale: 1 })
|
||||
|
||||
// Add ~50px for toolbar height relative to page height
|
||||
const aspectRatio = viewport.width / (viewport.height + 50)
|
||||
|
||||
viewerContainer.style.height = 'auto'
|
||||
viewerContainer.style.aspectRatio = `${aspectRatio}`
|
||||
} catch (e) {
|
||||
console.error('Error adjusting viewer height:', e)
|
||||
// Fallback if calculation fails
|
||||
viewerContainer.style.height = '70vh'
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPdfInViewer(file: File) {
|
||||
@@ -55,6 +136,10 @@ async function loadPdfInViewer(file: File) {
|
||||
}
|
||||
viewerIframe = null
|
||||
viewerReady = false
|
||||
|
||||
// Calculate and apply dynamic height
|
||||
await adjustViewerHeight(file)
|
||||
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file)
|
||||
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' })
|
||||
currentBlobUrl = URL.createObjectURL(blob)
|
||||
@@ -74,8 +159,11 @@ async function loadPdfInViewer(file: File) {
|
||||
iframe.className = 'w-full h-full border-0'
|
||||
iframe.allowFullscreen = true
|
||||
|
||||
const viewerUrl = new URL('/pdfjs-annotation-viewer/web/viewer.html', window.location.origin)
|
||||
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}`
|
||||
const viewerUrl = new URL(import.meta.env.BASE_URL + 'pdfjs-annotation-viewer/web/viewer.html', window.location.origin)
|
||||
const stampUserName = usernameInput?.value?.trim() || ''
|
||||
// ae_username is the hash parameter used by pdfjs-annotation-extension to set the username
|
||||
const hashParams = stampUserName ? `#ae_username=${encodeURIComponent(stampUserName)}` : ''
|
||||
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}${hashParams}`
|
||||
|
||||
iframe.addEventListener('load', () => {
|
||||
setupAnnotationViewer(iframe)
|
||||
@@ -128,6 +216,7 @@ function setupAnnotationViewer(iframe: HTMLIFrameElement) {
|
||||
async function onPdfSelected(file: File) {
|
||||
selectedFile = file
|
||||
updateFileList()
|
||||
if (saveStampedBtn) saveStampedBtn.classList.remove('hidden')
|
||||
await loadPdfInViewer(file)
|
||||
}
|
||||
|
||||
@@ -141,6 +230,26 @@ if (pdfInput) {
|
||||
})
|
||||
}
|
||||
|
||||
// Add drag/drop support
|
||||
const dropZone = document.getElementById('drop-zone')
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault()
|
||||
dropZone.classList.add('border-indigo-500')
|
||||
})
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('border-indigo-500')
|
||||
})
|
||||
dropZone.addEventListener('drop', async (e) => {
|
||||
e.preventDefault()
|
||||
dropZone.classList.remove('border-indigo-500')
|
||||
const file = e.dataTransfer?.files[0]
|
||||
if (file && file.type === 'application/pdf') {
|
||||
await onPdfSelected(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (saveStampedBtn) {
|
||||
saveStampedBtn.addEventListener('click', () => {
|
||||
if (!viewerIframe) {
|
||||
@@ -155,7 +264,10 @@ if (saveStampedBtn) {
|
||||
if (extensionInstance && typeof extensionInstance.exportPdf === 'function') {
|
||||
const result = extensionInstance.exportPdf()
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.catch((err: unknown) => {
|
||||
result.then(() => {
|
||||
// Reset state after successful export
|
||||
setTimeout(() => resetState(), 500)
|
||||
}).catch((err: unknown) => {
|
||||
console.error('Error while exporting stamped PDF via annotation extension:', err)
|
||||
})
|
||||
}
|
||||
|
||||
220
src/js/logic/add-watermark-page.ts
Normal file
220
src/js/logic/add-watermark-page.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, formatBytes, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb, degrees, StandardFonts } from 'pdf-lib';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
}
|
||||
|
||||
const pageState: PageState = { file: null, pdfDoc: null };
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
||||
}
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('border-indigo-500');
|
||||
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
|
||||
});
|
||||
}
|
||||
|
||||
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
|
||||
if (processBtn) processBtn.addEventListener('click', addWatermark);
|
||||
|
||||
setupWatermarkUI();
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files?.length) handleFiles(input.files);
|
||||
}
|
||||
|
||||
async function handleFiles(files: FileList) {
|
||||
const file = files[0];
|
||||
if (!file || file.type !== 'application/pdf') {
|
||||
showAlert('Invalid File', 'Please upload a valid PDF file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
pageState.file = file;
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = resetState;
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
document.getElementById('options-panel')?.classList.add('hidden');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
function setupWatermarkUI() {
|
||||
const watermarkTypeRadios = document.querySelectorAll('input[name="watermark-type"]');
|
||||
const textOptions = document.getElementById('text-watermark-options');
|
||||
const imageOptions = document.getElementById('image-watermark-options');
|
||||
|
||||
watermarkTypeRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.value === 'text') {
|
||||
textOptions?.classList.remove('hidden');
|
||||
imageOptions?.classList.add('hidden');
|
||||
} else {
|
||||
textOptions?.classList.add('hidden');
|
||||
imageOptions?.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const opacitySliderText = document.getElementById('opacity-text') as HTMLInputElement;
|
||||
const opacityValueText = document.getElementById('opacity-value-text');
|
||||
const angleSliderText = document.getElementById('angle-text') as HTMLInputElement;
|
||||
const angleValueText = document.getElementById('angle-value-text');
|
||||
|
||||
opacitySliderText?.addEventListener('input', () => { if (opacityValueText) opacityValueText.textContent = opacitySliderText.value; });
|
||||
angleSliderText?.addEventListener('input', () => { if (angleValueText) angleValueText.textContent = angleSliderText.value; });
|
||||
|
||||
const opacitySliderImage = document.getElementById('opacity-image') as HTMLInputElement;
|
||||
const opacityValueImage = document.getElementById('opacity-value-image');
|
||||
const angleSliderImage = document.getElementById('angle-image') as HTMLInputElement;
|
||||
const angleValueImage = document.getElementById('angle-value-image');
|
||||
|
||||
opacitySliderImage?.addEventListener('input', () => { if (opacityValueImage) opacityValueImage.textContent = opacitySliderImage.value; });
|
||||
angleSliderImage?.addEventListener('input', () => { if (angleValueImage) angleValueImage.textContent = angleSliderImage.value; });
|
||||
}
|
||||
|
||||
async function addWatermark() {
|
||||
if (!pageState.pdfDoc) {
|
||||
showAlert('Error', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const watermarkType = (document.querySelector('input[name="watermark-type"]:checked') as HTMLInputElement)?.value || 'text';
|
||||
showLoader('Adding watermark...');
|
||||
|
||||
try {
|
||||
const pages = pageState.pdfDoc.getPages();
|
||||
let watermarkAsset: any = null;
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
watermarkAsset = await pageState.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
} else {
|
||||
const imageFile = (document.getElementById('image-watermark-input') as HTMLInputElement).files?.[0];
|
||||
if (!imageFile) throw new Error('Please select an image file for the watermark.');
|
||||
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
||||
if (imageFile.type === 'image/png') {
|
||||
watermarkAsset = await pageState.pdfDoc.embedPng(imageBytes as ArrayBuffer);
|
||||
} else if (imageFile.type === 'image/jpeg') {
|
||||
watermarkAsset = await pageState.pdfDoc.embedJpg(imageBytes as ArrayBuffer);
|
||||
} else {
|
||||
throw new Error('Unsupported Image. Please use a PNG or JPG for the watermark.');
|
||||
}
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
const { width, height } = page.getSize();
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
const text = (document.getElementById('watermark-text') as HTMLInputElement).value;
|
||||
if (!text.trim()) throw new Error('Please enter text for the watermark.');
|
||||
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement).value) || 72;
|
||||
const angle = parseInt((document.getElementById('angle-text') as HTMLInputElement).value) || 0;
|
||||
const opacity = parseFloat((document.getElementById('opacity-text') as HTMLInputElement).value) || 0.3;
|
||||
const colorHex = (document.getElementById('text-color') as HTMLInputElement).value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
||||
|
||||
page.drawText(text, {
|
||||
x: (width - textWidth) / 2,
|
||||
y: height / 2,
|
||||
font: watermarkAsset,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
} else {
|
||||
const angle = parseInt((document.getElementById('angle-image') as HTMLInputElement).value) || 0;
|
||||
const opacity = parseFloat((document.getElementById('opacity-image') as HTMLInputElement).value) || 0.3;
|
||||
const scale = 0.5;
|
||||
const imgWidth = watermarkAsset.width * scale;
|
||||
const imgHeight = watermarkAsset.height * scale;
|
||||
|
||||
page.drawImage(watermarkAsset, {
|
||||
x: (width - imgWidth) / 2,
|
||||
y: (height - imgHeight) / 2,
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await pageState.pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'watermarked.pdf');
|
||||
showAlert('Success', 'Watermark added successfully!', 'success', () => { resetState(); });
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not add the watermark.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
hexToRgb,
|
||||
resetAndReloadTool,
|
||||
} from '../utils/helpers.js';
|
||||
import { state, resetState } from '../state.js';
|
||||
|
||||
import {
|
||||
PDFDocument as PDFLibDocument,
|
||||
rgb,
|
||||
degrees,
|
||||
StandardFonts,
|
||||
} from 'pdf-lib';
|
||||
|
||||
export function setupWatermarkUI() {
|
||||
const watermarkTypeRadios = document.querySelectorAll(
|
||||
'input[name="watermark-type"]'
|
||||
);
|
||||
const textOptions = document.getElementById('text-watermark-options');
|
||||
const imageOptions = document.getElementById('image-watermark-options');
|
||||
|
||||
watermarkTypeRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', (e) => {
|
||||
if (e.target.value === 'text') {
|
||||
textOptions.classList.remove('hidden');
|
||||
imageOptions.classList.add('hidden');
|
||||
} else {
|
||||
textOptions.classList.add('hidden');
|
||||
imageOptions.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const opacitySliderText = document.getElementById('opacity-text');
|
||||
const opacityValueText = document.getElementById('opacity-value-text');
|
||||
const angleSliderText = document.getElementById('angle-text');
|
||||
const angleValueText = document.getElementById('angle-value-text');
|
||||
|
||||
opacitySliderText.addEventListener(
|
||||
'input',
|
||||
() =>
|
||||
(opacityValueText.textContent = (
|
||||
opacitySliderText as HTMLInputElement
|
||||
).value)
|
||||
);
|
||||
|
||||
angleSliderText.addEventListener(
|
||||
'input',
|
||||
() =>
|
||||
(angleValueText.textContent = (angleSliderText as HTMLInputElement).value)
|
||||
);
|
||||
|
||||
const opacitySliderImage = document.getElementById('opacity-image');
|
||||
const opacityValueImage = document.getElementById('opacity-value-image');
|
||||
const angleSliderImage = document.getElementById('angle-image');
|
||||
const angleValueImage = document.getElementById('angle-value-image');
|
||||
|
||||
opacitySliderImage.addEventListener(
|
||||
'input',
|
||||
() =>
|
||||
(opacityValueImage.textContent = (
|
||||
opacitySliderImage as HTMLInputElement
|
||||
).value)
|
||||
);
|
||||
|
||||
angleSliderImage.addEventListener(
|
||||
'input',
|
||||
() =>
|
||||
(angleValueImage.textContent = (
|
||||
angleSliderImage as HTMLInputElement
|
||||
).value)
|
||||
);
|
||||
}
|
||||
|
||||
export async function addWatermark() {
|
||||
const watermarkType = (
|
||||
document.querySelector(
|
||||
'input[name="watermark-type"]:checked'
|
||||
) as HTMLInputElement
|
||||
).value;
|
||||
|
||||
showLoader('Adding watermark...');
|
||||
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
let watermarkAsset = null;
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
} else {
|
||||
// 'image'
|
||||
const imageFile = (
|
||||
document.getElementById('image-watermark-input') as HTMLInputElement
|
||||
).files?.[0];
|
||||
if (!imageFile)
|
||||
throw new Error('Please select an image file for the watermark.');
|
||||
|
||||
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
||||
if (imageFile.type === 'image/png') {
|
||||
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
|
||||
} else if (imageFile.type === 'image/jpeg') {
|
||||
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Unsupported Image. Please use a PNG or JPG for the watermark.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
const { width, height } = page.getSize();
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const text = document.getElementById('watermark-text').value;
|
||||
if (!text.trim())
|
||||
throw new Error('Please enter text for the watermark.');
|
||||
|
||||
const fontSize =
|
||||
parseInt(
|
||||
(document.getElementById('font-size') as HTMLInputElement).value
|
||||
) || 72;
|
||||
const angle =
|
||||
parseInt(
|
||||
(document.getElementById('angle-text') as HTMLInputElement).value
|
||||
) || 0;
|
||||
const opacity =
|
||||
parseFloat(
|
||||
(document.getElementById('opacity-text') as HTMLInputElement).value
|
||||
) || 0.3;
|
||||
const colorHex = (
|
||||
document.getElementById('text-color') as HTMLInputElement
|
||||
).value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
||||
|
||||
page.drawText(text, {
|
||||
x: (width - textWidth) / 2,
|
||||
y: height / 2,
|
||||
font: watermarkAsset,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
} else {
|
||||
const angle =
|
||||
parseInt(
|
||||
(document.getElementById('angle-image') as HTMLInputElement).value
|
||||
) || 0;
|
||||
const opacity =
|
||||
parseFloat(
|
||||
(document.getElementById('opacity-image') as HTMLInputElement).value
|
||||
) || 0.3;
|
||||
|
||||
const scale = 0.5;
|
||||
const imgWidth = watermarkAsset.width * scale;
|
||||
const imgHeight = watermarkAsset.height * scale;
|
||||
|
||||
page.drawImage(watermarkAsset, {
|
||||
x: (width - imgWidth) / 2,
|
||||
y: (height - imgHeight) / 2,
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'watermarked.pdf'
|
||||
);
|
||||
|
||||
resetAndReloadTool();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
e.message || 'Could not add the watermark. Please check your inputs.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
252
src/js/logic/alternate-merge-page.ts
Normal file
252
src/js/logic/alternate-merge-page.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
interface AlternateMergeState {
|
||||
files: File[];
|
||||
pdfBytes: Map<string, ArrayBuffer>;
|
||||
pdfDocs: Map<string, any>;
|
||||
}
|
||||
|
||||
const pageState: AlternateMergeState = {
|
||||
files: [],
|
||||
pdfBytes: new Map(),
|
||||
pdfDocs: new Map(),
|
||||
};
|
||||
|
||||
const alternateMergeWorker = new Worker(import.meta.env.BASE_URL + 'workers/alternate-merge.worker.js');
|
||||
|
||||
function resetState() {
|
||||
pageState.files = [];
|
||||
pageState.pdfBytes.clear();
|
||||
pageState.pdfDocs.clear();
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (fileList) fileList.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const fileList = document.getElementById('file-list');
|
||||
|
||||
if (!fileDisplayArea || !fileList) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.files.length > 0) {
|
||||
// Show file count summary
|
||||
const summaryDiv = document.createElement('div');
|
||||
summaryDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoSpan = document.createElement('span');
|
||||
infoSpan.className = 'text-gray-200';
|
||||
infoSpan.textContent = `${pageState.files.length} PDF files selected`;
|
||||
|
||||
const clearBtn = document.createElement('button');
|
||||
clearBtn.className = 'text-red-400 hover:text-red-300';
|
||||
clearBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
clearBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
summaryDiv.append(infoSpan, clearBtn);
|
||||
fileDisplayArea.appendChild(summaryDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
// Load PDFs and populate list
|
||||
showLoader('Loading PDF files...');
|
||||
fileList.innerHTML = '';
|
||||
|
||||
try {
|
||||
for (const file of pageState.files) {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState.pdfBytes.set(file.name, arrayBuffer);
|
||||
|
||||
const bytesForPdfJs = arrayBuffer.slice(0);
|
||||
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
||||
pageState.pdfDocs.set(file.name, 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;
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'flex items-center gap-2 truncate flex-1';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-white';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('span');
|
||||
metaSpan.className = 'text-sm text-gray-400 flex-shrink-0';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pageCount} pages`;
|
||||
|
||||
infoDiv.append(nameSpan, metaSpan);
|
||||
|
||||
const dragHandle = document.createElement('div');
|
||||
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded ml-2';
|
||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
||||
|
||||
li.append(infoDiv, dragHandle);
|
||||
fileList.appendChild(li);
|
||||
}
|
||||
|
||||
Sortable.create(fileList, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
});
|
||||
|
||||
hideLoader();
|
||||
|
||||
if (toolOptions && pageState.files.length >= 2) {
|
||||
toolOptions.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading PDFs:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load one or more PDF files.');
|
||||
resetState();
|
||||
}
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function mixPages() {
|
||||
if (pageState.pdfBytes.size < 2) {
|
||||
showAlert('Not Enough Files', 'Please upload at least two PDF files to alternate and mix.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Alternating and mixing pages...');
|
||||
|
||||
try {
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) throw new Error('File list not found');
|
||||
|
||||
const sortedFileNames = Array.from(fileList.children).map(function (li) {
|
||||
return (li as HTMLElement).dataset.fileName;
|
||||
}).filter(Boolean) as string[];
|
||||
|
||||
interface InterleaveFile {
|
||||
name: string;
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
|
||||
const filesToMerge: InterleaveFile[] = [];
|
||||
for (const name of sortedFileNames) {
|
||||
const bytes = pageState.pdfBytes.get(name);
|
||||
if (bytes) {
|
||||
filesToMerge.push({ name, data: bytes });
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToMerge.length < 2) {
|
||||
showAlert('Error', 'At least two valid PDFs are required.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
command: 'interleave',
|
||||
files: filesToMerge
|
||||
};
|
||||
|
||||
alternateMergeWorker.postMessage(message, filesToMerge.map(function (f) { return f.data; }));
|
||||
|
||||
alternateMergeWorker.onmessage = function (e: MessageEvent) {
|
||||
hideLoader();
|
||||
if (e.data.status === 'success') {
|
||||
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
||||
downloadFile(blob, 'alternated-mixed.pdf');
|
||||
showAlert('Success', 'PDFs have been mixed successfully!', 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} else {
|
||||
console.error('Worker interleave error:', e.data.message);
|
||||
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
|
||||
}
|
||||
};
|
||||
|
||||
alternateMergeWorker.onerror = function (e) {
|
||||
hideLoader();
|
||||
console.error('Worker error:', e);
|
||||
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
console.error('Alternate Merge error:', e);
|
||||
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||
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 = 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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileSelect(files);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', mixPages);
|
||||
}
|
||||
});
|
||||
@@ -1,150 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
interface AlternateMergeState {
|
||||
pdfDocs: Record<string, any>;
|
||||
pdfBytes: Record<string, ArrayBuffer>;
|
||||
}
|
||||
|
||||
const alternateMergeState: AlternateMergeState = {
|
||||
pdfDocs: {},
|
||||
pdfBytes: {},
|
||||
};
|
||||
|
||||
const alternateMergeWorker = new Worker(import.meta.env.BASE_URL + 'workers/alternate-merge.worker.js');
|
||||
|
||||
export async function setupAlternateMergeTool() {
|
||||
const optionsDiv = document.getElementById('alternate-merge-options');
|
||||
const processBtn = document.getElementById(
|
||||
'process-btn'
|
||||
) as HTMLButtonElement;
|
||||
const fileList = document.getElementById('alternate-file-list');
|
||||
|
||||
if (!optionsDiv || !processBtn || !fileList) return;
|
||||
|
||||
optionsDiv.classList.remove('hidden');
|
||||
processBtn.disabled = false;
|
||||
processBtn.onclick = alternateMerge;
|
||||
|
||||
fileList.innerHTML = '';
|
||||
alternateMergeState.pdfDocs = {};
|
||||
alternateMergeState.pdfBytes = {};
|
||||
|
||||
showLoader('Loading PDF documents...');
|
||||
try {
|
||||
for (const file of state.files) {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
alternateMergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
|
||||
|
||||
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
|
||||
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
||||
alternateMergeState.pdfDocs[file.name] = 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;
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'flex items-center gap-2 truncate';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-white';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const pagesSpan = document.createElement('span');
|
||||
pagesSpan.className = 'text-sm text-gray-400 flex-shrink-0';
|
||||
pagesSpan.textContent = `(${pageCount} pages)`;
|
||||
|
||||
infoDiv.append(nameSpan, pagesSpan);
|
||||
|
||||
const dragHandle = document.createElement('div');
|
||||
dragHandle.className =
|
||||
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded';
|
||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
||||
|
||||
li.append(infoDiv, dragHandle);
|
||||
fileList.appendChild(li);
|
||||
}
|
||||
|
||||
Sortable.create(fileList, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
});
|
||||
} catch (error) {
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to load one or more PDF files. They may be corrupted or password-protected.'
|
||||
);
|
||||
console.error(error);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export async function alternateMerge() {
|
||||
if (Object.keys(alternateMergeState.pdfBytes).length < 2) {
|
||||
showAlert(
|
||||
'Not Enough Files',
|
||||
'Please upload at least two PDF files to alternate and mix.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Alternating and mixing pages...');
|
||||
try {
|
||||
const fileList = document.getElementById('alternate-file-list');
|
||||
if (!fileList) throw new Error('File list not found');
|
||||
|
||||
const sortedFileNames = Array.from(fileList.children).map(
|
||||
(li) => (li as HTMLElement).dataset.fileName
|
||||
).filter(Boolean) as string[];
|
||||
|
||||
const filesToMerge: InterleaveFile[] = [];
|
||||
for (const name of sortedFileNames) {
|
||||
const bytes = alternateMergeState.pdfBytes[name];
|
||||
if (bytes) {
|
||||
filesToMerge.push({ name, data: bytes });
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToMerge.length < 2) {
|
||||
showAlert('Error', 'At least two valid PDFs are required.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const message: InterleaveMessage = {
|
||||
command: 'interleave',
|
||||
files: filesToMerge
|
||||
};
|
||||
|
||||
alternateMergeWorker.postMessage(message, filesToMerge.map(f => f.data));
|
||||
|
||||
alternateMergeWorker.onmessage = (e: MessageEvent<InterleaveResponse>) => {
|
||||
hideLoader();
|
||||
if (e.data.status === 'success') {
|
||||
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
||||
downloadFile(blob, 'alternated-mixed.pdf');
|
||||
showAlert('Success', 'PDFs have been mixed successfully!');
|
||||
} else {
|
||||
console.error('Worker interleave error:', e.data.message);
|
||||
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
|
||||
}
|
||||
};
|
||||
|
||||
alternateMergeWorker.onerror = (e) => {
|
||||
hideLoader();
|
||||
console.error('Worker error:', e);
|
||||
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
console.error('Alternate Merge error:', e);
|
||||
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
104
src/js/logic/background-color-page.ts
Normal file
104
src/js/logic/background-color-page.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
|
||||
interface PageState { file: File | null; pdfDoc: PDFLibDocument | null; }
|
||||
const pageState: PageState = { file: null, pdfDoc: null };
|
||||
|
||||
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); }
|
||||
else { initializePage(); }
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
||||
}
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault(); dropZone.classList.remove('border-indigo-500');
|
||||
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
|
||||
});
|
||||
}
|
||||
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
|
||||
if (processBtn) processBtn.addEventListener('click', changeBackgroundColor);
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); }
|
||||
|
||||
async function handleFiles(files: FileList) {
|
||||
const file = files[0];
|
||||
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
pageState.file = file;
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
|
||||
finally { hideLoader(); }
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = resetState;
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null; pageState.pdfDoc = null;
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
document.getElementById('options-panel')?.classList.add('hidden');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function changeBackgroundColor() {
|
||||
if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; }
|
||||
const colorHex = (document.getElementById('background-color') as HTMLInputElement).value;
|
||||
const color = hexToRgb(colorHex);
|
||||
showLoader('Changing background color...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
for (let i = 0; i < pageState.pdfDoc.getPageCount(); i++) {
|
||||
const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
const { width, height } = originalPage.getSize();
|
||||
const newPage = newPdfDoc.addPage([width, height]);
|
||||
newPage.drawRectangle({ x: 0, y: 0, width, height, color: rgb(color.r, color.g, color.b) });
|
||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||
newPage.drawPage(embeddedPage, { x: 0, y: 0, width, height });
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
|
||||
showAlert('Success', 'Background color changed successfully!', 'success', () => { resetState(); });
|
||||
} catch (e) { console.error(e); showAlert('Error', 'Could not change the background color.'); }
|
||||
finally { hideLoader(); }
|
||||
}
|
||||
204
src/js/logic/bmp-to-pdf-page.ts
Normal file
204
src/js/logic/bmp-to-pdf-page.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
async function convertImageToPngBytes(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('Failed to get canvas context'));
|
||||
return;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise<Blob | null>((res) =>
|
||||
canvas.toBlob(res, 'image/png')
|
||||
);
|
||||
if (!pngBlob) {
|
||||
reject(new Error('Failed to create PNG blob'));
|
||||
return;
|
||||
}
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||
img.src = e.target?.result as string;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!fileDisplayArea || !fileControls || !processBtn) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one BMP file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting BMP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_bmps.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert BMP to PDF. One of the files may be invalid.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.type === 'image/bmp' || file.name.toLowerCase().endsWith('.bmp')
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only BMP files are allowed.');
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
files = [...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);
|
||||
}
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
async function convertImageToPngBytes(file: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise((res) =>
|
||||
canvas.toBlob(res, 'image/png')
|
||||
);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function bmpToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one BMP file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting BMP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_bmps.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert BMP to PDF. One of the files may be invalid.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
|
||||
export async function changeBackgroundColor() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('background-color').value;
|
||||
const color = hexToRgb(colorHex);
|
||||
|
||||
showLoader('Changing background color...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
|
||||
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
const { width, height } = originalPage.getSize();
|
||||
|
||||
const newPage = newPdfDoc.addPage([width, height]);
|
||||
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
color: rgb(color.r, color.g, color.b),
|
||||
});
|
||||
|
||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||
newPage.drawPage(embeddedPage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'background-changed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not change the background color.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
286
src/js/logic/change-permissions-page.ts
Normal file
286
src/js/logic/change-permissions-page.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
file: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const currentPassword = document.getElementById('current-password') as HTMLInputElement;
|
||||
if (currentPassword) currentPassword.value = '';
|
||||
|
||||
const newUserPassword = document.getElementById('new-user-password') as HTMLInputElement;
|
||||
if (newUserPassword) newUserPassword.value = '';
|
||||
|
||||
const newOwnerPassword = document.getElementById('new-owner-password') as HTMLInputElement;
|
||||
if (newOwnerPassword) newOwnerPassword.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function changePermissions() {
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPassword = (document.getElementById('current-password') as HTMLInputElement)?.value || '';
|
||||
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement)?.value || '';
|
||||
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement)?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing...';
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Processing PDF permissions...';
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
if (currentPassword) {
|
||||
args.push('--password=' + currentPassword);
|
||||
}
|
||||
|
||||
const shouldEncrypt = newUserPassword || newOwnerPassword;
|
||||
|
||||
if (shouldEncrypt) {
|
||||
const finalUserPassword = newUserPassword;
|
||||
const finalOwnerPassword = newOwnerPassword;
|
||||
|
||||
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
|
||||
|
||||
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement)?.checked;
|
||||
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement)?.checked;
|
||||
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement)?.checked;
|
||||
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement)?.checked;
|
||||
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement)?.checked;
|
||||
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement)?.checked;
|
||||
const allowPageExtraction = (document.getElementById('allow-page-extraction') as HTMLInputElement)?.checked;
|
||||
|
||||
if (finalOwnerPassword) {
|
||||
if (!allowModifying) args.push('--modify=none');
|
||||
if (!allowCopying) args.push('--extract=n');
|
||||
if (!allowPrinting) args.push('--print=none');
|
||||
if (!allowAnnotating) args.push('--annotate=n');
|
||||
if (!allowDocumentAssembly) args.push('--assemble=n');
|
||||
if (!allowFillingForms) args.push('--form=n');
|
||||
if (!allowPageExtraction) args.push('--extract=n');
|
||||
if (!allowModifying) args.push('--modify-other=n');
|
||||
} else if (finalUserPassword) {
|
||||
args.push('--allow-insecure');
|
||||
}
|
||||
} else {
|
||||
args.push('--decrypt');
|
||||
}
|
||||
|
||||
args.push('--', outputPath);
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
|
||||
const errorMsg = qpdfError.message || '';
|
||||
|
||||
if (
|
||||
errorMsg.includes('invalid password') ||
|
||||
errorMsg.includes('incorrect password') ||
|
||||
errorMsg.includes('password')
|
||||
) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
if (
|
||||
errorMsg.includes('encrypted') ||
|
||||
errorMsg.includes('password required')
|
||||
) {
|
||||
throw new Error('PASSWORD_REQUIRED');
|
||||
}
|
||||
|
||||
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Processing resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `permissions-changed-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
let successMessage = 'PDF permissions changed successfully!';
|
||||
if (!shouldEncrypt) {
|
||||
successMessage = 'PDF decrypted successfully! All encryption and restrictions removed.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage, 'success', () => { resetState(); });
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF permission change:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
if (error.message === 'INVALID_PASSWORD') {
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The current password you entered is incorrect. Please try again.'
|
||||
);
|
||||
} else if (error.message === 'PASSWORD_REQUIRED') {
|
||||
showAlert(
|
||||
'Password Required',
|
||||
'This PDF is password-protected. Please enter the current password to proceed.'
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Processing Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password protected.'}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) { }
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) { }
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', changePermissions);
|
||||
}
|
||||
});
|
||||
@@ -1,169 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function changePermissions() {
|
||||
const file = state.files[0];
|
||||
const currentPassword =
|
||||
(document.getElementById('current-password') as HTMLInputElement)?.value ||
|
||||
'';
|
||||
const newUserPassword =
|
||||
(document.getElementById('new-user-password') as HTMLInputElement)?.value ||
|
||||
'';
|
||||
const newOwnerPassword =
|
||||
(document.getElementById('new-owner-password') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
showLoader('Initializing...');
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
showLoader('Reading PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
showLoader('Processing PDF permissions...');
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
// Add password if provided
|
||||
if (currentPassword) {
|
||||
args.push('--password=' + currentPassword);
|
||||
}
|
||||
|
||||
const shouldEncrypt = newUserPassword || newOwnerPassword;
|
||||
|
||||
if (shouldEncrypt) {
|
||||
const finalUserPassword = newUserPassword;
|
||||
const finalOwnerPassword = newOwnerPassword;
|
||||
|
||||
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
|
||||
|
||||
const allowPrinting = (
|
||||
document.getElementById('allow-printing') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowCopying = (
|
||||
document.getElementById('allow-copying') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowModifying = (
|
||||
document.getElementById('allow-modifying') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowAnnotating = (
|
||||
document.getElementById('allow-annotating') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowFillingForms = (
|
||||
document.getElementById('allow-filling-forms') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowDocumentAssembly = (
|
||||
document.getElementById('allow-document-assembly') as HTMLInputElement
|
||||
)?.checked;
|
||||
const allowPageExtraction = (
|
||||
document.getElementById('allow-page-extraction') as HTMLInputElement
|
||||
)?.checked;
|
||||
|
||||
if (finalOwnerPassword) {
|
||||
if (!allowModifying) args.push('--modify=none');
|
||||
if (!allowCopying) args.push('--extract=n');
|
||||
if (!allowPrinting) args.push('--print=none');
|
||||
if (!allowAnnotating) args.push('--annotate=n');
|
||||
if (!allowDocumentAssembly) args.push('--assemble=n');
|
||||
if (!allowFillingForms) args.push('--form=n');
|
||||
if (!allowPageExtraction) args.push('--extract=n');
|
||||
// --modify-other is not directly mapped, apply if modifying is disabled
|
||||
if (!allowModifying) args.push('--modify-other=n');
|
||||
} else if (finalUserPassword) {
|
||||
args.push('--allow-insecure');
|
||||
}
|
||||
} else {
|
||||
args.push('--decrypt');
|
||||
}
|
||||
|
||||
args.push('--', outputPath);
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
|
||||
const errorMsg = qpdfError.message || '';
|
||||
|
||||
if (
|
||||
errorMsg.includes('invalid password') ||
|
||||
errorMsg.includes('incorrect password') ||
|
||||
errorMsg.includes('password')
|
||||
) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
|
||||
if (
|
||||
errorMsg.includes('encrypted') ||
|
||||
errorMsg.includes('password required')
|
||||
) {
|
||||
throw new Error('PASSWORD_REQUIRED');
|
||||
}
|
||||
|
||||
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
|
||||
}
|
||||
|
||||
showLoader('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Processing resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
let successMessage = 'PDF permissions changed successfully!';
|
||||
if (!shouldEncrypt) {
|
||||
successMessage =
|
||||
'PDF decrypted successfully! All encryption and restrictions removed.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage);
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF permission change:', error);
|
||||
hideLoader();
|
||||
|
||||
if (error.message === 'INVALID_PASSWORD') {
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The current password you entered is incorrect. Please try again.'
|
||||
);
|
||||
} else if (error.message === 'PASSWORD_REQUIRED') {
|
||||
showAlert(
|
||||
'Password Required',
|
||||
'This PDF is password-protected. Please enter the current password to proceed.'
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Processing Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password protected.'}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
hexToRgb,
|
||||
readFileAsArrayBuffer,
|
||||
getPDFDocument,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
|
||||
let isRenderingPreview = false;
|
||||
let renderTimeout: any;
|
||||
|
||||
async function updateTextColorPreview() {
|
||||
if (isRenderingPreview) return;
|
||||
isRenderingPreview = true;
|
||||
|
||||
try {
|
||||
const textColorCanvas = document.getElementById('text-color-canvas') as HTMLCanvasElement;
|
||||
if (!textColorCanvas) return;
|
||||
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const page = await pdf.getPage(1); // Preview first page
|
||||
const viewport = page.getViewport({ scale: 0.8 });
|
||||
const context = textColorCanvas.getContext('2d');
|
||||
|
||||
textColorCanvas.width = viewport.width;
|
||||
textColorCanvas.height = viewport.height;
|
||||
|
||||
await page.render({ canvasContext: context, viewport, canvas: textColorCanvas }).promise;
|
||||
const imageData = context.getImageData(
|
||||
0,
|
||||
0,
|
||||
textColorCanvas.width,
|
||||
textColorCanvas.height
|
||||
);
|
||||
const data = imageData.data;
|
||||
const colorHex = (
|
||||
document.getElementById('text-color-input') as HTMLInputElement
|
||||
).value;
|
||||
const { r, g, b } = hexToRgb(colorHex);
|
||||
const darknessThreshold = 120;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (
|
||||
data[i] < darknessThreshold &&
|
||||
data[i + 1] < darknessThreshold &&
|
||||
data[i + 2] < darknessThreshold
|
||||
) {
|
||||
data[i] = r * 255;
|
||||
data[i + 1] = g * 255;
|
||||
data[i + 2] = b * 255;
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
} catch (error) {
|
||||
console.error('Error updating preview:', error);
|
||||
} finally {
|
||||
isRenderingPreview = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupTextColorTool() {
|
||||
const originalCanvas = document.getElementById('original-canvas');
|
||||
const colorInput = document.getElementById('text-color-input');
|
||||
|
||||
if (!originalCanvas || !colorInput) return;
|
||||
|
||||
// Debounce the preview update for performance
|
||||
colorInput.addEventListener('input', () => {
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(updateTextColorPreview, 250);
|
||||
});
|
||||
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const page = await pdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 0.8 });
|
||||
|
||||
(originalCanvas as HTMLCanvasElement).width = viewport.width;
|
||||
(originalCanvas as HTMLCanvasElement).height = viewport.height;
|
||||
|
||||
await page.render({
|
||||
canvasContext: (originalCanvas as HTMLCanvasElement).getContext('2d'),
|
||||
viewport,
|
||||
canvas: originalCanvas as HTMLCanvasElement,
|
||||
}).promise;
|
||||
await updateTextColorPreview();
|
||||
}
|
||||
|
||||
export async function changeTextColor() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
const colorHex = (document.getElementById('text-color-input') as HTMLInputElement).value;
|
||||
const { r, g, b } = hexToRgb(colorHex);
|
||||
const darknessThreshold = 120;
|
||||
|
||||
showLoader('Changing text color...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 }); // High resolution for quality
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
||||
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
if (
|
||||
data[j] < darknessThreshold &&
|
||||
data[j + 1] < darknessThreshold &&
|
||||
data[j + 2] < darknessThreshold
|
||||
) {
|
||||
data[j] = r * 255;
|
||||
data[j + 1] = g * 255;
|
||||
data[j + 2] = b * 255;
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
|
||||
canvas.toBlob((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
|
||||
reader.readAsArrayBuffer(blob!);
|
||||
}, 'image/png')
|
||||
);
|
||||
|
||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes);
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
newPage.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'text-color-changed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not change text color.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
313
src/js/logic/combine-single-page-page.ts
Normal file
313
src/js/logic/combine-single-page-page.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, hexToRgb, getPDFDocument } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
interface CombineState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
}
|
||||
|
||||
const pageState: CombineState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
showLoader('Loading PDF...');
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
resetState();
|
||||
}
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function combineToSinglePage() {
|
||||
if (!pageState.pdfDoc || !pageState.file) {
|
||||
showAlert('Error', 'Please upload a PDF first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const orientation = (document.getElementById('combine-orientation') as HTMLSelectElement).value;
|
||||
const spacing = parseInt((document.getElementById('page-spacing') as HTMLInputElement).value) || 0;
|
||||
const backgroundColorHex = (document.getElementById('background-color') as HTMLInputElement).value;
|
||||
const addSeparator = (document.getElementById('add-separator') as HTMLInputElement).checked;
|
||||
const separatorThickness = parseFloat((document.getElementById('separator-thickness') as HTMLInputElement).value) || 0.5;
|
||||
const separatorColorHex = (document.getElementById('separator-color') as HTMLInputElement).value;
|
||||
|
||||
const backgroundColor = hexToRgb(backgroundColorHex);
|
||||
const separatorColor = hexToRgb(separatorColorHex);
|
||||
|
||||
showLoader('Combining pages...');
|
||||
|
||||
try {
|
||||
const sourceDoc = pageState.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
const pdfBytes = await sourceDoc.save();
|
||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
let maxWidth = 0;
|
||||
let maxHeight = 0;
|
||||
let totalWidth = 0;
|
||||
let totalHeight = 0;
|
||||
|
||||
sourcePages.forEach(function (page) {
|
||||
const { width, height } = page.getSize();
|
||||
if (width > maxWidth) maxWidth = width;
|
||||
if (height > maxHeight) maxHeight = height;
|
||||
totalWidth += width;
|
||||
totalHeight += height;
|
||||
});
|
||||
|
||||
let finalWidth: number, finalHeight: number;
|
||||
if (orientation === 'horizontal') {
|
||||
finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing;
|
||||
finalHeight = maxHeight;
|
||||
} else {
|
||||
finalWidth = maxWidth;
|
||||
finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing;
|
||||
}
|
||||
|
||||
const newPage = newDoc.addPage([finalWidth, finalHeight]);
|
||||
|
||||
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||
});
|
||||
}
|
||||
|
||||
let currentX = 0;
|
||||
let currentY = finalHeight;
|
||||
|
||||
for (let i = 0; i < sourcePages.length; i++) {
|
||||
showLoader(`Processing page ${i + 1} of ${sourcePages.length}...`);
|
||||
const sourcePage = sourcePages[i];
|
||||
const { width, height } = sourcePage.getSize();
|
||||
|
||||
try {
|
||||
const page = await pdfjsDoc.getPage(i + 1);
|
||||
const scale = 2.0;
|
||||
const viewport = page.getViewport({ scale });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d')!;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport,
|
||||
canvas
|
||||
}).promise;
|
||||
|
||||
const pngDataUrl = canvas.toDataURL('image/png');
|
||||
const pngImage = await newDoc.embedPng(pngDataUrl);
|
||||
|
||||
if (orientation === 'horizontal') {
|
||||
const y = (finalHeight - height) / 2;
|
||||
newPage.drawImage(pngImage, { x: currentX, y, width, height });
|
||||
} else {
|
||||
currentY -= height;
|
||||
const x = (finalWidth - width) / 2;
|
||||
newPage.drawImage(pngImage, { x, y: currentY, width, height });
|
||||
}
|
||||
} catch (renderError) {
|
||||
console.warn(`Failed to render page ${i + 1}:`, renderError);
|
||||
}
|
||||
|
||||
if (addSeparator && i < sourcePages.length - 1) {
|
||||
if (orientation === 'horizontal') {
|
||||
const lineX = currentX + width + spacing / 2;
|
||||
newPage.drawLine({
|
||||
start: { x: lineX, y: 0 },
|
||||
end: { x: lineX, y: finalHeight },
|
||||
thickness: separatorThickness,
|
||||
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
|
||||
});
|
||||
currentX += width + spacing;
|
||||
} else {
|
||||
const lineY = currentY - spacing / 2;
|
||||
newPage.drawLine({
|
||||
start: { x: 0, y: lineY },
|
||||
end: { x: finalWidth, y: lineY },
|
||||
thickness: separatorThickness,
|
||||
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
|
||||
});
|
||||
currentY -= spacing;
|
||||
}
|
||||
} else {
|
||||
if (orientation === 'horizontal') {
|
||||
currentX += width + spacing;
|
||||
} else {
|
||||
currentY -= spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`${originalName}_combined.pdf`
|
||||
);
|
||||
|
||||
showAlert('Success', 'Pages combined successfully!', 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while combining pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const addSeparatorCheckbox = document.getElementById('add-separator');
|
||||
const separatorOptions = document.getElementById('separator-options');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (addSeparatorCheckbox && separatorOptions) {
|
||||
addSeparatorCheckbox.addEventListener('change', function () {
|
||||
if ((addSeparatorCheckbox as HTMLInputElement).checked) {
|
||||
separatorOptions.classList.remove('hidden');
|
||||
} else {
|
||||
separatorOptions.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', combineToSinglePage);
|
||||
}
|
||||
});
|
||||
@@ -1,219 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
document.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.id === 'add-separator') {
|
||||
const separatorOptions = document.getElementById('separator-options');
|
||||
if (separatorOptions) {
|
||||
const checkbox = target as HTMLInputElement;
|
||||
if (checkbox.checked) {
|
||||
separatorOptions.classList.remove('hidden');
|
||||
} else {
|
||||
separatorOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export async function combineToSinglePage() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const orientation = document.getElementById('combine-orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const backgroundColorHex = document.getElementById('background-color').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const addSeparator = document.getElementById('add-separator').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const separatorThickness = parseFloat(document.getElementById('separator-thickness').value) || 0.5;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const separatorColorHex = document.getElementById('separator-color').value;
|
||||
|
||||
const backgroundColor = hexToRgb(backgroundColorHex);
|
||||
const separatorColor = hexToRgb(separatorColorHex);
|
||||
|
||||
showLoader('Combining pages...');
|
||||
try {
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
const pdfBytes = await sourceDoc.save();
|
||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
let maxWidth = 0;
|
||||
let maxHeight = 0;
|
||||
let totalWidth = 0;
|
||||
let totalHeight = 0;
|
||||
|
||||
sourcePages.forEach((page: any) => {
|
||||
const { width, height } = page.getSize();
|
||||
if (width > maxWidth) maxWidth = width;
|
||||
if (height > maxHeight) maxHeight = height;
|
||||
totalWidth += width;
|
||||
totalHeight += height;
|
||||
});
|
||||
|
||||
let finalWidth, finalHeight;
|
||||
if (orientation === 'horizontal') {
|
||||
finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing;
|
||||
finalHeight = maxHeight;
|
||||
} else {
|
||||
finalWidth = maxWidth;
|
||||
finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing;
|
||||
}
|
||||
|
||||
const newPage = newDoc.addPage([finalWidth, finalHeight]);
|
||||
|
||||
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||
});
|
||||
}
|
||||
|
||||
let currentX = 0;
|
||||
let currentY = finalHeight;
|
||||
|
||||
for (let i = 0; i < sourcePages.length; i++) {
|
||||
const sourcePage = sourcePages[i];
|
||||
const { width, height } = sourcePage.getSize();
|
||||
|
||||
try {
|
||||
const page = await pdfjsDoc.getPage(i + 1);
|
||||
const scale = 2.0;
|
||||
const viewport = page.getViewport({ scale });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d')!;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport,
|
||||
canvas
|
||||
}).promise;
|
||||
|
||||
const pngDataUrl = canvas.toDataURL('image/png');
|
||||
const pngImage = await newDoc.embedPng(pngDataUrl);
|
||||
|
||||
if (orientation === 'horizontal') {
|
||||
const y = (finalHeight - height) / 2;
|
||||
newPage.drawImage(pngImage, { x: currentX, y, width, height });
|
||||
} else {
|
||||
// Vertical layout: stack top to bottom
|
||||
currentY -= height;
|
||||
const x = (finalWidth - width) / 2; // Center horizontally
|
||||
newPage.drawImage(pngImage, { x, y: currentY, width, height });
|
||||
}
|
||||
} catch (renderError) {
|
||||
console.warn(`Failed to render page ${i + 1} with PDF.js, trying fallback method:`, renderError);
|
||||
|
||||
// Fallback: try to copy and embed the page directly
|
||||
try {
|
||||
const [copiedPage] = await newDoc.copyPages(sourceDoc, [i]);
|
||||
|
||||
if (orientation === 'horizontal') {
|
||||
const y = (finalHeight - height) / 2;
|
||||
const embeddedPage = await newDoc.embedPage(copiedPage);
|
||||
newPage.drawPage(embeddedPage, { x: currentX, y, width, height });
|
||||
} else {
|
||||
currentY -= height;
|
||||
const x = (finalWidth - width) / 2;
|
||||
const embeddedPage = await newDoc.embedPage(copiedPage);
|
||||
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
||||
}
|
||||
} catch (embedError) {
|
||||
console.error(`Failed to process page ${i + 1}:`, embedError);
|
||||
|
||||
if (orientation === 'horizontal') {
|
||||
const y = (finalHeight - height) / 2;
|
||||
newPage.drawRectangle({
|
||||
x: currentX,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
borderColor: rgb(0.8, 0, 0),
|
||||
borderWidth: 2,
|
||||
});
|
||||
|
||||
newPage.drawText(`Page ${i + 1} could not be rendered`, {
|
||||
x: currentX + 10,
|
||||
y: y + height / 2,
|
||||
size: 12,
|
||||
color: rgb(0.8, 0, 0),
|
||||
});
|
||||
} else {
|
||||
currentY -= height;
|
||||
const x = (finalWidth - width) / 2;
|
||||
newPage.drawRectangle({
|
||||
x,
|
||||
y: currentY,
|
||||
width,
|
||||
height,
|
||||
borderColor: rgb(0.8, 0, 0),
|
||||
borderWidth: 2,
|
||||
});
|
||||
|
||||
newPage.drawText(`Page ${i + 1} could not be rendered`, {
|
||||
x: x + 10,
|
||||
y: currentY + height / 2,
|
||||
size: 12,
|
||||
color: rgb(0.8, 0, 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw separator line
|
||||
if (addSeparator && i < sourcePages.length - 1) {
|
||||
if (orientation === 'horizontal') {
|
||||
const lineX = currentX + width + spacing / 2;
|
||||
newPage.drawLine({
|
||||
start: { x: lineX, y: 0 },
|
||||
end: { x: lineX, y: finalHeight },
|
||||
thickness: separatorThickness,
|
||||
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
|
||||
});
|
||||
currentX += width + spacing;
|
||||
} else {
|
||||
const lineY = currentY - spacing / 2;
|
||||
newPage.drawLine({
|
||||
start: { x: 0, y: lineY },
|
||||
end: { x: finalWidth, y: lineY },
|
||||
thickness: separatorThickness,
|
||||
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
|
||||
});
|
||||
currentY -= spacing;
|
||||
}
|
||||
} else {
|
||||
if (orientation === 'horizontal') {
|
||||
currentX += width + spacing;
|
||||
} else {
|
||||
currentY -= spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'combined-page.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while combining pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
306
src/js/logic/compare-pdfs-page.ts
Normal file
306
src/js/logic/compare-pdfs-page.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { getPDFDocument } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
interface CompareState {
|
||||
pdfDoc1: pdfjsLib.PDFDocumentProxy | null;
|
||||
pdfDoc2: pdfjsLib.PDFDocumentProxy | null;
|
||||
currentPage: number;
|
||||
viewMode: 'overlay' | 'side-by-side';
|
||||
isSyncScroll: boolean;
|
||||
}
|
||||
|
||||
const pageState: CompareState = {
|
||||
pdfDoc1: null,
|
||||
pdfDoc2: null,
|
||||
currentPage: 1,
|
||||
viewMode: 'overlay',
|
||||
isSyncScroll: true,
|
||||
};
|
||||
|
||||
async function renderPage(
|
||||
pdfDoc: pdfjsLib.PDFDocumentProxy,
|
||||
pageNum: number,
|
||||
canvas: HTMLCanvasElement,
|
||||
container: HTMLElement
|
||||
) {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
|
||||
const containerWidth = container.clientWidth - 2;
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
const scale = containerWidth / viewport.width;
|
||||
const scaledViewport = page.getViewport({ scale: scale });
|
||||
|
||||
canvas.width = scaledViewport.width;
|
||||
canvas.height = scaledViewport.height;
|
||||
|
||||
await page.render({
|
||||
canvasContext: canvas.getContext('2d')!,
|
||||
viewport: scaledViewport,
|
||||
canvas
|
||||
}).promise;
|
||||
}
|
||||
|
||||
async function renderBothPages() {
|
||||
if (!pageState.pdfDoc1 || !pageState.pdfDoc2) return;
|
||||
|
||||
showLoader(`Loading page ${pageState.currentPage}...`);
|
||||
|
||||
const canvas1 = document.getElementById('canvas-compare-1') as HTMLCanvasElement;
|
||||
const canvas2 = document.getElementById('canvas-compare-2') as HTMLCanvasElement;
|
||||
const panel1 = document.getElementById('panel-1') as HTMLElement;
|
||||
const panel2 = document.getElementById('panel-2') as HTMLElement;
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper') as HTMLElement;
|
||||
|
||||
const container1 = pageState.viewMode === 'overlay' ? wrapper : panel1;
|
||||
const container2 = pageState.viewMode === 'overlay' ? wrapper : panel2;
|
||||
|
||||
await Promise.all([
|
||||
renderPage(
|
||||
pageState.pdfDoc1,
|
||||
Math.min(pageState.currentPage, pageState.pdfDoc1.numPages),
|
||||
canvas1,
|
||||
container1
|
||||
),
|
||||
renderPage(
|
||||
pageState.pdfDoc2,
|
||||
Math.min(pageState.currentPage, pageState.pdfDoc2.numPages),
|
||||
canvas2,
|
||||
container2
|
||||
),
|
||||
]);
|
||||
|
||||
updateNavControls();
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
function updateNavControls() {
|
||||
const maxPages = Math.max(
|
||||
pageState.pdfDoc1?.numPages || 0,
|
||||
pageState.pdfDoc2?.numPages || 0
|
||||
);
|
||||
const currentDisplay = document.getElementById('current-page-display-compare');
|
||||
const totalDisplay = document.getElementById('total-pages-display-compare');
|
||||
const prevBtn = document.getElementById('prev-page-compare') as HTMLButtonElement;
|
||||
const nextBtn = document.getElementById('next-page-compare') as HTMLButtonElement;
|
||||
|
||||
if (currentDisplay) currentDisplay.textContent = pageState.currentPage.toString();
|
||||
if (totalDisplay) totalDisplay.textContent = maxPages.toString();
|
||||
if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1;
|
||||
if (nextBtn) nextBtn.disabled = pageState.currentPage >= maxPages;
|
||||
}
|
||||
|
||||
function setViewMode(mode: 'overlay' | 'side-by-side') {
|
||||
pageState.viewMode = mode;
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||
const overlayControls = document.getElementById('overlay-controls');
|
||||
const sideControls = document.getElementById('side-by-side-controls');
|
||||
const btnOverlay = document.getElementById('view-mode-overlay');
|
||||
const btnSide = document.getElementById('view-mode-side');
|
||||
const canvas2 = document.getElementById('canvas-compare-2') as HTMLCanvasElement;
|
||||
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
|
||||
|
||||
if (mode === 'overlay') {
|
||||
if (wrapper) wrapper.className = 'compare-viewer-wrapper overlay-mode bg-gray-900 rounded-lg border border-gray-700 min-h-[400px] relative';
|
||||
if (overlayControls) overlayControls.classList.remove('hidden');
|
||||
if (sideControls) sideControls.classList.add('hidden');
|
||||
if (btnOverlay) {
|
||||
btnOverlay.classList.add('bg-indigo-600');
|
||||
btnOverlay.classList.remove('bg-gray-700');
|
||||
}
|
||||
if (btnSide) {
|
||||
btnSide.classList.remove('bg-indigo-600');
|
||||
btnSide.classList.add('bg-gray-700');
|
||||
}
|
||||
if (canvas2 && opacitySlider) canvas2.style.opacity = opacitySlider.value;
|
||||
} else {
|
||||
if (wrapper) wrapper.className = 'compare-viewer-wrapper side-by-side-mode bg-gray-900 rounded-lg border border-gray-700 min-h-[400px]';
|
||||
if (overlayControls) overlayControls.classList.add('hidden');
|
||||
if (sideControls) sideControls.classList.remove('hidden');
|
||||
if (btnOverlay) {
|
||||
btnOverlay.classList.remove('bg-indigo-600');
|
||||
btnOverlay.classList.add('bg-gray-700');
|
||||
}
|
||||
if (btnSide) {
|
||||
btnSide.classList.add('bg-indigo-600');
|
||||
btnSide.classList.remove('bg-gray-700');
|
||||
}
|
||||
if (canvas2) canvas2.style.opacity = '1';
|
||||
}
|
||||
renderBothPages();
|
||||
}
|
||||
|
||||
async function handleFileInput(inputId: string, docKey: 'pdfDoc1' | 'pdfDoc2', displayId: string) {
|
||||
const fileInput = document.getElementById(inputId) as HTMLInputElement;
|
||||
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
|
||||
|
||||
async function handleFile(file: File) {
|
||||
if (!file || file.type !== 'application/pdf') {
|
||||
showAlert('Invalid File', 'Please select a valid PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
const displayDiv = document.getElementById(displayId);
|
||||
if (displayDiv) {
|
||||
displayDiv.innerHTML = '';
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.setAttribute('data-lucide', 'check-circle');
|
||||
icon.className = 'w-10 h-10 mb-3 text-green-500';
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-sm text-gray-300 truncate';
|
||||
p.textContent = file.name;
|
||||
|
||||
displayDiv.append(icon, p);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
try {
|
||||
showLoader(`Loading ${file.name}...`);
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState[docKey] = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
|
||||
if (pageState.pdfDoc1 && pageState.pdfDoc2) {
|
||||
const compareViewer = document.getElementById('compare-viewer');
|
||||
if (compareViewer) compareViewer.classList.remove('hidden');
|
||||
pageState.currentPage = 1;
|
||||
await renderBothPages();
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('Error', 'Could not load PDF. It may be corrupt or password-protected.');
|
||||
console.error(e);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files && files[0]) handleFile(files[0]);
|
||||
});
|
||||
}
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files[0]) handleFile(files[0]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
handleFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
|
||||
handleFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
|
||||
|
||||
const prevBtn = document.getElementById('prev-page-compare');
|
||||
const nextBtn = document.getElementById('next-page-compare');
|
||||
|
||||
if (prevBtn) {
|
||||
prevBtn.addEventListener('click', function () {
|
||||
if (pageState.currentPage > 1) {
|
||||
pageState.currentPage--;
|
||||
renderBothPages();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (nextBtn) {
|
||||
nextBtn.addEventListener('click', function () {
|
||||
const maxPages = Math.max(
|
||||
pageState.pdfDoc1?.numPages || 0,
|
||||
pageState.pdfDoc2?.numPages || 0
|
||||
);
|
||||
if (pageState.currentPage < maxPages) {
|
||||
pageState.currentPage++;
|
||||
renderBothPages();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const btnOverlay = document.getElementById('view-mode-overlay');
|
||||
const btnSide = document.getElementById('view-mode-side');
|
||||
|
||||
if (btnOverlay) {
|
||||
btnOverlay.addEventListener('click', function () {
|
||||
setViewMode('overlay');
|
||||
});
|
||||
}
|
||||
|
||||
if (btnSide) {
|
||||
btnSide.addEventListener('click', function () {
|
||||
setViewMode('side-by-side');
|
||||
});
|
||||
}
|
||||
|
||||
const flickerBtn = document.getElementById('flicker-btn');
|
||||
const canvas2 = document.getElementById('canvas-compare-2') as HTMLCanvasElement;
|
||||
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
|
||||
|
||||
// Track flicker state
|
||||
let flickerVisible = true;
|
||||
|
||||
if (flickerBtn && canvas2) {
|
||||
flickerBtn.addEventListener('click', function () {
|
||||
flickerVisible = !flickerVisible;
|
||||
canvas2.style.transition = 'opacity 150ms ease-in-out';
|
||||
canvas2.style.opacity = flickerVisible ? (opacitySlider?.value || '0.5') : '0';
|
||||
});
|
||||
}
|
||||
|
||||
if (opacitySlider && canvas2) {
|
||||
opacitySlider.addEventListener('input', function () {
|
||||
flickerVisible = true; // Reset flicker state when slider changes
|
||||
canvas2.style.transition = '';
|
||||
canvas2.style.opacity = opacitySlider.value;
|
||||
});
|
||||
}
|
||||
|
||||
const panel1 = document.getElementById('panel-1');
|
||||
const panel2 = document.getElementById('panel-2');
|
||||
const syncToggle = document.getElementById('sync-scroll-toggle') as HTMLInputElement;
|
||||
|
||||
if (syncToggle) {
|
||||
syncToggle.addEventListener('change', function () {
|
||||
pageState.isSyncScroll = syncToggle.checked;
|
||||
});
|
||||
}
|
||||
|
||||
let scrollingPanel: HTMLElement | null = null;
|
||||
|
||||
if (panel1 && panel2) {
|
||||
panel1.addEventListener('scroll', function () {
|
||||
if (pageState.isSyncScroll && scrollingPanel !== panel2) {
|
||||
scrollingPanel = panel1;
|
||||
panel2.scrollTop = panel1.scrollTop;
|
||||
setTimeout(function () { scrollingPanel = null; }, 100);
|
||||
}
|
||||
});
|
||||
|
||||
panel2.addEventListener('scroll', function () {
|
||||
if (pageState.isSyncScroll && scrollingPanel !== panel1) {
|
||||
scrollingPanel = panel2;
|
||||
panel1.scrollTop = panel2.scrollTop;
|
||||
setTimeout(function () { scrollingPanel = null; }, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
});
|
||||
@@ -1,248 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
|
||||
const state = {
|
||||
pdfDoc1: null,
|
||||
pdfDoc2: null,
|
||||
currentPage: 1,
|
||||
viewMode: 'overlay',
|
||||
isSyncScroll: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a PDF page to fit the width of its container.
|
||||
* @param {PDFDocumentProxy} pdfDoc - The loaded PDF document from pdf.js.
|
||||
* @param {number} pageNum - The page number to render.
|
||||
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
|
||||
* @param {HTMLElement} container - The container to fit the canvas into.
|
||||
*/
|
||||
async function renderPage(
|
||||
pdfDoc: any,
|
||||
pageNum: any,
|
||||
canvas: any,
|
||||
container: any
|
||||
) {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
|
||||
// Calculate scale to fit the container width.
|
||||
const containerWidth = container.clientWidth - 2; // Subtract border width
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
const scale = containerWidth / viewport.width;
|
||||
const scaledViewport = page.getViewport({ scale: scale });
|
||||
|
||||
canvas.width = scaledViewport.width;
|
||||
canvas.height = scaledViewport.height;
|
||||
|
||||
await page.render({
|
||||
canvasContext: canvas.getContext('2d'),
|
||||
viewport: scaledViewport,
|
||||
}).promise;
|
||||
}
|
||||
|
||||
async function renderBothPages() {
|
||||
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
||||
|
||||
showLoader(`Loading page ${state.currentPage}...`);
|
||||
|
||||
const canvas1 = document.getElementById('canvas-compare-1');
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
const panel1 = document.getElementById('panel-1');
|
||||
const panel2 = document.getElementById('panel-2');
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||
|
||||
// Determine the correct container based on the view mode
|
||||
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
|
||||
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
|
||||
|
||||
await Promise.all([
|
||||
renderPage(
|
||||
state.pdfDoc1,
|
||||
Math.min(state.currentPage, state.pdfDoc1.numPages),
|
||||
canvas1,
|
||||
container1
|
||||
),
|
||||
renderPage(
|
||||
state.pdfDoc2,
|
||||
Math.min(state.currentPage, state.pdfDoc2.numPages),
|
||||
canvas2,
|
||||
container2
|
||||
),
|
||||
]);
|
||||
|
||||
updateNavControls();
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
function updateNavControls() {
|
||||
const maxPages = Math.max(
|
||||
state.pdfDoc1?.numPages || 0,
|
||||
state.pdfDoc2?.numPages || 0
|
||||
);
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
document.getElementById('current-page-display-compare').textContent =
|
||||
state.currentPage;
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
document.getElementById('total-pages-display-compare').textContent = maxPages;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page-compare').disabled =
|
||||
state.currentPage <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page-compare').disabled =
|
||||
state.currentPage >= maxPages;
|
||||
}
|
||||
|
||||
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
||||
const fileInput = document.getElementById(inputId);
|
||||
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
|
||||
|
||||
const handleFile = async (file: any) => {
|
||||
if (!file || file.type !== 'application/pdf')
|
||||
return showAlert('Invalid File', 'Please select a valid PDF file.');
|
||||
|
||||
const displayDiv = document.getElementById(displayId);
|
||||
displayDiv.textContent = '';
|
||||
|
||||
// 2. Create the icon element
|
||||
const icon = document.createElement('i');
|
||||
icon.setAttribute('data-lucide', 'check-circle');
|
||||
icon.className = 'w-10 h-10 mb-3 text-green-500';
|
||||
|
||||
// 3. Create the paragraph element for the file name
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-sm text-gray-300 truncate';
|
||||
|
||||
// 4. Set the file name safely using textContent
|
||||
p.textContent = file.name;
|
||||
|
||||
// 5. Append the safe elements to the container
|
||||
displayDiv.append(icon, p);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
showLoader(`Loading ${file.name}...`);
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
state[docKey] = await getPDFDocument(pdfBytes).promise;
|
||||
|
||||
if (state.pdfDoc1 && state.pdfDoc2) {
|
||||
document.getElementById('compare-viewer').classList.remove('hidden');
|
||||
state.currentPage = 1;
|
||||
await renderBothPages();
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert(
|
||||
'Error',
|
||||
'Could not load PDF. It may be corrupt or password-protected.'
|
||||
);
|
||||
console.error(e);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
|
||||
dropZone.addEventListener('dragover', (e) => e.preventDefault());
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
handleFile(e.dataTransfer.files[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the UI between Overlay and Side-by-Side views.
|
||||
* @param {'overlay' | 'side-by-side'} mode
|
||||
*/
|
||||
function setViewMode(mode: any) {
|
||||
state.viewMode = mode;
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||
const overlayControls = document.getElementById('overlay-controls');
|
||||
const sideControls = document.getElementById('side-by-side-controls');
|
||||
const btnOverlay = document.getElementById('view-mode-overlay');
|
||||
const btnSide = document.getElementById('view-mode-side');
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
const opacitySlider = document.getElementById('opacity-slider');
|
||||
|
||||
if (mode === 'overlay') {
|
||||
wrapper.className = 'compare-viewer-wrapper overlay-mode';
|
||||
overlayControls.classList.remove('hidden');
|
||||
sideControls.classList.add('hidden');
|
||||
btnOverlay.classList.add('bg-indigo-600');
|
||||
btnSide.classList.remove('bg-indigo-600');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
canvas2.style.opacity = opacitySlider.value;
|
||||
} else {
|
||||
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
|
||||
overlayControls.classList.add('hidden');
|
||||
sideControls.classList.remove('hidden');
|
||||
btnOverlay.classList.remove('bg-indigo-600');
|
||||
btnSide.classList.add('bg-indigo-600');
|
||||
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
|
||||
canvas2.style.opacity = '1';
|
||||
}
|
||||
renderBothPages();
|
||||
}
|
||||
|
||||
export function setupCompareTool() {
|
||||
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
|
||||
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
|
||||
|
||||
document.getElementById('prev-page-compare').addEventListener('click', () => {
|
||||
if (state.currentPage > 1) {
|
||||
state.currentPage--;
|
||||
renderBothPages();
|
||||
}
|
||||
});
|
||||
document.getElementById('next-page-compare').addEventListener('click', () => {
|
||||
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
||||
if (state.currentPage < maxPages) {
|
||||
state.currentPage++;
|
||||
renderBothPages();
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById('view-mode-overlay')
|
||||
.addEventListener('click', () => setViewMode('overlay'));
|
||||
document
|
||||
.getElementById('view-mode-side')
|
||||
.addEventListener('click', () => setViewMode('side-by-side'));
|
||||
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
document.getElementById('flicker-btn').addEventListener('click', () => {
|
||||
canvas2.style.transition = 'opacity 150ms ease-in-out';
|
||||
canvas2.style.opacity = canvas2.style.opacity === '0' ? '1' : '0';
|
||||
});
|
||||
document.getElementById('opacity-slider').addEventListener('input', (e) => {
|
||||
canvas2.style.transition = '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
canvas2.style.opacity = e.target.value;
|
||||
});
|
||||
|
||||
const panel1 = document.getElementById('panel-1');
|
||||
const panel2 = document.getElementById('panel-2');
|
||||
const syncToggle = document.getElementById('sync-scroll-toggle');
|
||||
(syncToggle as HTMLInputElement).addEventListener('change', () => {
|
||||
state.isSyncScroll = (syncToggle as HTMLInputElement).checked;
|
||||
});
|
||||
|
||||
let scrollingPanel: any = null;
|
||||
panel1.addEventListener('scroll', () => {
|
||||
if (state.isSyncScroll && scrollingPanel !== panel2) {
|
||||
scrollingPanel = panel1;
|
||||
panel2.scrollTop = panel1.scrollTop;
|
||||
setTimeout(() => (scrollingPanel = null), 100);
|
||||
}
|
||||
});
|
||||
panel2.addEventListener('scroll', () => {
|
||||
if (state.isSyncScroll && scrollingPanel !== panel1) {
|
||||
scrollingPanel = panel2;
|
||||
panel1.scrollTop = panel2.scrollTop;
|
||||
setTimeout(() => (scrollingPanel = null), 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -247,24 +247,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSizeContainer = document.createElement('div');
|
||||
nameSizeContainer.className = 'flex items-center gap-2';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
|
||||
|
||||
nameSizeContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
const pagesSpan = document.createElement('span');
|
||||
pagesSpan.className = 'text-xs text-gray-500 mt-0.5';
|
||||
pagesSpan.textContent = 'Loading pages...';
|
||||
|
||||
infoContainer.append(nameSizeContainer, pagesSpan);
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
@@ -280,10 +271,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
pagesSpan.textContent = `${pdfDoc.numPages} Pages`;
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdfDoc.numPages} pages`;
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
pagesSpan.textContent = 'Could not load page count';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Could not load page count`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,7 +503,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
@@ -539,8 +529,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
dropZone.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
381
src/js/logic/crop-pdf-page.ts
Normal file
381
src/js/logic/crop-pdf-page.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import Cropper from 'cropperjs';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
interface CropperState {
|
||||
pdfDoc: any;
|
||||
currentPageNum: number;
|
||||
cropper: any;
|
||||
originalPdfBytes: ArrayBuffer | null;
|
||||
pageCrops: Record<number, any>;
|
||||
file: File | null;
|
||||
}
|
||||
|
||||
const cropperState: CropperState = {
|
||||
pdfDoc: null,
|
||||
currentPageNum: 1,
|
||||
cropper: null,
|
||||
originalPdfBytes: null,
|
||||
pageCrops: {},
|
||||
file: null,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
if (fileInput) fileInput.addEventListener('change', handleFileUpload);
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const droppedFiles = e.dataTransfer?.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
|
||||
});
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
|
||||
document.getElementById('prev-page')?.addEventListener('click', () => changePage(-1));
|
||||
document.getElementById('next-page')?.addEventListener('click', () => changePage(1));
|
||||
document.getElementById('crop-button')?.addEventListener('click', performCrop);
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) handleFile(input.files[0]);
|
||||
}
|
||||
|
||||
async function handleFile(file: File) {
|
||||
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
|
||||
showAlert('Invalid File', 'Please select a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PDF...');
|
||||
cropperState.file = file;
|
||||
cropperState.pageCrops = {};
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
cropperState.originalPdfBytes = arrayBuffer as ArrayBuffer;
|
||||
cropperState.pdfDoc = await getPDFDocument({ data: (arrayBuffer as ArrayBuffer).slice(0) }).promise;
|
||||
cropperState.currentPageNum = 1;
|
||||
|
||||
updateFileDisplay();
|
||||
await displayPageAsImage(cropperState.currentPageNum);
|
||||
hideLoader();
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (!fileDisplayArea || !cropperState.file) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = cropperState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(cropperState.file.size)} • ${cropperState.pdfDoc?.numPages || 0} pages`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => resetState();
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function saveCurrentCrop() {
|
||||
if (cropperState.cropper) {
|
||||
const currentCrop = cropperState.cropper.getData(true);
|
||||
const imageData = cropperState.cropper.getImageData();
|
||||
const cropPercentages = {
|
||||
x: currentCrop.x / imageData.naturalWidth,
|
||||
y: currentCrop.y / imageData.naturalHeight,
|
||||
width: currentCrop.width / imageData.naturalWidth,
|
||||
height: currentCrop.height / imageData.naturalHeight,
|
||||
};
|
||||
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
|
||||
}
|
||||
}
|
||||
|
||||
async function displayPageAsImage(num: number) {
|
||||
showLoader(`Rendering Page ${num}...`);
|
||||
|
||||
try {
|
||||
const page = await cropperState.pdfDoc.getPage(num);
|
||||
const viewport = page.getViewport({ scale: 2.5 });
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||
|
||||
if (cropperState.cropper) cropperState.cropper.destroy();
|
||||
|
||||
const cropperEditor = document.getElementById('cropper-editor');
|
||||
if (cropperEditor) cropperEditor.classList.remove('hidden');
|
||||
|
||||
const container = document.getElementById('cropper-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
const image = document.createElement('img');
|
||||
image.src = tempCanvas.toDataURL('image/png');
|
||||
container.appendChild(image);
|
||||
|
||||
image.onload = () => {
|
||||
cropperState.cropper = new Cropper(image, {
|
||||
viewMode: 1,
|
||||
background: false,
|
||||
autoCropArea: 0.8,
|
||||
responsive: true,
|
||||
rotatable: false,
|
||||
zoomable: false,
|
||||
});
|
||||
|
||||
const savedCrop = cropperState.pageCrops[num];
|
||||
if (savedCrop) {
|
||||
const imageData = cropperState.cropper.getImageData();
|
||||
cropperState.cropper.setData({
|
||||
x: savedCrop.x * imageData.naturalWidth,
|
||||
y: savedCrop.y * imageData.naturalHeight,
|
||||
width: savedCrop.width * imageData.naturalWidth,
|
||||
height: savedCrop.height * imageData.naturalHeight,
|
||||
});
|
||||
}
|
||||
|
||||
updatePageInfo();
|
||||
enableControls();
|
||||
hideLoader();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error rendering page:', error);
|
||||
showAlert('Error', 'Failed to render page.');
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function changePage(offset: number) {
|
||||
saveCurrentCrop();
|
||||
const newPageNum = cropperState.currentPageNum + offset;
|
||||
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
|
||||
cropperState.currentPageNum = newPageNum;
|
||||
await displayPageAsImage(cropperState.currentPageNum);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePageInfo() {
|
||||
const pageInfo = document.getElementById('page-info');
|
||||
if (pageInfo) pageInfo.textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
||||
}
|
||||
|
||||
function enableControls() {
|
||||
const prevBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
||||
const nextBtn = document.getElementById('next-page') as HTMLButtonElement;
|
||||
const cropBtn = document.getElementById('crop-button') as HTMLButtonElement;
|
||||
|
||||
if (prevBtn) prevBtn.disabled = cropperState.currentPageNum <= 1;
|
||||
if (nextBtn) nextBtn.disabled = cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
|
||||
if (cropBtn) cropBtn.disabled = false;
|
||||
}
|
||||
|
||||
async function performCrop() {
|
||||
saveCurrentCrop();
|
||||
|
||||
const isDestructive = (document.getElementById('destructive-crop-toggle') as HTMLInputElement)?.checked;
|
||||
const isApplyToAll = (document.getElementById('apply-to-all-toggle') as HTMLInputElement)?.checked;
|
||||
|
||||
let finalCropData: Record<number, any> = {};
|
||||
|
||||
if (isApplyToAll) {
|
||||
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
|
||||
if (!currentCrop) {
|
||||
showAlert('No Crop Area', 'Please select an area to crop first.');
|
||||
return;
|
||||
}
|
||||
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
|
||||
finalCropData[i] = currentCrop;
|
||||
}
|
||||
} else {
|
||||
finalCropData = { ...cropperState.pageCrops };
|
||||
}
|
||||
|
||||
if (Object.keys(finalCropData).length === 0) {
|
||||
showAlert('No Crop Area', 'Please select an area on at least one page to crop.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Applying crop...');
|
||||
|
||||
try {
|
||||
let finalPdfBytes;
|
||||
if (isDestructive) {
|
||||
finalPdfBytes = await performFlatteningCrop(finalCropData);
|
||||
} else {
|
||||
finalPdfBytes = await performMetadataCrop(finalCropData);
|
||||
}
|
||||
|
||||
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
|
||||
downloadFile(new Blob([finalPdfBytes], { type: 'application/pdf' }), fileName);
|
||||
showAlert('Success', 'Crop complete! Your download has started.', 'success', () => resetState());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred during cropping.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function performMetadataCrop(cropData: Record<number, any>): Promise<Uint8Array> {
|
||||
const pdfToModify = await PDFLibDocument.load(cropperState.originalPdfBytes!, { ignoreEncryption: true, throwOnInvalidObject: false });
|
||||
|
||||
for (const pageNum in cropData) {
|
||||
const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
|
||||
const viewport = pdfJsPage.getViewport({ scale: 1 });
|
||||
const crop = cropData[pageNum];
|
||||
|
||||
const cropX = viewport.width * crop.x;
|
||||
const cropY = viewport.height * crop.y;
|
||||
const cropW = viewport.width * crop.width;
|
||||
const cropH = viewport.height * crop.height;
|
||||
|
||||
const visualCorners = [
|
||||
{ x: cropX, y: cropY },
|
||||
{ x: cropX + cropW, y: cropY },
|
||||
{ x: cropX + cropW, y: cropY + cropH },
|
||||
{ x: cropX, y: cropY + cropH },
|
||||
];
|
||||
|
||||
const pdfCorners = visualCorners.map(p => viewport.convertToPdfPoint(p.x, p.y));
|
||||
const pdfXs = pdfCorners.map(p => p[0]);
|
||||
const pdfYs = pdfCorners.map(p => p[1]);
|
||||
|
||||
const minX = Math.min(...pdfXs);
|
||||
const maxX = Math.max(...pdfXs);
|
||||
const minY = Math.min(...pdfYs);
|
||||
const maxY = Math.max(...pdfYs);
|
||||
|
||||
const page = pdfToModify.getPages()[Number(pageNum) - 1];
|
||||
page.setCropBox(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
return pdfToModify.save();
|
||||
}
|
||||
|
||||
async function performFlatteningCrop(cropData: Record<number, any>): Promise<Uint8Array> {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes!, { ignoreEncryption: true, throwOnInvalidObject: false });
|
||||
const totalPages = cropperState.pdfDoc.numPages;
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageNum = i + 1;
|
||||
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
|
||||
|
||||
if (cropData[pageNum]) {
|
||||
const page = await cropperState.pdfDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 2.5 });
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||
|
||||
const finalCanvas = document.createElement('canvas');
|
||||
const finalCtx = finalCanvas.getContext('2d');
|
||||
const crop = cropData[pageNum];
|
||||
const finalWidth = tempCanvas.width * crop.width;
|
||||
const finalHeight = tempCanvas.height * crop.height;
|
||||
finalCanvas.width = finalWidth;
|
||||
finalCanvas.height = finalHeight;
|
||||
|
||||
finalCtx?.drawImage(
|
||||
tempCanvas,
|
||||
tempCanvas.width * crop.x,
|
||||
tempCanvas.height * crop.y,
|
||||
finalWidth,
|
||||
finalHeight,
|
||||
0, 0, finalWidth, finalHeight
|
||||
);
|
||||
|
||||
const pngBytes = await new Promise<ArrayBuffer>((res) =>
|
||||
finalCanvas.toBlob((blob) => blob?.arrayBuffer().then(res), 'image/jpeg', 0.9)
|
||||
);
|
||||
const embeddedImage = await newPdfDoc.embedPng(pngBytes);
|
||||
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
||||
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight });
|
||||
} else {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
|
||||
return newPdfDoc.save();
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
if (cropperState.cropper) {
|
||||
cropperState.cropper.destroy();
|
||||
cropperState.cropper = null;
|
||||
}
|
||||
|
||||
cropperState.pdfDoc = null;
|
||||
cropperState.originalPdfBytes = null;
|
||||
cropperState.pageCrops = {};
|
||||
cropperState.currentPageNum = 1;
|
||||
cropperState.file = null;
|
||||
|
||||
const cropperEditor = document.getElementById('cropper-editor');
|
||||
if (cropperEditor) cropperEditor.classList.add('hidden');
|
||||
|
||||
const container = document.getElementById('cropper-container');
|
||||
if (container) container.innerHTML = '';
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const cropBtn = document.getElementById('crop-button') as HTMLButtonElement;
|
||||
if (cropBtn) cropBtn.disabled = true;
|
||||
}
|
||||
@@ -219,10 +219,14 @@ async function performFlatteningCrop(cropData: any) {
|
||||
finalHeight
|
||||
);
|
||||
|
||||
const pngBytes = await new Promise((res) =>
|
||||
finalCanvas.toBlob((blob) => blob.arrayBuffer().then(res), 'image/png')
|
||||
// Quality value from the compress-pdf.js settings.
|
||||
// 0.9 for "High Quality", 0.6 for "Balanced". Let's use High Quality.
|
||||
const jpegQuality = 0.9;
|
||||
|
||||
const jpegBytes = await new Promise((res) =>
|
||||
finalCanvas.toBlob((blob) => blob.arrayBuffer().then(res), 'image/jpeg', jpegQuality)
|
||||
);
|
||||
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const embeddedImage = await newPdfDoc.embedJpg(jpegBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
||||
newPage.drawImage(embeddedImage, {
|
||||
x: 0,
|
||||
|
||||
239
src/js/logic/decrypt-pdf-page.ts
Normal file
239
src/js/logic/decrypt-pdf-page.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
file: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const passwordInput = document.getElementById('password-input') as HTMLInputElement;
|
||||
if (passwordInput) passwordInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptPdf() {
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const password = (document.getElementById('password-input') as HTMLInputElement)?.value;
|
||||
|
||||
if (!password) {
|
||||
showAlert('Input Required', 'Please enter the PDF password.');
|
||||
return;
|
||||
}
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing decryption...';
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading encrypted PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Decrypting PDF...';
|
||||
|
||||
const args = [inputPath, '--password=' + password, '--decrypt', outputPath];
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
|
||||
if (
|
||||
qpdfError.message?.includes('invalid password') ||
|
||||
qpdfError.message?.includes('password')
|
||||
) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
throw qpdfError;
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (outputFile.length === 0) {
|
||||
throw new Error('Decryption resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `unlocked-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF decrypted successfully! Your download has started.',
|
||||
'success',
|
||||
() => { resetState(); }
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF decryption:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
if (error.message === 'INVALID_PASSWORD') {
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The password you entered is incorrect. Please try again.'
|
||||
);
|
||||
} else if (error.message?.includes('password')) {
|
||||
showAlert(
|
||||
'Password Error',
|
||||
'Unable to decrypt the PDF with the provided password.'
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Decryption Failed',
|
||||
`An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', decryptPdf);
|
||||
}
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function decrypt() {
|
||||
const file = state.files[0];
|
||||
const password = (
|
||||
document.getElementById('password-input') as HTMLInputElement
|
||||
)?.value;
|
||||
|
||||
if (!password) {
|
||||
showAlert('Input Required', 'Please enter the PDF password.');
|
||||
return;
|
||||
}
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
showLoader('Initializing decryption...');
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
showLoader('Reading encrypted PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
showLoader('Decrypting PDF...');
|
||||
|
||||
const args = [inputPath, '--password=' + password, '--decrypt', outputPath];
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
|
||||
if (
|
||||
qpdfError.message?.includes('invalid password') ||
|
||||
qpdfError.message?.includes('password')
|
||||
) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
}
|
||||
throw qpdfError;
|
||||
}
|
||||
|
||||
showLoader('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (outputFile.length === 0) {
|
||||
throw new Error('Decryption resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `unlocked-${file.name}`);
|
||||
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF decrypted successfully! Your download has started.'
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF decryption:', error);
|
||||
hideLoader();
|
||||
|
||||
if (error.message === 'INVALID_PASSWORD') {
|
||||
showAlert(
|
||||
'Incorrect Password',
|
||||
'The password you entered is incorrect. Please try again.'
|
||||
);
|
||||
} else if (error.message?.includes('password')) {
|
||||
showAlert(
|
||||
'Password Error',
|
||||
'Unable to decrypt the PDF with the provided password.'
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Decryption Failed',
|
||||
`An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
274
src/js/logic/delete-pages-page.ts
Normal file
274
src/js/logic/delete-pages-page.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument, parsePageRanges } from '../utils/helpers.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
interface DeleteState {
|
||||
file: File | null;
|
||||
pdfDoc: any;
|
||||
pdfJsDoc: any;
|
||||
totalPages: number;
|
||||
pagesToDelete: Set<number>;
|
||||
}
|
||||
|
||||
const deleteState: DeleteState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
pdfJsDoc: null,
|
||||
totalPages: 0,
|
||||
pagesToDelete: new Set(),
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const pagesInput = document.getElementById('pages-to-delete') as HTMLInputElement;
|
||||
|
||||
if (fileInput) fileInput.addEventListener('change', handleFileUpload);
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const droppedFiles = e.dataTransfer?.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
|
||||
});
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) processBtn.addEventListener('click', deletePages);
|
||||
if (pagesInput) pagesInput.addEventListener('input', updatePreview);
|
||||
|
||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) handleFile(input.files[0]);
|
||||
}
|
||||
|
||||
async function handleFile(file: File) {
|
||||
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
|
||||
showAlert('Invalid File', 'Please select a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PDF...');
|
||||
deleteState.file = file;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
deleteState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
|
||||
deleteState.pdfJsDoc = await getPDFDocument({ data: (arrayBuffer as ArrayBuffer).slice(0) }).promise;
|
||||
deleteState.totalPages = deleteState.pdfDoc.getPageCount();
|
||||
deleteState.pagesToDelete = new Set();
|
||||
|
||||
updateFileDisplay();
|
||||
showOptions();
|
||||
await renderThumbnails();
|
||||
hideLoader();
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (!fileDisplayArea || !deleteState.file) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = deleteState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(deleteState.file.size)} • ${deleteState.totalPages} pages`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => resetState();
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function showOptions() {
|
||||
const deleteOptions = document.getElementById('delete-options');
|
||||
const totalPagesSpan = document.getElementById('total-pages');
|
||||
|
||||
if (deleteOptions) deleteOptions.classList.remove('hidden');
|
||||
if (totalPagesSpan) totalPagesSpan.textContent = deleteState.totalPages.toString();
|
||||
}
|
||||
|
||||
async function renderThumbnails() {
|
||||
const container = document.getElementById('delete-pages-preview');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
for (let i = 1; i <= deleteState.totalPages; i++) {
|
||||
const page = await deleteState.pdfJsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.3 });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative cursor-pointer group';
|
||||
wrapper.dataset.page = i.toString();
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.className = 'w-full h-28 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'max-w-full max-h-full object-contain';
|
||||
|
||||
const pageLabel = document.createElement('span');
|
||||
pageLabel.className = 'absolute top-1 left-1 bg-gray-800 text-white text-xs px-1.5 py-0.5 rounded';
|
||||
pageLabel.textContent = `${i}`;
|
||||
|
||||
const deleteOverlay = document.createElement('div');
|
||||
deleteOverlay.className = 'absolute inset-0 bg-red-500/50 hidden items-center justify-center rounded-lg';
|
||||
deleteOverlay.innerHTML = '<i data-lucide="x" class="w-8 h-8 text-white"></i>';
|
||||
|
||||
imgContainer.appendChild(img);
|
||||
wrapper.append(imgContainer, pageLabel, deleteOverlay);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
wrapper.addEventListener('click', () => togglePageDelete(i, wrapper));
|
||||
}
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function togglePageDelete(pageNum: number, wrapper: HTMLElement) {
|
||||
const overlay = wrapper.querySelector('.bg-red-500\\/50');
|
||||
if (deleteState.pagesToDelete.has(pageNum)) {
|
||||
deleteState.pagesToDelete.delete(pageNum);
|
||||
overlay?.classList.add('hidden');
|
||||
overlay?.classList.remove('flex');
|
||||
} else {
|
||||
deleteState.pagesToDelete.add(pageNum);
|
||||
overlay?.classList.remove('hidden');
|
||||
overlay?.classList.add('flex');
|
||||
}
|
||||
updateInputFromSelection();
|
||||
}
|
||||
|
||||
function updateInputFromSelection() {
|
||||
const pagesInput = document.getElementById('pages-to-delete') as HTMLInputElement;
|
||||
if (pagesInput) {
|
||||
const sorted = Array.from(deleteState.pagesToDelete).sort((a, b) => a - b);
|
||||
pagesInput.value = sorted.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const pagesInput = document.getElementById('pages-to-delete') as HTMLInputElement;
|
||||
if (!pagesInput) return;
|
||||
|
||||
deleteState.pagesToDelete = new Set(parsePageRanges(pagesInput.value, deleteState.totalPages).map(i => i + 1));
|
||||
|
||||
const container = document.getElementById('delete-pages-preview');
|
||||
if (!container) return;
|
||||
|
||||
container.querySelectorAll('[data-page]').forEach((wrapper) => {
|
||||
const pageNum = parseInt((wrapper as HTMLElement).dataset.page || '0', 10);
|
||||
const overlay = wrapper.querySelector('.bg-red-500\\/50');
|
||||
if (deleteState.pagesToDelete.has(pageNum)) {
|
||||
overlay?.classList.remove('hidden');
|
||||
overlay?.classList.add('flex');
|
||||
} else {
|
||||
overlay?.classList.add('hidden');
|
||||
overlay?.classList.remove('flex');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function deletePages() {
|
||||
if (deleteState.pagesToDelete.size === 0) {
|
||||
showAlert('No Pages', 'Please select pages to delete.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteState.pagesToDelete.size >= deleteState.totalPages) {
|
||||
showAlert('Error', 'Cannot delete all pages.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Deleting pages...');
|
||||
|
||||
try {
|
||||
const pagesToKeep = [];
|
||||
for (let i = 0; i < deleteState.totalPages; i++) {
|
||||
if (!deleteState.pagesToDelete.has(i + 1)) pagesToKeep.push(i);
|
||||
}
|
||||
|
||||
const newPdf = await PDFDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(deleteState.pdfDoc, pagesToKeep);
|
||||
copiedPages.forEach(page => newPdf.addPage(page));
|
||||
|
||||
const pdfBytes = await newPdf.save();
|
||||
const baseName = deleteState.file?.name.replace('.pdf', '') || 'document';
|
||||
downloadFile(new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }), `${baseName}_pages_removed.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Success', `Deleted ${deleteState.pagesToDelete.size} page(s) successfully!`, 'success', () => resetState());
|
||||
} catch (error) {
|
||||
console.error('Error deleting pages:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to delete pages.');
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
deleteState.file = null;
|
||||
deleteState.pdfDoc = null;
|
||||
deleteState.pdfJsDoc = null;
|
||||
deleteState.totalPages = 0;
|
||||
deleteState.pagesToDelete = new Set();
|
||||
|
||||
document.getElementById('delete-options')?.classList.add('hidden');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
const pagesInput = document.getElementById('pages-to-delete') as HTMLInputElement;
|
||||
if (pagesInput) pagesInput.value = '';
|
||||
const container = document.getElementById('delete-pages-preview');
|
||||
if (container) container.innerHTML = '';
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function deletePages() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageInput = document.getElementById('pages-to-delete').value;
|
||||
if (!pageInput) {
|
||||
showAlert('Invalid Input', 'Please enter page numbers to delete.');
|
||||
return;
|
||||
}
|
||||
showLoader('Deleting pages...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
const indicesToDelete = new Set();
|
||||
const ranges = pageInput.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++) indicesToDelete.add(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToDelete.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (indicesToDelete.size === 0) {
|
||||
showAlert('Invalid Input', 'No valid pages selected for deletion.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
if (indicesToDelete.size >= totalPages) {
|
||||
showAlert('Invalid Input', 'You cannot delete all pages.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const indicesToKeep = Array.from(
|
||||
{ length: totalPages },
|
||||
(_, i) => i
|
||||
).filter((index) => !indicesToDelete.has(index));
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'deleted-pages.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not delete pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export function setupDeletePagesTool() {
|
||||
const input = document.getElementById('pages-to-delete') as HTMLInputElement;
|
||||
if (!input) return;
|
||||
|
||||
const updateHighlights = () => {
|
||||
const val = input.value;
|
||||
const pagesToDelete = new Set<number>();
|
||||
|
||||
const parts = val.split(',');
|
||||
for (const part of parts) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed.includes('-')) {
|
||||
const [start, end] = trimmed.split('-').map(Number);
|
||||
if (!isNaN(start) && !isNaN(end) && start <= end) {
|
||||
for (let i = start; i <= end; i++) pagesToDelete.add(i);
|
||||
}
|
||||
} else {
|
||||
const num = Number(trimmed);
|
||||
if (!isNaN(num)) pagesToDelete.add(num);
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnails = document.querySelectorAll('#delete-pages-preview .page-thumbnail');
|
||||
thumbnails.forEach((thumb) => {
|
||||
const pageNum = parseInt((thumb as HTMLElement).dataset.pageNumber || '0');
|
||||
const innerContainer = thumb.querySelector('div.relative');
|
||||
|
||||
if (pagesToDelete.has(pageNum)) {
|
||||
innerContainer?.classList.add('border-red-500');
|
||||
innerContainer?.classList.remove('border-gray-600');
|
||||
} else {
|
||||
innerContainer?.classList.remove('border-red-500');
|
||||
innerContainer?.classList.add('border-gray-600');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
input.addEventListener('input', updateHighlights);
|
||||
updateHighlights();
|
||||
}
|
||||
212
src/js/logic/divide-pages-page.ts
Normal file
212
src/js/logic/divide-pages-page.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
interface DividePagesState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
}
|
||||
|
||||
const pageState: DividePagesState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
|
||||
if (splitTypeSelect) splitTypeSelect.value = 'vertical';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
showLoader('Loading PDF...');
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
resetState();
|
||||
}
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function dividePages() {
|
||||
if (!pageState.pdfDoc || !pageState.file) {
|
||||
showAlert('Error', 'Please upload a PDF first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
|
||||
const splitType = splitTypeSelect.value;
|
||||
|
||||
showLoader('Splitting PDF pages...');
|
||||
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pages = pageState.pdfDoc.getPages();
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const originalPage = pages[i];
|
||||
const { width, height } = originalPage.getSize();
|
||||
|
||||
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
|
||||
|
||||
const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
const [page2] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
|
||||
|
||||
switch (splitType) {
|
||||
case 'vertical':
|
||||
page1.setCropBox(0, 0, width / 2, height);
|
||||
page2.setCropBox(width / 2, 0, width / 2, height);
|
||||
break;
|
||||
case 'horizontal':
|
||||
page1.setCropBox(0, height / 2, width, height / 2);
|
||||
page2.setCropBox(0, 0, width, height / 2);
|
||||
break;
|
||||
}
|
||||
|
||||
newPdfDoc.addPage(page1);
|
||||
newPdfDoc.addPage(page2);
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`${originalName}_divided.pdf`
|
||||
);
|
||||
|
||||
showAlert('Success', 'Pages have been divided successfully!', 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while dividing the PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', dividePages);
|
||||
}
|
||||
});
|
||||
365
src/js/logic/edit-attachments-page.ts
Normal file
365
src/js/logic/edit-attachments-page.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js');
|
||||
|
||||
interface AttachmentInfo {
|
||||
index: number;
|
||||
name: string;
|
||||
page: number;
|
||||
data: Uint8Array;
|
||||
}
|
||||
|
||||
interface EditAttachmentState {
|
||||
file: File | null;
|
||||
allAttachments: AttachmentInfo[];
|
||||
attachmentsToRemove: Set<number>;
|
||||
}
|
||||
|
||||
const pageState: EditAttachmentState = {
|
||||
file: null,
|
||||
allAttachments: [],
|
||||
attachmentsToRemove: new Set(),
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.allAttachments = [];
|
||||
pageState.attachmentsToRemove.clear();
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const attachmentsList = document.getElementById('attachments-list');
|
||||
if (attachmentsList) attachmentsList.innerHTML = '';
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) processBtn.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
worker.onmessage = function (e) {
|
||||
const data = e.data;
|
||||
|
||||
if (data.status === 'success' && data.attachments !== undefined) {
|
||||
pageState.allAttachments = data.attachments.map(function (att: any) {
|
||||
return {
|
||||
...att,
|
||||
data: new Uint8Array(att.data)
|
||||
};
|
||||
});
|
||||
|
||||
displayAttachments(data.attachments);
|
||||
hideLoader();
|
||||
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
|
||||
hideLoader();
|
||||
|
||||
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
||||
`${originalName}_edited.pdf`
|
||||
);
|
||||
|
||||
showAlert('Success', 'Attachments updated successfully!', 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} else if (data.status === 'error') {
|
||||
hideLoader();
|
||||
showAlert('Error', data.message || 'Unknown error occurred.');
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = function (error) {
|
||||
hideLoader();
|
||||
console.error('Worker error:', error);
|
||||
showAlert('Error', 'Worker error occurred. Check console for details.');
|
||||
};
|
||||
|
||||
function displayAttachments(attachments: AttachmentInfo[]) {
|
||||
const attachmentsList = document.getElementById('attachments-list');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!attachmentsList) return;
|
||||
|
||||
attachmentsList.innerHTML = '';
|
||||
|
||||
if (attachments.length === 0) {
|
||||
const noAttachments = document.createElement('p');
|
||||
noAttachments.className = 'text-gray-400 text-center py-4';
|
||||
noAttachments.textContent = 'No attachments found in this PDF.';
|
||||
attachmentsList.appendChild(noAttachments);
|
||||
return;
|
||||
}
|
||||
|
||||
// Controls container
|
||||
const controlsContainer = document.createElement('div');
|
||||
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
|
||||
|
||||
const removeAllBtn = document.createElement('button');
|
||||
removeAllBtn.className = 'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
|
||||
removeAllBtn.textContent = 'Remove All Attachments';
|
||||
removeAllBtn.onclick = function () {
|
||||
if (pageState.allAttachments.length === 0) return;
|
||||
|
||||
const allSelected = pageState.allAttachments.every(function (attachment) {
|
||||
return pageState.attachmentsToRemove.has(attachment.index);
|
||||
});
|
||||
|
||||
if (allSelected) {
|
||||
pageState.allAttachments.forEach(function (attachment) {
|
||||
pageState.attachmentsToRemove.delete(attachment.index);
|
||||
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
|
||||
if (element) {
|
||||
element.classList.remove('opacity-50', 'line-through');
|
||||
const btn = element.querySelector('button');
|
||||
if (btn) {
|
||||
btn.classList.remove('bg-gray-600');
|
||||
btn.classList.add('bg-red-600');
|
||||
}
|
||||
}
|
||||
});
|
||||
removeAllBtn.textContent = 'Remove All Attachments';
|
||||
} else {
|
||||
pageState.allAttachments.forEach(function (attachment) {
|
||||
pageState.attachmentsToRemove.add(attachment.index);
|
||||
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
|
||||
if (element) {
|
||||
element.classList.add('opacity-50', 'line-through');
|
||||
const btn = element.querySelector('button');
|
||||
if (btn) {
|
||||
btn.classList.add('bg-gray-600');
|
||||
btn.classList.remove('bg-red-600');
|
||||
}
|
||||
}
|
||||
});
|
||||
removeAllBtn.textContent = 'Deselect All';
|
||||
}
|
||||
};
|
||||
|
||||
controlsContainer.appendChild(removeAllBtn);
|
||||
attachmentsList.appendChild(controlsContainer);
|
||||
|
||||
// Attachment items
|
||||
for (const attachment of attachments) {
|
||||
const attachmentDiv = document.createElement('div');
|
||||
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
||||
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'flex-1';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'text-white font-medium block';
|
||||
nameSpan.textContent = attachment.name;
|
||||
|
||||
const levelSpan = document.createElement('span');
|
||||
levelSpan.className = 'text-gray-400 text-sm block';
|
||||
if (attachment.page === 0) {
|
||||
levelSpan.textContent = 'Document-level attachment';
|
||||
} else {
|
||||
levelSpan.textContent = `Page ${attachment.page} attachment`;
|
||||
}
|
||||
|
||||
infoDiv.append(nameSpan, levelSpan);
|
||||
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'flex items-center gap-2';
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = `${pageState.attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.title = 'Remove attachment';
|
||||
removeBtn.onclick = function () {
|
||||
if (pageState.attachmentsToRemove.has(attachment.index)) {
|
||||
pageState.attachmentsToRemove.delete(attachment.index);
|
||||
attachmentDiv.classList.remove('opacity-50', 'line-through');
|
||||
removeBtn.classList.remove('bg-gray-600');
|
||||
removeBtn.classList.add('bg-red-600');
|
||||
} else {
|
||||
pageState.attachmentsToRemove.add(attachment.index);
|
||||
attachmentDiv.classList.add('opacity-50', 'line-through');
|
||||
removeBtn.classList.add('bg-gray-600');
|
||||
removeBtn.classList.remove('bg-red-600');
|
||||
}
|
||||
|
||||
const allSelected = pageState.allAttachments.every(function (att) {
|
||||
return pageState.attachmentsToRemove.has(att.index);
|
||||
});
|
||||
removeAllBtn.textContent = allSelected ? 'Deselect All' : 'Remove All Attachments';
|
||||
};
|
||||
|
||||
actionsDiv.append(removeBtn);
|
||||
attachmentDiv.append(infoDiv, actionsDiv);
|
||||
attachmentsList.appendChild(attachmentDiv);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
|
||||
if (processBtn) processBtn.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function loadAttachments() {
|
||||
if (!pageState.file) return;
|
||||
|
||||
showLoader('Loading attachments...');
|
||||
|
||||
try {
|
||||
const fileBuffer = await pageState.file.arrayBuffer();
|
||||
|
||||
const message = {
|
||||
command: 'get-attachments',
|
||||
fileBuffer: fileBuffer,
|
||||
fileName: pageState.file.name
|
||||
};
|
||||
|
||||
worker.postMessage(message, [fileBuffer]);
|
||||
} catch (error) {
|
||||
console.error('Error loading attachments:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load attachments from PDF.');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
if (!pageState.file) {
|
||||
showAlert('Error', 'No PDF file loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageState.attachmentsToRemove.size === 0) {
|
||||
showAlert('No Changes', 'No attachments selected for removal.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Processing attachments...');
|
||||
|
||||
try {
|
||||
const fileBuffer = await pageState.file.arrayBuffer();
|
||||
|
||||
const message = {
|
||||
command: 'edit-attachments',
|
||||
fileBuffer: fileBuffer,
|
||||
fileName: pageState.file.name,
|
||||
attachmentsToRemove: Array.from(pageState.attachmentsToRemove)
|
||||
};
|
||||
|
||||
worker.postMessage(message, [fileBuffer]);
|
||||
} catch (error) {
|
||||
console.error('Error editing attachments:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to edit attachments.');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
|
||||
await loadAttachments();
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', saveChanges);
|
||||
}
|
||||
});
|
||||
@@ -1,218 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js');
|
||||
|
||||
let allAttachments: Array<{ index: number; name: string; page: number; data: Uint8Array }> = [];
|
||||
let attachmentsToRemove: Set<number> = new Set();
|
||||
|
||||
export async function setupEditAttachmentsTool() {
|
||||
const optionsDiv = document.getElementById('edit-attachments-options');
|
||||
if (!optionsDiv || !state.files || state.files.length === 0) return;
|
||||
|
||||
optionsDiv.classList.remove('hidden');
|
||||
await loadAttachmentsList();
|
||||
}
|
||||
|
||||
async function loadAttachmentsList() {
|
||||
const attachmentsList = document.getElementById('attachments-list');
|
||||
if (!attachmentsList || !state.files || state.files.length === 0) return;
|
||||
|
||||
attachmentsList.innerHTML = '';
|
||||
attachmentsToRemove.clear();
|
||||
allAttachments = [];
|
||||
|
||||
try {
|
||||
showLoader('Loading attachments...');
|
||||
|
||||
const file = state.files[0];
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
|
||||
const message = {
|
||||
command: 'get-attachments',
|
||||
fileBuffer: fileBuffer,
|
||||
fileName: file.name
|
||||
};
|
||||
|
||||
worker.postMessage(message, [fileBuffer]);
|
||||
} catch (error) {
|
||||
console.error('Error loading attachments:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load attachments from PDF.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
worker.onmessage = (e) => {
|
||||
const data = e.data;
|
||||
|
||||
if (data.status === 'success' && data.attachments !== undefined) {
|
||||
const attachments = data.attachments;
|
||||
allAttachments = attachments.map(att => ({
|
||||
...att,
|
||||
data: new Uint8Array(att.data)
|
||||
}));
|
||||
|
||||
displayAttachments(attachments);
|
||||
hideLoader();
|
||||
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
|
||||
hideLoader();
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
|
||||
`edited-attachments-${data.fileName}`
|
||||
);
|
||||
|
||||
showAlert('Success', 'Attachments updated successfully!');
|
||||
} else if (data.status === 'error') {
|
||||
hideLoader();
|
||||
showAlert('Error', data.message || 'Unknown error occurred.');
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (error) => {
|
||||
hideLoader();
|
||||
console.error('Worker error:', error);
|
||||
showAlert('Error', 'Worker error occurred. Check console for details.');
|
||||
};
|
||||
|
||||
function displayAttachments(attachments) {
|
||||
const attachmentsList = document.getElementById('attachments-list');
|
||||
if (!attachmentsList) return;
|
||||
|
||||
const existingControls = attachmentsList.querySelector('.attachments-controls');
|
||||
attachmentsList.innerHTML = '';
|
||||
if (existingControls) {
|
||||
attachmentsList.appendChild(existingControls);
|
||||
}
|
||||
|
||||
if (attachments.length === 0) {
|
||||
const noAttachments = document.createElement('p');
|
||||
noAttachments.className = 'text-gray-400 text-center py-4';
|
||||
noAttachments.textContent = 'No attachments found in this PDF.';
|
||||
attachmentsList.appendChild(noAttachments);
|
||||
return;
|
||||
}
|
||||
|
||||
const controlsContainer = document.createElement('div');
|
||||
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
|
||||
const removeAllBtn = document.createElement('button');
|
||||
removeAllBtn.className = 'btn bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
|
||||
removeAllBtn.textContent = 'Remove All Attachments';
|
||||
removeAllBtn.onclick = () => {
|
||||
if (allAttachments.length === 0) return;
|
||||
|
||||
const allSelected = allAttachments.every(attachment => attachmentsToRemove.has(attachment.index));
|
||||
|
||||
if (allSelected) {
|
||||
allAttachments.forEach(attachment => {
|
||||
attachmentsToRemove.delete(attachment.index);
|
||||
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
|
||||
if (element) {
|
||||
element.classList.remove('opacity-50', 'line-through');
|
||||
const removeBtn = element.querySelector('button');
|
||||
if (removeBtn) {
|
||||
removeBtn.classList.remove('bg-gray-600');
|
||||
removeBtn.classList.add('bg-red-600');
|
||||
}
|
||||
}
|
||||
});
|
||||
removeAllBtn.textContent = 'Remove All Attachments';
|
||||
} else {
|
||||
allAttachments.forEach(attachment => {
|
||||
attachmentsToRemove.add(attachment.index);
|
||||
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
|
||||
if (element) {
|
||||
element.classList.add('opacity-50', 'line-through');
|
||||
const removeBtn = element.querySelector('button');
|
||||
if (removeBtn) {
|
||||
removeBtn.classList.add('bg-gray-600');
|
||||
removeBtn.classList.remove('bg-red-600');
|
||||
}
|
||||
}
|
||||
});
|
||||
removeAllBtn.textContent = 'Deselect All';
|
||||
}
|
||||
};
|
||||
|
||||
controlsContainer.appendChild(removeAllBtn);
|
||||
attachmentsList.appendChild(controlsContainer);
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const attachmentDiv = document.createElement('div');
|
||||
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
||||
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'flex-1';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'text-white font-medium block';
|
||||
nameSpan.textContent = attachment.name;
|
||||
|
||||
const levelSpan = document.createElement('span');
|
||||
levelSpan.className = 'text-gray-400 text-sm block';
|
||||
if (attachment.page === 0) {
|
||||
levelSpan.textContent = 'Document-level attachment';
|
||||
} else {
|
||||
levelSpan.textContent = `Page ${attachment.page} attachment`;
|
||||
}
|
||||
|
||||
infoDiv.append(nameSpan, levelSpan);
|
||||
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'flex items-center gap-2';
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = `btn ${attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.title = 'Remove attachment';
|
||||
removeBtn.onclick = () => {
|
||||
if (attachmentsToRemove.has(attachment.index)) {
|
||||
attachmentsToRemove.delete(attachment.index);
|
||||
attachmentDiv.classList.remove('opacity-50', 'line-through');
|
||||
removeBtn.classList.remove('bg-gray-600');
|
||||
removeBtn.classList.add('bg-red-600');
|
||||
} else {
|
||||
attachmentsToRemove.add(attachment.index);
|
||||
attachmentDiv.classList.add('opacity-50', 'line-through');
|
||||
removeBtn.classList.add('bg-gray-600');
|
||||
removeBtn.classList.remove('bg-red-600');
|
||||
}
|
||||
const allSelected = allAttachments.every(attachment => attachmentsToRemove.has(attachment.index));
|
||||
removeAllBtn.textContent = allSelected ? 'Deselect All' : 'Remove All Attachments';
|
||||
};
|
||||
|
||||
actionsDiv.append(removeBtn);
|
||||
attachmentDiv.append(infoDiv, actionsDiv);
|
||||
attachmentsList.appendChild(attachmentDiv);
|
||||
}
|
||||
}
|
||||
|
||||
export async function editAttachments() {
|
||||
if (!state.files || state.files.length === 0) {
|
||||
showAlert('Error', 'No PDF file loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Processing attachments...');
|
||||
|
||||
try {
|
||||
const file = state.files[0];
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
|
||||
const message = {
|
||||
command: 'edit-attachments',
|
||||
fileBuffer: fileBuffer,
|
||||
fileName: file.name,
|
||||
attachmentsToRemove: Array.from(attachmentsToRemove)
|
||||
};
|
||||
|
||||
worker.postMessage(message, [fileBuffer]);
|
||||
} catch (error) {
|
||||
console.error('Error editing attachments:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to edit attachments.');
|
||||
}
|
||||
}
|
||||
373
src/js/logic/edit-metadata-page.ts
Normal file
373
src/js/logic/edit-metadata-page.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument, PDFName, PDFString } from 'pdf-lib';
|
||||
|
||||
interface EditMetadataState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
}
|
||||
|
||||
const pageState: EditMetadataState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
// Clear form fields
|
||||
const fields = ['meta-title', 'meta-author', 'meta-subject', 'meta-keywords', 'meta-creator', 'meta-producer', 'meta-creation-date', 'meta-mod-date'];
|
||||
fields.forEach(function (fieldId) {
|
||||
const field = document.getElementById(fieldId) as HTMLInputElement;
|
||||
if (field) field.value = '';
|
||||
});
|
||||
|
||||
// Clear custom fields
|
||||
const customFieldsContainer = document.getElementById('custom-fields-container');
|
||||
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
function formatDateForInput(date: Date | undefined): string {
|
||||
if (!date) return '';
|
||||
try {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function addCustomFieldRow(key: string = '', value: string = '') {
|
||||
const container = document.getElementById('custom-fields-container');
|
||||
if (!container) return;
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex flex-col gap-2';
|
||||
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'text';
|
||||
keyInput.placeholder = 'Key (e.g., Department)';
|
||||
keyInput.value = key;
|
||||
keyInput.className = 'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
|
||||
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'text';
|
||||
valueInput.placeholder = 'Value (e.g., Marketing)';
|
||||
valueInput.value = value;
|
||||
valueInput.className = 'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'text-red-400 hover:text-red-300 p-2 self-center';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-5 h-5"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
row.remove();
|
||||
};
|
||||
|
||||
row.append(keyInput, valueInput, removeBtn);
|
||||
container.appendChild(row);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function populateMetadataFields() {
|
||||
if (!pageState.pdfDoc) return;
|
||||
|
||||
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
|
||||
const authorInput = document.getElementById('meta-author') as HTMLInputElement;
|
||||
const subjectInput = document.getElementById('meta-subject') as HTMLInputElement;
|
||||
const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement;
|
||||
const creatorInput = document.getElementById('meta-creator') as HTMLInputElement;
|
||||
const producerInput = document.getElementById('meta-producer') as HTMLInputElement;
|
||||
const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement;
|
||||
const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement;
|
||||
|
||||
if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || '';
|
||||
if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || '';
|
||||
if (subjectInput) subjectInput.value = pageState.pdfDoc.getSubject() || '';
|
||||
if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || '';
|
||||
if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || '';
|
||||
if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || '';
|
||||
if (creationDateInput) creationDateInput.value = formatDateForInput(pageState.pdfDoc.getCreationDate());
|
||||
if (modDateInput) modDateInput.value = formatDateForInput(pageState.pdfDoc.getModificationDate());
|
||||
|
||||
// Load custom fields
|
||||
const customFieldsContainer = document.getElementById('custom-fields-container');
|
||||
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
|
||||
|
||||
try {
|
||||
// @ts-expect-error getInfoDict is private but accessible at runtime
|
||||
const infoDict = pageState.pdfDoc.getInfoDict();
|
||||
const standardKeys = new Set([
|
||||
'Title', 'Author', 'Subject', 'Keywords', 'Creator',
|
||||
'Producer', 'CreationDate', 'ModDate'
|
||||
]);
|
||||
|
||||
const allKeys = infoDict
|
||||
.keys()
|
||||
.map(function (key: { asString: () => string }) {
|
||||
return key.asString().substring(1);
|
||||
});
|
||||
|
||||
allKeys.forEach(function (key: string) {
|
||||
if (!standardKeys.has(key)) {
|
||||
const rawValue = infoDict.lookup(key);
|
||||
let displayValue = '';
|
||||
|
||||
if (rawValue && typeof rawValue.decodeText === 'function') {
|
||||
displayValue = rawValue.decodeText();
|
||||
} else if (rawValue && typeof rawValue.asString === 'function') {
|
||||
displayValue = rawValue.asString();
|
||||
} else if (rawValue) {
|
||||
displayValue = String(rawValue);
|
||||
}
|
||||
|
||||
addCustomFieldRow(key, displayValue);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Could not read custom metadata fields:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
showLoader('Loading PDF...');
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
|
||||
populateMetadataFields();
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
resetState();
|
||||
}
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMetadata() {
|
||||
if (!pageState.pdfDoc || !pageState.file) {
|
||||
showAlert('Error', 'Please upload a PDF first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Updating metadata...');
|
||||
|
||||
try {
|
||||
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
|
||||
const authorInput = document.getElementById('meta-author') as HTMLInputElement;
|
||||
const subjectInput = document.getElementById('meta-subject') as HTMLInputElement;
|
||||
const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement;
|
||||
const creatorInput = document.getElementById('meta-creator') as HTMLInputElement;
|
||||
const producerInput = document.getElementById('meta-producer') as HTMLInputElement;
|
||||
const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement;
|
||||
const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement;
|
||||
|
||||
pageState.pdfDoc.setTitle(titleInput.value);
|
||||
pageState.pdfDoc.setAuthor(authorInput.value);
|
||||
pageState.pdfDoc.setSubject(subjectInput.value);
|
||||
pageState.pdfDoc.setCreator(creatorInput.value);
|
||||
pageState.pdfDoc.setProducer(producerInput.value);
|
||||
|
||||
const keywords = keywordsInput.value;
|
||||
pageState.pdfDoc.setKeywords(
|
||||
keywords
|
||||
.split(',')
|
||||
.map(function (k) { return k.trim(); })
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
// Handle creation date
|
||||
if (creationDateInput.value) {
|
||||
pageState.pdfDoc.setCreationDate(new Date(creationDateInput.value));
|
||||
}
|
||||
|
||||
// Handle modification date
|
||||
if (modDateInput.value) {
|
||||
pageState.pdfDoc.setModificationDate(new Date(modDateInput.value));
|
||||
} else {
|
||||
pageState.pdfDoc.setModificationDate(new Date());
|
||||
}
|
||||
|
||||
// Handle custom fields
|
||||
// @ts-expect-error getInfoDict is private but accessible at runtime
|
||||
const infoDict = pageState.pdfDoc.getInfoDict();
|
||||
const standardKeys = new Set([
|
||||
'Title', 'Author', 'Subject', 'Keywords', 'Creator',
|
||||
'Producer', 'CreationDate', 'ModDate'
|
||||
]);
|
||||
|
||||
// Remove existing custom keys
|
||||
const allKeys = infoDict
|
||||
.keys()
|
||||
.map(function (key: { asString: () => string }) {
|
||||
return key.asString().substring(1);
|
||||
});
|
||||
|
||||
allKeys.forEach(function (key: string) {
|
||||
if (!standardKeys.has(key)) {
|
||||
infoDict.delete(PDFName.of(key));
|
||||
}
|
||||
});
|
||||
|
||||
// Add new custom fields
|
||||
const customKeys = document.querySelectorAll('.custom-meta-key');
|
||||
const customValues = document.querySelectorAll('.custom-meta-value');
|
||||
|
||||
customKeys.forEach(function (keyInput, index) {
|
||||
const key = (keyInput as HTMLInputElement).value.trim();
|
||||
const value = (customValues[index] as HTMLInputElement).value.trim();
|
||||
if (key && value) {
|
||||
infoDict.set(PDFName.of(key), PDFString.of(value));
|
||||
}
|
||||
});
|
||||
|
||||
const newPdfBytes = await pageState.pdfDoc.save();
|
||||
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`${originalName}_metadata-edited.pdf`
|
||||
);
|
||||
|
||||
showAlert('Success', 'Metadata updated successfully!', 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const addCustomFieldBtn = document.getElementById('add-custom-field');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (addCustomFieldBtn) {
|
||||
addCustomFieldBtn.addEventListener('click', function () {
|
||||
addCustomFieldRow();
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', saveMetadata);
|
||||
}
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFName, PDFString } from 'pdf-lib';
|
||||
|
||||
export async function editMetadata() {
|
||||
showLoader('Updating metadata...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const keywords = document.getElementById('meta-keywords').value;
|
||||
state.pdfDoc.setKeywords(
|
||||
keywords
|
||||
.split(',')
|
||||
.map((k: any) => k.trim())
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const creationDate = document.getElementById('meta-creation-date').value;
|
||||
if (creationDate) {
|
||||
state.pdfDoc.setCreationDate(new Date(creationDate));
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const modDate = document.getElementById('meta-mod-date').value;
|
||||
if (modDate) {
|
||||
state.pdfDoc.setModificationDate(new Date(modDate));
|
||||
} else {
|
||||
state.pdfDoc.setModificationDate(new Date());
|
||||
}
|
||||
|
||||
const infoDict = state.pdfDoc.getInfoDict();
|
||||
const standardKeys = new Set([
|
||||
'Title',
|
||||
'Author',
|
||||
'Subject',
|
||||
'Keywords',
|
||||
'Creator',
|
||||
'Producer',
|
||||
'CreationDate',
|
||||
'ModDate',
|
||||
]);
|
||||
|
||||
const allKeys = infoDict
|
||||
.keys()
|
||||
.map((key: any) => key.asString().substring(1)); // Clean keys
|
||||
|
||||
allKeys.forEach((key: any) => {
|
||||
if (!standardKeys.has(key)) {
|
||||
infoDict.delete(PDFName.of(key));
|
||||
}
|
||||
});
|
||||
|
||||
const customKeys = document.querySelectorAll('.custom-meta-key');
|
||||
const customValues = document.querySelectorAll('.custom-meta-value');
|
||||
|
||||
customKeys.forEach((keyInput, index) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const key = keyInput.value.trim();
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const value = customValues[index].value.trim();
|
||||
if (key && value) {
|
||||
// Now we add the fields to a clean slate
|
||||
infoDict.set(PDFName.of(key), PDFString.of(value));
|
||||
}
|
||||
});
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'metadata-edited.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Could not update metadata. Please check that date formats are correct.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
// Logic for PDF Editor Page
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { formatBytes } from '../utils/helpers.js';
|
||||
|
||||
const embedPdfWasmUrl = new URL(
|
||||
'embedpdf-snippet/dist/pdfium.wasm',
|
||||
import.meta.url
|
||||
).href;
|
||||
|
||||
let currentPdfUrl: string | null = null;
|
||||
|
||||
@@ -39,8 +45,9 @@ function initializePage() {
|
||||
}
|
||||
});
|
||||
|
||||
dropZone.addEventListener('click', () => {
|
||||
fileInput?.click();
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +61,6 @@ async function handleFileUpload(e: Event) {
|
||||
if (input.files && input.files.length > 0) {
|
||||
await handleFiles(input.files);
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
async function handleFiles(files: FileList) {
|
||||
@@ -71,11 +77,51 @@ async function handleFiles(files: FileList) {
|
||||
const pdfContainer = document.getElementById('embed-pdf-container');
|
||||
const uploader = document.getElementById('tool-uploader');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
|
||||
if (!pdfWrapper || !pdfContainer || !uploader || !dropZone) return;
|
||||
if (!pdfWrapper || !pdfContainer || !uploader || !dropZone || !fileDisplayArea) return;
|
||||
|
||||
// Hide uploader elements but keep the container
|
||||
dropZone.classList.add('hidden');
|
||||
// Hide uploader elements but keep the container
|
||||
// dropZone.classList.add('hidden');
|
||||
|
||||
// Show file display
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
|
||||
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 = () => {
|
||||
if (currentPdfUrl) {
|
||||
URL.revokeObjectURL(currentPdfUrl);
|
||||
currentPdfUrl = null;
|
||||
}
|
||||
pdfContainer.textContent = '';
|
||||
pdfWrapper.classList.add('hidden');
|
||||
fileDisplayArea.innerHTML = '';
|
||||
// dropZone.classList.remove('hidden');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
// Clear previous content
|
||||
pdfContainer.textContent = '';
|
||||
@@ -89,18 +135,14 @@ async function handleFiles(files: FileList) {
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
currentPdfUrl = fileURL;
|
||||
|
||||
// Dynamically load EmbedPDF script
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.textContent = `
|
||||
import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js';
|
||||
const { default: EmbedPDF } = await import('embedpdf-snippet');
|
||||
EmbedPDF.init({
|
||||
type: 'container',
|
||||
target: document.getElementById('embed-pdf-container'),
|
||||
src: '${fileURL}',
|
||||
target: pdfContainer,
|
||||
src: fileURL,
|
||||
worker: true,
|
||||
wasmUrl: embedPdfWasmUrl,
|
||||
});
|
||||
`;
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Update back button to reset state
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
246
src/js/logic/encrypt-pdf-page.ts
Normal file
246
src/js/logic/encrypt-pdf-page.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
file: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const userPasswordInput = document.getElementById('user-password-input') as HTMLInputElement;
|
||||
if (userPasswordInput) userPasswordInput.value = '';
|
||||
|
||||
const ownerPasswordInput = document.getElementById('owner-password-input') as HTMLInputElement;
|
||||
if (ownerPasswordInput) ownerPasswordInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function encryptPdf() {
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const userPassword = (document.getElementById('user-password-input') as HTMLInputElement)?.value || '';
|
||||
const ownerPasswordInput = (document.getElementById('owner-password-input') as HTMLInputElement)?.value || '';
|
||||
|
||||
if (!userPassword) {
|
||||
showAlert('Input Required', 'Please enter a user password.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ownerPassword = ownerPasswordInput || userPassword;
|
||||
const hasDistinctOwnerPassword = ownerPasswordInput !== '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing encryption...';
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Encrypting PDF with 256-bit AES...';
|
||||
|
||||
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
|
||||
|
||||
// Only add restrictions if a distinct owner password was provided
|
||||
if (hasDistinctOwnerPassword) {
|
||||
args.push(
|
||||
'--modify=none',
|
||||
'--extract=n',
|
||||
'--print=none',
|
||||
'--accessibility=n',
|
||||
'--annotate=n',
|
||||
'--assemble=n',
|
||||
'--form=n',
|
||||
'--modify-other=n'
|
||||
);
|
||||
}
|
||||
|
||||
args.push('--', outputPath);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
throw new Error(
|
||||
'Encryption failed: ' + (qpdfError.message || 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Encryption resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `encrypted-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
let successMessage = 'PDF encrypted successfully with 256-bit AES!';
|
||||
if (!hasDistinctOwnerPassword) {
|
||||
successMessage +=
|
||||
' Note: Without a separate owner password, the PDF has no usage restrictions.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage, 'success', () => { resetState(); });
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF encryption:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert(
|
||||
'Encryption Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', encryptPdf);
|
||||
}
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function encrypt() {
|
||||
const file = state.files[0];
|
||||
const userPassword =
|
||||
(document.getElementById('user-password-input') as HTMLInputElement)
|
||||
?.value || '';
|
||||
const ownerPasswordInput =
|
||||
(document.getElementById('owner-password-input') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
if (!userPassword) {
|
||||
showAlert('Input Required', 'Please enter a user password.');
|
||||
return;
|
||||
}
|
||||
|
||||
const ownerPassword = ownerPasswordInput || userPassword;
|
||||
const hasDistinctOwnerPassword = ownerPasswordInput !== '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
showLoader('Initializing encryption...');
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
showLoader('Reading PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
showLoader('Encrypting PDF with 256-bit AES...');
|
||||
|
||||
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
|
||||
|
||||
// Only add restrictions if a distinct owner password was provided
|
||||
if (hasDistinctOwnerPassword) {
|
||||
args.push(
|
||||
'--modify=none',
|
||||
'--extract=n',
|
||||
'--print=none',
|
||||
'--accessibility=n',
|
||||
'--annotate=n',
|
||||
'--assemble=n',
|
||||
'--form=n',
|
||||
'--modify-other=n'
|
||||
);
|
||||
}
|
||||
|
||||
args.push('--', outputPath);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
throw new Error(
|
||||
'Encryption failed: ' + (qpdfError.message || 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
showLoader('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Encryption resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `encrypted-${file.name}`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
let successMessage = 'PDF encrypted successfully with 256-bit AES!';
|
||||
if (!hasDistinctOwnerPassword) {
|
||||
successMessage +=
|
||||
' Note: Without a separate owner password, the PDF has no usage restrictions.';
|
||||
}
|
||||
|
||||
showAlert('Success', successMessage);
|
||||
} catch (error: any) {
|
||||
console.error('Error during PDF encryption:', error);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Encryption Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
260
src/js/logic/extract-attachments-page.ts
Normal file
260
src/js/logic/extract-attachments-page.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js');
|
||||
|
||||
interface ExtractState {
|
||||
files: File[];
|
||||
}
|
||||
|
||||
const pageState: ExtractState = {
|
||||
files: [],
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.files = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const statusMessage = document.getElementById('status-message');
|
||||
if (statusMessage) statusMessage.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
processBtn.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
||||
const statusMessage = document.getElementById('status-message') as HTMLElement;
|
||||
if (!statusMessage) return;
|
||||
|
||||
statusMessage.textContent = message;
|
||||
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
|
||||
? 'bg-green-900 text-green-200'
|
||||
: type === 'error'
|
||||
? 'bg-red-900 text-red-200'
|
||||
: 'bg-blue-900 text-blue-200'
|
||||
}`;
|
||||
statusMessage.classList.remove('hidden');
|
||||
}
|
||||
|
||||
worker.onmessage = function (e) {
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
processBtn.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
if (e.data.status === 'success') {
|
||||
const attachments = e.data.attachments;
|
||||
|
||||
if (attachments.length === 0) {
|
||||
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
let totalSize = 0;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
zip.file(attachment.name, new Uint8Array(attachment.data));
|
||||
totalSize += attachment.data.byteLength;
|
||||
}
|
||||
|
||||
zip.generateAsync({ type: 'blob' }).then(function (zipBlob) {
|
||||
downloadFile(zipBlob, 'extracted-attachments.zip');
|
||||
|
||||
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
|
||||
|
||||
showStatus(
|
||||
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
|
||||
'success'
|
||||
);
|
||||
|
||||
resetState();
|
||||
});
|
||||
} else if (e.data.status === 'error') {
|
||||
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
||||
console.error('Worker Error:', errorMessage);
|
||||
|
||||
if (errorMessage.includes('No attachments were found')) {
|
||||
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
||||
resetState();
|
||||
} else {
|
||||
showStatus(`Error: ${errorMessage}`, 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = function (error) {
|
||||
console.error('Worker error:', error);
|
||||
showStatus('Worker error occurred. Check console for details.', 'error');
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
processBtn.removeAttribute('disabled');
|
||||
}
|
||||
};
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.files.length > 0) {
|
||||
const summaryDiv = document.createElement('div');
|
||||
summaryDiv.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 countSpan = document.createElement('div');
|
||||
countSpan.className = 'font-medium text-gray-200 text-sm mb-1';
|
||||
countSpan.textContent = `${pageState.files.length} PDF file(s) selected`;
|
||||
|
||||
const sizeSpan = document.createElement('div');
|
||||
sizeSpan.className = 'text-xs text-gray-400';
|
||||
const totalSize = pageState.files.reduce(function (sum, f) { return sum + f.size; }, 0);
|
||||
sizeSpan.textContent = formatBytes(totalSize);
|
||||
|
||||
infoContainer.append(countSpan, sizeSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
summaryDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(summaryDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function extractAttachments() {
|
||||
if (pageState.files.length === 0) {
|
||||
showStatus('No Files', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
processBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
processBtn.setAttribute('disabled', 'true');
|
||||
}
|
||||
|
||||
showStatus('Reading files...', 'info');
|
||||
|
||||
try {
|
||||
const fileBuffers: ArrayBuffer[] = [];
|
||||
const fileNames: string[] = [];
|
||||
|
||||
for (const file of pageState.files) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
fileBuffers.push(buffer);
|
||||
fileNames.push(file.name);
|
||||
}
|
||||
|
||||
showStatus(`Extracting attachments from ${pageState.files.length} file(s)...`, 'info');
|
||||
|
||||
const message = {
|
||||
command: 'extract-attachments',
|
||||
fileBuffers,
|
||||
fileNames,
|
||||
};
|
||||
|
||||
const transferables = fileBuffers.map(function (buf) { return buf; });
|
||||
worker.postMessage(message, transferables);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reading files:', error);
|
||||
showStatus(
|
||||
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
|
||||
'error'
|
||||
);
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
processBtn.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = 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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileSelect(files);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', extractAttachments);
|
||||
}
|
||||
});
|
||||
@@ -1,185 +0,0 @@
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { showAlert } from '../ui.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const worker = new Worker(import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js');
|
||||
|
||||
interface ExtractAttachmentSuccessResponse {
|
||||
status: 'success';
|
||||
attachments: Array<{ name: string; data: ArrayBuffer }>;
|
||||
}
|
||||
|
||||
interface ExtractAttachmentErrorResponse {
|
||||
status: 'error';
|
||||
message: string;
|
||||
}
|
||||
|
||||
type ExtractAttachmentResponse = ExtractAttachmentSuccessResponse | ExtractAttachmentErrorResponse;
|
||||
|
||||
export async function extractAttachments() {
|
||||
if (state.files.length === 0) {
|
||||
showStatus('No Files', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
document.getElementById('process-btn')?.setAttribute('disabled', 'true');
|
||||
|
||||
showStatus('Reading files (Main Thread)...', 'info');
|
||||
|
||||
try {
|
||||
const fileBuffers: ArrayBuffer[] = [];
|
||||
const fileNames: string[] = [];
|
||||
|
||||
for (const file of state.files) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
fileBuffers.push(buffer);
|
||||
fileNames.push(file.name);
|
||||
}
|
||||
|
||||
showStatus(`Extracting attachments from ${state.files.length} file(s)...`, 'info');
|
||||
|
||||
const message: ExtractAttachmentsMessage = {
|
||||
command: 'extract-attachments',
|
||||
fileBuffers,
|
||||
fileNames,
|
||||
};
|
||||
|
||||
const transferables = fileBuffers.map(buf => buf);
|
||||
worker.postMessage(message, transferables);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reading files:', error);
|
||||
showStatus(
|
||||
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
|
||||
'error'
|
||||
);
|
||||
document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
document.getElementById('process-btn')?.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
worker.onmessage = (e: MessageEvent<ExtractAttachmentResponse>) => {
|
||||
document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
document.getElementById('process-btn')?.removeAttribute('disabled');
|
||||
|
||||
if (e.data.status === 'success') {
|
||||
const attachments = e.data.attachments;
|
||||
|
||||
if (attachments.length === 0) {
|
||||
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
||||
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
}
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) {
|
||||
fileControls.classList.add('hidden');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
let totalSize = 0;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
zip.file(attachment.name, new Uint8Array(attachment.data));
|
||||
totalSize += attachment.data.byteLength;
|
||||
}
|
||||
|
||||
zip.generateAsync({ type: 'blob' }).then((zipBlob) => {
|
||||
downloadFile(zipBlob, 'extracted-attachments.zip');
|
||||
|
||||
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
|
||||
|
||||
showStatus(
|
||||
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
|
||||
'success'
|
||||
);
|
||||
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
}
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) {
|
||||
fileControls.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
} else if (e.data.status === 'error') {
|
||||
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
||||
console.error('Worker Error:', errorMessage);
|
||||
|
||||
if (errorMessage.includes('No attachments were found')) {
|
||||
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
|
||||
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
}
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) {
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) {
|
||||
fileControls.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
showStatus(`Error: ${errorMessage}`, 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (error) => {
|
||||
console.error('Worker error:', error);
|
||||
showStatus('Worker error occurred. Check console for details.', 'error');
|
||||
document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
document.getElementById('process-btn')?.removeAttribute('disabled');
|
||||
};
|
||||
|
||||
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
|
||||
const statusMessage = document.getElementById('status-message') as HTMLElement;
|
||||
if (!statusMessage) return;
|
||||
|
||||
statusMessage.textContent = message;
|
||||
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
|
||||
? 'bg-green-900 text-green-200'
|
||||
: type === 'error'
|
||||
? 'bg-red-900 text-red-200'
|
||||
: 'bg-blue-900 text-blue-200'
|
||||
}`;
|
||||
statusMessage.classList.remove('hidden');
|
||||
}
|
||||
|
||||
interface ExtractAttachmentsMessage {
|
||||
command: 'extract-attachments';
|
||||
fileBuffers: ArrayBuffer[];
|
||||
fileNames: string[];
|
||||
}
|
||||
209
src/js/logic/extract-pages-page.ts
Normal file
209
src/js/logic/extract-pages-page.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { readFileAsArrayBuffer, formatBytes, downloadFile, parsePageRanges } from '../utils/helpers.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
interface ExtractState {
|
||||
file: File | null;
|
||||
pdfDoc: any;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const extractState: ExtractState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
totalPages: 0,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
}
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const droppedFiles = e.dataTransfer?.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) {
|
||||
handleFile(droppedFiles[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', extractPages);
|
||||
}
|
||||
|
||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
handleFile(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFile(file: File) {
|
||||
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
|
||||
showAlert('Invalid File', 'Please select a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PDF...');
|
||||
extractState.file = file;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
extractState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false,
|
||||
});
|
||||
extractState.totalPages = extractState.pdfDoc.getPageCount();
|
||||
|
||||
updateFileDisplay();
|
||||
showOptions();
|
||||
hideLoader();
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (!fileDisplayArea || !extractState.file) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = extractState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(extractState.file.size)} • ${extractState.totalPages} pages`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => resetState();
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function showOptions() {
|
||||
const extractOptions = document.getElementById('extract-options');
|
||||
const totalPagesSpan = document.getElementById('total-pages');
|
||||
|
||||
if (extractOptions) {
|
||||
extractOptions.classList.remove('hidden');
|
||||
}
|
||||
if (totalPagesSpan) {
|
||||
totalPagesSpan.textContent = extractState.totalPages.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function extractPages() {
|
||||
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement;
|
||||
if (!pagesInput || !pagesInput.value.trim()) {
|
||||
showAlert('No Pages', 'Please enter page numbers to extract.');
|
||||
return;
|
||||
}
|
||||
|
||||
const pagesToExtract = parsePageRanges(pagesInput.value, extractState.totalPages).map(i => i + 1);
|
||||
if (pagesToExtract.length === 0) {
|
||||
showAlert('Invalid Pages', 'No valid page numbers found.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Extracting pages...');
|
||||
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
const baseName = extractState.file?.name.replace('.pdf', '') || 'document';
|
||||
|
||||
for (const pageNum of pagesToExtract) {
|
||||
const newPdf = await PDFDocument.create();
|
||||
const [copiedPage] = await newPdf.copyPages(extractState.pdfDoc, [pageNum - 1]);
|
||||
newPdf.addPage(copiedPage);
|
||||
const pdfBytes = await newPdf.save();
|
||||
zip.file(`${baseName}_page_${pageNum}.pdf`, pdfBytes);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, `${baseName}_extracted_pages.zip`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Success', `Extracted ${pagesToExtract.length} page(s) successfully!`, 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error extracting pages:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to extract pages.');
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
extractState.file = null;
|
||||
extractState.pdfDoc = null;
|
||||
extractState.totalPages = 0;
|
||||
|
||||
const extractOptions = document.getElementById('extract-options');
|
||||
if (extractOptions) {
|
||||
extractOptions.classList.add('hidden');
|
||||
}
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) {
|
||||
fileDisplayArea.innerHTML = '';
|
||||
}
|
||||
|
||||
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement;
|
||||
if (pagesInput) {
|
||||
pagesInput.value = '';
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function extractPages() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageInput = document.getElementById('pages-to-extract').value;
|
||||
if (!pageInput.trim()) {
|
||||
showAlert('Invalid Input', 'Please enter page numbers to extract.');
|
||||
return;
|
||||
}
|
||||
showLoader('Extracting pages...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
const indicesToExtract = new Set();
|
||||
const ranges = pageInput.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.add(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToExtract.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (indicesToExtract.size === 0) {
|
||||
showAlert('Invalid Input', 'No valid pages selected for extraction.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
const sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
|
||||
|
||||
for (const index of sortedIndices) {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
|
||||
index as number,
|
||||
]);
|
||||
newPdf.addPage(copiedPage);
|
||||
const newPdfBytes = await newPdf.save();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'extracted-pages.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not extract pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
|
||||
export function setupFixDimensionsUI() {
|
||||
const targetSizeSelect = document.getElementById('target-size');
|
||||
const customSizeWrapper = document.getElementById('custom-size-wrapper');
|
||||
if (targetSizeSelect && customSizeWrapper) {
|
||||
targetSizeSelect.addEventListener('change', () => {
|
||||
customSizeWrapper.classList.toggle(
|
||||
'hidden',
|
||||
(targetSizeSelect as HTMLSelectElement).value !== 'Custom'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function fixDimensions() {
|
||||
const targetSizeKey = (
|
||||
document.getElementById('target-size') as HTMLSelectElement
|
||||
).value;
|
||||
const orientation = (
|
||||
document.getElementById('orientation') as HTMLSelectElement
|
||||
).value;
|
||||
|
||||
const scalingMode = (
|
||||
document.querySelector(
|
||||
'input[name="scaling-mode"]:checked'
|
||||
) as HTMLInputElement
|
||||
).value;
|
||||
const backgroundColor = hexToRgb(
|
||||
(document.getElementById('background-color') as HTMLInputElement).value
|
||||
);
|
||||
|
||||
showLoader('Standardizing pages...');
|
||||
try {
|
||||
let targetWidth, targetHeight;
|
||||
|
||||
if (targetSizeKey === 'Custom') {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const width = parseFloat(document.getElementById('custom-width').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const height = parseFloat(document.getElementById('custom-height').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const units = document.getElementById('custom-units').value;
|
||||
if (units === 'in') {
|
||||
targetWidth = width * 72;
|
||||
targetHeight = height * 72;
|
||||
} else {
|
||||
// mm
|
||||
targetWidth = width * (72 / 25.4);
|
||||
targetHeight = height * (72 / 25.4);
|
||||
}
|
||||
} else {
|
||||
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
|
||||
}
|
||||
|
||||
if (orientation === 'landscape' && targetWidth < targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
}
|
||||
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const sourcePage of sourceDoc.getPages()) {
|
||||
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||
});
|
||||
|
||||
const scaleX = targetWidth / sourceWidth;
|
||||
const scaleY = targetHeight / sourceHeight;
|
||||
const scale =
|
||||
scalingMode === 'fit'
|
||||
? Math.min(scaleX, scaleY)
|
||||
: Math.max(scaleX, scaleY);
|
||||
|
||||
const scaledWidth = sourceWidth * scale;
|
||||
const scaledHeight = sourceHeight * scale;
|
||||
|
||||
const x = (targetWidth - scaledWidth) / 2;
|
||||
const y = (targetHeight - scaledHeight) / 2;
|
||||
|
||||
newPage.drawPage(embeddedPage, {
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'standardized.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while standardizing pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
233
src/js/logic/fix-page-size-page.ts
Normal file
233
src/js/logic/fix-page-size-page.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
file: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fixPageSize() {
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetSizeKey = (document.getElementById('target-size') as HTMLSelectElement).value;
|
||||
const orientation = (document.getElementById('orientation') as HTMLSelectElement).value;
|
||||
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
|
||||
const backgroundColor = hexToRgb((document.getElementById('background-color') as HTMLInputElement).value);
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Standardizing pages...';
|
||||
|
||||
try {
|
||||
let targetWidth, targetHeight;
|
||||
|
||||
if (targetSizeKey === 'Custom') {
|
||||
const width = parseFloat((document.getElementById('custom-width') as HTMLInputElement).value);
|
||||
const height = parseFloat((document.getElementById('custom-height') as HTMLInputElement).value);
|
||||
const units = (document.getElementById('custom-units') as HTMLSelectElement).value;
|
||||
|
||||
if (units === 'in') {
|
||||
targetWidth = width * 72;
|
||||
targetHeight = height * 72;
|
||||
} else {
|
||||
// mm
|
||||
targetWidth = width * (72 / 25.4);
|
||||
targetHeight = height * (72 / 25.4);
|
||||
}
|
||||
} else {
|
||||
[targetWidth, targetHeight] = PageSizes[targetSizeKey as keyof typeof PageSizes];
|
||||
}
|
||||
|
||||
if (orientation === 'landscape' && targetWidth < targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
}
|
||||
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
const sourceDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const sourcePage of sourceDoc.getPages()) {
|
||||
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||
});
|
||||
|
||||
const scaleX = targetWidth / sourceWidth;
|
||||
const scaleY = targetHeight / sourceHeight;
|
||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
||||
|
||||
const scaledWidth = sourceWidth * scale;
|
||||
const scaledHeight = sourceHeight * scale;
|
||||
|
||||
const x = (targetWidth - scaledWidth) / 2;
|
||||
const y = (targetHeight - scaledHeight) / 2;
|
||||
|
||||
newPage.drawPage(embeddedPage, {
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'standardized.pdf'
|
||||
);
|
||||
showAlert('Success', 'Page sizes standardized successfully!', 'success', () => { resetState(); });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while standardizing pages.');
|
||||
} 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 backBtn = document.getElementById('back-to-tools');
|
||||
const targetSizeSelect = document.getElementById('target-size');
|
||||
const customSizeWrapper = document.getElementById('custom-size-wrapper');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
// Setup custom size toggle
|
||||
if (targetSizeSelect && customSizeWrapper) {
|
||||
targetSizeSelect.addEventListener('change', function () {
|
||||
customSizeWrapper.classList.toggle(
|
||||
'hidden',
|
||||
(targetSizeSelect as HTMLSelectElement).value !== 'Custom'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', fixPageSize);
|
||||
}
|
||||
});
|
||||
234
src/js/logic/flatten-pdf-page.ts
Normal file
234
src/js/logic/flatten-pdf-page.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
interface PageState {
|
||||
files: File[];
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
files: [],
|
||||
};
|
||||
|
||||
function flattenFormsInDoc(pdfDoc: PDFDocument) {
|
||||
const form = pdfDoc.getForm();
|
||||
form.flatten();
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.files = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
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 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');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
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';
|
||||
|
||||
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 = function () {
|
||||
pageState.files.splice(index, 1);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function flattenPdf() {
|
||||
if (pageState.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
|
||||
try {
|
||||
if (pageState.files.length === 1) {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Flattening PDF...';
|
||||
|
||||
const file = pageState.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
|
||||
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('getForm')) {
|
||||
// Ignore if no form found
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
|
||||
`flattened_${file.name}`
|
||||
);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
} else {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Flattening multiple PDFs...';
|
||||
|
||||
const zip = new JSZip();
|
||||
let processedCount = 0;
|
||||
|
||||
for (let i = 0; i < pageState.files.length; i++) {
|
||||
const file = pageState.files[i];
|
||||
if (loaderText) loaderText.textContent = `Flattening ${i + 1}/${pageState.files.length}: ${file.name}...`;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
|
||||
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('getForm')) {
|
||||
// Ignore if no form found
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const flattenedBytes = await pdfDoc.save();
|
||||
zip.file(`flattened_${file.name}`, flattenedBytes);
|
||||
processedCount++;
|
||||
} catch (e) {
|
||||
console.error(`Error processing ${file.name}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (processedCount > 0) {
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'flattened_pdfs.zip');
|
||||
showAlert('Success', `Processed ${processedCount} PDFs.`, 'success', () => { resetState(); });
|
||||
} else {
|
||||
showAlert('Error', 'No PDFs could be processed.');
|
||||
}
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert('Error', e.message || 'An unexpected error occurred.');
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', flattenPdf);
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', function () {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
export function flattenFormsInDoc(pdfDoc) {
|
||||
const form = pdfDoc.getForm();
|
||||
form.flatten();
|
||||
}
|
||||
|
||||
export async function flatten() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (state.files.length === 1) {
|
||||
showLoader('Flattening PDF...');
|
||||
const file = state.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
|
||||
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
} catch (e) {
|
||||
if (e.message.includes('getForm')) {
|
||||
// Ignore if no form found
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const flattenedBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([flattenedBytes as any], { type: 'application/pdf' }),
|
||||
`flattened_${file.name}`
|
||||
);
|
||||
hideLoader();
|
||||
} else {
|
||||
showLoader('Flattening multiple PDFs...');
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const zip = new JSZip();
|
||||
let processedCount = 0;
|
||||
|
||||
for (let i = 0; i < state.files.length; i++) {
|
||||
const file = state.files[i];
|
||||
showLoader(`Flattening ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
|
||||
|
||||
try {
|
||||
flattenFormsInDoc(pdfDoc);
|
||||
} catch (e) {
|
||||
if (e.message.includes('getForm')) {
|
||||
// Ignore if no form found
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const flattenedBytes = await pdfDoc.save();
|
||||
zip.file(`flattened_${file.name}`, flattenedBytes);
|
||||
processedCount++;
|
||||
} catch (e) {
|
||||
console.error(`Error processing ${file.name}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (processedCount > 0) {
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'flattened_pdfs.zip');
|
||||
showAlert('Success', `Processed ${processedCount} PDFs.`);
|
||||
} else {
|
||||
showAlert('Error', 'No PDFs could be processed.');
|
||||
}
|
||||
hideLoader();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
hideLoader();
|
||||
showAlert('Error', e.message || 'An unexpected error occurred.');
|
||||
}
|
||||
}
|
||||
@@ -2065,7 +2065,7 @@ async function renderCanvas(): Promise<void> {
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.src = `/pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}#page=${currentPageIndex + 1}&toolbar=0`
|
||||
iframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}#page=${currentPageIndex + 1}&toolbar=0`
|
||||
iframe.style.width = '100%'
|
||||
iframe.style.height = `${canvasHeight}px`
|
||||
iframe.style.border = 'none'
|
||||
|
||||
255
src/js/logic/form-filler-page.ts
Normal file
255
src/js/logic/form-filler-page.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
// Self-contained Form Filler logic for standalone page
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { getPDFDocument } from '../utils/helpers.js';
|
||||
|
||||
let viewerIframe: HTMLIFrameElement | null = null;
|
||||
let viewerReady = false;
|
||||
let currentFile: File | null = null;
|
||||
|
||||
// UI helpers
|
||||
function showLoader(message: string = 'Processing...') {
|
||||
const loader = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loader) loader.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = message;
|
||||
}
|
||||
|
||||
function hideLoader() {
|
||||
const loader = document.getElementById('loader-modal');
|
||||
if (loader) loader.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) {
|
||||
const modal = document.getElementById('alert-modal');
|
||||
const alertTitle = document.getElementById('alert-title');
|
||||
const alertMessage = document.getElementById('alert-message');
|
||||
const okBtn = document.getElementById('alert-ok');
|
||||
|
||||
if (alertTitle) alertTitle.textContent = title;
|
||||
if (alertMessage) alertMessage.textContent = message;
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
|
||||
if (okBtn) {
|
||||
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
|
||||
okBtn.replaceWith(newOkBtn);
|
||||
newOkBtn.addEventListener('click', () => {
|
||||
modal?.classList.add('hidden');
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const displayArea = document.getElementById('file-display-area');
|
||||
if (!displayArea || !currentFile) return;
|
||||
|
||||
const fileSize = currentFile.size < 1024 * 1024
|
||||
? `${(currentFile.size / 1024).toFixed(1)} KB`
|
||||
: `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`;
|
||||
|
||||
displayArea.innerHTML = `
|
||||
<div class="bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="truncate font-medium text-white">${currentFile.name}</p>
|
||||
<p class="text-gray-400 text-sm">${fileSize}</p>
|
||||
</div>
|
||||
<button id="remove-file" class="text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2" title="Remove file">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
createIcons({ icons });
|
||||
|
||||
document.getElementById('remove-file')?.addEventListener('click', () => resetState());
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
viewerIframe = null;
|
||||
viewerReady = false;
|
||||
currentFile = null;
|
||||
const displayArea = document.getElementById('file-display-area');
|
||||
if (displayArea) displayArea.innerHTML = '';
|
||||
document.getElementById('form-filler-options')?.classList.add('hidden');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
// Clear viewer
|
||||
const viewerContainer = document.getElementById('pdf-viewer-container');
|
||||
if (viewerContainer) {
|
||||
viewerContainer.innerHTML = '';
|
||||
viewerContainer.style.height = '';
|
||||
viewerContainer.style.aspectRatio = '';
|
||||
}
|
||||
|
||||
const toolUploader = document.getElementById('tool-uploader');
|
||||
const isFullWidth = localStorage.getItem('fullWidthMode') === 'true';
|
||||
if (toolUploader && !isFullWidth) {
|
||||
toolUploader.classList.remove('max-w-6xl');
|
||||
toolUploader.classList.add('max-w-2xl');
|
||||
}
|
||||
}
|
||||
|
||||
// File handling
|
||||
async function handleFileUpload(file: File) {
|
||||
if (!file || file.type !== 'application/pdf') {
|
||||
showAlert('Error', 'Please upload a valid PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
currentFile = file;
|
||||
updateFileDisplay();
|
||||
await setupFormViewer();
|
||||
}
|
||||
|
||||
async function adjustViewerHeight(file: File) {
|
||||
const viewerContainer = document.getElementById('pdf-viewer-container');
|
||||
if (!viewerContainer) return;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const loadingTask = getPDFDocument({ data: arrayBuffer });
|
||||
const pdf = await loadingTask.promise;
|
||||
const page = await pdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
|
||||
// Add ~50px for toolbar height
|
||||
const aspectRatio = viewport.width / (viewport.height + 50);
|
||||
|
||||
viewerContainer.style.height = 'auto';
|
||||
viewerContainer.style.aspectRatio = `${aspectRatio}`;
|
||||
} catch (e) {
|
||||
console.error('Error adjusting viewer height:', e);
|
||||
viewerContainer.style.height = '80vh';
|
||||
}
|
||||
}
|
||||
|
||||
async function setupFormViewer() {
|
||||
if (!currentFile) return;
|
||||
|
||||
showLoader('Loading PDF form...');
|
||||
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
|
||||
|
||||
if (!pdfViewerContainer) {
|
||||
console.error('PDF viewer container not found');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const toolUploader = document.getElementById('tool-uploader');
|
||||
const isFullWidth = localStorage.getItem('fullWidthMode') === 'true';
|
||||
if (toolUploader && !isFullWidth) {
|
||||
toolUploader.classList.remove('max-w-2xl');
|
||||
toolUploader.classList.add('max-w-6xl');
|
||||
}
|
||||
|
||||
try {
|
||||
// Apply dynamic height
|
||||
await adjustViewerHeight(currentFile);
|
||||
|
||||
pdfViewerContainer.innerHTML = '';
|
||||
|
||||
const arrayBuffer = await currentFile.arrayBuffer();
|
||||
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
viewerIframe = document.createElement('iframe');
|
||||
viewerIframe.src = `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`;
|
||||
viewerIframe.style.width = '100%';
|
||||
viewerIframe.style.height = '100%';
|
||||
viewerIframe.style.border = 'none';
|
||||
|
||||
viewerIframe.onload = () => {
|
||||
viewerReady = true;
|
||||
hideLoader();
|
||||
};
|
||||
|
||||
pdfViewerContainer.appendChild(viewerIframe);
|
||||
|
||||
const formFillerOptions = document.getElementById('form-filler-options');
|
||||
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
||||
} catch (e) {
|
||||
console.error('Critical error setting up form filler:', e);
|
||||
showAlert('Error', 'Failed to load PDF form viewer.');
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function processAndDownloadForm() {
|
||||
if (!viewerIframe || !viewerReady) {
|
||||
showAlert('Viewer not ready', 'Please wait for the form to finish loading.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const viewerWindow = viewerIframe.contentWindow;
|
||||
if (!viewerWindow) {
|
||||
console.error('Cannot access iframe window');
|
||||
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
|
||||
return;
|
||||
}
|
||||
|
||||
const viewerDoc = viewerWindow.document;
|
||||
if (!viewerDoc) {
|
||||
console.error('Cannot access iframe document');
|
||||
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadBtn = viewerDoc.getElementById('downloadButton') as HTMLButtonElement | null;
|
||||
|
||||
if (downloadBtn) {
|
||||
console.log('Clicking download button...');
|
||||
downloadBtn.click();
|
||||
} else {
|
||||
console.error('Download button not found in viewer');
|
||||
const secondaryDownload = viewerDoc.getElementById('secondaryDownload') as HTMLButtonElement | null;
|
||||
if (secondaryDownload) {
|
||||
console.log('Clicking secondary download button...');
|
||||
secondaryDownload.click();
|
||||
} else {
|
||||
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to trigger download:', e);
|
||||
showAlert('Download', 'Cannot access viewer controls. Please use the Download button in the PDF viewer toolbar above.');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
fileInput?.addEventListener('change', (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) handleFileUpload(file);
|
||||
});
|
||||
|
||||
dropZone?.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('border-indigo-500');
|
||||
});
|
||||
|
||||
dropZone?.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('border-indigo-500');
|
||||
});
|
||||
|
||||
dropZone?.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('border-indigo-500');
|
||||
const file = e.dataTransfer?.files[0];
|
||||
if (file) handleFileUpload(file);
|
||||
});
|
||||
|
||||
processBtn?.addEventListener('click', processAndDownloadForm);
|
||||
|
||||
backBtn?.addEventListener('click', () => {
|
||||
window.location.href = '../../index.html';
|
||||
});
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
let viewerIframe: HTMLIFrameElement | null = null;
|
||||
let viewerReady = false;
|
||||
|
||||
|
||||
export async function setupFormFiller() {
|
||||
if (!state.files || !state.files[0]) return;
|
||||
|
||||
showLoader('Loading PDF form...');
|
||||
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
|
||||
|
||||
if (!pdfViewerContainer) {
|
||||
console.error('PDF viewer container not found');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
pdfViewerContainer.innerHTML = '';
|
||||
|
||||
const file = state.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
const blob = new Blob([arrayBuffer as ArrayBuffer], { type: 'application/pdf' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
viewerIframe = document.createElement('iframe');
|
||||
viewerIframe.src = `/pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`;
|
||||
viewerIframe.style.width = '100%';
|
||||
viewerIframe.style.height = '100%';
|
||||
viewerIframe.style.border = 'none';
|
||||
|
||||
viewerIframe.onload = () => {
|
||||
viewerReady = true;
|
||||
hideLoader();
|
||||
};
|
||||
|
||||
pdfViewerContainer.appendChild(viewerIframe);
|
||||
|
||||
const formFillerOptions = document.getElementById('form-filler-options');
|
||||
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
||||
} catch (e) {
|
||||
console.error('Critical error setting up form filler:', e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to load PDF form viewer.'
|
||||
);
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export async function processAndDownloadForm() {
|
||||
if (!viewerIframe || !viewerReady) {
|
||||
showAlert('Viewer not ready', 'Please wait for the form to finish loading.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const viewerWindow = viewerIframe.contentWindow;
|
||||
if (!viewerWindow) {
|
||||
console.error('Cannot access iframe window');
|
||||
showAlert(
|
||||
'Download',
|
||||
'Please use the Download button in the PDF viewer toolbar above.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const viewerDoc = viewerWindow.document;
|
||||
if (!viewerDoc) {
|
||||
console.error('Cannot access iframe document');
|
||||
showAlert(
|
||||
'Download',
|
||||
'Please use the Download button in the PDF viewer toolbar above.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadBtn = viewerDoc.getElementById('downloadButton') as HTMLButtonElement | null;
|
||||
|
||||
if (downloadBtn) {
|
||||
console.log('Clicking download button...');
|
||||
downloadBtn.click();
|
||||
} else {
|
||||
console.error('Download button not found in viewer');
|
||||
const secondaryDownload = viewerDoc.getElementById('secondaryDownload') as HTMLButtonElement | null;
|
||||
if (secondaryDownload) {
|
||||
console.log('Clicking secondary download button...');
|
||||
secondaryDownload.click();
|
||||
} else {
|
||||
showAlert(
|
||||
'Download',
|
||||
'Please use the Download button in the PDF viewer toolbar above.'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to trigger download:', e);
|
||||
showAlert(
|
||||
'Download',
|
||||
'Cannot access viewer controls. Please use the Download button in the PDF viewer toolbar above.'
|
||||
);
|
||||
}
|
||||
}
|
||||
132
src/js/logic/header-footer-page.ts
Normal file
132
src/js/logic/header-footer-page.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, formatBytes, parsePageRanges } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||
|
||||
interface PageState { file: File | null; pdfDoc: PDFLibDocument | null; }
|
||||
const pageState: PageState = { file: null, pdfDoc: null };
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else { initializePage(); }
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
||||
}
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault(); dropZone.classList.remove('border-indigo-500');
|
||||
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
|
||||
});
|
||||
}
|
||||
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
|
||||
if (processBtn) processBtn.addEventListener('click', addHeaderFooter);
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); }
|
||||
|
||||
async function handleFiles(files: FileList) {
|
||||
const file = files[0];
|
||||
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
pageState.file = file;
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
const totalPagesSpan = document.getElementById('total-pages');
|
||||
if (totalPagesSpan) totalPagesSpan.textContent = String(pageState.pdfDoc.getPageCount());
|
||||
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
|
||||
finally { hideLoader(); }
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = resetState;
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null; pageState.pdfDoc = null;
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
document.getElementById('options-panel')?.classList.add('hidden');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function addHeaderFooter() {
|
||||
if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; }
|
||||
showLoader('Adding header & footer...');
|
||||
try {
|
||||
const helveticaFont = await pageState.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const allPages = pageState.pdfDoc.getPages();
|
||||
const totalPages = allPages.length;
|
||||
const margin = 40;
|
||||
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement)?.value || '10') || 10;
|
||||
const colorHex = (document.getElementById('font-color') as HTMLInputElement)?.value || '#000000';
|
||||
const fontColor = hexToRgb(colorHex);
|
||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement)?.value || '';
|
||||
const texts = {
|
||||
headerLeft: (document.getElementById('header-left') as HTMLInputElement)?.value || '',
|
||||
headerCenter: (document.getElementById('header-center') as HTMLInputElement)?.value || '',
|
||||
headerRight: (document.getElementById('header-right') as HTMLInputElement)?.value || '',
|
||||
footerLeft: (document.getElementById('footer-left') as HTMLInputElement)?.value || '',
|
||||
footerCenter: (document.getElementById('footer-center') as HTMLInputElement)?.value || '',
|
||||
footerRight: (document.getElementById('footer-right') as HTMLInputElement)?.value || '',
|
||||
};
|
||||
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
if (indicesToProcess.length === 0) throw new Error("Invalid page range specified.");
|
||||
const drawOptions = { font: helveticaFont, size: fontSize, color: rgb(fontColor.r, fontColor.g, fontColor.b) };
|
||||
|
||||
for (const pageIndex of indicesToProcess) {
|
||||
const page = allPages[pageIndex];
|
||||
const { width, height } = page.getSize();
|
||||
const pageNumber = pageIndex + 1;
|
||||
const processText = (text: string) => text.replace(/{page}/g, String(pageNumber)).replace(/{total}/g, String(totalPages));
|
||||
const processed = {
|
||||
headerLeft: processText(texts.headerLeft), headerCenter: processText(texts.headerCenter), headerRight: processText(texts.headerRight),
|
||||
footerLeft: processText(texts.footerLeft), footerCenter: processText(texts.footerCenter), footerRight: processText(texts.footerRight),
|
||||
};
|
||||
if (processed.headerLeft) page.drawText(processed.headerLeft, { ...drawOptions, x: margin, y: height - margin });
|
||||
if (processed.headerCenter) page.drawText(processed.headerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.headerCenter, fontSize) / 2, y: height - margin });
|
||||
if (processed.headerRight) page.drawText(processed.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.headerRight, fontSize), y: height - margin });
|
||||
if (processed.footerLeft) page.drawText(processed.footerLeft, { ...drawOptions, x: margin, y: margin });
|
||||
if (processed.footerCenter) page.drawText(processed.footerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.footerCenter, fontSize) / 2, y: margin });
|
||||
if (processed.footerRight) page.drawText(processed.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.footerRight, fontSize), y: margin });
|
||||
}
|
||||
const newPdfBytes = await pageState.pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'header-footer-added.pdf');
|
||||
showAlert('Success', 'Header & Footer added successfully!', 'success', () => { resetState(); });
|
||||
} catch (e: any) { console.error(e); showAlert('Error', e.message || 'Could not add header or footer.'); }
|
||||
finally { hideLoader(); }
|
||||
}
|
||||
179
src/js/logic/heic-to-pdf-page.ts
Normal file
179
src/js/logic/heic-to-pdf-page.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import heic2any from 'heic2any';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!fileDisplayArea || !fileControls || !processBtn) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
fileControls.classList.remove('hidden');
|
||||
processBtn.classList.remove('hidden');
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
fileControls.classList.add('hidden');
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one HEIC file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting HEIC to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of files) {
|
||||
const conversionResult = await heic2any({
|
||||
blob: file,
|
||||
toType: 'image/png',
|
||||
quality: 0.92,
|
||||
});
|
||||
const pngBlob = Array.isArray(conversionResult)
|
||||
? conversionResult[0]
|
||||
: conversionResult;
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_heic.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert HEIC to PDF. One of the files may be invalid.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.name.toLowerCase().endsWith('.heic') || file.name.toLowerCase().endsWith('.heif')
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only HEIC/HEIF files are allowed.');
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
files = [...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);
|
||||
}
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import heic2any from 'heic2any';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function heicToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one HEIC file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting HEIC to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const conversionResult = await heic2any({
|
||||
blob: file,
|
||||
toType: 'image/png',
|
||||
});
|
||||
const pngBlob = Array.isArray(conversionResult)
|
||||
? conversionResult[0]
|
||||
: conversionResult;
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_heic.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
321
src/js/logic/image-to-pdf-page.ts
Normal file
321
src/js/logic/image-to-pdf-page.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
}
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const droppedFiles = e.dataTransfer?.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) {
|
||||
handleFiles(droppedFiles);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput?.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
handleFiles(input.files);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFiles(newFiles: FileList) {
|
||||
const validFiles = Array.from(newFiles).filter(file =>
|
||||
file.type.startsWith('image/')
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only image files are allowed.');
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
files = [...files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const optionsDiv = document.getElementById('jpg-to-pdf-options');
|
||||
|
||||
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
fileControls.classList.remove('hidden');
|
||||
optionsDiv.classList.remove('hidden');
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
fileControls.classList.add('hidden');
|
||||
optionsDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeImageAsJpeg(imageBytes: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([imageBytes]);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
return reject(new Error('Could not get canvas context'));
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
canvas.toBlob(
|
||||
async (jpegBlob) => {
|
||||
if (!jpegBlob) {
|
||||
return reject(new Error('Canvas toBlob conversion failed.'));
|
||||
}
|
||||
const arrayBuffer = await jpegBlob.arrayBuffer();
|
||||
resolve(new Uint8Array(arrayBuffer));
|
||||
},
|
||||
'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(
|
||||
new Error(
|
||||
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
// Special handler for SVG files - must read as text
|
||||
function svgToPng(svgText: string): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const width = img.naturalWidth || img.width || 800;
|
||||
const height = img.naturalHeight || img.height || 600;
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
URL.revokeObjectURL(url);
|
||||
return reject(new Error('Could not get canvas context'));
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
canvas.toBlob(
|
||||
async (pngBlob) => {
|
||||
URL.revokeObjectURL(url);
|
||||
if (!pngBlob) {
|
||||
return reject(new Error('Canvas toBlob conversion failed.'));
|
||||
}
|
||||
const arrayBuffer = await pngBlob.arrayBuffer();
|
||||
resolve(new Uint8Array(arrayBuffer));
|
||||
},
|
||||
'image/png'
|
||||
);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('Failed to load SVG image'));
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
async function convertToPdf() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one image file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Creating PDF from images...');
|
||||
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const isSvg = file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg');
|
||||
|
||||
if (isSvg) {
|
||||
// Handle SVG files - read as text
|
||||
const svgText = await file.text();
|
||||
const pngBytes = await svgToPng(svgText);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
} else if (file.type === 'image/png') {
|
||||
// Handle PNG files
|
||||
const originalBytes = await readFileAsArrayBuffer(file);
|
||||
const pngImage = await pdfDoc.embedPng(originalBytes as Uint8Array);
|
||||
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
} else {
|
||||
// Handle JPG/other raster images
|
||||
const originalBytes = await readFileAsArrayBuffer(file);
|
||||
let jpgImage;
|
||||
|
||||
try {
|
||||
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
||||
} catch (e) {
|
||||
// Fallback: convert to JPEG via canvas
|
||||
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
||||
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
|
||||
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
|
||||
page.drawImage(jpgImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: jpgImage.width,
|
||||
height: jpgImage.height,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to process ${file.name}:`, error);
|
||||
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_images.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
showAlert('Conversion Error', e.message);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { jpgToPdf } from './jpg-to-pdf.js';
|
||||
import { pngToPdf } from './png-to-pdf.js';
|
||||
import { webpToPdf } from './webp-to-pdf.js';
|
||||
import { bmpToPdf } from './bmp-to-pdf.js';
|
||||
import { tiffToPdf } from './tiff-to-pdf.js';
|
||||
import { svgToPdf } from './svg-to-pdf.js';
|
||||
import { heicToPdf } from './heic-to-pdf.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function imageToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one image file.');
|
||||
return;
|
||||
}
|
||||
|
||||
const filesByType: { [key: string]: File[] } = {};
|
||||
|
||||
for (const file of state.files) {
|
||||
const type = file.type || '';
|
||||
if (!filesByType[type]) {
|
||||
filesByType[type] = [];
|
||||
}
|
||||
filesByType[type].push(file);
|
||||
}
|
||||
|
||||
const types = Object.keys(filesByType);
|
||||
if (types.length === 1) {
|
||||
const type = types[0];
|
||||
const originalFiles = state.files;
|
||||
|
||||
if (type === 'image/jpeg' || type === 'image/jpg') {
|
||||
state.files = filesByType[type] as File[];
|
||||
await jpgToPdf();
|
||||
} else if (type === 'image/png') {
|
||||
state.files = filesByType[type] as File[];
|
||||
await pngToPdf();
|
||||
} else if (type === 'image/webp') {
|
||||
state.files = filesByType[type] as File[];
|
||||
await webpToPdf();
|
||||
} else if (type === 'image/bmp') {
|
||||
state.files = filesByType[type] as File[];
|
||||
await bmpToPdf();
|
||||
} else if (type === 'image/tiff' || type === 'image/tif') {
|
||||
state.files = filesByType[type] as File[];
|
||||
await tiffToPdf();
|
||||
} else if (type === 'image/svg+xml') {
|
||||
state.files = filesByType[type] as File[];
|
||||
await svgToPdf();
|
||||
} else {
|
||||
const firstFile = filesByType[type][0];
|
||||
if (firstFile.name.toLowerCase().endsWith('.heic') ||
|
||||
firstFile.name.toLowerCase().endsWith('.heif')) {
|
||||
state.files = filesByType[type] as File[];
|
||||
await heicToPdf();
|
||||
} else {
|
||||
showLoader('Converting images to PDF...');
|
||||
try {
|
||||
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const file of filesByType[type]) {
|
||||
const imageBitmap = await createImageBitmap(file);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = imageBitmap.width;
|
||||
canvas.height = imageBitmap.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(imageBitmap, 0, 0);
|
||||
|
||||
const pngBlob = await new Promise<Blob>((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/png')
|
||||
);
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
imageBitmap.close();
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from-images.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert images to PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.files = originalFiles;
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Converting mixed image types to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
|
||||
const imageList = document.getElementById('image-list');
|
||||
const sortedFiles = imageList
|
||||
? Array.from(imageList.children)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
.map((li) => state.files.find((f) => f.name === li.dataset.fileName))
|
||||
.filter(Boolean)
|
||||
: state.files;
|
||||
|
||||
const qualityInput = document.getElementById('image-pdf-quality') as HTMLInputElement;
|
||||
const quality = qualityInput ? Math.max(0.3, Math.min(1.0, parseFloat(qualityInput.value))) : 0.9;
|
||||
|
||||
for (const file of sortedFiles) {
|
||||
const type = file.type || '';
|
||||
let image;
|
||||
|
||||
try {
|
||||
const imageBitmap = await createImageBitmap(file);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = imageBitmap.width;
|
||||
canvas.height = imageBitmap.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(imageBitmap, 0, 0);
|
||||
const jpegBlob = await new Promise<Blob>((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/jpeg', quality)
|
||||
);
|
||||
const jpegBytes = new Uint8Array(await jpegBlob.arrayBuffer());
|
||||
image = await pdfDoc.embedJpg(jpegBytes);
|
||||
imageBitmap.close();
|
||||
|
||||
const page = pdfDoc.addPage([image.width, image.height]);
|
||||
page.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`Failed to process ${file.name}:`, e);
|
||||
// Continue with next file
|
||||
}
|
||||
}
|
||||
|
||||
if (pdfDoc.getPageCount() === 0) {
|
||||
throw new Error(
|
||||
'No valid images could be processed. Please check your files.'
|
||||
);
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from-images.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,150 +1,81 @@
|
||||
|
||||
|
||||
import { encrypt } from './encrypt.js';
|
||||
import { decrypt } from './decrypt.js';
|
||||
import { organize } from './organize.js';
|
||||
import { rotate } from './rotate.js';
|
||||
import { addPageNumbers } from './add-page-numbers.js';
|
||||
import { pdfToJpg } from './pdf-to-jpg.js';
|
||||
import { jpgToPdf } from './jpg-to-pdf.js';
|
||||
import { scanToPdf } from './scan-to-pdf.js';
|
||||
|
||||
import { pdfToGreyscale } from './pdf-to-greyscale.js';
|
||||
import { pdfToZip } from './pdf-to-zip.js';
|
||||
import { editMetadata } from './edit-metadata.js';
|
||||
import { removeMetadata } from './remove-metadata.js';
|
||||
import { flatten } from './flatten.js';
|
||||
import { pdfToPng } from './pdf-to-png.js';
|
||||
import { pngToPdf } from './png-to-pdf.js';
|
||||
import { pdfToWebp } from './pdf-to-webp.js';
|
||||
import { webpToPdf } from './webp-to-pdf.js';
|
||||
import { deletePages, setupDeletePagesTool } from './delete-pages.js';
|
||||
import { addBlankPage } from './add-blank-page.js';
|
||||
import { extractPages } from './extract-pages.js';
|
||||
import { addWatermark, setupWatermarkUI } from './add-watermark.js';
|
||||
import { addHeaderFooter } from './add-header-footer.js';
|
||||
import { imageToPdf } from './image-to-pdf.js';
|
||||
import { changePermissions } from './change-permissions.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { pdfToMarkdown } from './pdf-to-markdown.js';
|
||||
import { txtToPdf, setupTxtToPdfTool } from './txt-to-pdf.js';
|
||||
import { invertColors } from './invert-colors.js';
|
||||
// import { viewMetadata } from './view-metadata.js';
|
||||
import { reversePages } from './reverse-pages.js';
|
||||
// import { mdToPdf } from './md-to-pdf.js';
|
||||
import { svgToPdf } from './svg-to-pdf.js';
|
||||
import { bmpToPdf } from './bmp-to-pdf.js';
|
||||
import { heicToPdf } from './heic-to-pdf.js';
|
||||
import { tiffToPdf } from './tiff-to-pdf.js';
|
||||
import { pdfToBmp } from './pdf-to-bmp.js';
|
||||
import { pdfToTiff } from './pdf-to-tiff.js';
|
||||
import { splitInHalf } from './split-in-half.js';
|
||||
import { analyzeAndDisplayDimensions } from './page-dimensions.js';
|
||||
import { nUpTool, setupNUpUI } from './n-up.js';
|
||||
import { processAndSave } from './duplicate-organize.js';
|
||||
import { combineToSinglePage } from './combine-single-page.js';
|
||||
import { fixDimensions, setupFixDimensionsUI } from './fix-dimensions.js';
|
||||
import { changeBackgroundColor } from './change-background-color.js';
|
||||
import { changeTextColor, setupTextColorTool } from './change-text-color.js';
|
||||
import { setupCompareTool } from './compare-pdfs.js';
|
||||
import { setupOcrTool } from './ocr-pdf.js';
|
||||
import { wordToPdf } from './word-to-pdf.js';
|
||||
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
|
||||
import {
|
||||
removeAnnotations,
|
||||
setupRemoveAnnotationsTool,
|
||||
} from './remove-annotations.js';
|
||||
import { setupCropperTool } from './cropper.js';
|
||||
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
|
||||
import { posterize, setupPosterizeTool } from './posterize.js';
|
||||
import {
|
||||
removeBlankPages,
|
||||
setupRemoveBlankPagesTool,
|
||||
} from './remove-blank-pages.js';
|
||||
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
||||
import { linearizePdf } from './linearize.js';
|
||||
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
|
||||
import { extractAttachments } from './extract-attachments.js';
|
||||
import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
|
||||
import { sanitizePdf } from './sanitize-pdf.js';
|
||||
import { removeRestrictions } from './remove-restrictions.js';
|
||||
import { repairPdf } from './repair-pdf.js';
|
||||
|
||||
|
||||
// import { mdToPdf } from './md-to-pdf.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { processAndSave } from './duplicate-organize.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { wordToPdf } from './word-to-pdf.js';
|
||||
|
||||
import { setupCropperTool } from './cropper.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const toolLogic = {
|
||||
|
||||
encrypt,
|
||||
decrypt,
|
||||
'remove-restrictions': removeRestrictions,
|
||||
'repair-pdf': repairPdf,
|
||||
organize,
|
||||
rotate,
|
||||
'add-page-numbers': addPageNumbers,
|
||||
'pdf-to-jpg': pdfToJpg,
|
||||
'jpg-to-pdf': jpgToPdf,
|
||||
'scan-to-pdf': scanToPdf,
|
||||
|
||||
'pdf-to-greyscale': pdfToGreyscale,
|
||||
'pdf-to-zip': pdfToZip,
|
||||
'edit-metadata': editMetadata,
|
||||
'remove-metadata': removeMetadata,
|
||||
flatten,
|
||||
'pdf-to-png': pdfToPng,
|
||||
'png-to-pdf': pngToPdf,
|
||||
'pdf-to-webp': pdfToWebp,
|
||||
'webp-to-pdf': webpToPdf,
|
||||
'delete-pages': { process: deletePages, setup: setupDeletePagesTool },
|
||||
'add-blank-page': addBlankPage,
|
||||
'extract-pages': extractPages,
|
||||
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
||||
'add-header-footer': addHeaderFooter,
|
||||
'image-to-pdf': imageToPdf,
|
||||
'change-permissions': changePermissions,
|
||||
'pdf-to-markdown': pdfToMarkdown,
|
||||
'txt-to-pdf': { process: txtToPdf, setup: setupTxtToPdfTool },
|
||||
'invert-colors': invertColors,
|
||||
'reverse-pages': reversePages,
|
||||
// 'md-to-pdf': mdToPdf,
|
||||
'svg-to-pdf': svgToPdf,
|
||||
'bmp-to-pdf': bmpToPdf,
|
||||
'heic-to-pdf': heicToPdf,
|
||||
'tiff-to-pdf': tiffToPdf,
|
||||
'pdf-to-bmp': pdfToBmp,
|
||||
'pdf-to-tiff': pdfToTiff,
|
||||
'split-in-half': splitInHalf,
|
||||
'page-dimensions': analyzeAndDisplayDimensions,
|
||||
'n-up': { process: nUpTool, setup: setupNUpUI },
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
'duplicate-organize': { process: processAndSave },
|
||||
'combine-single-page': combineToSinglePage,
|
||||
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
|
||||
'change-background-color': changeBackgroundColor,
|
||||
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
|
||||
'compare-pdfs': { setup: setupCompareTool },
|
||||
'ocr-pdf': { setup: setupOcrTool },
|
||||
'word-to-pdf': wordToPdf,
|
||||
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
|
||||
'remove-annotations': {
|
||||
process: removeAnnotations,
|
||||
setup: setupRemoveAnnotationsTool,
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
cropper: { setup: setupCropperTool },
|
||||
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller },
|
||||
posterize: { process: posterize, setup: setupPosterizeTool },
|
||||
'remove-blank-pages': {
|
||||
process: removeBlankPages,
|
||||
setup: setupRemoveBlankPagesTool,
|
||||
},
|
||||
'alternate-merge': {
|
||||
process: alternateMerge,
|
||||
setup: setupAlternateMergeTool,
|
||||
},
|
||||
linearize: linearizePdf,
|
||||
'add-attachments': {
|
||||
process: addAttachments,
|
||||
setup: setupAddAttachmentsTool,
|
||||
},
|
||||
'extract-attachments': extractAttachments,
|
||||
'edit-attachments': {
|
||||
process: editAttachments,
|
||||
setup: setupEditAttachmentsTool,
|
||||
},
|
||||
'sanitize-pdf': sanitizePdf,
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
131
src/js/logic/invert-colors-page.ts
Normal file
131
src/js/logic/invert-colors-page.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
interface PageState { file: File | null; pdfDoc: PDFLibDocument | null; }
|
||||
const pageState: PageState = { file: null, pdfDoc: null };
|
||||
|
||||
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); }
|
||||
else { initializePage(); }
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
||||
}
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault(); dropZone.classList.remove('border-indigo-500');
|
||||
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
|
||||
});
|
||||
}
|
||||
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
|
||||
if (processBtn) processBtn.addEventListener('click', invertColors);
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); }
|
||||
|
||||
async function handleFiles(files: FileList) {
|
||||
const file = files[0];
|
||||
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
pageState.file = file;
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
|
||||
finally { hideLoader(); }
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = resetState;
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null; pageState.pdfDoc = null;
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
document.getElementById('options-panel')?.classList.add('hidden');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function invertColors() {
|
||||
if (!pageState.pdfDoc || !pageState.file) { showAlert('Error', 'Please upload a PDF file first.'); return; }
|
||||
showLoader('Inverting PDF colors...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdfBytes = await pageState.pdfDoc.save();
|
||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdfjsDoc.numPages}...`);
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
data[j] = 255 - data[j];
|
||||
data[j + 1] = 255 - data[j + 1];
|
||||
data[j + 2] = 255 - data[j + 2];
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
|
||||
canvas.toBlob((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
|
||||
reader.readAsArrayBuffer(blob!);
|
||||
}, 'image/png')
|
||||
);
|
||||
|
||||
const image = await newPdfDoc.embedPng(pngImageBytes);
|
||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'inverted.pdf');
|
||||
showAlert('Success', 'Colors inverted successfully!', 'success', () => { resetState(); });
|
||||
} catch (e) { console.error(e); showAlert('Error', 'Could not invert PDF colors.'); }
|
||||
finally { hideLoader(); }
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
|
||||
export async function invertColors() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Inverting PDF colors...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
data[j] = 255 - data[j]; // red
|
||||
data[j + 1] = 255 - data[j + 1]; // green
|
||||
data[j + 2] = 255 - data[j + 2]; // blue
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngImageBytes = await new Promise((resolve) =>
|
||||
canvas.toBlob((blob) => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png')
|
||||
);
|
||||
|
||||
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||
newPage.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'inverted.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not invert PDF colors.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,9 @@ function initializePage() {
|
||||
}
|
||||
});
|
||||
|
||||
dropZone.addEventListener('click', () => {
|
||||
fileInput?.click();
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,7 +76,6 @@ function handleFileUpload(e: Event) {
|
||||
if (input.files && input.files.length > 0) {
|
||||
handleFiles(input.files);
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function handleFiles(newFiles: FileList) {
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
/**
|
||||
* Takes any image byte array and uses the browser's canvas to convert it
|
||||
* into a standard, web-friendly (baseline, sRGB) JPEG byte array.
|
||||
* @param {Uint8Array} imageBytes The raw bytes of the image file.
|
||||
* @returns {Promise<Uint8Array>} A promise that resolves with the sanitized JPEG bytes.
|
||||
*/
|
||||
function sanitizeImageAsJpeg(imageBytes: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([imageBytes]);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
canvas.toBlob(
|
||||
async (jpegBlob) => {
|
||||
if (!jpegBlob) {
|
||||
return reject(new Error('Canvas toBlob conversion failed.'));
|
||||
}
|
||||
const arrayBuffer = await jpegBlob.arrayBuffer();
|
||||
resolve(new Uint8Array(arrayBuffer));
|
||||
},
|
||||
'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(
|
||||
new Error(
|
||||
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
export async function jpgToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one JPG file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Creating PDF from JPGs...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const file of state.files) {
|
||||
const originalBytes = await readFileAsArrayBuffer(file);
|
||||
let jpgImage;
|
||||
|
||||
try {
|
||||
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
||||
} catch (e) {
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
showAlert(
|
||||
`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`
|
||||
);
|
||||
try {
|
||||
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
||||
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||
} catch (fallbackError) {
|
||||
console.error(
|
||||
`Failed to process ${file.name} after sanitization:`,
|
||||
fallbackError
|
||||
);
|
||||
throw new Error(
|
||||
`Could not process "${file.name}". The file may be corrupted.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
|
||||
page.drawImage(jpgImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: jpgImage.width,
|
||||
height: jpgImage.height,
|
||||
});
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_jpgs.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Conversion Error', e.message);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
240
src/js/logic/linearize-pdf-page.ts
Normal file
240
src/js/logic/linearize-pdf-page.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
interface PageState {
|
||||
files: File[];
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
files: [],
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.files = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
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 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');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
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';
|
||||
|
||||
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 = function () {
|
||||
pageState.files.splice(index, 1);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
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`;
|
||||
|
||||
if (loaderText) loaderText.textContent = `Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`;
|
||||
|
||||
try {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
const args = [inputPath, '--linearize', outputPath];
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', linearizePdf);
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', function () {
|
||||
resetState();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
import createModule from '@neslinesli93/qpdf-wasm';
|
||||
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
|
||||
import { state } from '../state';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
let qpdfInstance: any = null;
|
||||
|
||||
async function initializeQpdf() {
|
||||
if (qpdfInstance) {
|
||||
return qpdfInstance;
|
||||
}
|
||||
showLoader('Initializing optimization engine...');
|
||||
try {
|
||||
qpdfInstance = await createModule({
|
||||
locateFile: () => '/qpdf.wasm',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize qpdf-wasm:', error);
|
||||
showAlert(
|
||||
'Initialization Error',
|
||||
'Could not load the optimization engine. Please refresh the page and try again.'
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
return qpdfInstance;
|
||||
}
|
||||
|
||||
export async function linearizePdf() {
|
||||
// Check if there are files and at least one PDF
|
||||
const pdfFiles = state.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;
|
||||
}
|
||||
|
||||
showLoader('Optimizing PDFs for web view (linearizing)...');
|
||||
const zip = new JSZip(); // Create a JSZip instance
|
||||
let qpdf: any;
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
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`;
|
||||
|
||||
showLoader(`Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`);
|
||||
|
||||
try {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
const args = [inputPath, '--linearize', outputPath];
|
||||
|
||||
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);
|
||||
// Optionally add an error marker/file to the zip? For now, we just skip.
|
||||
} finally {
|
||||
// Clean up WASM filesystem for this file
|
||||
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.');
|
||||
}
|
||||
|
||||
showLoader('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);
|
||||
} catch (error: any) {
|
||||
console.error('Linearization process error:', error);
|
||||
showAlert(
|
||||
'Linearization Failed',
|
||||
`An error occurred: ${error.message || 'Unknown error'}.`
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -435,7 +435,7 @@ export async function refreshMergeUI() {
|
||||
if (!fileModeBtn || !pageModeBtn || !filePanel || !pagePanel || !fileList) return;
|
||||
|
||||
fileList.textContent = ''; // Clear list safely
|
||||
(state.files as File[]).forEach((f) => {
|
||||
(state.files as File[]).forEach((f, index) => {
|
||||
const doc = mergeState.pdfDocs[f.name];
|
||||
const pageCount = doc ? doc.numPages : 'N/A';
|
||||
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
@@ -461,7 +461,10 @@ export async function refreshMergeUI() {
|
||||
mainDiv.append(nameSpan, dragHandle);
|
||||
|
||||
const rangeDiv = document.createElement('div');
|
||||
rangeDiv.className = 'mt-2';
|
||||
rangeDiv.className = 'mt-2 flex items-center gap-2';
|
||||
|
||||
const inputWrapper = document.createElement('div');
|
||||
inputWrapper.className = 'flex-1';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `range-${safeFileName}`;
|
||||
@@ -475,11 +478,24 @@ export async function refreshMergeUI() {
|
||||
'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
||||
input.placeholder = 'Leave blank for all pages';
|
||||
|
||||
rangeDiv.append(label, input);
|
||||
inputWrapper.append(label, input);
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 self-end';
|
||||
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
deleteBtn.title = 'Remove file';
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
state.files = state.files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
rangeDiv.append(inputWrapper, deleteBtn);
|
||||
li.append(mainDiv, rangeDiv);
|
||||
fileList.appendChild(li);
|
||||
});
|
||||
|
||||
createIcons({ icons });
|
||||
initializeFileListSortable();
|
||||
|
||||
const newFileModeBtn = fileModeBtn.cloneNode(true) as HTMLElement;
|
||||
@@ -558,7 +574,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
await updateUI();
|
||||
}
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
@@ -584,13 +599,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
dropZone.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
266
src/js/logic/n-up-pdf-page.ts
Normal file
266
src/js/logic/n-up-pdf-page.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
|
||||
interface NUpState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
}
|
||||
|
||||
const pageState: NUpState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
try {
|
||||
showLoader('Loading PDF...');
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
throwOnInvalidObject: false
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageCount} pages`;
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
resetState();
|
||||
}
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function nUpTool() {
|
||||
if (!pageState.pdfDoc || !pageState.file) {
|
||||
showAlert('Error', 'Please upload a PDF first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const n = parseInt((document.getElementById('pages-per-sheet') as HTMLSelectElement).value);
|
||||
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes;
|
||||
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
|
||||
const useMargins = (document.getElementById('add-margins') as HTMLInputElement).checked;
|
||||
const addBorder = (document.getElementById('add-border') as HTMLInputElement).checked;
|
||||
const borderColor = hexToRgb((document.getElementById('border-color') as HTMLInputElement).value);
|
||||
|
||||
showLoader('Creating N-Up PDF...');
|
||||
|
||||
try {
|
||||
const sourceDoc = pageState.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
|
||||
const gridDims: Record<number, [number, number]> = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] };
|
||||
const dims = gridDims[n];
|
||||
|
||||
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
|
||||
|
||||
if (orientation === 'auto') {
|
||||
const firstPage = sourcePages[0];
|
||||
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
||||
orientation = isSourceLandscape && dims[0] > dims[1] ? 'landscape' : 'portrait';
|
||||
}
|
||||
|
||||
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||
}
|
||||
|
||||
const margin = useMargins ? 36 : 0;
|
||||
const gutter = useMargins ? 10 : 0;
|
||||
|
||||
const usableWidth = pageWidth - margin * 2;
|
||||
const usableHeight = pageHeight - margin * 2;
|
||||
|
||||
for (let i = 0; i < sourcePages.length; i += n) {
|
||||
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
|
||||
const chunk = sourcePages.slice(i, i + n);
|
||||
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
||||
|
||||
const cellWidth = (usableWidth - gutter * (dims[0] - 1)) / dims[0];
|
||||
const cellHeight = (usableHeight - gutter * (dims[1] - 1)) / dims[1];
|
||||
|
||||
for (let j = 0; j < chunk.length; j++) {
|
||||
const sourcePage = chunk[j];
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
const scale = Math.min(
|
||||
cellWidth / embeddedPage.width,
|
||||
cellHeight / embeddedPage.height
|
||||
);
|
||||
const scaledWidth = embeddedPage.width * scale;
|
||||
const scaledHeight = embeddedPage.height * scale;
|
||||
|
||||
const row = Math.floor(j / dims[0]);
|
||||
const col = j % dims[0];
|
||||
const cellX = margin + col * (cellWidth + gutter);
|
||||
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
||||
|
||||
const x = cellX + (cellWidth - scaledWidth) / 2;
|
||||
const y = cellY + (cellHeight - scaledHeight) / 2;
|
||||
|
||||
outputPage.drawPage(embeddedPage, {
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
|
||||
if (addBorder) {
|
||||
outputPage.drawRectangle({
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
||||
borderWidth: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
|
||||
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`${originalName}_${n}-up.pdf`
|
||||
);
|
||||
|
||||
showAlert('Success', 'N-Up PDF created successfully!', 'success', function () {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const addBorderCheckbox = document.getElementById('add-border');
|
||||
const borderColorWrapper = document.getElementById('border-color-wrapper');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (addBorderCheckbox && borderColorWrapper) {
|
||||
addBorderCheckbox.addEventListener('change', function () {
|
||||
borderColorWrapper.classList.toggle('hidden', !(addBorderCheckbox as HTMLInputElement).checked);
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', nUpTool);
|
||||
}
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
|
||||
export function setupNUpUI() {
|
||||
const addBorderCheckbox = document.getElementById('add-border');
|
||||
const borderColorWrapper = document.getElementById('border-color-wrapper');
|
||||
if (addBorderCheckbox && borderColorWrapper) {
|
||||
addBorderCheckbox.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function nUpTool() {
|
||||
// 1. Gather all options from the UI
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const n = parseInt(document.getElementById('pages-per-sheet').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageSizeKey = document.getElementById('output-page-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
let orientation = document.getElementById('output-orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const useMargins = document.getElementById('add-margins').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const addBorder = document.getElementById('add-border').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const borderColor = hexToRgb(document.getElementById('border-color').value);
|
||||
|
||||
showLoader('Creating N-Up PDF...');
|
||||
try {
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
|
||||
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
|
||||
|
||||
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
|
||||
|
||||
if (orientation === 'auto') {
|
||||
const firstPage = sourcePages[0];
|
||||
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
||||
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
|
||||
orientation =
|
||||
isSourceLandscape && gridDims[0] > gridDims[1]
|
||||
? 'landscape'
|
||||
: 'portrait';
|
||||
}
|
||||
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||
}
|
||||
|
||||
const margin = useMargins ? 36 : 0;
|
||||
const gutter = useMargins ? 10 : 0;
|
||||
|
||||
const usableWidth = pageWidth - margin * 2;
|
||||
const usableHeight = pageHeight - margin * 2;
|
||||
|
||||
// Loop through the source pages in chunks of 'n'
|
||||
for (let i = 0; i < sourcePages.length; i += n) {
|
||||
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
|
||||
const chunk = sourcePages.slice(i, i + n);
|
||||
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
||||
|
||||
// Calculate dimensions of each cell in the grid
|
||||
const cellWidth =
|
||||
(usableWidth - gutter * (gridDims[0] - 1)) / gridDims[0];
|
||||
const cellHeight =
|
||||
(usableHeight - gutter * (gridDims[1] - 1)) / gridDims[1];
|
||||
|
||||
for (let j = 0; j < chunk.length; j++) {
|
||||
const sourcePage = chunk[j];
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
||||
const scale = Math.min(
|
||||
cellWidth / embeddedPage.width,
|
||||
cellHeight / embeddedPage.height
|
||||
);
|
||||
const scaledWidth = embeddedPage.width * scale;
|
||||
const scaledHeight = embeddedPage.height * scale;
|
||||
|
||||
// Calculate position (x, y) for this cell
|
||||
const row = Math.floor(j / gridDims[0]);
|
||||
const col = j % gridDims[0];
|
||||
const cellX = margin + col * (cellWidth + gutter);
|
||||
const cellY =
|
||||
pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
||||
|
||||
// Center the page within its cell
|
||||
const x = cellX + (cellWidth - scaledWidth) / 2;
|
||||
const y = cellY + (cellHeight - scaledHeight) / 2;
|
||||
|
||||
outputPage.drawPage(embeddedPage, {
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
|
||||
if (addBorder) {
|
||||
outputPage.drawRectangle({
|
||||
x,
|
||||
y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
||||
borderWidth: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
`n-up_${n}.pdf`
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
567
src/js/logic/ocr-pdf-page.ts
Normal file
567
src/js/logic/ocr-pdf-page.ts
Normal file
@@ -0,0 +1,567 @@
|
||||
import { tesseractLanguages } from '../config/tesseract-languages.js';
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
|
||||
import Tesseract from 'tesseract.js';
|
||||
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { getFontForLanguage } from '../utils/font-loader.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
interface Word {
|
||||
text: string;
|
||||
bbox: { x0: number; y0: number; x1: number; y1: number };
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
interface OcrState {
|
||||
file: File | null;
|
||||
searchablePdfBytes: Uint8Array | null;
|
||||
}
|
||||
|
||||
const pageState: OcrState = {
|
||||
file: null,
|
||||
searchablePdfBytes: null,
|
||||
};
|
||||
|
||||
const whitelistPresets: Record<string, string> = {
|
||||
alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"',
|
||||
'numbers-currency': '0123456789$€£¥.,- ',
|
||||
'letters-only': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ',
|
||||
'numbers-only': '0123456789',
|
||||
invoice: '0123456789$.,/-#: ',
|
||||
forms: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,()-_/@#:',
|
||||
};
|
||||
|
||||
function parseHOCR(hocrText: string): Word[] {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(hocrText, 'text/html');
|
||||
const words: Word[] = [];
|
||||
|
||||
const wordElements = doc.querySelectorAll('.ocrx_word');
|
||||
|
||||
wordElements.forEach(function (wordEl) {
|
||||
const titleAttr = wordEl.getAttribute('title');
|
||||
const text = wordEl.textContent?.trim() || '';
|
||||
|
||||
if (!titleAttr || !text) return;
|
||||
|
||||
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
|
||||
const confMatch = titleAttr.match(/x_wconf (\d+)/);
|
||||
|
||||
if (bboxMatch) {
|
||||
words.push({
|
||||
text: text,
|
||||
bbox: {
|
||||
x0: parseInt(bboxMatch[1]),
|
||||
y0: parseInt(bboxMatch[2]),
|
||||
x1: parseInt(bboxMatch[3]),
|
||||
y1: parseInt(bboxMatch[4]),
|
||||
},
|
||||
confidence: confMatch ? parseInt(confMatch[1]) : 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
function binarizeCanvas(ctx: CanvasRenderingContext2D) {
|
||||
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const brightness = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||||
const color = brightness > 128 ? 255 : 0;
|
||||
data[i] = data[i + 1] = data[i + 2] = color;
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
function updateProgress(status: string, progress: number) {
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressStatus = document.getElementById('progress-status');
|
||||
const progressLog = document.getElementById('progress-log');
|
||||
|
||||
if (!progressBar || !progressStatus || !progressLog) return;
|
||||
|
||||
progressStatus.textContent = status;
|
||||
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
|
||||
|
||||
const logMessage = `Status: ${status}`;
|
||||
progressLog.textContent += logMessage + '\n';
|
||||
progressLog.scrollTop = progressLog.scrollHeight;
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.searchablePdfBytes = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const ocrProgress = document.getElementById('ocr-progress');
|
||||
if (ocrProgress) ocrProgress.classList.add('hidden');
|
||||
|
||||
const ocrResults = document.getElementById('ocr-results');
|
||||
if (ocrResults) ocrResults.classList.add('hidden');
|
||||
|
||||
const progressLog = document.getElementById('progress-log');
|
||||
if (progressLog) progressLog.textContent = '';
|
||||
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
if (progressBar) progressBar.style.width = '0%';
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
// Reset selected languages
|
||||
const langCheckboxes = document.querySelectorAll('.lang-checkbox') as NodeListOf<HTMLInputElement>;
|
||||
langCheckboxes.forEach(function (cb) { cb.checked = false; });
|
||||
|
||||
const selectedLangsDisplay = document.getElementById('selected-langs-display');
|
||||
if (selectedLangsDisplay) selectedLangsDisplay.textContent = 'None';
|
||||
|
||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
||||
if (processBtn) processBtn.disabled = true;
|
||||
}
|
||||
|
||||
async function runOCR() {
|
||||
const selectedLangs = Array.from(
|
||||
document.querySelectorAll('.lang-checkbox:checked')
|
||||
).map(function (cb) { return (cb as HTMLInputElement).value; });
|
||||
|
||||
const scale = parseFloat(
|
||||
(document.getElementById('ocr-resolution') as HTMLSelectElement).value
|
||||
);
|
||||
const binarize = (document.getElementById('ocr-binarize') as HTMLInputElement).checked;
|
||||
const whitelist = (document.getElementById('ocr-whitelist') as HTMLInputElement).value;
|
||||
|
||||
if (selectedLangs.length === 0) {
|
||||
showAlert('No Languages Selected', 'Please select at least one language for OCR.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const langString = selectedLangs.join('+');
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const ocrProgress = document.getElementById('ocr-progress');
|
||||
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
if (ocrProgress) ocrProgress.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const worker = await Tesseract.createWorker(langString, 1, {
|
||||
logger: function (m: { status: string; progress: number }) {
|
||||
updateProgress(m.status, m.progress || 0);
|
||||
},
|
||||
});
|
||||
|
||||
await worker.setParameters({
|
||||
tessjs_create_hocr: '1',
|
||||
tessedit_pageseg_mode: Tesseract.PSM.AUTO,
|
||||
});
|
||||
|
||||
if (whitelist) {
|
||||
await worker.setParameters({
|
||||
tessedit_char_whitelist: whitelist,
|
||||
});
|
||||
}
|
||||
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
const pdf = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
newPdfDoc.registerFontkit(fontkit);
|
||||
|
||||
updateProgress('Loading fonts...', 0);
|
||||
|
||||
const cjkLangs = ['jpn', 'chi_sim', 'chi_tra', 'kor'];
|
||||
const indicLangs = ['hin', 'ben', 'guj', 'kan', 'mal', 'ori', 'pan', 'tam', 'tel', 'sin'];
|
||||
const priorityLangs = [...cjkLangs, ...indicLangs, 'ara', 'rus', 'ukr'];
|
||||
|
||||
const primaryLang = selectedLangs.find(function (l) { return priorityLangs.includes(l); }) || selectedLangs[0] || 'eng';
|
||||
|
||||
const hasCJK = selectedLangs.some(function (l) { return cjkLangs.includes(l); });
|
||||
const hasIndic = selectedLangs.some(function (l) { return indicLangs.includes(l); });
|
||||
const hasLatin = selectedLangs.some(function (l) { return !priorityLangs.includes(l); }) || selectedLangs.includes('eng');
|
||||
const isIndicPlusLatin = hasIndic && hasLatin && !hasCJK;
|
||||
|
||||
let primaryFont;
|
||||
let latinFont;
|
||||
|
||||
try {
|
||||
if (isIndicPlusLatin) {
|
||||
const [scriptFontBytes, latinFontBytes] = await Promise.all([
|
||||
getFontForLanguage(primaryLang),
|
||||
getFontForLanguage('eng')
|
||||
]);
|
||||
primaryFont = await newPdfDoc.embedFont(scriptFontBytes, { subset: false });
|
||||
latinFont = await newPdfDoc.embedFont(latinFontBytes, { subset: false });
|
||||
} else {
|
||||
const fontBytes = await getFontForLanguage(primaryLang);
|
||||
primaryFont = await newPdfDoc.embedFont(fontBytes, { subset: false });
|
||||
latinFont = primaryFont;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Font loading failed, falling back to Helvetica', e);
|
||||
primaryFont = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
latinFont = primaryFont;
|
||||
showAlert('Font Warning', 'Could not load the specific font for this language. Some characters may not appear correctly.');
|
||||
}
|
||||
|
||||
let fullText = '';
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
updateProgress(`Processing page ${i} of ${pdf.numPages}`, (i - 1) / pdf.numPages);
|
||||
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d')!;
|
||||
|
||||
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
||||
|
||||
if (binarize) {
|
||||
binarizeCanvas(context);
|
||||
}
|
||||
|
||||
const result = await worker.recognize(canvas, {}, { text: true, hocr: true });
|
||||
const data = result.data;
|
||||
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
|
||||
const pngImageBytes = await new Promise<Uint8Array>(function (resolve) {
|
||||
canvas.toBlob(function (blob) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
resolve(new Uint8Array(reader.result as ArrayBuffer));
|
||||
};
|
||||
reader.readAsArrayBuffer(blob!);
|
||||
}, 'image/png');
|
||||
});
|
||||
|
||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes);
|
||||
newPage.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
|
||||
if (data.hocr) {
|
||||
const words = parseHOCR(data.hocr);
|
||||
|
||||
words.forEach(function (word: Word) {
|
||||
const { x0, y0, x1, y1 } = word.bbox;
|
||||
const text = word.text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '');
|
||||
|
||||
if (!text.trim()) return;
|
||||
|
||||
const hasNonLatin = /[^\u0000-\u007F]/.test(text);
|
||||
const font = hasNonLatin ? primaryFont : latinFont;
|
||||
|
||||
if (!font) {
|
||||
console.warn(`Font not available for text: "${text}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const bboxWidth = x1 - x0;
|
||||
const bboxHeight = y1 - y0;
|
||||
|
||||
if (bboxWidth <= 0 || bboxHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fontSize = bboxHeight * 0.9;
|
||||
try {
|
||||
let textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
while (textWidth > bboxWidth && fontSize > 1) {
|
||||
fontSize -= 0.5;
|
||||
textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Could not calculate text width for "${text}":`, error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
newPage.drawText(text, {
|
||||
x: x0,
|
||||
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
|
||||
font,
|
||||
size: fontSize,
|
||||
color: rgb(0, 0, 0),
|
||||
opacity: 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`Could not draw text "${text}":`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fullText += data.text + '\n\n';
|
||||
}
|
||||
|
||||
await worker.terminate();
|
||||
|
||||
pageState.searchablePdfBytes = await newPdfDoc.save();
|
||||
|
||||
const ocrResults = document.getElementById('ocr-results');
|
||||
if (ocrProgress) ocrProgress.classList.add('hidden');
|
||||
if (ocrResults) ocrResults.classList.remove('hidden');
|
||||
|
||||
createIcons({ icons });
|
||||
|
||||
const textOutput = document.getElementById('ocr-text-output') as HTMLTextAreaElement;
|
||||
if (textOutput) textOutput.value = fullText.trim();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('OCR Error', 'An error occurred during the OCR process. The worker may have failed to load. Please try again.');
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
if (ocrProgress) ocrProgress.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateLanguageList() {
|
||||
const langList = document.getElementById('lang-list');
|
||||
if (!langList) return;
|
||||
|
||||
langList.innerHTML = '';
|
||||
|
||||
Object.entries(tesseractLanguages).forEach(function ([code, name]) {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'flex items-center gap-2 p-2 rounded-md hover:bg-gray-700 cursor-pointer';
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = code;
|
||||
checkbox.className = 'lang-checkbox w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||
|
||||
label.append(checkbox);
|
||||
label.append(document.createTextNode(' ' + name));
|
||||
langList.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const langSearch = document.getElementById('lang-search') as HTMLInputElement;
|
||||
const langList = document.getElementById('lang-list');
|
||||
const selectedLangsDisplay = document.getElementById('selected-langs-display');
|
||||
const presetSelect = document.getElementById('whitelist-preset') as HTMLSelectElement;
|
||||
const whitelistInput = document.getElementById('ocr-whitelist') as HTMLInputElement;
|
||||
const copyBtn = document.getElementById('copy-text-btn');
|
||||
const downloadTxtBtn = document.getElementById('download-txt-btn');
|
||||
const downloadPdfBtn = document.getElementById('download-searchable-pdf');
|
||||
|
||||
populateLanguageList();
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Language search
|
||||
if (langSearch && langList) {
|
||||
langSearch.addEventListener('input', function () {
|
||||
const searchTerm = langSearch.value.toLowerCase();
|
||||
langList.querySelectorAll('label').forEach(function (label) {
|
||||
(label as HTMLElement).style.display = label.textContent?.toLowerCase().includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
langList.addEventListener('change', function () {
|
||||
const selected = Array.from(
|
||||
langList.querySelectorAll('.lang-checkbox:checked')
|
||||
).map(function (cb) {
|
||||
return tesseractLanguages[(cb as HTMLInputElement).value as keyof typeof tesseractLanguages];
|
||||
});
|
||||
|
||||
if (selectedLangsDisplay) {
|
||||
selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None';
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.disabled = selected.length === 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Whitelist preset
|
||||
if (presetSelect && whitelistInput) {
|
||||
presetSelect.addEventListener('change', function () {
|
||||
const preset = presetSelect.value;
|
||||
if (preset && preset !== 'custom') {
|
||||
whitelistInput.value = whitelistPresets[preset] || '';
|
||||
whitelistInput.disabled = true;
|
||||
} else {
|
||||
whitelistInput.disabled = false;
|
||||
if (preset === '') {
|
||||
whitelistInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Details toggle
|
||||
document.querySelectorAll('details').forEach(function (details) {
|
||||
details.addEventListener('toggle', function () {
|
||||
const icon = details.querySelector('.details-icon') as HTMLElement;
|
||||
if (icon) {
|
||||
icon.style.transform = (details as HTMLDetailsElement).open ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Process button
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', runOCR);
|
||||
}
|
||||
|
||||
// Copy button
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener('click', function () {
|
||||
const textOutput = document.getElementById('ocr-text-output') as HTMLTextAreaElement;
|
||||
if (textOutput) {
|
||||
navigator.clipboard.writeText(textOutput.value).then(function () {
|
||||
copyBtn.innerHTML = '<i data-lucide="check" class="w-4 h-4 text-green-400"></i>';
|
||||
createIcons({ icons });
|
||||
|
||||
setTimeout(function () {
|
||||
copyBtn.innerHTML = '<i data-lucide="clipboard-copy" class="w-4 h-4 text-gray-300"></i>';
|
||||
createIcons({ icons });
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Download txt
|
||||
if (downloadTxtBtn) {
|
||||
downloadTxtBtn.addEventListener('click', function () {
|
||||
const textOutput = document.getElementById('ocr-text-output') as HTMLTextAreaElement;
|
||||
if (textOutput) {
|
||||
const blob = new Blob([textOutput.value], { type: 'text/plain' });
|
||||
downloadFile(blob, 'ocr-text.txt');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Download PDF
|
||||
if (downloadPdfBtn) {
|
||||
downloadPdfBtn.addEventListener('click', function () {
|
||||
if (pageState.searchablePdfBytes) {
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pageState.searchablePdfBytes)], { type: 'application/pdf' }),
|
||||
'searchable.pdf'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,421 +0,0 @@
|
||||
import { tesseractLanguages } from '../config/tesseract-languages.js';
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import Tesseract from 'tesseract.js';
|
||||
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||
import fontkit from '@pdf-lib/fontkit';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
import type { Word } from '../types/index.js';
|
||||
|
||||
let searchablePdfBytes: Uint8Array | null = null;
|
||||
|
||||
import { getFontForLanguage } from '../utils/font-loader.js';
|
||||
|
||||
|
||||
// function sanitizeTextForWinAnsi(text: string): string {
|
||||
// // Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
|
||||
// return text
|
||||
// .replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
|
||||
// .replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
||||
// }
|
||||
|
||||
function parseHOCR(hocrText: string) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(hocrText, 'text/html');
|
||||
const words = [];
|
||||
|
||||
// Find all word elements in hOCR
|
||||
const wordElements = doc.querySelectorAll('.ocrx_word');
|
||||
|
||||
wordElements.forEach((wordEl) => {
|
||||
const titleAttr = wordEl.getAttribute('title');
|
||||
const text = wordEl.textContent?.trim() || '';
|
||||
|
||||
if (!titleAttr || !text) return;
|
||||
|
||||
// Parse bbox coordinates from title attribute
|
||||
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
|
||||
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
|
||||
const confMatch = titleAttr.match(/x_wconf (\d+)/);
|
||||
|
||||
if (bboxMatch) {
|
||||
words.push({
|
||||
text: text,
|
||||
bbox: {
|
||||
x0: parseInt(bboxMatch[1]),
|
||||
y0: parseInt(bboxMatch[2]),
|
||||
x1: parseInt(bboxMatch[3]),
|
||||
y1: parseInt(bboxMatch[4]),
|
||||
},
|
||||
confidence: confMatch ? parseInt(confMatch[1]) : 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
function binarizeCanvas(ctx: CanvasRenderingContext2D) {
|
||||
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// A simple luminance-based threshold for determining black or white
|
||||
const brightness =
|
||||
0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||||
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
||||
data[i] = data[i + 1] = data[i + 2] = color;
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
function updateProgress(status: string, progress: number) {
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressStatus = document.getElementById('progress-status');
|
||||
const progressLog = document.getElementById('progress-log');
|
||||
|
||||
if (!progressBar || !progressStatus || !progressLog) return;
|
||||
|
||||
progressStatus.textContent = status;
|
||||
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
|
||||
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
|
||||
|
||||
const logMessage = `Status: ${status}`;
|
||||
progressLog.textContent += logMessage + '\n';
|
||||
progressLog.scrollTop = progressLog.scrollHeight;
|
||||
}
|
||||
|
||||
async function runOCR() {
|
||||
const selectedLangs = Array.from(
|
||||
document.querySelectorAll('.lang-checkbox:checked')
|
||||
).map((cb) => (cb as HTMLInputElement).value);
|
||||
const scale = parseFloat(
|
||||
(document.getElementById('ocr-resolution') as HTMLSelectElement).value
|
||||
);
|
||||
const binarize = (document.getElementById('ocr-binarize') as HTMLInputElement)
|
||||
.checked;
|
||||
const whitelist = (document.getElementById('ocr-whitelist') as HTMLInputElement)
|
||||
.value;
|
||||
|
||||
if (selectedLangs.length === 0) {
|
||||
showAlert(
|
||||
'No Languages Selected',
|
||||
'Please select at least one language for OCR.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const langString = selectedLangs.join('+');
|
||||
|
||||
document.getElementById('ocr-options').classList.add('hidden');
|
||||
document.getElementById('ocr-progress').classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const worker = await Tesseract.createWorker(langString, 1, {
|
||||
logger: (m: { status: string; progress: number }) =>
|
||||
updateProgress(m.status, m.progress || 0),
|
||||
});
|
||||
|
||||
await worker.setParameters({
|
||||
tessjs_create_hocr: '1',
|
||||
tessedit_pageseg_mode: Tesseract.PSM.AUTO,
|
||||
});
|
||||
|
||||
await worker.setParameters({
|
||||
tessedit_char_whitelist: whitelist,
|
||||
});
|
||||
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
newPdfDoc.registerFontkit(fontkit);
|
||||
|
||||
updateProgress('Loading fonts...', 0);
|
||||
|
||||
// Prioritize non-Latin languages for font selection if multiple are selected
|
||||
const cjkLangs = ['jpn', 'chi_sim', 'chi_tra', 'kor'];
|
||||
const indicLangs = ['hin', 'ben', 'guj', 'kan', 'mal', 'ori', 'pan', 'tam', 'tel', 'sin'];
|
||||
const priorityLangs = [...cjkLangs, ...indicLangs, 'ara', 'rus', 'ukr'];
|
||||
|
||||
const primaryLang = selectedLangs.find(l => priorityLangs.includes(l)) || selectedLangs[0] || 'eng';
|
||||
|
||||
const hasCJK = selectedLangs.some(l => cjkLangs.includes(l));
|
||||
const hasIndic = selectedLangs.some(l => indicLangs.includes(l));
|
||||
const hasLatin = selectedLangs.some(l => !priorityLangs.includes(l)) || selectedLangs.includes('eng');
|
||||
const isIndicPlusLatin = hasIndic && hasLatin && !hasCJK;
|
||||
|
||||
let primaryFont;
|
||||
let latinFont;
|
||||
|
||||
try {
|
||||
let fontBytes;
|
||||
if (isIndicPlusLatin) {
|
||||
const [scriptFontBytes, latinFontBytes] = await Promise.all([
|
||||
getFontForLanguage(primaryLang),
|
||||
getFontForLanguage('eng')
|
||||
]);
|
||||
primaryFont = await newPdfDoc.embedFont(scriptFontBytes, { subset: false });
|
||||
latinFont = await newPdfDoc.embedFont(latinFontBytes, { subset: false });
|
||||
} else {
|
||||
// For CJK or single-script, use one font
|
||||
fontBytes = await getFontForLanguage(primaryLang);
|
||||
primaryFont = await newPdfDoc.embedFont(fontBytes, { subset: false });
|
||||
latinFont = primaryFont;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Font loading failed, falling back to Helvetica', e);
|
||||
primaryFont = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
latinFont = primaryFont;
|
||||
showAlert('Font Warning', 'Could not load the specific font for this language. Some characters may not appear correctly.');
|
||||
}
|
||||
|
||||
let fullText = '';
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
updateProgress(
|
||||
`Processing page ${i} of ${pdf.numPages}`,
|
||||
(i - 1) / pdf.numPages
|
||||
);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
||||
|
||||
if (binarize) {
|
||||
binarizeCanvas(context);
|
||||
}
|
||||
|
||||
const result = await worker.recognize(
|
||||
canvas,
|
||||
{},
|
||||
{ text: true, hocr: true }
|
||||
);
|
||||
const data = result.data;
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
const pngImageBytes = await new Promise((resolve) =>
|
||||
canvas.toBlob((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () =>
|
||||
resolve(new Uint8Array(reader.result as ArrayBuffer));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png')
|
||||
);
|
||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
newPage.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
|
||||
// Parse hOCR to get word-level data
|
||||
if (data.hocr) {
|
||||
const words = parseHOCR(data.hocr);
|
||||
|
||||
words.forEach((word: Word) => {
|
||||
const { x0, y0, x1, y1 } = word.bbox;
|
||||
const text = word.text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '');
|
||||
|
||||
if (!text.trim()) return;
|
||||
|
||||
const hasNonLatin = /[^\u0000-\u007F]/.test(text);
|
||||
const font = hasNonLatin ? primaryFont : latinFont;
|
||||
|
||||
if (!font) {
|
||||
console.warn(`Font not available for text: "${text}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const bboxWidth = x1 - x0;
|
||||
const bboxHeight = y1 - y0;
|
||||
|
||||
if (bboxWidth <= 0 || bboxHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fontSize = bboxHeight * 0.9;
|
||||
try {
|
||||
let textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
while (textWidth > bboxWidth && fontSize > 1) {
|
||||
fontSize -= 0.5;
|
||||
textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Could not calculate text width for "${text}":`, error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
newPage.drawText(text, {
|
||||
x: x0,
|
||||
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
|
||||
font,
|
||||
size: fontSize,
|
||||
color: rgb(0, 0, 0),
|
||||
opacity: 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(`Could not draw text "${text}":`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
fullText += data.text + '\n\n';
|
||||
}
|
||||
|
||||
await worker.terminate();
|
||||
|
||||
searchablePdfBytes = await newPdfDoc.save();
|
||||
document.getElementById('ocr-progress').classList.add('hidden');
|
||||
document.getElementById('ocr-results').classList.remove('hidden');
|
||||
|
||||
createIcons({ icons });
|
||||
(
|
||||
document.getElementById('ocr-text-output') as HTMLTextAreaElement
|
||||
).value = fullText.trim();
|
||||
|
||||
document
|
||||
.getElementById('download-searchable-pdf')
|
||||
.addEventListener('click', () => {
|
||||
if (searchablePdfBytes) {
|
||||
downloadFile(
|
||||
new Blob([searchablePdfBytes as BlobPart], { type: 'application/pdf' }),
|
||||
'searchable.pdf'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// CHANGE: The copy button logic is updated to be safer.
|
||||
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
|
||||
const button = e.currentTarget as HTMLButtonElement;
|
||||
const textToCopy = (
|
||||
document.getElementById('ocr-text-output') as HTMLTextAreaElement
|
||||
).value;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
button.textContent = ''; // Clear the button safely
|
||||
const icon = document.createElement('i');
|
||||
icon.setAttribute('data-lucide', 'check');
|
||||
icon.className = 'w-4 h-4 text-green-400';
|
||||
button.appendChild(icon);
|
||||
createIcons({ icons });
|
||||
|
||||
setTimeout(() => {
|
||||
const currentButton = document.getElementById('copy-text-btn');
|
||||
if (currentButton) {
|
||||
currentButton.textContent = ''; // Clear the button safely
|
||||
const resetIcon = document.createElement('i');
|
||||
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
|
||||
resetIcon.className = 'w-4 h-4 text-gray-300';
|
||||
currentButton.appendChild(resetIcon);
|
||||
createIcons({ icons });
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById('download-txt-btn')
|
||||
.addEventListener('click', () => {
|
||||
const textToSave = (
|
||||
document.getElementById('ocr-text-output') as HTMLTextAreaElement
|
||||
).value;
|
||||
const blob = new Blob([textToSave], { type: 'text/plain' });
|
||||
downloadFile(blob, 'ocr-text.txt');
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'OCR Error',
|
||||
'An error occurred during the OCR process. The worker may have failed to load. Please try again.'
|
||||
);
|
||||
document.getElementById('ocr-options').classList.remove('hidden');
|
||||
document.getElementById('ocr-progress').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the UI and event listeners for the OCR tool.
|
||||
*/
|
||||
export function setupOcrTool() {
|
||||
const langSearch = document.getElementById('lang-search');
|
||||
const langList = document.getElementById('lang-list');
|
||||
const selectedLangsDisplay = document.getElementById(
|
||||
'selected-langs-display'
|
||||
);
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
// Whitelist presets
|
||||
const whitelistPresets: Record<string, string> = {
|
||||
alphanumeric:
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"',
|
||||
'numbers-currency': '0123456789$€£¥.,- ',
|
||||
'letters-only': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ',
|
||||
'numbers-only': '0123456789',
|
||||
invoice: '0123456789$.,/-#: ',
|
||||
forms:
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,()-_/@#:',
|
||||
};
|
||||
|
||||
// Handle whitelist preset selection
|
||||
const presetSelect = document.getElementById(
|
||||
'whitelist-preset'
|
||||
) as HTMLSelectElement;
|
||||
const whitelistInput = document.getElementById(
|
||||
'ocr-whitelist'
|
||||
) as HTMLInputElement;
|
||||
|
||||
presetSelect?.addEventListener('change', (e) => {
|
||||
const preset = (e.target as HTMLSelectElement).value;
|
||||
if (preset && preset !== 'custom') {
|
||||
whitelistInput.value = whitelistPresets[preset];
|
||||
whitelistInput.disabled = true;
|
||||
} else {
|
||||
whitelistInput.disabled = false;
|
||||
if (preset === '') {
|
||||
whitelistInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle details toggle icon rotation
|
||||
document.querySelectorAll('details').forEach((details) => {
|
||||
details.addEventListener('toggle', () => {
|
||||
const icon = details.querySelector('.details-icon') as HTMLElement;
|
||||
if (icon) {
|
||||
icon.style.transform = details.open ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
langSearch.addEventListener('input', () => {
|
||||
const searchTerm = (langSearch as HTMLInputElement).value.toLowerCase();
|
||||
langList.querySelectorAll('label').forEach((label) => {
|
||||
label.style.display = label.textContent.toLowerCase().includes(searchTerm)
|
||||
? ''
|
||||
: 'none';
|
||||
});
|
||||
});
|
||||
|
||||
langList.addEventListener('change', () => {
|
||||
const selected = Array.from(
|
||||
langList.querySelectorAll('.lang-checkbox:checked')
|
||||
).map((cb) => tesseractLanguages[(cb as HTMLInputElement).value]);
|
||||
selectedLangsDisplay.textContent =
|
||||
selected.length > 0 ? selected.join(', ') : 'None';
|
||||
(processBtn as HTMLButtonElement).disabled = selected.length === 0;
|
||||
});
|
||||
|
||||
processBtn.addEventListener('click', runOCR);
|
||||
}
|
||||
294
src/js/logic/organize-pdf-page.ts
Normal file
294
src/js/logic/organize-pdf-page.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
interface OrganizeState {
|
||||
file: File | null;
|
||||
pdfDoc: any;
|
||||
pdfJsDoc: any;
|
||||
totalPages: number;
|
||||
sortableInstance: any;
|
||||
}
|
||||
|
||||
const organizeState: OrganizeState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
pdfJsDoc: null,
|
||||
totalPages: 0,
|
||||
sortableInstance: null,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (fileInput) fileInput.addEventListener('change', handleFileUpload);
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
|
||||
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); });
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const droppedFiles = e.dataTransfer?.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
|
||||
});
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) processBtn.addEventListener('click', saveChanges);
|
||||
|
||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) handleFile(input.files[0]);
|
||||
}
|
||||
|
||||
async function handleFile(file: File) {
|
||||
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
|
||||
showAlert('Invalid File', 'Please select a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PDF...');
|
||||
organizeState.file = file;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
organizeState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
|
||||
organizeState.pdfJsDoc = await getPDFDocument({ data: (arrayBuffer as ArrayBuffer).slice(0) }).promise;
|
||||
organizeState.totalPages = organizeState.pdfDoc.getPageCount();
|
||||
|
||||
updateFileDisplay();
|
||||
await renderThumbnails();
|
||||
hideLoader();
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (!fileDisplayArea || !organizeState.file) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = organizeState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(organizeState.file.size)} • ${organizeState.totalPages} pages`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => resetState();
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function renumberPages() {
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (!grid) return;
|
||||
const labels = grid.querySelectorAll('.page-number');
|
||||
labels.forEach((label, index) => {
|
||||
label.textContent = (index + 1).toString();
|
||||
});
|
||||
}
|
||||
|
||||
function attachEventListeners(element: HTMLElement) {
|
||||
const duplicateBtn = element.querySelector('.duplicate-btn');
|
||||
const deleteBtn = element.querySelector('.delete-btn');
|
||||
|
||||
duplicateBtn?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const clone = element.cloneNode(true) as HTMLElement;
|
||||
element.after(clone);
|
||||
attachEventListeners(clone);
|
||||
renumberPages();
|
||||
createIcons({ icons });
|
||||
initializeSortable();
|
||||
});
|
||||
|
||||
deleteBtn?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (grid && grid.children.length > 1) {
|
||||
element.remove();
|
||||
renumberPages();
|
||||
initializeSortable();
|
||||
} else {
|
||||
showAlert('Cannot Delete', 'You cannot delete the last page of the document.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function renderThumbnails() {
|
||||
const grid = document.getElementById('page-grid');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (!grid) return;
|
||||
|
||||
grid.innerHTML = '';
|
||||
grid.classList.remove('hidden');
|
||||
processBtn?.classList.remove('hidden');
|
||||
|
||||
for (let i = 1; i <= organizeState.totalPages; i++) {
|
||||
const page = await organizeState.pdfJsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.5 });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
||||
wrapper.dataset.originalPageIndex = (i - 1).toString();
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'max-w-full max-h-full object-contain';
|
||||
imgContainer.appendChild(img);
|
||||
|
||||
const pageLabel = document.createElement('span');
|
||||
pageLabel.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||
pageLabel.textContent = i.toString();
|
||||
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'flex items-center justify-center gap-4';
|
||||
|
||||
const duplicateBtn = document.createElement('button');
|
||||
duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
duplicateBtn.title = 'Duplicate Page';
|
||||
duplicateBtn.innerHTML = '<i data-lucide="copy-plus" class="w-5 h-5"></i>';
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||
deleteBtn.title = 'Delete Page';
|
||||
deleteBtn.innerHTML = '<i data-lucide="x-circle" class="w-5 h-5"></i>';
|
||||
|
||||
controlsDiv.append(duplicateBtn, deleteBtn);
|
||||
wrapper.append(imgContainer, pageLabel, controlsDiv);
|
||||
grid.appendChild(wrapper);
|
||||
|
||||
attachEventListeners(wrapper);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
initializeSortable();
|
||||
}
|
||||
|
||||
function initializeSortable() {
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (!grid) return;
|
||||
|
||||
if (organizeState.sortableInstance) organizeState.sortableInstance.destroy();
|
||||
|
||||
organizeState.sortableInstance = Sortable.create(grid, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
filter: '.duplicate-btn, .delete-btn',
|
||||
preventOnFilter: true,
|
||||
onStart: (evt) => {
|
||||
if (evt.item) evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: (evt) => {
|
||||
if (evt.item) evt.item.style.opacity = '1';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
showLoader('Building new PDF...');
|
||||
|
||||
try {
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (!grid) return;
|
||||
|
||||
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
||||
const finalIndices = Array.from(finalPageElements)
|
||||
.map(el => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10))
|
||||
.filter(index => !isNaN(index) && index >= 0);
|
||||
|
||||
if (finalIndices.length === 0) {
|
||||
showAlert('Error', 'No valid pages to save.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newPdf = await PDFDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(organizeState.pdfDoc, finalIndices);
|
||||
copiedPages.forEach(page => newPdf.addPage(page));
|
||||
|
||||
const pdfBytes = await newPdf.save();
|
||||
const baseName = organizeState.file?.name.replace('.pdf', '') || 'document';
|
||||
downloadFile(new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }), `${baseName}_organized.pdf`);
|
||||
|
||||
hideLoader();
|
||||
showAlert('Success', 'PDF organized successfully!', 'success', () => resetState());
|
||||
} catch (error) {
|
||||
console.error('Error saving changes:', error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'Failed to save changes.');
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
if (organizeState.sortableInstance) {
|
||||
organizeState.sortableInstance.destroy();
|
||||
organizeState.sortableInstance = null;
|
||||
}
|
||||
|
||||
organizeState.file = null;
|
||||
organizeState.pdfDoc = null;
|
||||
organizeState.pdfJsDoc = null;
|
||||
organizeState.totalPages = 0;
|
||||
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (grid) {
|
||||
grid.innerHTML = '';
|
||||
grid.classList.add('hidden');
|
||||
}
|
||||
document.getElementById('process-btn')?.classList.add('hidden');
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function organize() {
|
||||
showLoader('Saving changes...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageContainer = document.getElementById('page-organizer');
|
||||
const pageIndices = Array.from(pageContainer.children).map((child) =>
|
||||
parseInt((child as HTMLElement).dataset.pageIndex)
|
||||
);
|
||||
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'organized.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not save the changes.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
359
src/js/logic/page-dimensions-page.ts
Normal file
359
src/js/logic/page-dimensions-page.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { formatBytes, getStandardPageName, convertPoints } from '../utils/helpers.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFDocument | null;
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
};
|
||||
|
||||
let analyzedPagesData: any[] = [];
|
||||
|
||||
function calculateAspectRatio(width: number, height: number): string {
|
||||
const ratio = width / height;
|
||||
return ratio.toFixed(3);
|
||||
}
|
||||
|
||||
function calculateArea(width: number, height: number, unit: string): string {
|
||||
const areaInPoints = width * height;
|
||||
let convertedArea = 0;
|
||||
let unitSuffix = '';
|
||||
|
||||
switch (unit) {
|
||||
case 'in':
|
||||
convertedArea = areaInPoints / (72 * 72);
|
||||
unitSuffix = 'in²';
|
||||
break;
|
||||
case 'mm':
|
||||
convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4);
|
||||
unitSuffix = 'mm²';
|
||||
break;
|
||||
case 'px':
|
||||
const pxPerPoint = 96 / 72;
|
||||
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
|
||||
unitSuffix = 'px²';
|
||||
break;
|
||||
default:
|
||||
convertedArea = areaInPoints;
|
||||
unitSuffix = 'pt²';
|
||||
break;
|
||||
}
|
||||
|
||||
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
|
||||
}
|
||||
|
||||
function getSummaryStats() {
|
||||
const totalPages = analyzedPagesData.length;
|
||||
|
||||
const uniqueSizes = new Map();
|
||||
analyzedPagesData.forEach((pageData: any) => {
|
||||
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
|
||||
const label = `${pageData.standardSize} (${pageData.orientation})`;
|
||||
uniqueSizes.set(key, {
|
||||
count: (uniqueSizes.get(key)?.count || 0) + 1,
|
||||
label: label,
|
||||
width: pageData.width,
|
||||
height: pageData.height
|
||||
});
|
||||
});
|
||||
|
||||
const hasMixedSizes = uniqueSizes.size > 1;
|
||||
|
||||
return {
|
||||
totalPages,
|
||||
uniqueSizesCount: uniqueSizes.size,
|
||||
uniqueSizes: Array.from(uniqueSizes.values()),
|
||||
hasMixedSizes
|
||||
};
|
||||
}
|
||||
|
||||
function renderSummary() {
|
||||
const summaryContainer = document.getElementById('dimensions-summary');
|
||||
if (!summaryContainer) return;
|
||||
|
||||
const stats = getSummaryStats();
|
||||
|
||||
let summaryHTML = `
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-400 mb-1">Total Pages</p>
|
||||
<p class="text-2xl font-bold text-white">${stats.totalPages}</p>
|
||||
</div>
|
||||
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-400 mb-1">Unique Page Sizes</p>
|
||||
<p class="text-2xl font-bold text-white">${stats.uniqueSizesCount}</p>
|
||||
</div>
|
||||
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-400 mb-1">Document Type</p>
|
||||
<p class="text-2xl font-bold ${stats.hasMixedSizes ? 'text-yellow-400' : 'text-green-400'}">
|
||||
${stats.hasMixedSizes ? 'Mixed Sizes' : 'Uniform'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (stats.hasMixedSizes) {
|
||||
summaryHTML += `
|
||||
<div class="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4>
|
||||
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
|
||||
<ul class="space-y-1 text-sm text-gray-300">
|
||||
${stats.uniqueSizes.map((size: any) => `
|
||||
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
summaryContainer.innerHTML = summaryHTML;
|
||||
|
||||
if (stats.hasMixedSizes) {
|
||||
createIcons({ icons });
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(unit: string) {
|
||||
const tableBody = document.getElementById('dimensions-table-body');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.textContent = '';
|
||||
|
||||
analyzedPagesData.forEach((pageData) => {
|
||||
const width = convertPoints(pageData.width, unit);
|
||||
const height = convertPoints(pageData.height, unit);
|
||||
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
|
||||
const area = calculateArea(pageData.width, pageData.height, unit);
|
||||
|
||||
const row = document.createElement('tr');
|
||||
|
||||
const pageNumCell = document.createElement('td');
|
||||
pageNumCell.className = 'px-4 py-3 text-white';
|
||||
pageNumCell.textContent = pageData.pageNum;
|
||||
|
||||
const dimensionsCell = document.createElement('td');
|
||||
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
||||
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
||||
|
||||
const sizeCell = document.createElement('td');
|
||||
sizeCell.className = 'px-4 py-3 text-gray-300';
|
||||
sizeCell.textContent = pageData.standardSize;
|
||||
|
||||
const orientationCell = document.createElement('td');
|
||||
orientationCell.className = 'px-4 py-3 text-gray-300';
|
||||
orientationCell.textContent = pageData.orientation;
|
||||
|
||||
const aspectRatioCell = document.createElement('td');
|
||||
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
|
||||
aspectRatioCell.textContent = aspectRatio;
|
||||
|
||||
const areaCell = document.createElement('td');
|
||||
areaCell.className = 'px-4 py-3 text-gray-300';
|
||||
areaCell.textContent = area;
|
||||
|
||||
const rotationCell = document.createElement('td');
|
||||
rotationCell.className = 'px-4 py-3 text-gray-300';
|
||||
rotationCell.textContent = `${pageData.rotation}°`;
|
||||
|
||||
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function exportToCSV() {
|
||||
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
|
||||
const unit = unitsSelect?.value || 'pt';
|
||||
|
||||
const headers = ['Page #', `Width (${unit})`, `Height (${unit})`, 'Standard Size', 'Orientation', 'Aspect Ratio', `Area (${unit}²)`, 'Rotation'];
|
||||
const csvRows = [headers.join(',')];
|
||||
|
||||
analyzedPagesData.forEach((pageData: any) => {
|
||||
const width = convertPoints(pageData.width, unit);
|
||||
const height = convertPoints(pageData.height, unit);
|
||||
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
|
||||
const area = calculateArea(pageData.width, pageData.height, unit);
|
||||
|
||||
const row = [
|
||||
pageData.pageNum,
|
||||
width,
|
||||
height,
|
||||
pageData.standardSize,
|
||||
pageData.orientation,
|
||||
aspectRatio,
|
||||
area,
|
||||
`${pageData.rotation}°`
|
||||
];
|
||||
csvRows.push(row.join(','));
|
||||
});
|
||||
|
||||
const csvContent = csvRows.join('\n');
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'page-dimensions.csv';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function analyzeAndDisplayDimensions() {
|
||||
if (!pageState.pdfDoc) return;
|
||||
|
||||
analyzedPagesData = [];
|
||||
const pages = pageState.pdfDoc.getPages();
|
||||
|
||||
pages.forEach((page: any, index: number) => {
|
||||
const { width, height } = page.getSize();
|
||||
const rotation = page.getRotation().angle || 0;
|
||||
|
||||
analyzedPagesData.push({
|
||||
pageNum: index + 1,
|
||||
width,
|
||||
height,
|
||||
orientation: width > height ? 'Landscape' : 'Portrait',
|
||||
standardSize: getStandardPageName(width, height),
|
||||
rotation: rotation
|
||||
});
|
||||
});
|
||||
|
||||
const resultsContainer = document.getElementById('dimensions-results');
|
||||
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
|
||||
|
||||
renderSummary();
|
||||
renderTable(unitsSelect.value);
|
||||
|
||||
if (resultsContainer) resultsContainer.classList.remove('hidden');
|
||||
|
||||
unitsSelect.addEventListener('change', (e) => {
|
||||
renderTable((e.target as HTMLSelectElement).value);
|
||||
});
|
||||
|
||||
const exportButton = document.getElementById('export-csv-btn');
|
||||
if (exportButton) {
|
||||
exportButton.addEventListener('click', exportToCSV);
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
analyzedPagesData = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const resultsContainer = document.getElementById('dimensions-results');
|
||||
if (resultsContainer) resultsContainer.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||
updateUI();
|
||||
analyzeAndDisplayDimensions();
|
||||
} catch (e) {
|
||||
console.error('Error loading PDF:', e);
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,258 +0,0 @@
|
||||
import { state } from '../state.js';
|
||||
import { getStandardPageName, convertPoints } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing
|
||||
|
||||
function calculateAspectRatio(width: number, height: number): string {
|
||||
const ratio = width / height;
|
||||
return ratio.toFixed(3);
|
||||
}
|
||||
|
||||
function calculateArea(width: number, height: number, unit: string): string {
|
||||
const areaInPoints = width * height;
|
||||
let convertedArea = 0;
|
||||
let unitSuffix = '';
|
||||
|
||||
switch (unit) {
|
||||
case 'in':
|
||||
convertedArea = areaInPoints / (72 * 72); // 72 points per inch
|
||||
unitSuffix = 'in²';
|
||||
break;
|
||||
case 'mm':
|
||||
convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4); // Convert to mm²
|
||||
unitSuffix = 'mm²';
|
||||
break;
|
||||
case 'px':
|
||||
const pxPerPoint = 96 / 72;
|
||||
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
|
||||
unitSuffix = 'px²';
|
||||
break;
|
||||
default: // 'pt'
|
||||
convertedArea = areaInPoints;
|
||||
unitSuffix = 'pt²';
|
||||
break;
|
||||
}
|
||||
|
||||
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
|
||||
}
|
||||
|
||||
|
||||
function getSummaryStats() {
|
||||
const totalPages = analyzedPagesData.length;
|
||||
|
||||
// Count unique page sizes
|
||||
const uniqueSizes = new Map();
|
||||
analyzedPagesData.forEach((pageData: any) => {
|
||||
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
|
||||
const label = `${pageData.standardSize} (${pageData.orientation})`;
|
||||
uniqueSizes.set(key, {
|
||||
count: (uniqueSizes.get(key)?.count || 0) + 1,
|
||||
label: label,
|
||||
width: pageData.width,
|
||||
height: pageData.height
|
||||
});
|
||||
});
|
||||
|
||||
const hasMixedSizes = uniqueSizes.size > 1;
|
||||
|
||||
return {
|
||||
totalPages,
|
||||
uniqueSizesCount: uniqueSizes.size,
|
||||
uniqueSizes: Array.from(uniqueSizes.values()),
|
||||
hasMixedSizes
|
||||
};
|
||||
}
|
||||
|
||||
function renderSummary() {
|
||||
const summaryContainer = document.getElementById('dimensions-summary');
|
||||
if (!summaryContainer) return;
|
||||
|
||||
const stats = getSummaryStats();
|
||||
|
||||
let summaryHTML = `
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-400 mb-1">Total Pages</p>
|
||||
<p class="text-2xl font-bold text-white">${stats.totalPages}</p>
|
||||
</div>
|
||||
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-400 mb-1">Unique Page Sizes</p>
|
||||
<p class="text-2xl font-bold text-white">${stats.uniqueSizesCount}</p>
|
||||
</div>
|
||||
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-400 mb-1">Document Type</p>
|
||||
<p class="text-2xl font-bold ${stats.hasMixedSizes ? 'text-yellow-400' : 'text-green-400'}">
|
||||
${stats.hasMixedSizes ? 'Mixed Sizes' : 'Uniform'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (stats.hasMixedSizes) {
|
||||
summaryHTML += `
|
||||
<div class="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0"></i>
|
||||
<div>
|
||||
<h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4>
|
||||
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
|
||||
<ul class="space-y-1 text-sm text-gray-300">
|
||||
${stats.uniqueSizes.map((size: any) => `
|
||||
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
summaryContainer.innerHTML = summaryHTML;
|
||||
|
||||
if (stats.hasMixedSizes) {
|
||||
createIcons({ icons });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the dimensions table based on the stored data and selected unit.
|
||||
* @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px').
|
||||
*/
|
||||
function renderTable(unit: any) {
|
||||
const tableBody = document.getElementById('dimensions-table-body');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.textContent = ''; // Clear the table body safely
|
||||
|
||||
analyzedPagesData.forEach((pageData) => {
|
||||
const width = convertPoints(pageData.width, unit);
|
||||
const height = convertPoints(pageData.height, unit);
|
||||
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
|
||||
const area = calculateArea(pageData.width, pageData.height, unit);
|
||||
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Page number
|
||||
const pageNumCell = document.createElement('td');
|
||||
pageNumCell.className = 'px-4 py-3 text-white';
|
||||
pageNumCell.textContent = pageData.pageNum;
|
||||
|
||||
// Dimensions
|
||||
const dimensionsCell = document.createElement('td');
|
||||
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
||||
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
||||
|
||||
// Standard size
|
||||
const sizeCell = document.createElement('td');
|
||||
sizeCell.className = 'px-4 py-3 text-gray-300';
|
||||
sizeCell.textContent = pageData.standardSize;
|
||||
|
||||
// Orientation
|
||||
const orientationCell = document.createElement('td');
|
||||
orientationCell.className = 'px-4 py-3 text-gray-300';
|
||||
orientationCell.textContent = pageData.orientation;
|
||||
|
||||
// Aspect Ratio
|
||||
const aspectRatioCell = document.createElement('td');
|
||||
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
|
||||
aspectRatioCell.textContent = aspectRatio;
|
||||
|
||||
// Area
|
||||
const areaCell = document.createElement('td');
|
||||
areaCell.className = 'px-4 py-3 text-gray-300';
|
||||
areaCell.textContent = area;
|
||||
|
||||
// Rotation
|
||||
const rotationCell = document.createElement('td');
|
||||
rotationCell.className = 'px-4 py-3 text-gray-300';
|
||||
rotationCell.textContent = `${pageData.rotation}°`;
|
||||
|
||||
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function exportToCSV() {
|
||||
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
|
||||
const unit = unitsSelect?.value || 'pt';
|
||||
|
||||
const headers = ['Page #', `Width (${unit})`, `Height (${unit})`, 'Standard Size', 'Orientation', 'Aspect Ratio', `Area (${unit}²)`, 'Rotation'];
|
||||
const csvRows = [headers.join(',')];
|
||||
|
||||
analyzedPagesData.forEach((pageData: any) => {
|
||||
const width = convertPoints(pageData.width, unit);
|
||||
const height = convertPoints(pageData.height, unit);
|
||||
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
|
||||
const area = calculateArea(pageData.width, pageData.height, unit);
|
||||
|
||||
const row = [
|
||||
pageData.pageNum,
|
||||
width,
|
||||
height,
|
||||
pageData.standardSize,
|
||||
pageData.orientation,
|
||||
aspectRatio,
|
||||
area,
|
||||
`${pageData.rotation}°`
|
||||
];
|
||||
csvRows.push(row.join(','));
|
||||
});
|
||||
|
||||
const csvContent = csvRows.join('\n');
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'page-dimensions.csv';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to analyze the PDF and display dimensions.
|
||||
* This is called once after the file is loaded.
|
||||
*/
|
||||
export function analyzeAndDisplayDimensions() {
|
||||
if (!state.pdfDoc) return;
|
||||
|
||||
analyzedPagesData = []; // Reset stored data
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
pages.forEach((page: any, index: any) => {
|
||||
const { width, height } = page.getSize();
|
||||
const rotation = page.getRotation().angle || 0;
|
||||
|
||||
analyzedPagesData.push({
|
||||
pageNum: index + 1,
|
||||
width, // Store raw width in points
|
||||
height, // Store raw height in points
|
||||
orientation: width > height ? 'Landscape' : 'Portrait',
|
||||
standardSize: getStandardPageName(width, height),
|
||||
rotation: rotation
|
||||
});
|
||||
});
|
||||
|
||||
const resultsContainer = document.getElementById('dimensions-results');
|
||||
const unitsSelect = document.getElementById('units-select');
|
||||
|
||||
renderSummary();
|
||||
|
||||
// Initial render with default unit (points)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
renderTable(unitsSelect.value);
|
||||
|
||||
resultsContainer.classList.remove('hidden');
|
||||
|
||||
unitsSelect.addEventListener('change', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
renderTable(e.target.value);
|
||||
});
|
||||
|
||||
const exportButton = document.getElementById('export-csv-btn');
|
||||
if (exportButton) {
|
||||
exportButton.addEventListener('click', exportToCSV);
|
||||
}
|
||||
}
|
||||
230
src/js/logic/page-numbers-page.ts
Normal file
230
src/js/logic/page-numbers-page.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
pdfDoc: PDFLibDocument | null;
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
file: null,
|
||||
pdfDoc: null,
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
fileInput.addEventListener('click', () => { fileInput.value = ''; });
|
||||
}
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('border-indigo-500');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('border-indigo-500');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('border-indigo-500');
|
||||
if (e.dataTransfer?.files.length) {
|
||||
handleFiles(e.dataTransfer.files);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', addPageNumbers);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files?.length) {
|
||||
handleFiles(input.files);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFiles(files: FileList) {
|
||||
const file = files[0];
|
||||
if (!file || file.type !== 'application/pdf') {
|
||||
showAlert('Invalid File', 'Please upload a valid PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
pageState.file = file;
|
||||
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col flex-1 min-w-0';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(pageState.file.size)} • ${pageState.pdfDoc.getPageCount()} pages`;
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = resetState;
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfDoc = null;
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
document.getElementById('options-panel')?.classList.add('hidden');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function addPageNumbers() {
|
||||
if (!pageState.pdfDoc) {
|
||||
showAlert('Error', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Adding page numbers...');
|
||||
try {
|
||||
const position = (document.getElementById('position') as HTMLSelectElement).value;
|
||||
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement).value) || 12;
|
||||
const format = (document.getElementById('number-format') as HTMLSelectElement).value;
|
||||
const colorHex = (document.getElementById('text-color') as HTMLInputElement).value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
|
||||
const pages = pageState.pdfDoc.getPages();
|
||||
const totalPages = pages.length;
|
||||
const helveticaFont = await pageState.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const page = pages[i];
|
||||
|
||||
const mediaBox = page.getMediaBox();
|
||||
const cropBox = page.getCropBox();
|
||||
const bounds = cropBox || mediaBox;
|
||||
const width = bounds.width;
|
||||
const height = bounds.height;
|
||||
const xOffset = bounds.x || 0;
|
||||
const yOffset = bounds.y || 0;
|
||||
|
||||
let pageNumText = format === 'page_x_of_y' ? `${i + 1} / ${totalPages}` : `${i + 1}`;
|
||||
|
||||
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
|
||||
const textHeight = fontSize;
|
||||
|
||||
const minMargin = 8;
|
||||
const maxMargin = 40;
|
||||
const marginPercentage = 0.04;
|
||||
|
||||
const horizontalMargin = Math.max(minMargin, Math.min(maxMargin, width * marginPercentage));
|
||||
const verticalMargin = Math.max(minMargin, Math.min(maxMargin, height * marginPercentage));
|
||||
|
||||
const safeHorizontalMargin = Math.max(horizontalMargin, textWidth / 2 + 3);
|
||||
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
||||
|
||||
let x = 0, y = 0;
|
||||
|
||||
switch (position) {
|
||||
case 'bottom-center':
|
||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'top-center':
|
||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-right':
|
||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
}
|
||||
|
||||
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
|
||||
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
|
||||
|
||||
page.drawText(pageNumText, {
|
||||
x,
|
||||
y,
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await pageState.pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'paginated.pdf');
|
||||
showAlert('Success', 'Page numbers added successfully!', 'success', () => { resetState(); });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not add page numbers.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
import { t } from '../i18n/i18n';
|
||||
|
||||
interface PageData {
|
||||
id: string; // Unique ID for DOM reconciliation
|
||||
pdfIndex: number;
|
||||
@@ -107,7 +109,7 @@ function showLoading(current: number, total: number) {
|
||||
loader.classList.remove('hidden');
|
||||
const percentage = Math.round((current / total) * 100);
|
||||
progress.style.width = `${percentage}%`;
|
||||
text.textContent = `Rendering pages...`;
|
||||
text.textContent = t('multiTool.renderingPages');
|
||||
}
|
||||
|
||||
async function withButtonLoading(buttonId: string, action: () => Promise<void>) {
|
||||
@@ -158,7 +160,7 @@ function initializeTool() {
|
||||
document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => {
|
||||
console.log('Upload button clicked, isRendering:', isRendering);
|
||||
if (isRendering) {
|
||||
showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info');
|
||||
showModal(t('multiTool.pleaseWait'), t('multiTool.pagesRendering'), 'info');
|
||||
return;
|
||||
}
|
||||
document.getElementById('pdf-file-input')?.click();
|
||||
@@ -193,9 +195,10 @@ function initializeTool() {
|
||||
bulkSplit();
|
||||
});
|
||||
document.getElementById('bulk-download-btn')?.addEventListener('click', () => {
|
||||
if (isRendering) return;
|
||||
if (isRendering) return;
|
||||
if (selectedPages.size === 0) {
|
||||
showModal('No Pages Selected', 'Please select at least one page to download.', 'info');
|
||||
showModal(t('multiTool.noPagesSelected'), t('multiTool.selectOnePage'), 'info');
|
||||
return;
|
||||
}
|
||||
withButtonLoading('bulk-download-btn', async () => {
|
||||
@@ -216,9 +219,10 @@ function initializeTool() {
|
||||
});
|
||||
|
||||
document.getElementById('export-pdf-btn')?.addEventListener('click', () => {
|
||||
if (isRendering) return;
|
||||
if (isRendering) return;
|
||||
if (allPages.length === 0) {
|
||||
showModal('No Pages', 'There are no pages to export.', 'info');
|
||||
showModal(t('multiTool.noPages'), t('multiTool.noPagesToExport'), 'info');
|
||||
return;
|
||||
}
|
||||
withButtonLoading('export-pdf-btn', async () => {
|
||||
@@ -329,7 +333,7 @@ async function handlePdfUpload(e: Event) {
|
||||
|
||||
async function loadPdfs(files: File[]) {
|
||||
if (isRendering) {
|
||||
showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info');
|
||||
showModal(t('multiTool.pleaseWait'), t('multiTool.pagesRendering'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -424,7 +428,7 @@ async function loadPdfs(files: File[]) {
|
||||
|
||||
} catch (e) {
|
||||
console.error(`Failed to load PDF ${file.name}:`, e);
|
||||
showModal('Error', `Failed to load ${file.name}. The file may be corrupted.`, 'error');
|
||||
showModal(t('multiTool.error'), `${t('multiTool.failedToLoad')} ${file.name}.`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,7 +505,7 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
|
||||
loading.className = 'flex flex-col items-center justify-center text-gray-400';
|
||||
loading.innerHTML = `
|
||||
<i data-lucide="loader" class="w-8 h-8 animate-spin mb-2"></i>
|
||||
<span class="text-xs">Loading...</span>
|
||||
<span class="text-xs">${t('common.loading')}</span>
|
||||
`;
|
||||
preview.appendChild(loading);
|
||||
preview.classList.add('bg-gray-700'); // Darker background for loading
|
||||
@@ -510,7 +514,7 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
|
||||
// Page info
|
||||
const info = document.createElement('div');
|
||||
info.className = 'text-xs text-gray-400 text-center mb-2';
|
||||
info.textContent = `Page ${index + 1}`;
|
||||
info.textContent = `${t('common.page')} ${index + 1}`;
|
||||
|
||||
// Actions toolbar
|
||||
const actions = document.createElement('div');
|
||||
@@ -551,7 +555,7 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
|
||||
const duplicateBtn = document.createElement('button');
|
||||
duplicateBtn.className = 'p-1 rounded hover:bg-gray-700';
|
||||
duplicateBtn.innerHTML = '<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>';
|
||||
duplicateBtn.title = 'Duplicate this page';
|
||||
duplicateBtn.title = t('multiTool.actions.duplicatePage');
|
||||
duplicateBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
snapshot();
|
||||
@@ -562,7 +566,7 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'p-1 rounded hover:bg-gray-700';
|
||||
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>';
|
||||
deleteBtn.title = 'Delete this page';
|
||||
deleteBtn.title = t('multiTool.actions.deletePage');
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
snapshot();
|
||||
@@ -573,7 +577,7 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
|
||||
const insertBtn = document.createElement('button');
|
||||
insertBtn.className = 'p-1 rounded hover:bg-gray-700';
|
||||
insertBtn.innerHTML = '<i data-lucide="file-plus" class="w-4 h-4 text-gray-300"></i>';
|
||||
insertBtn.title = 'Insert PDF after this page';
|
||||
insertBtn.title = t('multiTool.actions.insertPdf');
|
||||
insertBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
snapshot();
|
||||
@@ -584,7 +588,7 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
|
||||
const splitBtn = document.createElement('button');
|
||||
splitBtn.className = 'p-1 rounded hover:bg-gray-700';
|
||||
splitBtn.innerHTML = '<i data-lucide="scissors" class="w-4 h-4 text-gray-300"></i>';
|
||||
splitBtn.title = 'Toggle split after this page';
|
||||
splitBtn.title = t('multiTool.actions.toggleSplit');
|
||||
splitBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
snapshot();
|
||||
|
||||
199
src/js/logic/pdf-to-bmp-page.ts
Normal file
199
src/js/logic/pdf-to-bmp-page.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument, getCleanPdfFilename } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
// 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 = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
// Fetch page count asynchronously
|
||||
readFileAsArrayBuffer(file).then(buffer => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
}).then(pdf => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
|
||||
}).catch(e => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize icons immediately after synchronous render
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting to BMP...');
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(files[0])
|
||||
).promise;
|
||||
|
||||
if (pdf.numPages === 1) {
|
||||
const page = await pdf.getPage(1);
|
||||
const blob = await renderPage(page);
|
||||
downloadFile(blob, getCleanPdfFilename(files[0].name) + '.bmp');
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const blob = await renderPage(page);
|
||||
if (blob) {
|
||||
zip.file(`page_${i}.bmp`, blob);
|
||||
}
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(
|
||||
zipBlob,
|
||||
getCleanPdfFilename(files[0].name) + '_bmps.zip'
|
||||
);
|
||||
}
|
||||
|
||||
showAlert('Success', 'PDF converted to BMPs successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to BMP. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage(page: PDFPageProxy): Promise<Blob | null> {
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context!,
|
||||
viewport: viewport,
|
||||
canvas,
|
||||
}).promise;
|
||||
|
||||
const blob = await new Promise<Blob | null>((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/bmp')
|
||||
);
|
||||
return blob;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.type === 'application/pdf'
|
||||
);
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
files = [validFiles[0]];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
@@ -1,125 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
const yieldToUI = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
/**
|
||||
* Creates a BMP file buffer from raw pixel data (ImageData).
|
||||
* This function is self-contained and has no external dependencies.
|
||||
* @param {ImageData} imageData The pixel data from a canvas context.
|
||||
* @returns {ArrayBuffer} The complete BMP file as an ArrayBuffer.
|
||||
*/
|
||||
function encodeBMP(imageData: any) {
|
||||
const { width, height, data } = imageData;
|
||||
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
|
||||
const fileSize = stride * height + 54; // 54 byte header
|
||||
const buffer = new ArrayBuffer(fileSize);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// BMP File Header (14 bytes)
|
||||
view.setUint16(0, 0x4d42, true); // 'BM'
|
||||
view.setUint32(2, fileSize, true);
|
||||
view.setUint32(10, 54, true); // Offset to pixel data
|
||||
|
||||
// DIB Header (BITMAPINFOHEADER) (40 bytes)
|
||||
view.setUint32(14, 40, true); // DIB header size
|
||||
view.setUint32(18, width, true);
|
||||
view.setUint32(22, -height, true); // Negative height for top-down scanline order
|
||||
view.setUint16(26, 1, true); // Color planes
|
||||
view.setUint16(28, 24, true); // Bits per pixel
|
||||
view.setUint32(30, 0, true); // No compression
|
||||
view.setUint32(34, stride * height, true); // Image size
|
||||
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
|
||||
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
|
||||
|
||||
// Pixel Data
|
||||
let offset = 54;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = (y * width + x) * 4;
|
||||
// BMP is BGR, not RGB
|
||||
view.setUint8(offset++, data[i + 2]); // Blue
|
||||
view.setUint8(offset++, data[i + 1]); // Green
|
||||
view.setUint8(offset++, data[i]); // Red
|
||||
}
|
||||
// Add padding to make the row a multiple of 4 bytes
|
||||
for (let p = 0; p < stride - width * 3; p++) {
|
||||
view.setUint8(offset++, 0);
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export async function pdfToBmp() {
|
||||
showLoader('Converting PDF to BMP images...');
|
||||
await yieldToUI();
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
|
||||
if(pdf.numPages === 1) {
|
||||
showLoader(`Processing the single page...`);
|
||||
await yieldToUI();
|
||||
const page = await pdf.getPage(1);
|
||||
const bmpBuffer = await pageToBlob(page);
|
||||
downloadFile(bmpBuffer, getCleanFilename() +'.bmp');
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
await yieldToUI();
|
||||
const page = await pdf.getPage(i);
|
||||
const bmpBuffer = await pageToBlob(page);
|
||||
|
||||
// Add the generated BMP file to the zip archive
|
||||
zip.file(`page_${i}.bmp`, bmpBuffer);
|
||||
}
|
||||
|
||||
showLoader('Compressing files into a ZIP...');
|
||||
await yieldToUI();
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, getCleanFilename() + '_bmps.zip');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to BMP. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function pageToBlob(page: PDFPageProxy): Promise<Blob> {
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
// Render the PDF page directly to the canvas
|
||||
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
|
||||
|
||||
// Get the raw pixel data from this canvas
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Use our new self-contained function to create the BMP file
|
||||
return new Blob([encodeBMP(imageData)]);
|
||||
}
|
||||
|
||||
function getCleanFilename(): string {
|
||||
let clean = state.files[0].name.replace(/\.pdf$/i, '').trim();
|
||||
if (clean.length > 80) {
|
||||
clean = clean.slice(0, 80);
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
206
src/js/logic/pdf-to-greyscale-page.ts
Normal file
206
src/js/logic/pdf-to-greyscale-page.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
|
||||
// Render files synchronously first
|
||||
files.forEach((file) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
// Fetch page count asynchronously
|
||||
readFileAsArrayBuffer(file).then(buffer => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
}).then(pdf => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
|
||||
}).catch(e => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize icons immediately after synchronous render
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting to greyscale...');
|
||||
try {
|
||||
const pdfBytes = await readFileAsArrayBuffer(files[0]) as ArrayBuffer;
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({ canvasContext: context!, viewport: viewport, canvas }).promise;
|
||||
|
||||
const imageData = context!.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
// Convert to greyscale
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
const grey = Math.round(0.299 * data[j] + 0.587 * data[j + 1] + 0.114 * data[j + 2]);
|
||||
data[j] = grey;
|
||||
data[j + 1] = grey;
|
||||
data[j + 2] = grey;
|
||||
}
|
||||
|
||||
context!.putImageData(imageData, 0, 0);
|
||||
|
||||
const jpegBlob = await new Promise<Blob | null>((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/jpeg', 0.9)
|
||||
);
|
||||
|
||||
if (jpegBlob) {
|
||||
const jpegBytes = await jpegBlob.arrayBuffer();
|
||||
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
newPage.drawImage(jpegImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const resultBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(resultBytes)], { type: 'application/pdf' }),
|
||||
'greyscale.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF converted to greyscale successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to greyscale. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.type === 'application/pdf'
|
||||
);
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
files = [validFiles[0]];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
export async function pdfToGreyscale() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting to greyscale...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
|
||||
data[j] = avg; // red
|
||||
data[j + 1] = avg; // green
|
||||
data[j + 2] = avg; // blue
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const imageBytes = await new Promise((resolve) =>
|
||||
canvas.toBlob((blob) => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png')
|
||||
);
|
||||
|
||||
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
|
||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||
newPage.drawImage(image, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'greyscale.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not convert to greyscale.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
216
src/js/logic/pdf-to-jpg-page.ts
Normal file
216
src/js/logic/pdf-to-jpg-page.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument, getCleanPdfFilename } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
// Fetch page count asynchronously
|
||||
readFileAsArrayBuffer(file).then(buffer => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
}).then(pdf => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
|
||||
}).catch(e => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize icons immediately after synchronous render
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('jpg-quality-value');
|
||||
if (qualitySlider) qualitySlider.value = '0.9';
|
||||
if (qualityValue) qualityValue.textContent = '90%';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting to JPG...');
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(files[0])
|
||||
).promise;
|
||||
|
||||
const qualityInput = document.getElementById('jpg-quality') as HTMLInputElement;
|
||||
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9;
|
||||
|
||||
if (pdf.numPages === 1) {
|
||||
const page = await pdf.getPage(1);
|
||||
const blob = await renderPage(page, quality);
|
||||
downloadFile(blob, getCleanPdfFilename(files[0].name) + '.jpg');
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const blob = await renderPage(page, quality);
|
||||
if (blob) {
|
||||
zip.file(`page_${i}.jpg`, blob);
|
||||
}
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(
|
||||
zipBlob,
|
||||
getCleanPdfFilename(files[0].name) + '_jpgs.zip'
|
||||
);
|
||||
}
|
||||
|
||||
showAlert('Success', 'PDF converted to JPGs successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to JPG. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage(
|
||||
page: PDFPageProxy,
|
||||
quality: number
|
||||
): Promise<Blob | null> {
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context!,
|
||||
viewport: viewport,
|
||||
canvas,
|
||||
}).promise;
|
||||
|
||||
const blob = await new Promise<Blob | null>((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/jpeg', quality)
|
||||
);
|
||||
return blob;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('jpg-quality-value');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (qualitySlider && qualityValue) {
|
||||
qualitySlider.addEventListener('input', () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.type === 'application/pdf'
|
||||
);
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
files = [validFiles[0]];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
const yieldToUI = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
export async function pdfToJpg() {
|
||||
showLoader('Converting to JPG...');
|
||||
await yieldToUI();
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
|
||||
const qualityInput = document.getElementById('jpg-quality') as HTMLInputElement;
|
||||
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9;
|
||||
|
||||
if(pdf.numPages === 1) {
|
||||
showLoader(`Processing the single page...`);
|
||||
await yieldToUI();
|
||||
const page = await pdf.getPage(1);
|
||||
const blob = await pageToBlob(page, quality);
|
||||
downloadFile(blob, getCleanFilename() + '.jpg');
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
await yieldToUI();
|
||||
const page = await pdf.getPage(i);
|
||||
const blob = await pageToBlob(page, quality);
|
||||
zip.file(`page_${i}.jpg`, blob as Blob);
|
||||
}
|
||||
|
||||
showLoader('Compressing files into a ZIP...');
|
||||
await yieldToUI();
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, getCleanFilename() + '_jpgs.zip');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to JPG. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function pageToBlob(page: PDFPageProxy, quality: number): Promise<Blob> {
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
|
||||
|
||||
const blob = await new Promise((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/jpeg', quality)
|
||||
);
|
||||
return blob as Blob;
|
||||
}
|
||||
|
||||
function getCleanFilename(): string {
|
||||
let clean = state.files[0].name.replace(/\.pdf$/i, '').trim();
|
||||
if (clean.length > 80) {
|
||||
clean = clean.slice(0, 80);
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
216
src/js/logic/pdf-to-png-page.ts
Normal file
216
src/js/logic/pdf-to-png-page.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument, getCleanPdfFilename } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
// Fetch page count asynchronously
|
||||
readFileAsArrayBuffer(file).then(buffer => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
}).then(pdf => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
|
||||
}).catch(e => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize icons immediately after synchronous render
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const scaleSlider = document.getElementById('png-scale') as HTMLInputElement;
|
||||
const scaleValue = document.getElementById('png-scale-value');
|
||||
if (scaleSlider) scaleSlider.value = '2.0';
|
||||
if (scaleValue) scaleValue.textContent = '2.0x';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting to PNG...');
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(files[0])
|
||||
).promise;
|
||||
|
||||
const scaleInput = document.getElementById('png-scale') as HTMLInputElement;
|
||||
const scale = scaleInput ? parseFloat(scaleInput.value) : 2.0;
|
||||
|
||||
if (pdf.numPages === 1) {
|
||||
const page = await pdf.getPage(1);
|
||||
const blob = await renderPage(page, scale);
|
||||
downloadFile(blob, getCleanPdfFilename(files[0].name) + '.png');
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const blob = await renderPage(page, scale);
|
||||
if (blob) {
|
||||
zip.file(`page_${i}.png`, blob);
|
||||
}
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(
|
||||
zipBlob,
|
||||
getCleanPdfFilename(files[0].name) + '_pngs.zip'
|
||||
);
|
||||
}
|
||||
|
||||
showAlert('Success', 'PDF converted to PNGs successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to PNG. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage(
|
||||
page: PDFPageProxy,
|
||||
scale: number
|
||||
): Promise<Blob | null> {
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context!,
|
||||
viewport: viewport,
|
||||
canvas,
|
||||
}).promise;
|
||||
|
||||
const blob = await new Promise<Blob | null>((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/png')
|
||||
);
|
||||
return blob;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const scaleSlider = document.getElementById('png-scale') as HTMLInputElement;
|
||||
const scaleValue = document.getElementById('png-scale-value');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (scaleSlider && scaleValue) {
|
||||
scaleSlider.addEventListener('input', () => {
|
||||
scaleValue.textContent = `${parseFloat(scaleSlider.value).toFixed(1)}x`;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.type === 'application/pdf'
|
||||
);
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
files = [validFiles[0]];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
readFileAsArrayBuffer,
|
||||
getPDFDocument,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const yieldToUI = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
export async function pdfToPng() {
|
||||
showLoader('Converting to PNG...');
|
||||
await yieldToUI();
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
|
||||
const qualityInput = document.getElementById(
|
||||
'png-quality'
|
||||
) as HTMLInputElement;
|
||||
const scale = qualityInput ? parseFloat(qualityInput.value) : 2.0;
|
||||
|
||||
if (pdf.numPages === 1) {
|
||||
showLoader(`Processing the single page...`);
|
||||
await yieldToUI();
|
||||
downloadFile(
|
||||
await pageToBlob(await pdf.getPage(1), scale),
|
||||
getCleanFilename() + '.png'
|
||||
);
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
await yieldToUI();
|
||||
const page = await pdf.getPage(i);
|
||||
zip.file(`page_${i}.png`, await pageToBlob(page, scale));
|
||||
}
|
||||
|
||||
showLoader('Compressing files into a ZIP...');
|
||||
await yieldToUI();
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(
|
||||
zipBlob,
|
||||
getCleanFilename() + '_pngs.zip'
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to PNG.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function pageToBlob(page: PDFPageProxy, scale: number): Promise<Blob> {
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
canvas,
|
||||
}).promise;
|
||||
const blob = await new Promise((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/png')
|
||||
);
|
||||
return blob as Blob;
|
||||
}
|
||||
|
||||
function getCleanFilename(): string {
|
||||
let clean = state.files[0].name.replace(/\.pdf$/i, '').trim();
|
||||
if (clean.length > 80) {
|
||||
clean = clean.slice(0, 80);
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
232
src/js/logic/pdf-to-tiff-page.ts
Normal file
232
src/js/logic/pdf-to-tiff-page.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument, getCleanPdfFilename } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import UTIF from 'utif';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
// Fetch page count asynchronously
|
||||
readFileAsArrayBuffer(file).then(buffer => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
}).then(pdf => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
|
||||
}).catch(e => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize icons immediately after synchronous render
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting to TIFF...');
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(files[0])
|
||||
).promise;
|
||||
|
||||
if (pdf.numPages === 1) {
|
||||
const page = await pdf.getPage(1);
|
||||
const blob = await renderPage(page, 1);
|
||||
downloadFile(blob.blobData, getCleanPdfFilename(files[0].name) + '.' + blob.ending);
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const blob = await renderPage(page, i);
|
||||
if (blob.blobData) {
|
||||
zip.file(`page_${i}.` + blob.ending, blob.blobData);
|
||||
}
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(
|
||||
zipBlob,
|
||||
getCleanPdfFilename(files[0].name) + '_tiffs.zip'
|
||||
);
|
||||
}
|
||||
showAlert('Success', 'PDF converted to TIFFs successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to TIFF. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage(
|
||||
page: PDFPageProxy,
|
||||
pageNumber: number
|
||||
): Promise<{ blobData: Blob | null; ending: string; }> {
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context!,
|
||||
viewport: viewport,
|
||||
canvas,
|
||||
}).promise;
|
||||
|
||||
const imageData = context!.getImageData(
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
const rgba = imageData.data;
|
||||
|
||||
try {
|
||||
const tiffData = UTIF.encodeImage(
|
||||
new Uint8Array(rgba),
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
const tiffBlob = new Blob([tiffData], { type: 'image/tiff' });
|
||||
return {
|
||||
blobData: tiffBlob,
|
||||
ending: 'tiff'
|
||||
}
|
||||
} catch (encodeError: any) {
|
||||
console.warn(
|
||||
`TIFF encoding failed for page ${pageNumber}, using PNG fallback:`,
|
||||
encodeError
|
||||
);
|
||||
// Fallback to PNG if TIFF encoding fails (e.g., PackBits compression issues)
|
||||
const pngBlob = await new Promise<Blob | null>((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/png')
|
||||
);
|
||||
if (pngBlob) {
|
||||
return {
|
||||
blobData: pngBlob,
|
||||
ending: 'png'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.type === 'application/pdf'
|
||||
);
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
files = [validFiles[0]];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
import UTIF from 'utif';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
const yieldToUI = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
export async function pdfToTiff() {
|
||||
showLoader('Converting PDF to TIFF...');
|
||||
await yieldToUI();
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
|
||||
if(pdf.numPages === 1) {
|
||||
showLoader(`Processing the single page...`);
|
||||
await yieldToUI();
|
||||
const page = await pdf.getPage(1);
|
||||
const tiffBuffer = await pageToBlob(page);
|
||||
downloadFile(tiffBuffer, getCleanFilename() + '.tiff');
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
await yieldToUI();
|
||||
const page = await pdf.getPage(i);
|
||||
const tiffBuffer = await pageToBlob(page);
|
||||
zip.file(`page_${i}.tiff`, tiffBuffer);
|
||||
}
|
||||
|
||||
showLoader('Compressing files into a ZIP...');
|
||||
await yieldToUI();
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, getCleanFilename() + '_tiffs.zip');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to TIFF. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function pageToBlob(page: PDFPageProxy): Promise<Blob> {
|
||||
const viewport = page.getViewport({ scale: 2.0 }); // Use 2x scale for high quality
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
canvas: canvas,
|
||||
}).promise;
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const rgba = imageData.data;
|
||||
const tiffBuffer = UTIF.encodeImage(
|
||||
new Uint8Array(rgba),
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
return new Blob([tiffBuffer]);
|
||||
}
|
||||
|
||||
function getCleanFilename(): string {
|
||||
let clean = state.files[0].name.replace(/\.pdf$/i, '').trim();
|
||||
if (clean.length > 80) {
|
||||
clean = clean.slice(0, 80);
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
212
src/js/logic/pdf-to-webp-page.ts
Normal file
212
src/js/logic/pdf-to-webp-page.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument, getCleanPdfFilename } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
const updateUI = () => {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const optionsPanel = document.getElementById('options-panel');
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
optionsPanel.classList.remove('hidden');
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
|
||||
// Fetch page count asynchronously
|
||||
readFileAsArrayBuffer(file).then(buffer => {
|
||||
return getPDFDocument(buffer).promise;
|
||||
}).then(pdf => {
|
||||
metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
|
||||
}).catch(e => {
|
||||
console.warn('Error loading PDF page count:', e);
|
||||
metaSpan.textContent = formatBytes(file.size);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize icons immediately after synchronous render
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
optionsPanel.classList.add('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('webp-quality-value');
|
||||
if (qualitySlider) qualitySlider.value = '0.85';
|
||||
if (qualityValue) qualityValue.textContent = '85%';
|
||||
updateUI();
|
||||
};
|
||||
|
||||
async function convert() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting to WebP...');
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(files[0])
|
||||
).promise;
|
||||
|
||||
const qualityInput = document.getElementById('webp-quality') as HTMLInputElement;
|
||||
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.85;
|
||||
|
||||
if (pdf.numPages === 1) {
|
||||
const page = await pdf.getPage(1);
|
||||
const blob = await renderPage(page, quality);
|
||||
downloadFile(blob, getCleanPdfFilename(files[0].name) + '.webp');
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const blob = await renderPage(page, quality);
|
||||
if (blob) {
|
||||
zip.file(`page_${i}.webp`, blob);
|
||||
}
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, getCleanPdfFilename(files[0].name) + '_webps.zip');
|
||||
}
|
||||
showAlert('Success', 'PDF converted to WebPs successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to convert PDF to WebP. The file might be corrupted.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage(
|
||||
page: PDFPageProxy,
|
||||
quality: number
|
||||
): Promise<Blob | null> {
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context!,
|
||||
viewport: viewport,
|
||||
canvas,
|
||||
}).promise;
|
||||
|
||||
const blob = await new Promise<Blob | null>((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/webp', quality)
|
||||
);
|
||||
return blob;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement;
|
||||
const qualityValue = document.getElementById('webp-quality-value');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (qualitySlider && qualityValue) {
|
||||
qualitySlider.addEventListener('input', () => {
|
||||
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
|
||||
});
|
||||
}
|
||||
|
||||
const handleFileSelect = (newFiles: FileList | null) => {
|
||||
if (!newFiles || newFiles.length === 0) return;
|
||||
const validFiles = Array.from(newFiles).filter(
|
||||
(file) => file.type === 'application/pdf'
|
||||
);
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showAlert('Invalid File', 'Please upload a PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
files = [validFiles[0]];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files ?? null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convert);
|
||||
}
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
const yieldToUI = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
|
||||
export async function pdfToWebp() {
|
||||
showLoader('Converting to WebP...');
|
||||
await yieldToUI();
|
||||
try {
|
||||
const pdf = await getPDFDocument(
|
||||
await readFileAsArrayBuffer(state.files[0])
|
||||
).promise;
|
||||
|
||||
if(pdf.numPages === 1) {
|
||||
showLoader(`Processing the single page...`);
|
||||
await yieldToUI();
|
||||
const page = await pdf.getPage(1);
|
||||
const blob = await pageToBlob(page);
|
||||
downloadFile(blob, getCleanFilename() + '.webp');
|
||||
} else {
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
await yieldToUI();
|
||||
const page = await pdf.getPage(i);
|
||||
const blob = await pageToBlob(page);
|
||||
zip.file(`page_${i}.webp`, blob as Blob);
|
||||
}
|
||||
|
||||
showLoader('Compressing files into a ZIP...');
|
||||
await yieldToUI();
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, getCleanFilename() + '_webps.zip');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to WebP.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function pageToBlob(page: PDFPageProxy): Promise<Blob> {
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
|
||||
const qualityInput = document.getElementById('webp-quality') as HTMLInputElement;
|
||||
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9;
|
||||
|
||||
const blob = await new Promise((resolve) =>
|
||||
canvas.toBlob(resolve, 'image/webp', quality)
|
||||
);
|
||||
return blob as Blob;
|
||||
}
|
||||
|
||||
function getCleanFilename(): string {
|
||||
let clean = state.files[0].name.replace(/\.pdf$/i, '').trim();
|
||||
if (clean.length > 80) {
|
||||
clean = clean.slice(0, 80);
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
160
src/js/logic/pdf-to-zip-page.ts
Normal file
160
src/js/logic/pdf-to-zip-page.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
interface PdfToZipState {
|
||||
files: File[];
|
||||
}
|
||||
|
||||
const pageState: PdfToZipState = {
|
||||
files: [],
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.files = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
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';
|
||||
|
||||
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 = function () {
|
||||
pageState.files = pageState.files.filter(function (_, i) { return i !== index; });
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
showLoader('Creating ZIP archive...');
|
||||
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files || null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', createZipArchive);
|
||||
}
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function pdfToZip() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select one or more PDF files.');
|
||||
return;
|
||||
}
|
||||
showLoader('Creating ZIP file...');
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
for (const file of state.files) {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
zip.file(file.name, fileBuffer as ArrayBuffer);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'pdfs.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to create ZIP file.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
248
src/js/logic/png-to-pdf-page.ts
Normal file
248
src/js/logic/png-to-pdf-page.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { showAlert, showLoader, hideLoader } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
let files: File[] = [];
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializePage);
|
||||
} else {
|
||||
initializePage();
|
||||
}
|
||||
|
||||
function initializePage() {
|
||||
createIcons({ icons });
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', handleFileUpload);
|
||||
}
|
||||
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const droppedFiles = e.dataTransfer?.files;
|
||||
if (droppedFiles && droppedFiles.length > 0) {
|
||||
handleFiles(droppedFiles);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput?.addEventListener('click', () => {
|
||||
if (fileInput) fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput?.click();
|
||||
});
|
||||
}
|
||||
|
||||
if (clearFilesBtn) {
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', convertToPdf);
|
||||
}
|
||||
|
||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileUpload(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
handleFiles(input.files);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFiles(newFiles: FileList) {
|
||||
const validFiles = Array.from(newFiles).filter(file =>
|
||||
file.type === 'image/png' || file.name.toLowerCase().endsWith('.png')
|
||||
);
|
||||
|
||||
if (validFiles.length < newFiles.length) {
|
||||
showAlert('Invalid Files', 'Some files were skipped. Only PNG images are allowed.');
|
||||
}
|
||||
|
||||
if (validFiles.length > 0) {
|
||||
files = [...files, ...validFiles];
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
files = [];
|
||||
updateUI();
|
||||
};
|
||||
|
||||
function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
const optionsDiv = document.getElementById('jpg-to-pdf-options');
|
||||
|
||||
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (files.length > 0) {
|
||||
fileControls.classList.remove('hidden');
|
||||
optionsDiv.classList.remove('hidden');
|
||||
|
||||
files.forEach((file, index) => {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
|
||||
sizeSpan.textContent = `(${formatBytes(file.size)})`;
|
||||
|
||||
infoContainer.append(nameSpan, sizeSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = () => {
|
||||
files = files.filter((_, i) => i !== index);
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
createIcons({ icons });
|
||||
} else {
|
||||
fileControls.classList.add('hidden');
|
||||
optionsDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeImageAsJpeg(imageBytes: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([imageBytes]);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
canvas.toBlob(
|
||||
async (jpegBlob) => {
|
||||
if (!jpegBlob) {
|
||||
return reject(new Error('Canvas toBlob conversion failed.'));
|
||||
}
|
||||
const arrayBuffer = await jpegBlob.arrayBuffer();
|
||||
resolve(new Uint8Array(arrayBuffer));
|
||||
},
|
||||
'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(
|
||||
new Error(
|
||||
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
async function convertToPdf() {
|
||||
if (files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one JPG file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Creating PDF from JPGs...');
|
||||
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const file of files) {
|
||||
const originalBytes = await readFileAsArrayBuffer(file);
|
||||
let jpgImage;
|
||||
|
||||
try {
|
||||
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
||||
} catch (e) {
|
||||
showAlert(
|
||||
'Warning',
|
||||
`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`
|
||||
);
|
||||
try {
|
||||
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
||||
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||
} catch (fallbackError) {
|
||||
console.error(
|
||||
`Failed to process ${file.name} after sanitization:`,
|
||||
fallbackError
|
||||
);
|
||||
throw new Error(
|
||||
`Could not process "${file.name}". The file may be corrupted.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
|
||||
page.drawImage(jpgImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: jpgImage.width,
|
||||
height: jpgImage.height,
|
||||
});
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_jpgs.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDF created successfully!', 'success', () => {
|
||||
resetState();
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
showAlert('Conversion Error', e.message);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function pngToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PNG file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Creating PDF from PNGs...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await readFileAsArrayBuffer(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
'from_pngs.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to create PDF from PNG images. Ensure all files are valid PNGs.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
400
src/js/logic/posterize-page.ts
Normal file
400
src/js/logic/posterize-page.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, parsePageRanges, getPDFDocument, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument, PageSizes } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
interface PosterizeState {
|
||||
file: File | null;
|
||||
pdfJsDoc: pdfjsLib.PDFDocumentProxy | null;
|
||||
pdfBytes: Uint8Array | null;
|
||||
pageSnapshots: Record<number, ImageData>;
|
||||
currentPage: number;
|
||||
}
|
||||
|
||||
const pageState: PosterizeState = {
|
||||
file: null,
|
||||
pdfJsDoc: null,
|
||||
pdfBytes: null,
|
||||
pageSnapshots: {},
|
||||
currentPage: 1,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
pageState.pdfJsDoc = null;
|
||||
pageState.pdfBytes = null;
|
||||
pageState.pageSnapshots = {};
|
||||
pageState.currentPage = 1;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
||||
if (processBtn) processBtn.disabled = true;
|
||||
|
||||
const totalPages = document.getElementById('total-pages');
|
||||
if (totalPages) totalPages.textContent = '0';
|
||||
}
|
||||
|
||||
async function renderPosterizePreview(pageNum: number) {
|
||||
if (!pageState.pdfJsDoc) return;
|
||||
|
||||
pageState.currentPage = pageNum;
|
||||
showLoader(`Rendering preview for page ${pageNum}...`);
|
||||
|
||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) {
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageState.pageSnapshots[pageNum]) {
|
||||
canvas.width = pageState.pageSnapshots[pageNum].width;
|
||||
canvas.height = pageState.pageSnapshots[pageNum].height;
|
||||
context.putImageData(pageState.pageSnapshots[pageNum], 0, 0);
|
||||
} else {
|
||||
const page = await pageState.pdfJsDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
||||
pageState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
updatePreviewNav();
|
||||
drawGridOverlay();
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
function drawGridOverlay() {
|
||||
if (!pageState.pageSnapshots[pageState.currentPage] || !pageState.pdfJsDoc) return;
|
||||
|
||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) return;
|
||||
|
||||
context.putImageData(pageState.pageSnapshots[pageState.currentPage], 0, 0);
|
||||
|
||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
||||
const pagesToProcess = parsePageRanges(pageRangeInput, pageState.pdfJsDoc.numPages);
|
||||
|
||||
if (pagesToProcess.includes(pageState.currentPage - 1)) {
|
||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
||||
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
||||
|
||||
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
||||
context.lineWidth = 2;
|
||||
context.setLineDash([10, 5]);
|
||||
|
||||
const cellWidth = canvas.width / cols;
|
||||
const cellHeight = canvas.height / rows;
|
||||
|
||||
for (let i = 1; i < cols; i++) {
|
||||
context.beginPath();
|
||||
context.moveTo(i * cellWidth, 0);
|
||||
context.lineTo(i * cellWidth, canvas.height);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
for (let i = 1; i < rows; i++) {
|
||||
context.beginPath();
|
||||
context.moveTo(0, i * cellHeight);
|
||||
context.lineTo(canvas.width, i * cellHeight);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
context.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreviewNav() {
|
||||
if (!pageState.pdfJsDoc) return;
|
||||
|
||||
const currentPageSpan = document.getElementById('current-preview-page');
|
||||
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement;
|
||||
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement;
|
||||
|
||||
if (currentPageSpan) currentPageSpan.textContent = pageState.currentPage.toString();
|
||||
if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1;
|
||||
if (nextBtn) nextBtn.disabled = pageState.currentPage >= pageState.pdfJsDoc.numPages;
|
||||
}
|
||||
|
||||
async function posterize() {
|
||||
if (!pageState.pdfJsDoc || !pageState.pdfBytes) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Posterizing PDF...');
|
||||
|
||||
try {
|
||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
||||
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
||||
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes;
|
||||
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
|
||||
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
|
||||
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0;
|
||||
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value;
|
||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
||||
|
||||
let overlapInPoints = overlap;
|
||||
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
||||
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
|
||||
|
||||
const newDoc = await PDFDocument.create();
|
||||
const totalPages = pageState.pdfJsDoc.numPages;
|
||||
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
|
||||
if (pageIndicesToProcess.length === 0) {
|
||||
throw new Error('Invalid page range specified.');
|
||||
}
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
|
||||
if (!tempCtx) {
|
||||
throw new Error('Could not create canvas context.');
|
||||
}
|
||||
|
||||
for (const pageIndex of pageIndicesToProcess) {
|
||||
const page = await pageState.pdfJsDoc.getPage(Number(pageIndex) + 1);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport, canvas: tempCanvas }).promise;
|
||||
|
||||
let [targetWidth, targetHeight] = PageSizes[pageSizeKey] || PageSizes.A4;
|
||||
let currentOrientation = orientation;
|
||||
|
||||
if (currentOrientation === 'auto') {
|
||||
currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait';
|
||||
}
|
||||
|
||||
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
} else if (currentOrientation === 'portrait' && targetWidth > targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
}
|
||||
|
||||
const tileWidth = tempCanvas.width / cols;
|
||||
const tileHeight = tempCanvas.height / rows;
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
||||
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
||||
const sWidth = tileWidth + (c > 0 ? overlapInPoints : 0) + (c < cols - 1 ? overlapInPoints : 0);
|
||||
const sHeight = tileHeight + (r > 0 ? overlapInPoints : 0) + (r < rows - 1 ? overlapInPoints : 0);
|
||||
|
||||
const tileCanvas = document.createElement('canvas');
|
||||
tileCanvas.width = sWidth;
|
||||
tileCanvas.height = sHeight;
|
||||
const tileCtx = tileCanvas.getContext('2d');
|
||||
|
||||
if (tileCtx) {
|
||||
tileCtx.drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
||||
|
||||
const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png'));
|
||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||
|
||||
const scaleX = newPage.getWidth() / sWidth;
|
||||
const scaleY = newPage.getHeight() / sHeight;
|
||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
||||
|
||||
const scaledWidth = sWidth * scale;
|
||||
const scaledHeight = sHeight * scale;
|
||||
|
||||
newPage.drawImage(tileImage, {
|
||||
x: (newPage.getWidth() - scaledWidth) / 2,
|
||||
y: (newPage.getHeight() - scaledHeight) / 2,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'posterized.pdf'
|
||||
);
|
||||
|
||||
showAlert('Success', 'Your PDF has been posterized.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', (e as Error).message || 'Could not posterize the PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
if (processBtn) processBtn.disabled = false;
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
pageState.pdfBytes = new Uint8Array(await file.arrayBuffer());
|
||||
pageState.pdfJsDoc = await getPDFDocument({ data: pageState.pdfBytes }).promise;
|
||||
pageState.pageSnapshots = {};
|
||||
pageState.currentPage = 1;
|
||||
|
||||
const totalPagesSpan = document.getElementById('total-pages');
|
||||
const totalPreviewPages = document.getElementById('total-preview-pages');
|
||||
|
||||
if (totalPagesSpan && pageState.pdfJsDoc) {
|
||||
totalPagesSpan.textContent = pageState.pdfJsDoc.numPages.toString();
|
||||
}
|
||||
if (totalPreviewPages && pageState.pdfJsDoc) {
|
||||
totalPreviewPages.textContent = pageState.pdfJsDoc.numPages.toString();
|
||||
}
|
||||
|
||||
await updateUI();
|
||||
await renderPosterizePreview(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
const prevBtn = document.getElementById('prev-preview-page');
|
||||
const nextBtn = document.getElementById('next-preview-page');
|
||||
const rowsInput = document.getElementById('posterize-rows');
|
||||
const colsInput = document.getElementById('posterize-cols');
|
||||
const pageRangeInput = document.getElementById('page-range');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Preview navigation
|
||||
if (prevBtn) {
|
||||
prevBtn.addEventListener('click', function () {
|
||||
if (pageState.currentPage > 1) {
|
||||
renderPosterizePreview(pageState.currentPage - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (nextBtn) {
|
||||
nextBtn.addEventListener('click', function () {
|
||||
if (pageState.pdfJsDoc && pageState.currentPage < pageState.pdfJsDoc.numPages) {
|
||||
renderPosterizePreview(pageState.currentPage + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Grid input changes trigger overlay redraw
|
||||
if (rowsInput) {
|
||||
rowsInput.addEventListener('input', drawGridOverlay);
|
||||
}
|
||||
if (colsInput) {
|
||||
colsInput.addEventListener('input', drawGridOverlay);
|
||||
}
|
||||
if (pageRangeInput) {
|
||||
pageRangeInput.addEventListener('input', drawGridOverlay);
|
||||
}
|
||||
|
||||
// Process button
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', posterize);
|
||||
}
|
||||
});
|
||||
@@ -1,289 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, parsePageRanges, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument, PageSizes } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
const posterizeState = {
|
||||
pdfJsDoc: null,
|
||||
pageSnapshots: {},
|
||||
currentPage: 1,
|
||||
};
|
||||
|
||||
async function renderPosterizePreview(pageNum: number) {
|
||||
if (!posterizeState.pdfJsDoc) return;
|
||||
|
||||
posterizeState.currentPage = pageNum;
|
||||
showLoader(`Rendering preview for page ${pageNum}...`);
|
||||
|
||||
const canvas = document.getElementById(
|
||||
'posterize-preview-canvas'
|
||||
) as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (posterizeState.pageSnapshots[pageNum]) {
|
||||
canvas.width = posterizeState.pageSnapshots[pageNum].width;
|
||||
canvas.height = posterizeState.pageSnapshots[pageNum].height;
|
||||
context.putImageData(posterizeState.pageSnapshots[pageNum], 0, 0);
|
||||
} else {
|
||||
const page = await posterizeState.pdfJsDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
posterizeState.pageSnapshots[pageNum] = context.getImageData(
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
}
|
||||
|
||||
updatePreviewNav();
|
||||
drawGridOverlay();
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
function drawGridOverlay() {
|
||||
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
|
||||
|
||||
const canvas = document.getElementById(
|
||||
'posterize-preview-canvas'
|
||||
) as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
context.putImageData(
|
||||
posterizeState.pageSnapshots[posterizeState.currentPage],
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
const pageRangeInput = (
|
||||
document.getElementById('page-range') as HTMLInputElement
|
||||
).value;
|
||||
const pagesToProcess = parsePageRanges(
|
||||
pageRangeInput,
|
||||
posterizeState.pdfJsDoc.numPages
|
||||
);
|
||||
|
||||
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
|
||||
const rows =
|
||||
parseInt(
|
||||
(document.getElementById('posterize-rows') as HTMLInputElement).value
|
||||
) || 1;
|
||||
const cols =
|
||||
parseInt(
|
||||
(document.getElementById('posterize-cols') as HTMLInputElement).value
|
||||
) || 1;
|
||||
|
||||
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
||||
context.lineWidth = 2;
|
||||
context.setLineDash([10, 5]);
|
||||
|
||||
const cellWidth = canvas.width / cols;
|
||||
const cellHeight = canvas.height / rows;
|
||||
|
||||
for (let i = 1; i < cols; i++) {
|
||||
context.beginPath();
|
||||
context.moveTo(i * cellWidth, 0);
|
||||
context.lineTo(i * cellWidth, canvas.height);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
for (let i = 1; i < rows; i++) {
|
||||
context.beginPath();
|
||||
context.moveTo(0, i * cellHeight);
|
||||
context.lineTo(canvas.width, i * cellHeight);
|
||||
context.stroke();
|
||||
}
|
||||
context.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreviewNav() {
|
||||
const currentPageSpan = document.getElementById('current-preview-page');
|
||||
const prevBtn = document.getElementById(
|
||||
'prev-preview-page'
|
||||
) as HTMLButtonElement;
|
||||
const nextBtn = document.getElementById(
|
||||
'next-preview-page'
|
||||
) as HTMLButtonElement;
|
||||
|
||||
currentPageSpan.textContent = posterizeState.currentPage.toString();
|
||||
prevBtn.disabled = posterizeState.currentPage <= 1;
|
||||
nextBtn.disabled =
|
||||
posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
|
||||
}
|
||||
|
||||
export async function setupPosterizeTool() {
|
||||
if (state.pdfDoc) {
|
||||
document.getElementById('total-pages').textContent = state.pdfDoc
|
||||
.getPageCount()
|
||||
.toString();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
posterizeState.pdfJsDoc = await getPDFDocument({ data: pdfBytes })
|
||||
.promise;
|
||||
posterizeState.pageSnapshots = {};
|
||||
posterizeState.currentPage = 1;
|
||||
|
||||
document.getElementById('total-preview-pages').textContent =
|
||||
posterizeState.pdfJsDoc.numPages.toString();
|
||||
await renderPosterizePreview(1);
|
||||
|
||||
document.getElementById('prev-preview-page').onclick = () =>
|
||||
renderPosterizePreview(posterizeState.currentPage - 1);
|
||||
document.getElementById('next-preview-page').onclick = () =>
|
||||
renderPosterizePreview(posterizeState.currentPage + 1);
|
||||
|
||||
['posterize-rows', 'posterize-cols', 'page-range'].forEach((id) => {
|
||||
document.getElementById(id).addEventListener('input', drawGridOverlay);
|
||||
});
|
||||
createIcons({ icons });
|
||||
}
|
||||
}
|
||||
|
||||
export async function posterize() {
|
||||
showLoader('Posterizing PDF...');
|
||||
try {
|
||||
const rows =
|
||||
parseInt(
|
||||
(document.getElementById('posterize-rows') as HTMLInputElement).value
|
||||
) || 1;
|
||||
const cols =
|
||||
parseInt(
|
||||
(document.getElementById('posterize-cols') as HTMLInputElement).value
|
||||
) || 1;
|
||||
const pageSizeKey = (
|
||||
document.getElementById('output-page-size') as HTMLSelectElement
|
||||
).value;
|
||||
let orientation = (
|
||||
document.getElementById('output-orientation') as HTMLSelectElement
|
||||
).value;
|
||||
const scalingMode = (
|
||||
document.querySelector(
|
||||
'input[name="scaling-mode"]:checked'
|
||||
) as HTMLInputElement
|
||||
).value;
|
||||
const overlap =
|
||||
parseFloat(
|
||||
(document.getElementById('overlap') as HTMLInputElement).value
|
||||
) || 0;
|
||||
const overlapUnits = (
|
||||
document.getElementById('overlap-units') as HTMLSelectElement
|
||||
).value;
|
||||
const pageRangeInput = (
|
||||
document.getElementById('page-range') as HTMLInputElement
|
||||
).value;
|
||||
|
||||
let overlapInPoints = overlap;
|
||||
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
||||
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
|
||||
|
||||
const newDoc = await PDFDocument.create();
|
||||
const totalPages = posterizeState.pdfJsDoc.numPages;
|
||||
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
|
||||
if (pageIndicesToProcess.length === 0) {
|
||||
throw new Error('Invalid page range specified.');
|
||||
}
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
|
||||
for (const pageIndex of pageIndicesToProcess) {
|
||||
const page = await posterizeState.pdfJsDoc.getPage(Number(pageIndex) + 1);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport }).promise;
|
||||
|
||||
let [targetWidth, targetHeight] = PageSizes[pageSizeKey];
|
||||
let currentOrientation = orientation;
|
||||
|
||||
if (currentOrientation === 'auto') {
|
||||
currentOrientation =
|
||||
viewport.width > viewport.height ? 'landscape' : 'portrait';
|
||||
}
|
||||
|
||||
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
} else if (
|
||||
currentOrientation === 'portrait' &&
|
||||
targetWidth > targetHeight
|
||||
) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
}
|
||||
|
||||
const tileWidth = tempCanvas.width / cols;
|
||||
const tileHeight = tempCanvas.height / rows;
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
||||
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
||||
const sWidth =
|
||||
tileWidth +
|
||||
(c > 0 ? overlapInPoints : 0) +
|
||||
(c < cols - 1 ? overlapInPoints : 0);
|
||||
const sHeight =
|
||||
tileHeight +
|
||||
(r > 0 ? overlapInPoints : 0) +
|
||||
(r < rows - 1 ? overlapInPoints : 0);
|
||||
|
||||
const tileCanvas = document.createElement('canvas');
|
||||
tileCanvas.width = sWidth;
|
||||
tileCanvas.height = sHeight;
|
||||
tileCanvas
|
||||
.getContext('2d')
|
||||
.drawImage(
|
||||
tempCanvas,
|
||||
sx,
|
||||
sy,
|
||||
sWidth,
|
||||
sHeight,
|
||||
0,
|
||||
0,
|
||||
sWidth,
|
||||
sHeight
|
||||
);
|
||||
|
||||
const tileImage = await newDoc.embedPng(
|
||||
tileCanvas.toDataURL('image/png')
|
||||
);
|
||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||
|
||||
const scaleX = newPage.getWidth() / sWidth;
|
||||
const scaleY = newPage.getHeight() / sHeight;
|
||||
const scale =
|
||||
scalingMode === 'fit'
|
||||
? Math.min(scaleX, scaleY)
|
||||
: Math.max(scaleX, scaleY);
|
||||
|
||||
const scaledWidth = sWidth * scale;
|
||||
const scaledHeight = sHeight * scale;
|
||||
|
||||
newPage.drawImage(tileImage, {
|
||||
x: (newPage.getWidth() - scaledWidth) / 2,
|
||||
y: (newPage.getHeight() - scaledHeight) / 2,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'posterized.pdf'
|
||||
);
|
||||
showAlert('Success', 'Your PDF has been posterized.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not posterize the PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
176
src/js/logic/remove-annotations-page.ts
Normal file
176
src/js/logic/remove-annotations-page.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { PDFDocument, PDFName } from 'pdf-lib';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
// State management
|
||||
const pageState: { pdfDoc: PDFDocument | null; file: File | null } = {
|
||||
pdfDoc: null,
|
||||
file: null,
|
||||
};
|
||||
|
||||
// UI helpers
|
||||
function showLoader(message: string = 'Processing...') {
|
||||
const loader = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loader) loader.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = message;
|
||||
}
|
||||
|
||||
function hideLoader() {
|
||||
const loader = document.getElementById('loader-modal');
|
||||
if (loader) loader.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) {
|
||||
const modal = document.getElementById('alert-modal');
|
||||
const alertTitle = document.getElementById('alert-title');
|
||||
const alertMessage = document.getElementById('alert-message');
|
||||
const okBtn = document.getElementById('alert-ok');
|
||||
|
||||
if (alertTitle) alertTitle.textContent = title;
|
||||
if (alertMessage) alertMessage.textContent = message;
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
|
||||
if (okBtn) {
|
||||
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
|
||||
okBtn.replaceWith(newOkBtn);
|
||||
newOkBtn.addEventListener('click', () => {
|
||||
modal?.classList.add('hidden');
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const displayArea = document.getElementById('file-display-area');
|
||||
if (!displayArea || !pageState.file || !pageState.pdfDoc) return;
|
||||
|
||||
const fileSize = pageState.file.size < 1024 * 1024
|
||||
? `${(pageState.file.size / 1024).toFixed(1)} KB`
|
||||
: `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`;
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
|
||||
displayArea.innerHTML = `
|
||||
<div class="bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="truncate font-medium text-white">${pageState.file.name}</p>
|
||||
<p class="text-gray-400 text-sm">${fileSize} • ${pageCount} page${pageCount !== 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
<button id="remove-file" class="text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2" title="Remove file">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
createIcons({ icons });
|
||||
|
||||
document.getElementById('remove-file')?.addEventListener('click', () => resetState());
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.pdfDoc = null;
|
||||
pageState.file = null;
|
||||
const displayArea = document.getElementById('file-display-area');
|
||||
if (displayArea) displayArea.innerHTML = '';
|
||||
document.getElementById('options-panel')?.classList.add('hidden');
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
// File handling
|
||||
async function handleFileUpload(file: File) {
|
||||
if (!file || file.type !== 'application/pdf') {
|
||||
showAlert('Error', 'Please upload a valid PDF file.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||
pageState.file = file;
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
// Process function
|
||||
async function processRemoveAnnotations() {
|
||||
if (!pageState.pdfDoc) {
|
||||
showAlert('Error', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Removing annotations...');
|
||||
try {
|
||||
const pages = pageState.pdfDoc.getPages();
|
||||
|
||||
// Remove all annotations from all pages
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||
if (annotRefs.length > 0) {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await pageState.pdfDoc.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'annotations-removed.pdf');
|
||||
showAlert('Success', 'Annotations removed successfully!', 'success', () => { resetState(); });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not remove annotations.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
fileInput?.addEventListener('change', (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) handleFileUpload(file);
|
||||
});
|
||||
|
||||
dropZone?.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('border-indigo-500');
|
||||
});
|
||||
|
||||
dropZone?.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('border-indigo-500');
|
||||
});
|
||||
|
||||
dropZone?.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('border-indigo-500');
|
||||
const file = e.dataTransfer?.files[0];
|
||||
if (file) handleFileUpload(file);
|
||||
});
|
||||
|
||||
processBtn?.addEventListener('click', processRemoveAnnotations);
|
||||
|
||||
backBtn?.addEventListener('click', () => {
|
||||
window.location.href = '../../index.html';
|
||||
});
|
||||
});
|
||||
@@ -1,141 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFName } from 'pdf-lib';
|
||||
|
||||
export function setupRemoveAnnotationsTool() {
|
||||
if (state.pdfDoc) {
|
||||
document.getElementById('total-pages').textContent =
|
||||
state.pdfDoc.getPageCount();
|
||||
}
|
||||
|
||||
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
|
||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||
pageScopeRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
|
||||
});
|
||||
});
|
||||
|
||||
const selectAllCheckbox = document.getElementById('select-all-annotations');
|
||||
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
|
||||
selectAllCheckbox.addEventListener('change', () => {
|
||||
allAnnotCheckboxes.forEach((checkbox) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function removeAnnotationsFromDoc(
|
||||
pdfDoc,
|
||||
pageIndices = null,
|
||||
annotationTypes = null
|
||||
) {
|
||||
const pages = pdfDoc.getPages();
|
||||
const targetPages =
|
||||
pageIndices || Array.from({ length: pages.length }, (_, i) => i);
|
||||
|
||||
for (const pageIndex of targetPages) {
|
||||
const page = pages[pageIndex];
|
||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||
|
||||
if (!annotationTypes) {
|
||||
if (annotRefs.length > 0) {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
} else {
|
||||
const annotsToKeep = [];
|
||||
|
||||
for (const ref of annotRefs) {
|
||||
const annot = pdfDoc.context.lookup(ref);
|
||||
const subtype = annot
|
||||
.get(PDFName.of('Subtype'))
|
||||
?.toString()
|
||||
.substring(1);
|
||||
|
||||
if (!subtype || !annotationTypes.has(subtype)) {
|
||||
annotsToKeep.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (annotsToKeep.length > 0) {
|
||||
const newAnnotsArray = pdfDoc.context.obj(annotsToKeep);
|
||||
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||
} else {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeAnnotations() {
|
||||
showLoader('Removing annotations...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
let targetPageIndices = [];
|
||||
|
||||
const pageScope = (
|
||||
document.querySelector(
|
||||
'input[name="page-scope"]:checked'
|
||||
) as HTMLInputElement
|
||||
).value;
|
||||
if (pageScope === 'all') {
|
||||
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const rangeInput = document.getElementById('page-range-input').value;
|
||||
if (!rangeInput.trim()) throw new Error('Please enter a page range.');
|
||||
const ranges = rangeInput.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++) targetPageIndices.push(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
targetPageIndices.push(pageNum - 1);
|
||||
}
|
||||
}
|
||||
targetPageIndices = [...new Set(targetPageIndices)];
|
||||
}
|
||||
|
||||
if (targetPageIndices.length === 0)
|
||||
throw new Error('No valid pages were selected.');
|
||||
|
||||
const typesToRemove = new Set(
|
||||
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(
|
||||
(cb) => (cb as HTMLInputElement).value
|
||||
)
|
||||
);
|
||||
|
||||
if (typesToRemove.size === 0)
|
||||
throw new Error('Please select at least one annotation type to remove.');
|
||||
|
||||
removeAnnotationsFromDoc(state.pdfDoc, targetPageIndices, typesToRemove);
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'annotations-removed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert(
|
||||
'Error',
|
||||
e.message || 'Could not remove annotations. Please check your page range.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
328
src/js/logic/remove-blank-pages-page.ts
Normal file
328
src/js/logic/remove-blank-pages-page.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
// State
|
||||
const pageState: {
|
||||
pdfDoc: PDFDocument | null;
|
||||
file: File | null;
|
||||
detectedBlankPages: number[];
|
||||
pageThumbnails: Map<number, string>;
|
||||
} = {
|
||||
pdfDoc: null,
|
||||
file: null,
|
||||
detectedBlankPages: [],
|
||||
pageThumbnails: new Map()
|
||||
};
|
||||
|
||||
function showLoader(msg = 'Processing...') {
|
||||
document.getElementById('loader-modal')?.classList.remove('hidden');
|
||||
const txt = document.getElementById('loader-text');
|
||||
if (txt) txt.textContent = msg;
|
||||
}
|
||||
|
||||
function hideLoader() { document.getElementById('loader-modal')?.classList.add('hidden'); }
|
||||
|
||||
function showAlert(title: string, msg: string, type = 'error', cb?: () => void) {
|
||||
const modal = document.getElementById('alert-modal');
|
||||
const t = document.getElementById('alert-title');
|
||||
const m = document.getElementById('alert-message');
|
||||
if (t) t.textContent = title;
|
||||
if (m) m.textContent = msg;
|
||||
modal?.classList.remove('hidden');
|
||||
const okBtn = document.getElementById('alert-ok');
|
||||
if (okBtn) {
|
||||
const newBtn = okBtn.cloneNode(true) as HTMLElement;
|
||||
okBtn.replaceWith(newBtn);
|
||||
newBtn.addEventListener('click', () => {
|
||||
modal?.classList.add('hidden');
|
||||
if (cb) cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFile(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = filename; a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function updateFileDisplay() {
|
||||
const area = document.getElementById('file-display-area');
|
||||
if (!area || !pageState.file || !pageState.pdfDoc) return;
|
||||
|
||||
const fileSize = pageState.file.size < 1024 * 1024
|
||||
? `${(pageState.file.size / 1024).toFixed(1)} KB`
|
||||
: `${(pageState.file.size / 1024 / 1024).toFixed(2)} MB`;
|
||||
const pageCount = pageState.pdfDoc.getPageCount();
|
||||
|
||||
area.innerHTML = `
|
||||
<div class="bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="truncate font-medium text-white">${pageState.file.name}</p>
|
||||
<p class="text-gray-400 text-sm">${fileSize} • ${pageCount} page${pageCount !== 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
<button id="remove-file" class="text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2" title="Remove file">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
createIcons({ icons });
|
||||
document.getElementById('remove-file')?.addEventListener('click', resetState);
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.pdfDoc = null;
|
||||
pageState.file = null;
|
||||
pageState.detectedBlankPages = [];
|
||||
pageState.pageThumbnails.forEach(url => URL.revokeObjectURL(url));
|
||||
pageState.pageThumbnails.clear();
|
||||
|
||||
const area = document.getElementById('file-display-area');
|
||||
if (area) area.innerHTML = '';
|
||||
document.getElementById('options-panel')?.classList.add('hidden');
|
||||
document.getElementById('preview-panel')?.classList.add('hidden');
|
||||
const inp = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (inp) inp.value = '';
|
||||
}
|
||||
|
||||
async function handleFileUpload(file: File) {
|
||||
if (!file || file.type !== 'application/pdf') {
|
||||
showAlert('Error', 'Please upload a valid PDF file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const buf = await file.arrayBuffer();
|
||||
pageState.pdfDoc = await PDFDocument.load(buf);
|
||||
pageState.file = file;
|
||||
pageState.detectedBlankPages = [];
|
||||
updateFileDisplay();
|
||||
document.getElementById('options-panel')?.classList.remove('hidden');
|
||||
document.getElementById('preview-panel')?.classList.add('hidden');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to load PDF file.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
async function isPageBlank(page: any, threshold = 250): Promise<boolean> {
|
||||
const viewport = page.getViewport({ scale: 0.5 }); // Lower scale for faster processing
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return false;
|
||||
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
let totalBrightness = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i], g = data[i + 1], b = data[i + 2];
|
||||
totalBrightness += (r + g + b) / 3;
|
||||
}
|
||||
|
||||
const avgBrightness = totalBrightness / (data.length / 4);
|
||||
return avgBrightness > threshold;
|
||||
}
|
||||
|
||||
async function generateThumbnail(page: any): Promise<string> {
|
||||
const viewport = page.getViewport({ scale: 0.3 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return '';
|
||||
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
return canvas.toDataURL('image/jpeg', 0.7);
|
||||
}
|
||||
|
||||
async function detectBlankPages() {
|
||||
if (!pageState.pdfDoc || !pageState.file) return showAlert('Error', 'Please upload a PDF first.');
|
||||
|
||||
const sensitivitySlider = document.getElementById('sensitivity-slider') as HTMLInputElement;
|
||||
const sensitivityPercent = parseInt(sensitivitySlider?.value || '80');
|
||||
const threshold = Math.round(255 - (sensitivityPercent * 2.55));
|
||||
|
||||
showLoader('Detecting blank pages...');
|
||||
try {
|
||||
const pdfData = await pageState.file.arrayBuffer();
|
||||
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
const totalPages = pdfDoc.numPages;
|
||||
|
||||
pageState.detectedBlankPages = [];
|
||||
pageState.pageThumbnails.forEach(url => URL.revokeObjectURL(url));
|
||||
pageState.pageThumbnails.clear();
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const page = await pdfDoc.getPage(i);
|
||||
if (await isPageBlank(page, threshold)) {
|
||||
pageState.detectedBlankPages.push(i - 1); // 0-indexed
|
||||
const thumbnail = await generateThumbnail(page);
|
||||
pageState.pageThumbnails.set(i - 1, thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
if (pageState.detectedBlankPages.length === 0) {
|
||||
showAlert('Info', 'No blank pages detected in this PDF.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show preview panel
|
||||
updatePreviewPanel();
|
||||
document.getElementById('preview-panel')?.classList.remove('hidden');
|
||||
hideLoader();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not detect blank pages.');
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreviewPanel() {
|
||||
const previewInfo = document.getElementById('preview-info');
|
||||
const previewContainer = document.getElementById('blank-pages-preview');
|
||||
|
||||
if (!previewInfo || !previewContainer) return;
|
||||
|
||||
previewInfo.textContent = `Found ${pageState.detectedBlankPages.length} blank page(s). Click on a page to deselect it.`;
|
||||
previewContainer.innerHTML = '';
|
||||
|
||||
pageState.detectedBlankPages.forEach((pageIndex) => {
|
||||
const thumbnail = pageState.pageThumbnails.get(pageIndex) || '';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'relative cursor-pointer group';
|
||||
div.dataset.pageIndex = String(pageIndex);
|
||||
div.dataset.selected = 'true';
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="relative border-2 border-red-500 rounded-lg overflow-hidden transition-all">
|
||||
<img src="${thumbnail}" alt="Page ${pageIndex + 1}" class="w-full h-auto">
|
||||
<div class="absolute bottom-0 left-0 right-0 bg-black/70 text-white text-xs text-center py-1">
|
||||
Page ${pageIndex + 1}
|
||||
</div>
|
||||
<div class="absolute top-1 right-1 bg-red-500 rounded-full w-5 h-5 flex items-center justify-center check-mark">
|
||||
<i data-lucide="check" class="w-3 h-3 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
div.addEventListener('click', () => togglePageSelection(div, pageIndex));
|
||||
previewContainer.appendChild(div);
|
||||
});
|
||||
|
||||
createIcons({ icons });
|
||||
}
|
||||
|
||||
function togglePageSelection(div: HTMLElement, pageIndex: number) {
|
||||
const isSelected = div.dataset.selected === 'true';
|
||||
const border = div.querySelector('.border-2') as HTMLElement;
|
||||
const checkMark = div.querySelector('.check-mark') as HTMLElement;
|
||||
|
||||
if (isSelected) {
|
||||
div.dataset.selected = 'false';
|
||||
border?.classList.remove('border-red-500');
|
||||
border?.classList.add('border-gray-500', 'opacity-50');
|
||||
checkMark?.classList.add('hidden');
|
||||
} else {
|
||||
div.dataset.selected = 'true';
|
||||
border?.classList.add('border-red-500');
|
||||
border?.classList.remove('border-gray-500', 'opacity-50');
|
||||
checkMark?.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function processRemoveBlankPages() {
|
||||
if (!pageState.pdfDoc || !pageState.file) return showAlert('Error', 'Please upload a PDF first.');
|
||||
|
||||
// Get selected pages to remove
|
||||
const previewContainer = document.getElementById('blank-pages-preview');
|
||||
const selectedPages: number[] = [];
|
||||
previewContainer?.querySelectorAll('[data-selected="true"]').forEach(el => {
|
||||
const pageIndex = parseInt((el as HTMLElement).dataset.pageIndex || '-1');
|
||||
if (pageIndex >= 0) selectedPages.push(pageIndex);
|
||||
});
|
||||
|
||||
if (selectedPages.length === 0) {
|
||||
showAlert('Info', 'No pages selected for removal.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader(`Removing ${selectedPages.length} blank page(s)...`);
|
||||
try {
|
||||
const newPdf = await PDFDocument.create();
|
||||
const pages = pageState.pdfDoc.getPages();
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
if (!selectedPages.includes(i)) {
|
||||
const [copiedPage] = await newPdf.copyPages(pageState.pdfDoc, [i]);
|
||||
newPdf.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'blank-pages-removed.pdf');
|
||||
showAlert('Success', `Removed ${selectedPages.length} blank page(s) successfully!`, 'success', resetState);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not remove blank pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
const detectBtn = document.getElementById('detect-btn');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
const sensitivitySlider = document.getElementById('sensitivity-slider') as HTMLInputElement;
|
||||
const sensitivityValue = document.getElementById('sensitivity-value');
|
||||
|
||||
sensitivitySlider?.addEventListener('input', (e) => {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
if (sensitivityValue) sensitivityValue.textContent = value;
|
||||
});
|
||||
|
||||
fileInput?.addEventListener('change', (e) => {
|
||||
const f = (e.target as HTMLInputElement).files?.[0];
|
||||
if (f) handleFileUpload(f);
|
||||
});
|
||||
|
||||
dropZone?.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('border-indigo-500');
|
||||
});
|
||||
|
||||
dropZone?.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('border-indigo-500');
|
||||
});
|
||||
|
||||
dropZone?.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('border-indigo-500');
|
||||
const f = e.dataTransfer?.files[0];
|
||||
if (f) handleFileUpload(f);
|
||||
});
|
||||
|
||||
detectBtn?.addEventListener('click', detectBlankPages);
|
||||
processBtn?.addEventListener('click', processRemoveBlankPages);
|
||||
|
||||
document.getElementById('back-to-tools')?.addEventListener('click', () => {
|
||||
window.location.href = '../../index.html';
|
||||
});
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFPageProxy } from 'pdfjs-dist/types/src/display/api.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
let analysisCache = [];
|
||||
|
||||
async function isPageBlank(page: PDFPageProxy, threshold: number) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
const viewport = page.getViewport({ scale: 0.2 });
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
await page.render({ canvasContext: context, viewport, canvas: canvas })
|
||||
.promise;
|
||||
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
const totalPixels = data.length / 4;
|
||||
let nonWhitePixels = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i] < 245 || data[i + 1] < 245 || data[i + 2] < 245) {
|
||||
nonWhitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
const blankness = 1 - nonWhitePixels / totalPixels;
|
||||
return blankness >= threshold / 100;
|
||||
}
|
||||
|
||||
async function analyzePages() {
|
||||
if (!state.pdfDoc) return;
|
||||
showLoader('Analyzing for blank pages...');
|
||||
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
const pdf = await getPDFDocument({ data: pdfBytes }).promise;
|
||||
|
||||
analysisCache = [];
|
||||
const promises = [];
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
promises.push(
|
||||
pdf.getPage(i).then((page) =>
|
||||
isPageBlank(page, 0).then((isActuallyBlank) => ({
|
||||
pageNum: i,
|
||||
isInitiallyBlank: isActuallyBlank,
|
||||
pageRef: page,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
analysisCache = await Promise.all(promises);
|
||||
hideLoader();
|
||||
updateAnalysisUI();
|
||||
}
|
||||
|
||||
async function updateAnalysisUI() {
|
||||
const sensitivity = parseInt(
|
||||
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
|
||||
);
|
||||
(
|
||||
document.getElementById('sensitivity-value') as HTMLSpanElement
|
||||
).textContent = sensitivity.toString();
|
||||
|
||||
const previewContainer = document.getElementById('analysis-preview');
|
||||
const analysisText = document.getElementById('analysis-text');
|
||||
const thumbnailsContainer = document.getElementById(
|
||||
'removed-pages-thumbnails'
|
||||
);
|
||||
|
||||
thumbnailsContainer.innerHTML = '';
|
||||
|
||||
const pagesToRemove = [];
|
||||
|
||||
for (const pageData of analysisCache) {
|
||||
const isConsideredBlank = await isPageBlank(pageData.pageRef, sensitivity);
|
||||
if (isConsideredBlank) {
|
||||
pagesToRemove.push(pageData.pageNum);
|
||||
}
|
||||
}
|
||||
|
||||
if (pagesToRemove.length > 0) {
|
||||
analysisText.textContent = `Found ${pagesToRemove.length} blank page(s) to remove: ${pagesToRemove.join(', ')}`;
|
||||
previewContainer.classList.remove('hidden');
|
||||
|
||||
for (const pageNum of pagesToRemove) {
|
||||
const pageData = analysisCache[pageNum - 1];
|
||||
const viewport = pageData.pageRef.getViewport({ scale: 0.1 });
|
||||
const thumbCanvas = document.createElement('canvas');
|
||||
thumbCanvas.width = viewport.width;
|
||||
thumbCanvas.height = viewport.height;
|
||||
await pageData.pageRef.render({
|
||||
canvasContext: thumbCanvas.getContext('2d'),
|
||||
viewport,
|
||||
}).promise;
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = thumbCanvas.toDataURL();
|
||||
img.className = 'rounded border border-gray-600';
|
||||
img.title = `Page ${pageNum}`;
|
||||
thumbnailsContainer.appendChild(img);
|
||||
}
|
||||
} else {
|
||||
analysisText.textContent =
|
||||
'No blank pages found at this sensitivity level.';
|
||||
previewContainer.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupRemoveBlankPagesTool() {
|
||||
await analyzePages();
|
||||
document
|
||||
.getElementById('sensitivity-slider')
|
||||
.addEventListener('input', updateAnalysisUI);
|
||||
}
|
||||
|
||||
export async function removeBlankPages() {
|
||||
showLoader('Removing blank pages...');
|
||||
try {
|
||||
const sensitivity = parseInt(
|
||||
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
|
||||
);
|
||||
const indicesToKeep = [];
|
||||
|
||||
for (const pageData of analysisCache) {
|
||||
const isConsideredBlank = await isPageBlank(
|
||||
pageData.pageRef,
|
||||
sensitivity
|
||||
);
|
||||
if (!isConsideredBlank) {
|
||||
indicesToKeep.push(pageData.pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (indicesToKeep.length === 0) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'No Content Found',
|
||||
'All pages were identified as blank at the current sensitivity setting. No new file was created. Try lowering the sensitivity if you believe this is an error.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (indicesToKeep.length === state.pdfDoc.getPageCount()) {
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'No Pages Removed',
|
||||
'No pages were identified as blank at the current sensitivity level.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const newPdf = await PDFDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||
'non-blank.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not remove blank pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
203
src/js/logic/remove-metadata-page.ts
Normal file
203
src/js/logic/remove-metadata-page.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { PDFDocument, PDFName } from 'pdf-lib';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
file: null,
|
||||
};
|
||||
|
||||
function removeMetadataFromDoc(pdfDoc: PDFDocument) {
|
||||
const infoDict = (pdfDoc as any).getInfoDict();
|
||||
const allKeys = infoDict.keys();
|
||||
allKeys.forEach((key: any) => {
|
||||
infoDict.delete(key);
|
||||
});
|
||||
|
||||
pdfDoc.setTitle('');
|
||||
pdfDoc.setAuthor('');
|
||||
pdfDoc.setSubject('');
|
||||
pdfDoc.setKeywords([]);
|
||||
pdfDoc.setCreator('');
|
||||
pdfDoc.setProducer('');
|
||||
|
||||
try {
|
||||
const catalogDict = (pdfDoc.catalog as any).dict;
|
||||
if (catalogDict.has(PDFName.of('Metadata'))) {
|
||||
catalogDict.delete(PDFName.of('Metadata'));
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn('Could not remove XMP metadata:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const context = pdfDoc.context;
|
||||
if ((context as any).trailerInfo) {
|
||||
delete (context as any).trailerInfo.ID;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn('Could not remove document IDs:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const catalogDict = (pdfDoc.catalog as any).dict;
|
||||
if (catalogDict.has(PDFName.of('PieceInfo'))) {
|
||||
catalogDict.delete(PDFName.of('PieceInfo'));
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn('Could not remove PieceInfo:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMetadata() {
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Removing all metadata...';
|
||||
|
||||
try {
|
||||
const arrayBuffer = await pageState.file.arrayBuffer();
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer);
|
||||
|
||||
removeMetadataFromDoc(pdfDoc);
|
||||
|
||||
const newPdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
|
||||
'metadata-removed.pdf'
|
||||
);
|
||||
showAlert('Success', 'Metadata removed successfully!', 'success', () => { resetState(); });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
||||
} 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 backBtn = document.getElementById('back-to-tools');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', removeMetadata);
|
||||
}
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFName } from 'pdf-lib';
|
||||
|
||||
export function removeMetadataFromDoc(pdfDoc) {
|
||||
const infoDict = pdfDoc.getInfoDict();
|
||||
const allKeys = infoDict.keys();
|
||||
allKeys.forEach((key: any) => {
|
||||
infoDict.delete(key);
|
||||
});
|
||||
|
||||
pdfDoc.setTitle('');
|
||||
pdfDoc.setAuthor('');
|
||||
pdfDoc.setSubject('');
|
||||
pdfDoc.setKeywords([]);
|
||||
pdfDoc.setCreator('');
|
||||
pdfDoc.setProducer('');
|
||||
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
if (catalogDict.has(PDFName.of('Metadata'))) {
|
||||
catalogDict.delete(PDFName.of('Metadata'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove XMP metadata:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const context = pdfDoc.context;
|
||||
if (context.trailerInfo) {
|
||||
delete context.trailerInfo.ID;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove document IDs:', e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
const catalogDict = pdfDoc.catalog.dict;
|
||||
if (catalogDict.has(PDFName.of('PieceInfo'))) {
|
||||
catalogDict.delete(PDFName.of('PieceInfo'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not remove PieceInfo:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeMetadata() {
|
||||
showLoader('Removing all metadata...');
|
||||
try {
|
||||
removeMetadataFromDoc(state.pdfDoc);
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||
'metadata-removed.pdf'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
233
src/js/logic/remove-restrictions-page.ts
Normal file
233
src/js/logic/remove-restrictions-page.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
|
||||
interface PageState {
|
||||
file: File | null;
|
||||
}
|
||||
|
||||
const pageState: PageState = {
|
||||
file: null,
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
pageState.file = null;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
|
||||
const passwordInput = document.getElementById('owner-password-remove') as HTMLInputElement;
|
||||
if (passwordInput) passwordInput.value = '';
|
||||
}
|
||||
|
||||
async function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
fileDisplayArea.innerHTML = '';
|
||||
|
||||
if (pageState.file) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||
|
||||
const infoContainer = document.createElement('div');
|
||||
infoContainer.className = 'flex flex-col overflow-hidden';
|
||||
|
||||
const nameSpan = document.createElement('div');
|
||||
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
|
||||
nameSpan.textContent = pageState.file.name;
|
||||
|
||||
const metaSpan = document.createElement('div');
|
||||
metaSpan.className = 'text-xs text-gray-400';
|
||||
metaSpan.textContent = formatBytes(pageState.file.size);
|
||||
|
||||
infoContainer.append(nameSpan, metaSpan);
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.onclick = function () {
|
||||
resetState();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
createIcons({ icons });
|
||||
|
||||
if (toolOptions) toolOptions.classList.remove('hidden');
|
||||
} else {
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(files: FileList | null) {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
|
||||
pageState.file = file;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function removeRestrictions() {
|
||||
if (!pageState.file) {
|
||||
showAlert('No File', 'Please upload a PDF file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const password = (document.getElementById('owner-password-remove') as HTMLInputElement)?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
const loaderModal = document.getElementById('loader-modal');
|
||||
const loaderText = document.getElementById('loader-text');
|
||||
|
||||
try {
|
||||
if (loaderModal) loaderModal.classList.remove('hidden');
|
||||
if (loaderText) loaderText.textContent = 'Initializing...';
|
||||
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Reading PDF...';
|
||||
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Removing restrictions...';
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
if (password) {
|
||||
args.push(`--password=${password}`);
|
||||
}
|
||||
|
||||
args.push('--decrypt', '--remove-restrictions', '--', outputPath);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
if (
|
||||
qpdfError.message?.includes('password') ||
|
||||
qpdfError.message?.includes('encrypt')
|
||||
) {
|
||||
throw new Error(
|
||||
'Failed to remove restrictions. The PDF may require the correct owner password.'
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Failed to remove restrictions: ' +
|
||||
(qpdfError.message || 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
if (loaderText) loaderText.textContent = 'Preparing download...';
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Operation resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `unrestricted-${pageState.file.name}`);
|
||||
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF restrictions removed successfully! The file is now fully editable and printable.',
|
||||
'success',
|
||||
() => { resetState(); }
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error during restriction removal:', error);
|
||||
if (loaderModal) loaderModal.classList.add('hidden');
|
||||
showAlert(
|
||||
'Operation Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password-protected.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
const pdfFiles = Array.from(files).filter(function (f) {
|
||||
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
|
||||
});
|
||||
if (pdfFiles.length > 0) {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(pdfFiles[0]);
|
||||
handleFileSelect(dataTransfer.files);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', removeRestrictions);
|
||||
}
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import {
|
||||
downloadFile,
|
||||
initializeQpdf,
|
||||
readFileAsArrayBuffer,
|
||||
} from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function removeRestrictions() {
|
||||
const file = state.files[0];
|
||||
const password =
|
||||
(document.getElementById('owner-password-remove') as HTMLInputElement)
|
||||
?.value || '';
|
||||
|
||||
const inputPath = '/input.pdf';
|
||||
const outputPath = '/output.pdf';
|
||||
let qpdf: any;
|
||||
|
||||
try {
|
||||
showLoader('Initializing...');
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
showLoader('Reading PDF...');
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
showLoader('Removing restrictions...');
|
||||
|
||||
const args = [inputPath];
|
||||
|
||||
if (password) {
|
||||
args.push(`--password=${password}`);
|
||||
}
|
||||
|
||||
args.push('--decrypt', '--remove-restrictions', '--', outputPath);
|
||||
|
||||
try {
|
||||
qpdf.callMain(args);
|
||||
} catch (qpdfError: any) {
|
||||
console.error('qpdf execution error:', qpdfError);
|
||||
if (
|
||||
qpdfError.message?.includes('password') ||
|
||||
qpdfError.message?.includes('encrypt')
|
||||
) {
|
||||
throw new Error(
|
||||
'Failed to remove restrictions. The PDF may require the correct owner password.'
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Failed to remove restrictions: ' +
|
||||
(qpdfError.message || 'Unknown error')
|
||||
);
|
||||
}
|
||||
|
||||
showLoader('Preparing download...');
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
throw new Error('Operation resulted in an empty file.');
|
||||
}
|
||||
|
||||
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||
downloadFile(blob, `unrestricted-${file.name}`);
|
||||
|
||||
hideLoader();
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
'PDF restrictions removed successfully! The file is now fully editable and printable.'
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error during restriction removal:', error);
|
||||
hideLoader();
|
||||
showAlert(
|
||||
'Operation Failed',
|
||||
`An error occurred: ${error.message || 'The PDF might be corrupted or password-protected.'}`
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
if (qpdf?.FS) {
|
||||
try {
|
||||
qpdf.FS.unlink(inputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink input file:', e);
|
||||
}
|
||||
try {
|
||||
qpdf.FS.unlink(outputPath);
|
||||
} catch (e) {
|
||||
console.warn('Failed to unlink output file:', e);
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,12 +38,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
state.files = [...state.files, ...Array.from(files)];
|
||||
updateUI();
|
||||
}
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', (e) => {
|
||||
@@ -64,8 +63,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
dropZone.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
// Clear value on click to allow re-selecting the same file
|
||||
fileInput.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
207
src/js/logic/reverse-pages-page.ts
Normal file
207
src/js/logic/reverse-pages-page.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, formatBytes } from '../utils/helpers.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
interface ReverseState {
|
||||
files: File[];
|
||||
}
|
||||
|
||||
const reverseState: ReverseState = {
|
||||
files: [],
|
||||
};
|
||||
|
||||
function resetState() {
|
||||
reverseState.files = [];
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
if (toolOptions) toolOptions.classList.add('hidden');
|
||||
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
const toolOptions = document.getElementById('tool-options');
|
||||
|
||||
if (!fileDisplayArea) return;
|
||||
|
||||
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';
|
||||
|
||||
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 = function () {
|
||||
reverseState.files = reverseState.files.filter(function (_, i) { return i !== index; });
|
||||
updateUI();
|
||||
};
|
||||
|
||||
fileDiv.append(infoContainer, removeBtn);
|
||||
fileDisplayArea.appendChild(fileDiv);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
showLoader('Reversing page order...');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 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, '');
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
window.location.href = import.meta.env.BASE_URL;
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInput && dropZone) {
|
||||
fileInput.addEventListener('change', function (e) {
|
||||
handleFileSelect((e.target as HTMLInputElement).files);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('bg-gray-700');
|
||||
handleFileSelect(e.dataTransfer?.files || null);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('click', function () {
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (processBtn) {
|
||||
processBtn.addEventListener('click', reversePages);
|
||||
}
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function reversePages() {
|
||||
const pdfDocs = state.files.filter(
|
||||
(file: File) => file.type === 'application/pdf'
|
||||
);
|
||||
if (!pdfDocs.length) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Reversing page order...');
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
for (let j = 0; j < pdfDocs.length; j++) {
|
||||
const file = pdfDocs[j];
|
||||
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 },
|
||||
(_, i) => pageCount - 1 - i
|
||||
);
|
||||
|
||||
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
||||
copiedPages.forEach((page: any) => 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 zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'reversed_pdfs.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not reverse the PDF pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user