-
+
BentoPDF
diff --git a/public/images/favicon-no-bg.svg b/public/images/favicon-no-bg.svg
new file mode 100644
index 00000000..01d9df7b
--- /dev/null
+++ b/public/images/favicon-no-bg.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/public/images/favicon.svg b/public/images/favicon.svg
index 18be9121..90ddc27b 100644
--- a/public/images/favicon.svg
+++ b/public/images/favicon.svg
@@ -1,7 +1,7 @@
-
-
-
+
+
+
diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json
index 69645d1d..82598415 100644
--- a/public/locales/de/tools.json
+++ b/public/locales/de/tools.json
@@ -488,5 +488,27 @@
"subtitle": "Text aus PDF-Dateien extrahieren und als Textdatei (.txt) speichern. Unterstützt mehrere Dateien.",
"note": "Dieses Tool funktioniert NUR mit digital erstellten PDFs. Für gescannte Dokumente oder bildbasierte PDFs verwenden Sie stattdessen unser OCR PDF-Tool.",
"convertButton": "Text extrahieren"
+ },
+ "digitalSignPdf": {
+ "name": "Digitale PDF-Signatur",
+ "pageTitle": "Digitale PDF-Signatur - Kryptografische Signatur hinzufügen | BentoPDF",
+ "subtitle": "Fügen Sie eine kryptografische digitale Signatur mit X.509-Zertifikaten zu Ihrer PDF hinzu. Unterstützt PKCS#12 (.pfx, .p12) und PEM-Formate. Ihr privater Schlüssel verlässt niemals Ihren Browser.",
+ "certificateSection": "Zertifikat",
+ "uploadCert": "Zertifikat hochladen (.pfx, .p12)",
+ "certPassword": "Zertifikat-Passwort",
+ "certPasswordPlaceholder": "Zertifikat-Passwort eingeben",
+ "certInfo": "Zertifikat-Informationen",
+ "certSubject": "Betreff",
+ "certIssuer": "Aussteller",
+ "certValidity": "Gültig",
+ "signatureDetails": "Signatur-Details (Optional)",
+ "reason": "Grund",
+ "reasonPlaceholder": "z.B. Ich genehmige dieses Dokument",
+ "location": "Ort",
+ "locationPlaceholder": "z.B. Berlin, Deutschland",
+ "contactInfo": "Kontaktdaten",
+ "contactPlaceholder": "z.B. email@beispiel.de",
+ "applySignature": "Digitale Signatur anwenden",
+ "successMessage": "PDF erfolgreich signiert! Die Signatur kann in jedem PDF-Reader überprüft werden."
}
}
\ No newline at end of file
diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json
index 4025b906..7e11dea0 100644
--- a/public/locales/en/tools.json
+++ b/public/locales/en/tools.json
@@ -488,5 +488,27 @@
"subtitle": "Extract text from PDF files and save as plain text (.txt). Supports multiple files.",
"note": "This tool works ONLY with digitally created PDFs. For scanned documents or image-based PDFs, use our OCR PDF tool instead.",
"convertButton": "Extract Text"
+ },
+ "digitalSignPdf": {
+ "name": "Digital Signature PDF",
+ "pageTitle": "Digital Signature PDF - Add Cryptographic Signature | BentoPDF",
+ "subtitle": "Add a cryptographic digital signature to your PDF using X.509 certificates. Supports PKCS#12 (.pfx, .p12) and PEM formats. Your private key never leaves your browser.",
+ "certificateSection": "Certificate",
+ "uploadCert": "Upload certificate (.pfx, .p12)",
+ "certPassword": "Certificate Password",
+ "certPasswordPlaceholder": "Enter certificate password",
+ "certInfo": "Certificate Information",
+ "certSubject": "Subject",
+ "certIssuer": "Issuer",
+ "certValidity": "Valid",
+ "signatureDetails": "Signature Details (Optional)",
+ "reason": "Reason",
+ "reasonPlaceholder": "e.g., I approve this document",
+ "location": "Location",
+ "locationPlaceholder": "e.g., New York, USA",
+ "contactInfo": "Contact Info",
+ "contactPlaceholder": "e.g., email@example.com",
+ "applySignature": "Apply Digital Signature",
+ "successMessage": "PDF signed successfully! The signature can be verified in any PDF reader."
}
}
\ No newline at end of file
diff --git a/public/locales/vi/tools.json b/public/locales/vi/tools.json
index 86b55710..253c899a 100644
--- a/public/locales/vi/tools.json
+++ b/public/locales/vi/tools.json
@@ -488,5 +488,27 @@
"subtitle": "Trích xuất văn bản từ tệp PDF và lưu dưới dạng tệp văn bản (.txt). Hỗ trợ nhiều tệp.",
"note": "Công cụ này CHỈ hoạt động với các tệp PDF được tạo kỹ thuật số. Đối với tài liệu quét hoặc PDF dựa trên hình ảnh, hãy sử dụng công cụ OCR PDF của chúng tôi.",
"convertButton": "Trích xuất văn bản"
+ },
+ "digitalSignPdf": {
+ "name": "Chữ ký số PDF",
+ "pageTitle": "Chữ ký số PDF - Thêm chữ ký mật mã | BentoPDF",
+ "subtitle": "Thêm chữ ký số mật mã vào PDF của bạn bằng chứng chỉ X.509. Hỗ trợ định dạng PKCS#12 (.pfx, .p12) và PEM. Khóa riêng của bạn không bao giờ rời khỏi trình duyệt.",
+ "certificateSection": "Chứng chỉ",
+ "uploadCert": "Tải lên chứng chỉ (.pfx, .p12)",
+ "certPassword": "Mật khẩu chứng chỉ",
+ "certPasswordPlaceholder": "Nhập mật khẩu chứng chỉ",
+ "certInfo": "Thông tin chứng chỉ",
+ "certSubject": "Chủ thể",
+ "certIssuer": "Nhà phát hành",
+ "certValidity": "Hiệu lực",
+ "signatureDetails": "Chi tiết chữ ký (Tùy chọn)",
+ "reason": "Lý do",
+ "reasonPlaceholder": "ví dụ: Tôi phê duyệt tài liệu này",
+ "location": "Địa điểm",
+ "locationPlaceholder": "ví dụ: Hà Nội, Việt Nam",
+ "contactInfo": "Thông tin liên hệ",
+ "contactPlaceholder": "ví dụ: email@example.com",
+ "applySignature": "Áp dụng chữ ký số",
+ "successMessage": "Ký PDF thành công! Chữ ký có thể được xác minh trong bất kỳ trình đọc PDF nào."
}
}
\ No newline at end of file
diff --git a/public/locales/zh/tools.json b/public/locales/zh/tools.json
index f3097e4d..55cab23a 100644
--- a/public/locales/zh/tools.json
+++ b/public/locales/zh/tools.json
@@ -485,5 +485,27 @@
"subtitle": "从 PDF 文件中提取文本并保存为纯文本文件 (.txt)。支持多个文件。",
"note": "此工具仅适用于数字创建的 PDF。对于扫描文档或基于图像的 PDF,请使用我们的 OCR PDF 工具。",
"convertButton": "提取文本"
+ },
+ "digitalSignPdf": {
+ "name": "PDF 数字签名",
+ "pageTitle": "PDF 数字签名 - 添加加密签名 | BentoPDF",
+ "subtitle": "使用 X.509 证书为您的 PDF 添加加密数字签名。支持 PKCS#12 (.pfx, .p12) 和 PEM 格式。您的私钥永远不会离开您的浏览器。",
+ "certificateSection": "证书",
+ "uploadCert": "上传证书 (.pfx, .p12)",
+ "certPassword": "证书密码",
+ "certPasswordPlaceholder": "输入证书密码",
+ "certInfo": "证书信息",
+ "certSubject": "主体",
+ "certIssuer": "颁发者",
+ "certValidity": "有效期",
+ "signatureDetails": "签名详情(可选)",
+ "reason": "原因",
+ "reasonPlaceholder": "例如:我批准此文档",
+ "location": "位置",
+ "locationPlaceholder": "例如:北京,中国",
+ "contactInfo": "联系信息",
+ "contactPlaceholder": "例如:email@example.com",
+ "applySignature": "应用数字签名",
+ "successMessage": "PDF 签名成功!签名可在任何 PDF 阅读器中验证。"
}
}
\ No newline at end of file
diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts
index fe62b121..3e2b04fd 100644
--- a/src/js/config/tools.ts
+++ b/src/js/config/tools.ts
@@ -720,6 +720,12 @@ export const categories = [
icon: 'ph-shield-check',
subtitle: 'Set or change user permissions on a PDF.',
},
+ {
+ href: import.meta.env.BASE_URL + 'digital-sign-pdf.html',
+ name: 'Digital Signature',
+ icon: 'ph-certificate',
+ subtitle: 'Add a cryptographic digital signature using X.509 certificates.',
+ },
],
},
];
diff --git a/src/js/logic/add-stamps.ts b/src/js/logic/add-stamps.ts
index 27cf6fcf..a34c7f5f 100644
--- a/src/js/logic/add-stamps.ts
+++ b/src/js/logic/add-stamps.ts
@@ -35,8 +35,7 @@ function resetState() {
viewerContainer.style.aspectRatio = ''
}
- // Revert container width only if NOT in full width mode
- const isFullWidth = localStorage.getItem('fullWidthMode') === 'true'
+ const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-6xl')
toolUploader.classList.add('max-w-2xl')
@@ -56,8 +55,8 @@ function updateFileList() {
fileListDiv.classList.remove('hidden')
fileListDiv.innerHTML = ''
- // Expand container width for viewer if NOT in full width mode
- const isFullWidth = localStorage.getItem('fullWidthMode') === 'true'
+ // Expand container width for viewer if NOT in full width mode (default to true if not set)
+ const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false'
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-2xl')
toolUploader.classList.add('max-w-6xl')
diff --git a/src/js/logic/digital-sign-pdf-page.ts b/src/js/logic/digital-sign-pdf-page.ts
new file mode 100644
index 00000000..42f845e0
--- /dev/null
+++ b/src/js/logic/digital-sign-pdf-page.ts
@@ -0,0 +1,669 @@
+import { createIcons, icons } from 'lucide';
+import { showAlert, showLoader, hideLoader } from '../ui.js';
+import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js';
+import {
+ signPdf,
+ parsePfxFile,
+ parseCombinedPem,
+ getCertificateInfo,
+ type CertificateData,
+ type SignatureInfo,
+ type VisibleSignatureOptions,
+} from './digital-sign-pdf.js';
+
+interface DigitalSignState {
+ pdfFile: File | null;
+ pdfBytes: Uint8Array | null;
+ certFile: File | null;
+ certData: CertificateData | null;
+ sigImageData: ArrayBuffer | null;
+ sigImageType: 'png' | 'jpeg' | 'webp' | null;
+}
+
+const state: DigitalSignState = {
+ pdfFile: null,
+ pdfBytes: null,
+ certFile: null,
+ certData: null,
+ sigImageData: null,
+ sigImageType: null,
+};
+
+function resetState(): void {
+ state.pdfFile = null;
+ state.pdfBytes = null;
+ state.certFile = null;
+ state.certData = null;
+ state.sigImageData = null;
+ state.sigImageType = null;
+
+ const fileDisplayArea = getElement('file-display-area');
+ if (fileDisplayArea) fileDisplayArea.innerHTML = '';
+
+ const certDisplayArea = getElement('cert-display-area');
+ if (certDisplayArea) certDisplayArea.innerHTML = '';
+
+ const fileInput = getElement('file-input');
+ if (fileInput) fileInput.value = '';
+
+ const certInput = getElement('cert-input');
+ if (certInput) certInput.value = '';
+
+ const sigImageInput = getElement('sig-image-input');
+ if (sigImageInput) sigImageInput.value = '';
+
+ const sigImagePreview = getElement('sig-image-preview');
+ if (sigImagePreview) sigImagePreview.classList.add('hidden');
+
+ const certSection = getElement('certificate-section');
+ if (certSection) certSection.classList.add('hidden');
+
+ hidePasswordSection();
+ hideSignatureOptions();
+ hideCertInfo();
+ updateProcessButton();
+}
+
+function getElement(id: string): T | null {
+ return document.getElementById(id) as T | null;
+}
+
+function initializePage(): void {
+ createIcons({ icons });
+
+ const fileInput = getElement('file-input');
+ const dropZone = getElement('drop-zone');
+ const certInput = getElement('cert-input');
+ const certDropZone = getElement('cert-drop-zone');
+ const certPassword = getElement('cert-password');
+ const processBtn = getElement('process-btn');
+ const backBtn = getElement('back-to-tools');
+
+ if (fileInput) {
+ fileInput.addEventListener('change', handlePdfUpload);
+ fileInput.addEventListener('click', () => {
+ fileInput.value = '';
+ });
+ }
+
+ 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) {
+ handlePdfFile(droppedFiles[0]);
+ }
+ });
+ }
+
+ if (certInput) {
+ certInput.addEventListener('change', handleCertUpload);
+ certInput.addEventListener('click', () => {
+ certInput.value = '';
+ });
+ }
+
+ if (certDropZone) {
+ certDropZone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ certDropZone.classList.add('bg-gray-700');
+ });
+
+ certDropZone.addEventListener('dragleave', () => {
+ certDropZone.classList.remove('bg-gray-700');
+ });
+
+ certDropZone.addEventListener('drop', (e) => {
+ e.preventDefault();
+ certDropZone.classList.remove('bg-gray-700');
+ const droppedFiles = e.dataTransfer?.files;
+ if (droppedFiles && droppedFiles.length > 0) {
+ handleCertFile(droppedFiles[0]);
+ }
+ });
+ }
+
+ if (certPassword) {
+ certPassword.addEventListener('input', handlePasswordInput);
+ certPassword.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ handlePasswordInput();
+ }
+ });
+ }
+
+ if (processBtn) {
+ processBtn.addEventListener('click', processSignature);
+ }
+
+ if (backBtn) {
+ backBtn.addEventListener('click', () => {
+ window.location.href = import.meta.env.BASE_URL;
+ });
+ }
+
+ const enableVisibleSig = getElement('enable-visible-sig');
+ const visibleSigOptions = getElement('visible-sig-options');
+ const sigPage = getElement('sig-page');
+ const customPageWrapper = getElement('custom-page-wrapper');
+ const sigImageInput = getElement('sig-image-input');
+ const sigImagePreview = getElement('sig-image-preview');
+ const sigImageThumb = getElement('sig-image-thumb');
+ const removeSigImage = getElement('remove-sig-image');
+ const enableSigText = getElement('enable-sig-text');
+ const sigTextOptions = getElement('sig-text-options');
+
+ if (enableVisibleSig && visibleSigOptions) {
+ enableVisibleSig.addEventListener('change', () => {
+ if (enableVisibleSig.checked) {
+ visibleSigOptions.classList.remove('hidden');
+ } else {
+ visibleSigOptions.classList.add('hidden');
+ }
+ });
+ }
+
+ if (sigPage && customPageWrapper) {
+ sigPage.addEventListener('change', () => {
+ if (sigPage.value === 'custom') {
+ customPageWrapper.classList.remove('hidden');
+ } else {
+ customPageWrapper.classList.add('hidden');
+ }
+ });
+ }
+
+ if (sigImageInput) {
+ sigImageInput.addEventListener('change', async (e) => {
+ const input = e.target as HTMLInputElement;
+ if (input.files && input.files.length > 0) {
+ const file = input.files[0];
+ const validTypes = ['image/png', 'image/jpeg', 'image/webp'];
+ if (!validTypes.includes(file.type)) {
+ showAlert('Invalid Image', 'Please select a PNG, JPG, or WebP image.');
+ return;
+ }
+ state.sigImageData = await readFileAsArrayBuffer(file) as ArrayBuffer;
+ state.sigImageType = file.type.replace('image/', '') as 'png' | 'jpeg' | 'webp';
+
+ if (sigImageThumb && sigImagePreview) {
+ const url = URL.createObjectURL(file);
+ sigImageThumb.src = url;
+ sigImagePreview.classList.remove('hidden');
+ }
+ }
+ });
+ }
+
+ if (removeSigImage && sigImagePreview) {
+ removeSigImage.addEventListener('click', () => {
+ state.sigImageData = null;
+ state.sigImageType = null;
+ sigImagePreview.classList.add('hidden');
+ if (sigImageInput) sigImageInput.value = '';
+ });
+ }
+
+ if (enableSigText && sigTextOptions) {
+ enableSigText.addEventListener('change', () => {
+ if (enableSigText.checked) {
+ sigTextOptions.classList.remove('hidden');
+ } else {
+ sigTextOptions.classList.add('hidden');
+ }
+ });
+ }
+}
+
+function handlePdfUpload(e: Event): void {
+ const input = e.target as HTMLInputElement;
+ if (input.files && input.files.length > 0) {
+ handlePdfFile(input.files[0]);
+ }
+}
+
+async function handlePdfFile(file: File): Promise {
+ if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
+ showAlert('Invalid File', 'Please select a PDF file.');
+ return;
+ }
+
+ state.pdfFile = file;
+ state.pdfBytes = new Uint8Array(await readFileAsArrayBuffer(file) as ArrayBuffer);
+
+ updatePdfDisplay();
+ showCertificateSection();
+}
+
+async function updatePdfDisplay(): Promise {
+ const fileDisplayArea = getElement('file-display-area');
+
+ if (!fileDisplayArea || !state.pdfFile) 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 = state.pdfFile.name;
+
+ const metaSpan = document.createElement('div');
+ metaSpan.className = 'text-xs text-gray-400';
+ metaSpan.textContent = `${formatBytes(state.pdfFile.size)} • Loading 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 = ' ';
+ removeBtn.onclick = () => {
+ state.pdfFile = null;
+ state.pdfBytes = null;
+ fileDisplayArea.innerHTML = '';
+ hideCertificateSection();
+ updateProcessButton();
+ };
+
+ fileDiv.append(infoContainer, removeBtn);
+ fileDisplayArea.appendChild(fileDiv);
+ createIcons({ icons });
+
+ try {
+ if (state.pdfBytes) {
+ const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }).promise;
+ metaSpan.textContent = `${formatBytes(state.pdfFile.size)} • ${pdfDoc.numPages} pages`;
+ }
+ } catch (error) {
+ console.error('Error loading PDF:', error);
+ metaSpan.textContent = `${formatBytes(state.pdfFile.size)}`;
+ }
+}
+
+function showCertificateSection(): void {
+ const certSection = getElement('certificate-section');
+ if (certSection) {
+ certSection.classList.remove('hidden');
+ }
+}
+
+function hideCertificateSection(): void {
+ const certSection = getElement('certificate-section');
+ const signatureOptions = getElement('signature-options');
+
+ if (certSection) {
+ certSection.classList.add('hidden');
+ }
+ if (signatureOptions) {
+ signatureOptions.classList.add('hidden');
+ }
+
+ state.certFile = null;
+ state.certData = null;
+
+ const certDisplayArea = getElement('cert-display-area');
+ if (certDisplayArea) {
+ certDisplayArea.innerHTML = '';
+ }
+
+ const certInfo = getElement('cert-info');
+ if (certInfo) {
+ certInfo.classList.add('hidden');
+ }
+
+ const certPasswordSection = getElement('cert-password-section');
+ if (certPasswordSection) {
+ certPasswordSection.classList.add('hidden');
+ }
+}
+
+function handleCertUpload(e: Event): void {
+ const input = e.target as HTMLInputElement;
+ if (input.files && input.files.length > 0) {
+ handleCertFile(input.files[0]);
+ }
+}
+
+async function handleCertFile(file: File): Promise {
+ const validExtensions = ['.pfx', '.p12', '.pem'];
+ const hasValidExtension = validExtensions.some(ext =>
+ file.name.toLowerCase().endsWith(ext)
+ );
+
+ if (!hasValidExtension) {
+ showAlert('Invalid Certificate', 'Please select a .pfx, .p12, or .pem certificate file.');
+ return;
+ }
+
+ state.certFile = file;
+ state.certData = null;
+
+ updateCertDisplay();
+
+ const isPemFile = file.name.toLowerCase().endsWith('.pem');
+
+ if (isPemFile) {
+ try {
+ const pemContent = await file.text();
+ const isEncrypted = pemContent.includes('ENCRYPTED');
+
+ if (isEncrypted) {
+ showPasswordSection();
+ updatePasswordLabel('Private Key Password');
+ } else {
+ state.certData = parseCombinedPem(pemContent);
+ updateCertInfo();
+ showSignatureOptions();
+
+ const certStatus = getElement('cert-status');
+ if (certStatus) {
+ certStatus.innerHTML = 'Certificate loaded ';
+ createIcons({ icons });
+ certStatus.className = 'text-xs text-green-400';
+ }
+ }
+ } catch (error) {
+ const certStatus = getElement('cert-status');
+ if (certStatus) {
+ certStatus.textContent = 'Failed to parse PEM file';
+ certStatus.className = 'text-xs text-red-400';
+ }
+ }
+ } else {
+ showPasswordSection();
+ updatePasswordLabel('Certificate Password');
+ }
+
+ hideSignatureOptions();
+ updateProcessButton();
+}
+
+function updateCertDisplay(): void {
+ const certDisplayArea = getElement('cert-display-area');
+
+ if (!certDisplayArea || !state.certFile) return;
+
+ certDisplayArea.innerHTML = '';
+
+ const certDiv = document.createElement('div');
+ certDiv.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 = state.certFile.name;
+
+ const metaSpan = document.createElement('div');
+ metaSpan.className = 'text-xs text-gray-400';
+ metaSpan.id = 'cert-status';
+ metaSpan.textContent = 'Enter password to unlock';
+
+ 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 = () => {
+ state.certFile = null;
+ state.certData = null;
+ certDisplayArea.innerHTML = '';
+ hidePasswordSection();
+ hideCertInfo();
+ hideSignatureOptions();
+ updateProcessButton();
+ };
+
+ certDiv.append(infoContainer, removeBtn);
+ certDisplayArea.appendChild(certDiv);
+ createIcons({ icons });
+}
+
+function showPasswordSection(): void {
+ const certPasswordSection = getElement('cert-password-section');
+ if (certPasswordSection) {
+ certPasswordSection.classList.remove('hidden');
+ }
+
+ const certPassword = getElement('cert-password');
+ if (certPassword) {
+ certPassword.value = '';
+ certPassword.focus();
+ }
+}
+
+function updatePasswordLabel(labelText: string): void {
+ const label = document.querySelector('label[for="cert-password"]');
+ if (label) {
+ label.textContent = labelText;
+ }
+}
+
+function hidePasswordSection(): void {
+ const certPasswordSection = getElement('cert-password-section');
+ if (certPasswordSection) {
+ certPasswordSection.classList.add('hidden');
+ }
+}
+
+function showSignatureOptions(): void {
+ const signatureOptions = getElement('signature-options');
+ if (signatureOptions) {
+ signatureOptions.classList.remove('hidden');
+ }
+ const visibleSigSection = getElement('visible-signature-section');
+ if (visibleSigSection) {
+ visibleSigSection.classList.remove('hidden');
+ }
+}
+
+function hideSignatureOptions(): void {
+ const signatureOptions = getElement('signature-options');
+ if (signatureOptions) {
+ signatureOptions.classList.add('hidden');
+ }
+ const visibleSigSection = getElement('visible-signature-section');
+ if (visibleSigSection) {
+ visibleSigSection.classList.add('hidden');
+ }
+}
+
+function hideCertInfo(): void {
+ const certInfo = getElement('cert-info');
+ if (certInfo) {
+ certInfo.classList.add('hidden');
+ }
+}
+
+async function handlePasswordInput(): Promise {
+ const certPassword = getElement('cert-password');
+ const password = certPassword?.value ?? '';
+
+ if (!state.certFile || !password) {
+ return;
+ }
+
+ try {
+ const isPemFile = state.certFile.name.toLowerCase().endsWith('.pem');
+
+ if (isPemFile) {
+ const pemContent = await state.certFile.text();
+ state.certData = parseCombinedPem(pemContent, password);
+ } else {
+ const certBytes = await readFileAsArrayBuffer(state.certFile) as ArrayBuffer;
+ state.certData = parsePfxFile(certBytes, password);
+ }
+
+ updateCertInfo();
+ showSignatureOptions();
+ updateProcessButton();
+
+ const certStatus = getElement('cert-status');
+ if (certStatus) {
+ certStatus.innerHTML = 'Certificate unlocked ';
+ createIcons({ icons });
+ certStatus.className = 'text-xs text-green-400';
+ }
+ } catch (error) {
+ state.certData = null;
+ hideSignatureOptions();
+ updateProcessButton();
+
+ const certStatus = getElement('cert-status');
+ if (certStatus) {
+ const errorMessage = error instanceof Error ? error.message : 'Invalid password or certificate';
+ certStatus.textContent = errorMessage.includes('password')
+ ? 'Incorrect password'
+ : 'Failed to parse certificate';
+ certStatus.className = 'text-xs text-red-400';
+ }
+ }
+}
+
+function updateCertInfo(): void {
+ if (!state.certData) return;
+
+ const certInfo = getElement('cert-info');
+ const certSubject = getElement('cert-subject');
+ const certIssuer = getElement('cert-issuer');
+ const certValidity = getElement('cert-validity');
+
+ if (!certInfo) return;
+
+ const info = getCertificateInfo(state.certData.certificate);
+
+ if (certSubject) {
+ certSubject.textContent = info.subject;
+ }
+ if (certIssuer) {
+ certIssuer.textContent = info.issuer;
+ }
+ if (certValidity) {
+ const formatDate = (date: Date) => date.toLocaleDateString();
+ certValidity.textContent = `${formatDate(info.validFrom)} - ${formatDate(info.validTo)}`;
+ }
+
+ certInfo.classList.remove('hidden');
+}
+
+function updateProcessButton(): void {
+ const processBtn = getElement('process-btn');
+ if (!processBtn) return;
+
+ const canProcess = state.pdfBytes !== null && state.certData !== null;
+
+ if (canProcess) {
+ processBtn.style.display = '';
+ } else {
+ processBtn.style.display = 'none';
+ }
+}
+
+async function processSignature(): Promise {
+ if (!state.pdfBytes || !state.certData) {
+ showAlert('Missing Data', 'Please upload both a PDF and a valid certificate.');
+ return;
+ }
+
+ const reason = getElement('sign-reason')?.value ?? '';
+ const location = getElement('sign-location')?.value ?? '';
+ const contactInfo = getElement('sign-contact')?.value ?? '';
+
+ const signatureInfo: SignatureInfo = {};
+ if (reason) signatureInfo.reason = reason;
+ if (location) signatureInfo.location = location;
+ if (contactInfo) signatureInfo.contactInfo = contactInfo;
+
+ let visibleSignature: VisibleSignatureOptions | undefined;
+
+ const enableVisibleSig = getElement('enable-visible-sig');
+ if (enableVisibleSig?.checked) {
+ const sigX = parseInt(getElement('sig-x')?.value ?? '25', 10);
+ const sigY = parseInt(getElement('sig-y')?.value ?? '700', 10);
+ const sigWidth = parseInt(getElement('sig-width')?.value ?? '150', 10);
+ const sigHeight = parseInt(getElement('sig-height')?.value ?? '50', 10);
+
+ const sigPageSelect = getElement('sig-page');
+ let sigPage: number | string = 0;
+ if (sigPageSelect) {
+ if (sigPageSelect.value === 'last') {
+ sigPage = 'last';
+ } else if (sigPageSelect.value === 'all') {
+ sigPage = 'all';
+ } else if (sigPageSelect.value === 'custom') {
+ sigPage = parseInt(getElement('sig-custom-page')?.value ?? '1', 10) - 1;
+ } else {
+ sigPage = parseInt(sigPageSelect.value, 10);
+ }
+ }
+
+ const enableSigText = getElement('enable-sig-text');
+ let sigText = enableSigText?.checked ? getElement('sig-text')?.value : undefined;
+ const sigTextColor = getElement('sig-text-color')?.value ?? '#000000';
+ const sigTextSize = parseInt(getElement('sig-text-size')?.value ?? '12', 10);
+
+ if (!state.sigImageData && !sigText && state.certData) {
+ const certInfo = getCertificateInfo(state.certData.certificate);
+ const date = new Date().toLocaleDateString();
+ sigText = `Digitally signed by ${certInfo.subject}\n${date}`;
+ }
+
+ visibleSignature = {
+ enabled: true,
+ x: sigX,
+ y: sigY,
+ width: sigWidth,
+ height: sigHeight,
+ page: sigPage,
+ imageData: state.sigImageData ?? undefined,
+ imageType: state.sigImageType ?? undefined,
+ text: sigText,
+ textColor: sigTextColor,
+ textSize: sigTextSize,
+ };
+ }
+
+ showLoader('Applying digital signature...');
+
+ try {
+ const signedPdfBytes = await signPdf(state.pdfBytes, state.certData, {
+ signatureInfo,
+ visibleSignature,
+ });
+
+ const blob = new Blob([signedPdfBytes.slice().buffer], { type: 'application/pdf' });
+ const originalName = state.pdfFile?.name ?? 'document.pdf';
+ const signedName = originalName.replace(/\.pdf$/i, '_signed.pdf');
+
+ downloadFile(blob, signedName);
+
+ hideLoader();
+ showAlert('Success', 'PDF signed successfully! The signature can be verified in any PDF reader.', 'success', () => { resetState(); });
+ } catch (error) {
+ hideLoader();
+ console.error('Signing error:', error);
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
+ showAlert('Signing Failed', `Failed to sign PDF: ${errorMessage}`);
+ }
+}
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initializePage);
+} else {
+ initializePage();
+}
diff --git a/src/js/logic/digital-sign-pdf.ts b/src/js/logic/digital-sign-pdf.ts
new file mode 100644
index 00000000..36350a27
--- /dev/null
+++ b/src/js/logic/digital-sign-pdf.ts
@@ -0,0 +1,194 @@
+import { PdfSigner, type SignOption } from 'zgapdfsigner';
+import forge from 'node-forge';
+
+export interface SignatureInfo {
+ reason?: string;
+ location?: string;
+ contactInfo?: string;
+ name?: string;
+}
+
+export interface CertificateData {
+ p12Buffer: ArrayBuffer;
+ password: string;
+ certificate: forge.pki.Certificate;
+}
+
+export interface SignPdfOptions {
+ signatureInfo?: SignatureInfo;
+ visibleSignature?: VisibleSignatureOptions;
+}
+
+export interface VisibleSignatureOptions {
+ enabled: boolean;
+ imageData?: ArrayBuffer;
+ imageType?: 'png' | 'jpeg' | 'webp';
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ page: number | string;
+ text?: string;
+ textColor?: string;
+ textSize?: number;
+}
+
+export function parsePfxFile(pfxBytes: ArrayBuffer, password: string): CertificateData {
+ const pfxAsn1 = forge.asn1.fromDer(forge.util.createBuffer(new Uint8Array(pfxBytes)));
+ const pfx = forge.pkcs12.pkcs12FromAsn1(pfxAsn1, password);
+
+ const certBags = pfx.getBags({ bagType: forge.pki.oids.certBag });
+ const keyBags = pfx.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag });
+
+ const certBagArray = certBags[forge.pki.oids.certBag];
+ const keyBagArray = keyBags[forge.pki.oids.pkcs8ShroudedKeyBag];
+
+ if (!certBagArray || certBagArray.length === 0) {
+ throw new Error('No certificate found in PFX file');
+ }
+
+ if (!keyBagArray || keyBagArray.length === 0) {
+ throw new Error('No private key found in PFX file');
+ }
+
+ const certificate = certBagArray[0].cert;
+
+ if (!certificate) {
+ throw new Error('Failed to extract certificate from PFX file');
+ }
+
+ return { p12Buffer: pfxBytes, password, certificate };
+}
+
+export function parsePemFiles(
+ certPem: string,
+ keyPem: string,
+ keyPassword?: string
+): CertificateData {
+ const certificate = forge.pki.certificateFromPem(certPem);
+
+ let privateKey: forge.pki.PrivateKey;
+ if (keyPem.includes('ENCRYPTED')) {
+ if (!keyPassword) {
+ throw new Error('Password required for encrypted private key');
+ }
+ privateKey = forge.pki.decryptRsaPrivateKey(keyPem, keyPassword);
+ if (!privateKey) {
+ throw new Error('Failed to decrypt private key');
+ }
+ } else {
+ privateKey = forge.pki.privateKeyFromPem(keyPem);
+ }
+
+ const p12Password = keyPassword || 'temp-password';
+ const p12Asn1 = forge.pkcs12.toPkcs12Asn1(
+ privateKey,
+ [certificate],
+ p12Password,
+ { algorithm: '3des' }
+ );
+ const p12Der = forge.asn1.toDer(p12Asn1).getBytes();
+ const p12Buffer = new Uint8Array(p12Der.length);
+ for (let i = 0; i < p12Der.length; i++) {
+ p12Buffer[i] = p12Der.charCodeAt(i);
+ }
+
+ return { p12Buffer: p12Buffer.buffer, password: p12Password, certificate };
+}
+
+export function parseCombinedPem(pemContent: string, password?: string): CertificateData {
+ const certMatch = pemContent.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/);
+ const keyMatch = pemContent.match(/-----BEGIN (RSA |EC |ENCRYPTED )?PRIVATE KEY-----[\s\S]*?-----END (RSA |EC |ENCRYPTED )?PRIVATE KEY-----/);
+
+ if (!certMatch) {
+ throw new Error('No certificate found in PEM file');
+ }
+
+ if (!keyMatch) {
+ throw new Error('No private key found in PEM file');
+ }
+
+ return parsePemFiles(certMatch[0], keyMatch[0], password);
+}
+
+export async function signPdf(
+ pdfBytes: Uint8Array,
+ certificateData: CertificateData,
+ options: SignPdfOptions = {}
+): Promise {
+ const signatureInfo = options.signatureInfo ?? {};
+
+ const signOptions: SignOption = {
+ p12cert: certificateData.p12Buffer,
+ pwd: certificateData.password,
+ };
+
+ if (signatureInfo.reason) {
+ signOptions.reason = signatureInfo.reason;
+ }
+
+ if (signatureInfo.location) {
+ signOptions.location = signatureInfo.location;
+ }
+
+ if (signatureInfo.contactInfo) {
+ signOptions.contact = signatureInfo.contactInfo;
+ }
+
+ if (options.visibleSignature?.enabled) {
+ const vs = options.visibleSignature;
+
+ const drawinf = {
+ area: {
+ x: vs.x,
+ y: vs.y,
+ w: vs.width,
+ h: vs.height,
+ },
+ pageidx: vs.page,
+ imgInfo: undefined as { imgData: ArrayBuffer; imgType: string } | undefined,
+ textInfo: undefined as { text: string; size: number; color: string } | undefined,
+ };
+
+ if (vs.imageData && vs.imageType) {
+ drawinf.imgInfo = {
+ imgData: vs.imageData,
+ imgType: vs.imageType,
+ };
+ }
+
+ if (vs.text) {
+ drawinf.textInfo = {
+ text: vs.text,
+ size: vs.textSize ?? 12,
+ color: vs.textColor ?? '#000000',
+ };
+ }
+
+ signOptions.drawinf = drawinf as SignOption['drawinf'];
+ }
+
+ const signer = new PdfSigner(signOptions);
+ const signedPdfBytes = await signer.sign(pdfBytes);
+
+ return new Uint8Array(signedPdfBytes);
+}
+
+export function getCertificateInfo(certificate: forge.pki.Certificate): {
+ subject: string;
+ issuer: string;
+ validFrom: Date;
+ validTo: Date;
+ serialNumber: string;
+} {
+ const subjectCN = certificate.subject.getField('CN');
+ const issuerCN = certificate.issuer.getField('CN');
+
+ return {
+ subject: subjectCN?.value as string ?? 'Unknown',
+ issuer: issuerCN?.value as string ?? 'Unknown',
+ validFrom: certificate.validity.notBefore,
+ validTo: certificate.validity.notAfter,
+ serialNumber: certificate.serialNumber,
+ };
+}
diff --git a/src/js/logic/form-filler-page.ts b/src/js/logic/form-filler-page.ts
index 1deb9d11..e7c58af4 100644
--- a/src/js/logic/form-filler-page.ts
+++ b/src/js/logic/form-filler-page.ts
@@ -85,7 +85,7 @@ function resetState() {
}
const toolUploader = document.getElementById('tool-uploader');
- const isFullWidth = localStorage.getItem('fullWidthMode') === 'true';
+ const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-6xl');
toolUploader.classList.add('max-w-2xl');
@@ -139,7 +139,8 @@ async function setupFormViewer() {
}
const toolUploader = document.getElementById('tool-uploader');
- const isFullWidth = localStorage.getItem('fullWidthMode') === 'true';
+ // Default to true if not set
+ const isFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-2xl');
toolUploader.classList.add('max-w-6xl');
diff --git a/src/js/main.ts b/src/js/main.ts
index 31949cc2..923c27bc 100644
--- a/src/js/main.ts
+++ b/src/js/main.ts
@@ -321,30 +321,63 @@ const init = async () => {
const searchBar = document.getElementById('search-bar');
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
+ const searchResultsContainer = document.createElement('div');
+ searchResultsContainer.id = 'search-results';
+ searchResultsContainer.className = 'hidden grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6 col-span-full';
+ dom.toolGrid.insertBefore(searchResultsContainer, dom.toolGrid.firstChild);
+
searchBar.addEventListener('input', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const searchTerm = searchBar.value.toLowerCase().trim();
+ if (!searchTerm) {
+ searchResultsContainer.classList.add('hidden');
+ searchResultsContainer.innerHTML = '';
+ categoryGroups.forEach((group) => {
+ (group as HTMLElement).style.display = '';
+ const toolCards = group.querySelectorAll('.tool-card');
+ toolCards.forEach((card) => {
+ (card as HTMLElement).style.display = '';
+ });
+ });
+ return;
+ }
+
+ categoryGroups.forEach((group) => {
+ (group as HTMLElement).style.display = 'none';
+ });
+
+ searchResultsContainer.innerHTML = '';
+ searchResultsContainer.classList.remove('hidden');
+
+ const seenToolIds = new Set();
+ const allTools: HTMLElement[] = [];
+
categoryGroups.forEach((group) => {
const toolCards = Array.from(group.querySelectorAll('.tool-card'));
- let visibleToolsInCategory = 0;
-
toolCards.forEach((card) => {
const toolName = (card.querySelector('h3')?.textContent || '').toLowerCase();
const toolSubtitle = (card.querySelector('p')?.textContent || '').toLowerCase();
+ const toolHref = (card as HTMLAnchorElement).href || (card as HTMLElement).dataset.toolId || '';
- const isMatch = !searchTerm || toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
+ const toolId = toolHref.split('/').pop()?.replace('.html', '') || toolName;
- (card as HTMLElement).style.display = isMatch ? '' : 'none';
+ const isMatch = toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
+ const isDuplicate = seenToolIds.has(toolId);
- if (isMatch) {
- visibleToolsInCategory++;
+ if (isMatch && !isDuplicate) {
+ seenToolIds.add(toolId);
+ allTools.push(card.cloneNode(true) as HTMLElement);
}
});
-
- (group as HTMLElement).style.display = visibleToolsInCategory === 0 ? 'none' : '';
});
+
+ allTools.forEach((tool) => {
+ searchResultsContainer.appendChild(tool);
+ });
+
+ createIcons({ icons });
});
window.addEventListener('keydown', function (e) {
@@ -465,8 +498,7 @@ const init = async () => {
const fullWidthToggle = document.getElementById('full-width-toggle') as HTMLInputElement;
const toolInterface = document.getElementById('tool-interface');
- // Load saved preference
- const savedFullWidth = localStorage.getItem('fullWidthMode') === 'true';
+ const savedFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (fullWidthToggle) {
fullWidthToggle.checked = savedFullWidth;
applyFullWidthMode(savedFullWidth);
diff --git a/src/js/utils/full-width.ts b/src/js/utils/full-width.ts
index efc5c898..4a07d686 100644
--- a/src/js/utils/full-width.ts
+++ b/src/js/utils/full-width.ts
@@ -2,7 +2,7 @@
// This script applies the full-width preference from localStorage to page uploaders
export function initFullWidthMode() {
- const savedFullWidth = localStorage.getItem('fullWidthMode') === 'true';
+ const savedFullWidth = localStorage.getItem('fullWidthMode') !== 'false';
if (savedFullWidth) {
applyFullWidthMode(true);
diff --git a/src/pages/.backup-related-tools-fix/compress-pdf.html b/src/pages/.backup-related-tools-fix/compress-pdf.html
index fd1a146a..99e56c6b 100644
--- a/src/pages/.backup-related-tools-fix/compress-pdf.html
+++ b/src/pages/.backup-related-tools-fix/compress-pdf.html
@@ -57,7 +57,7 @@
-
+
BentoPDF
@@ -391,7 +391,7 @@
-
+
BentoPDF
diff --git a/src/pages/.backup-related-tools-fix/edit-pdf.html b/src/pages/.backup-related-tools-fix/edit-pdf.html
index 480b0771..296995a0 100644
--- a/src/pages/.backup-related-tools-fix/edit-pdf.html
+++ b/src/pages/.backup-related-tools-fix/edit-pdf.html
@@ -60,7 +60,7 @@
-
+
BentoPDF
@@ -264,7 +264,7 @@
-
+
BentoPDF
diff --git a/src/pages/.backup-related-tools-fix/excel-to-pdf.html b/src/pages/.backup-related-tools-fix/excel-to-pdf.html
index 59472ba4..e684e774 100644
--- a/src/pages/.backup-related-tools-fix/excel-to-pdf.html
+++ b/src/pages/.backup-related-tools-fix/excel-to-pdf.html
@@ -60,7 +60,7 @@
-
+
BentoPDF
@@ -277,7 +277,7 @@
-
+
BentoPDF
diff --git a/src/pages/.backup-related-tools-fix/merge-pdf.html b/src/pages/.backup-related-tools-fix/merge-pdf.html
index a38606df..3d6f9246 100644
--- a/src/pages/.backup-related-tools-fix/merge-pdf.html
+++ b/src/pages/.backup-related-tools-fix/merge-pdf.html
@@ -56,7 +56,7 @@
-
+
BentoPDF
@@ -322,7 +322,7 @@
-
+
BentoPDF
diff --git a/src/pages/.backup-related-tools-fix/organize-pdf.html b/src/pages/.backup-related-tools-fix/organize-pdf.html
index 6bedcbd6..01ed82f4 100644
--- a/src/pages/.backup-related-tools-fix/organize-pdf.html
+++ b/src/pages/.backup-related-tools-fix/organize-pdf.html
@@ -59,7 +59,7 @@
@@ -235,7 +235,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/.backup-related-tools-fix/pdf-to-docx.html b/src/pages/.backup-related-tools-fix/pdf-to-docx.html
index 6aa615a9..3f955414 100644
--- a/src/pages/.backup-related-tools-fix/pdf-to-docx.html
+++ b/src/pages/.backup-related-tools-fix/pdf-to-docx.html
@@ -58,7 +58,7 @@
-
+
BentoPDF
@@ -272,7 +272,7 @@
-
+
BentoPDF
diff --git a/src/pages/.backup-related-tools-fix/pdf-to-jpg.html b/src/pages/.backup-related-tools-fix/pdf-to-jpg.html
index af6e5a7c..f4a8ff1a 100644
--- a/src/pages/.backup-related-tools-fix/pdf-to-jpg.html
+++ b/src/pages/.backup-related-tools-fix/pdf-to-jpg.html
@@ -59,7 +59,7 @@
-
+
BentoPDF
@@ -283,7 +283,7 @@
-
+
BentoPDF
diff --git a/src/pages/.backup-related-tools-fix/rotate-pdf.html b/src/pages/.backup-related-tools-fix/rotate-pdf.html
index c558b2c3..e496a466 100644
--- a/src/pages/.backup-related-tools-fix/rotate-pdf.html
+++ b/src/pages/.backup-related-tools-fix/rotate-pdf.html
@@ -62,7 +62,7 @@
-
+
BentoPDF
@@ -286,7 +286,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/.backup-related-tools-fix/split-pdf.html b/src/pages/.backup-related-tools-fix/split-pdf.html
index 02c98c2b..be22d73b 100644
--- a/src/pages/.backup-related-tools-fix/split-pdf.html
+++ b/src/pages/.backup-related-tools-fix/split-pdf.html
@@ -60,7 +60,7 @@
-
+
BentoPDF
@@ -400,7 +400,7 @@
-
+
BentoPDF
diff --git a/src/pages/.backup-related-tools-fix/word-to-pdf.html b/src/pages/.backup-related-tools-fix/word-to-pdf.html
index d80efc54..04a11b6f 100644
--- a/src/pages/.backup-related-tools-fix/word-to-pdf.html
+++ b/src/pages/.backup-related-tools-fix/word-to-pdf.html
@@ -60,7 +60,7 @@
-
+
BentoPDF
@@ -295,7 +295,7 @@
-
+
BentoPDF
diff --git a/src/pages/add-attachments.html b/src/pages/add-attachments.html
index 44fd3e34..ebe615a0 100644
--- a/src/pages/add-attachments.html
+++ b/src/pages/add-attachments.html
@@ -72,7 +72,7 @@
-
+
BentoPDF
@@ -312,7 +312,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/add-blank-page.html b/src/pages/add-blank-page.html
index 07813d94..a460815c 100644
--- a/src/pages/add-blank-page.html
+++ b/src/pages/add-blank-page.html
@@ -72,7 +72,7 @@
-
+
BentoPDF
@@ -305,7 +305,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/add-stamps.html b/src/pages/add-stamps.html
index 8b326e12..011d2d4a 100644
--- a/src/pages/add-stamps.html
+++ b/src/pages/add-stamps.html
@@ -70,7 +70,7 @@
-
+
BentoPDF
@@ -277,7 +277,7 @@
-
+
BentoPDF
diff --git a/src/pages/add-watermark.html b/src/pages/add-watermark.html
index e3637f3b..0765bb72 100644
--- a/src/pages/add-watermark.html
+++ b/src/pages/add-watermark.html
@@ -70,7 +70,7 @@
-
+
BentoPDF
@@ -349,7 +349,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/alternate-merge.html b/src/pages/alternate-merge.html
index ec004111..c9ca0f49 100644
--- a/src/pages/alternate-merge.html
+++ b/src/pages/alternate-merge.html
@@ -72,7 +72,7 @@
-
+
BentoPDF
@@ -279,7 +279,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/background-color.html b/src/pages/background-color.html
index dde22bdc..82028541 100644
--- a/src/pages/background-color.html
+++ b/src/pages/background-color.html
@@ -70,7 +70,7 @@
-
+
BentoPDF
@@ -279,7 +279,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/bmp-to-pdf.html b/src/pages/bmp-to-pdf.html
index 4c954708..4b33889e 100644
--- a/src/pages/bmp-to-pdf.html
+++ b/src/pages/bmp-to-pdf.html
@@ -69,7 +69,7 @@
-
+
BentoPDF
@@ -282,7 +282,7 @@
-
+
BentoPDF
diff --git a/src/pages/bookmark.html b/src/pages/bookmark.html
index 6b72f4da..15c74cc6 100644
--- a/src/pages/bookmark.html
+++ b/src/pages/bookmark.html
@@ -71,7 +71,7 @@
-
+
BentoPDF
@@ -573,7 +573,7 @@
-
+
BentoPDF
diff --git a/src/pages/cbz-to-pdf.html b/src/pages/cbz-to-pdf.html
index 71d30ee4..87965fc5 100644
--- a/src/pages/cbz-to-pdf.html
+++ b/src/pages/cbz-to-pdf.html
@@ -70,7 +70,7 @@
-
+
BentoPDF
@@ -284,7 +284,7 @@
-
+
BentoPDF
diff --git a/src/pages/change-permissions.html b/src/pages/change-permissions.html
index c77a39c9..855da421 100644
--- a/src/pages/change-permissions.html
+++ b/src/pages/change-permissions.html
@@ -72,7 +72,7 @@
-
+
BentoPDF
@@ -335,7 +335,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/combine-single-page.html b/src/pages/combine-single-page.html
index 34e857b6..f8571636 100644
--- a/src/pages/combine-single-page.html
+++ b/src/pages/combine-single-page.html
@@ -72,7 +72,7 @@
-
+
BentoPDF
@@ -322,7 +322,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/compare-pdfs.html b/src/pages/compare-pdfs.html
index 48196d28..afda203a 100644
--- a/src/pages/compare-pdfs.html
+++ b/src/pages/compare-pdfs.html
@@ -102,7 +102,7 @@
-
+
BentoPDF
@@ -369,7 +369,7 @@
-
+
BentoPDF
© 2026 BentoPDF. All rights reserved.
diff --git a/src/pages/compress-pdf.html b/src/pages/compress-pdf.html
index a58ffbbc..55e08728 100644
--- a/src/pages/compress-pdf.html
+++ b/src/pages/compress-pdf.html
@@ -67,7 +67,7 @@
-
+
BentoPDF
@@ -391,7 +391,7 @@
-
+
BentoPDF
diff --git a/src/pages/crop-pdf.html b/src/pages/crop-pdf.html
index 13f66f6f..d6bce78e 100644
--- a/src/pages/crop-pdf.html
+++ b/src/pages/crop-pdf.html
@@ -71,7 +71,7 @@
-
+
BentoPDF
@@ -315,7 +315,7 @@
-
+
BentoPDF
diff --git a/src/pages/csv-to-pdf.html b/src/pages/csv-to-pdf.html
index 2a6765d8..f28106ed 100644
--- a/src/pages/csv-to-pdf.html
+++ b/src/pages/csv-to-pdf.html
@@ -70,7 +70,7 @@