import { EditMetadataState } from '@/types';
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';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
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 = '';
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 = '';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
const result = await loadPdfWithPasswordPrompt(pageState.file);
if (!result) {
resetState();
return;
}
showLoader('Loading PDF...');
result.pdf.destroy();
pageState.file = result.file;
pageState.pdfDoc = await PDFLibDocument.load(result.bytes, {
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);
}
});