feat(signature): add font and color customization for typed signatures

feat(stamps): implement new add stamps tool with image stamp support

fix(form-filler): improve form filler UI and XFA form support

refactor(sign-pdf): improve signature tool initialization and error handling

style(ui): update text color for better visibility in dark mode

chore: update navigation links to use root-relative paths
This commit is contained in:
abdullahalam123
2025-11-14 20:35:43 +05:30
parent c31704eb0e
commit ae8bd3a004
12 changed files with 579 additions and 96 deletions

165
src/js/logic/add-stamps.ts Normal file
View File

@@ -0,0 +1,165 @@
import { formatBytes, readFileAsArrayBuffer } from '../utils/helpers'
let selectedFile: File | null = null
let viewerIframe: HTMLIFrameElement | null = null
let viewerReady = false
let currentBlobUrl: string | null = null
const pdfInput = document.getElementById('pdfFile') as HTMLInputElement
const fileListDiv = document.getElementById('fileList') as HTMLDivElement
const viewerContainer = document.getElementById('stamp-viewer-container') as HTMLDivElement
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
function updateFileList() {
if (!selectedFile) {
fileListDiv.classList.add('hidden')
fileListDiv.innerHTML = ''
return
}
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'
const nameSpan = document.createElement('span')
nameSpan.className = 'truncate font-medium text-gray-200'
nameSpan.textContent = selectedFile.name
const sizeSpan = document.createElement('span')
sizeSpan.className = 'ml-3 text-gray-400 text-xs flex-shrink-0'
sizeSpan.textContent = formatBytes(selectedFile.size)
wrapper.append(nameSpan, sizeSpan)
fileListDiv.appendChild(wrapper)
}
async function loadPdfInViewer(file: File) {
if (!viewerContainer) return
if (viewerCard) {
viewerCard.classList.remove('hidden')
}
// Clear existing iframe and blob URL
if (viewerIframe && viewerIframe.parentElement === viewerContainer) {
viewerContainer.removeChild(viewerIframe)
}
if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl)
currentBlobUrl = null
}
viewerIframe = null
viewerReady = false
const arrayBuffer = await readFileAsArrayBuffer(file)
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' })
currentBlobUrl = URL.createObjectURL(blob)
const iframe = document.createElement('iframe')
iframe.className = 'w-full h-full border-0'
iframe.allowFullscreen = true
iframe.src = `/pdfjs-annotation-viewer/web/viewer.html?file=${encodeURIComponent(currentBlobUrl)}`
iframe.addEventListener('load', () => {
setupAnnotationViewer(iframe)
})
viewerContainer.appendChild(iframe)
viewerIframe = iframe
}
function setupAnnotationViewer(iframe: HTMLIFrameElement) {
try {
const win = iframe.contentWindow as any
const doc = win?.document as Document | null
if (!win || !doc) return
const initialize = async () => {
try {
const app = win.PDFViewerApplication
if (app?.initializedPromise) {
await app.initializedPromise
}
const stampBtn = doc.getElementById('editorStamp') as HTMLButtonElement | null
if (stampBtn) {
stampBtn.classList.remove('hidden')
stampBtn.disabled = false
}
const AnnotationEditorType = win.pdfjsLib?.AnnotationEditorType
if (app?.pdfViewer && AnnotationEditorType?.STAMP != null) {
app.pdfViewer.annotationEditorMode = AnnotationEditorType.STAMP
}
const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null
if (root) {
root.classList.add('PdfjsAnnotationExtension_Comment_hidden')
}
viewerReady = true
} catch (e) {
console.error('Failed to initialize annotation viewer for Add Stamps:', e)
}
}
void initialize()
} catch (e) {
console.error('Error wiring Add Stamps viewer:', e)
}
}
async function onPdfSelected(file: File) {
selectedFile = file
updateFileList()
await loadPdfInViewer(file)
}
if (pdfInput) {
pdfInput.addEventListener('change', async (e) => {
const target = e.target as HTMLInputElement
if (target.files && target.files.length > 0) {
const file = target.files[0]
await onPdfSelected(file)
}
})
}
if (saveStampedBtn) {
saveStampedBtn.addEventListener('click', () => {
if (!viewerIframe) {
alert('Viewer not ready. Please upload a PDF and wait for it to finish loading.')
return
}
try {
const win = viewerIframe.contentWindow as any
const extensionInstance = win?.pdfjsAnnotationExtensionInstance as any
if (extensionInstance && typeof extensionInstance.exportPdf === 'function') {
const result = extensionInstance.exportPdf()
if (result && typeof result.then === 'function') {
result.catch((err: unknown) => {
console.error('Error while exporting stamped PDF via annotation extension:', err)
})
}
return
}
alert('Could not access the stamped-PDF exporter. Please use the Export → PDF button in the viewer toolbar as a fallback.')
} catch (e) {
console.error('Failed to trigger stamped PDF export:', e)
alert('Could not export the stamped PDF. Please use the Export → PDF button in the viewer toolbar as a fallback.')
}
})
}
if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => {
window.location.href = '/'
})
}

View File

@@ -65,9 +65,26 @@ export async function setupFormFiller() {
}
export async function processAndDownloadForm() {
if (viewerIframe && viewerReady) {
viewerIframe.contentWindow?.postMessage({ type: 'getData' }, '*');
} else {
if (!viewerIframe || !viewerReady) {
showAlert('Viewer not ready', 'Please wait for the form to finish loading.');
return;
}
try {
const win: any = viewerIframe.contentWindow;
const doc: Document | null = win?.document ?? null;
// Prefer to trigger the same behavior as the toolbar's Download button
const downloadBtn = doc?.getElementById('download') as HTMLButtonElement | null;
if (downloadBtn) {
downloadBtn.click();
return;
}
// Fallback: use the postMessage-based getData flow
win?.postMessage({ type: 'getData' }, '*');
} catch (e) {
console.error('Failed to trigger form download:', e);
showAlert('Export failed', 'Could not export the filled form. Please try again.');
}
}

View File

@@ -1,5 +1,5 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
const signState = {
@@ -12,79 +12,87 @@ export async function setupSignTool() {
document.getElementById('signature-editor').classList.remove('hidden');
showLoader('Loading PDF viewer...');
const container = document.getElementById('canvas-container-sign');
if (container) {
container.textContent = '';
const iframe = document.createElement('iframe');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none';
container.appendChild(iframe);
signState.viewerIframe = iframe;
const pdfBytes = await state.pdfDoc.save();
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
const container = document.getElementById('canvas-container-sign');
if (!container) {
console.error('Sign tool canvas container not found');
hideLoader();
return;
}
// PDF.js expects the file URL in the query string, while
// the annotation extension reads its options from the URL hash.
const viewerBase = '/pdfjs-annotation-viewer/web/viewer.html';
const query = new URLSearchParams({ file: blobUrl });
const hash = new URLSearchParams({
// Annotation extension params (must be in the hash, not the query)
ae_username: 'Bento User',
ae_default_editor_active: 'true',
ae_default_sidebar_open: 'true',
// We intentionally do NOT set ae_post_url because Bento uses
// client-side export only (no backend save endpoint).
});
if (!state.files || !state.files[0]) {
console.error('No file loaded into state for signing');
hideLoader();
return;
}
iframe.src = `${viewerBase}?${query.toString()}#${hash.toString()}`;
iframe.onload = () => {
hideLoader();
signState.viewerReady = true;
try {
const viewerWindow: any = iframe.contentWindow;
if (viewerWindow) {
setTimeout(() => {
const sigButton = viewerWindow.document.getElementById('editorSignature');
if (sigButton) {
sigButton.removeAttribute('hidden');
const sigButtonElement = viewerWindow.document.getElementById('editorSignatureButton');
if (sigButtonElement) {
sigButtonElement.removeAttribute('disabled');
}
}
container.textContent = '';
const iframe = document.createElement('iframe');
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = 'none';
container.appendChild(iframe);
signState.viewerIframe = iframe;
// Make the annotation extension's "Save" button behave like
// "Export PDF" (purely client-side) instead of POSTing to
// ae_post_url, which we don't use in Bento.
const ext = viewerWindow.pdfjsAnnotationExtensionInstance;
if (ext && typeof ext.exportPdf === 'function') {
ext.saveData = async () => {
try {
await ext.exportPdf();
} catch (err) {
console.error('Failed to export annotated PDF via Save button:', err);
viewerWindow.alert?.('Failed to export the signed PDF. Please try again.');
}
};
}
}, 500);
// Use original uploaded bytes to avoid re-writing the PDF structure
const file = state.files[0];
const pdfBytes = await readFileAsArrayBuffer(file);
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
const viewerBase = '/pdfjs-viewer/viewer.html';
const query = new URLSearchParams({ file: blobUrl });
iframe.src = `${viewerBase}?${query.toString()}`;
iframe.onload = () => {
hideLoader();
signState.viewerReady = true;
try {
const viewerWindow: any = iframe.contentWindow;
if (viewerWindow && viewerWindow.PDFViewerApplication) {
const app = viewerWindow.PDFViewerApplication;
const doc = viewerWindow.document;
const editorModeButtons = doc.getElementById('editorModeButtons');
editorModeButtons?.classList.remove('hidden');
const editorSignature = doc.getElementById('editorSignature');
editorSignature?.removeAttribute('hidden');
const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null;
if (editorSignatureButton) {
editorSignatureButton.disabled = false;
}
const editorStamp = doc.getElementById('editorStamp');
editorStamp?.removeAttribute('hidden');
const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null;
if (editorStampButton) {
editorStampButton.disabled = false;
}
// Ensure annotation editor is fully enabled; start in Signature mode
const pdfViewer = app.pdfViewer;
const AnnotationEditorType = viewerWindow.pdfjsLib?.AnnotationEditorType;
if (pdfViewer && AnnotationEditorType) {
pdfViewer.annotationEditorMode = {
mode: AnnotationEditorType.SIGNATURE,
};
}
} catch (e) {
console.error('Could not enable signature button:', e);
}
};
}
const saveBtn = document.getElementById('process-btn');
if (saveBtn) {
saveBtn.style.display = 'none';
}
} catch (e) {
console.error('Could not initialize base PDF.js viewer for signing:', e);
}
// Now that the viewer is ready, expose the Save Signed PDF button in the Bento UI
const saveBtn = document.getElementById('process-btn') as HTMLButtonElement | null;
if (saveBtn) {
saveBtn.style.display = '';
saveBtn.disabled = false;
saveBtn.onclick = () => {
void applyAndSaveSignatures();
};
}
};
}
export async function applyAndSaveSignatures() {
@@ -95,15 +103,16 @@ export async function applyAndSaveSignatures() {
try {
const viewerWindow: any = signState.viewerIframe.contentWindow;
if (!viewerWindow || !viewerWindow.pdfjsAnnotationExtensionInstance) {
showAlert('Annotations not ready', 'Please wait for the annotation tools to finish loading.');
if (!viewerWindow || !viewerWindow.PDFViewerApplication) {
showAlert('Viewer not ready', 'The PDF viewer is still initializing.');
return;
}
// Trigger the extension's Export PDF flow so annotations are baked into the downloaded file.
await viewerWindow.pdfjsAnnotationExtensionInstance.exportPdf();
// Delegate to the built-in download behavior of the base viewer.
const app = viewerWindow.PDFViewerApplication;
app.eventBus?.dispatch('download', { source: app });
} catch (error) {
console.error('Failed to export annotated PDF:', error);
showAlert('Export failed', 'Could not export the PDF with annotations. Please try again.');
console.error('Failed to trigger download in base PDF.js viewer:', error);
showAlert('Export failed', 'Could not export the signed PDF. Please try again.');
}
}