From 3093dcbf4a5d830207a3a107980bf9ec431a7a04 Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 17:26:08 +0100 Subject: [PATCH 01/14] Add convert strings --- public/locales/de/tools.json | 15 ++++++++++----- public/locales/en/tools.json | 15 ++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index ea93f3a..0233515 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -204,23 +204,28 @@ }, "pdfToJpg": { "name": "PDF zu JPG", - "subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren.", + "convertButton": "Konvertieren" }, "pdfToPng": { "name": "PDF zu PNG", - "subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren.", + "convertButton": "Konvertieren" }, "pdfToWebp": { "name": "PDF zu WebP", - "subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren.", + "convertButton": "Konvertieren" }, "pdfToBmp": { "name": "PDF zu BMP", - "subtitle": "Jede PDF-Seite in ein BMP-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein BMP-Bild konvertieren.", + "convertButton": "Konvertieren" }, "pdfToTiff": { "name": "PDF zu TIFF", - "subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren.", + "convertButton": "Konvertieren" }, "pdfToGreyscale": { "name": "PDF zu Graustufen", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index 9783bee..fc2f1a8 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -204,23 +204,28 @@ }, "pdfToJpg": { "name": "PDF to JPG", - "subtitle": "Convert each PDF page into a JPG image." + "subtitle": "Convert each PDF page into a JPG image.", + "convertButton": "Convert" }, "pdfToPng": { "name": "PDF to PNG", - "subtitle": "Convert each PDF page into a PNG image." + "subtitle": "Convert each PDF page into a PNG image.", + "convertButton": "Convert" }, "pdfToWebp": { "name": "PDF to WebP", - "subtitle": "Convert each PDF page into a WebP image." + "subtitle": "Convert each PDF page into a WebP image.", + "convertButton": "Convert" }, "pdfToBmp": { "name": "PDF to BMP", - "subtitle": "Convert each PDF page into a BMP image." + "subtitle": "Convert each PDF page into a BMP image.", + "convertButton": "Convert" }, "pdfToTiff": { "name": "PDF to TIFF", - "subtitle": "Convert each PDF page into a TIFF image." + "subtitle": "Convert each PDF page into a TIFF image.", + "convertButton": "Convert" }, "pdfToGreyscale": { "name": "PDF to Greyscale", From e21ee9605bdb1184b855d56bf819e7a4b6930103 Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 17:44:11 +0100 Subject: [PATCH 02/14] Centralize convert local string into common This reverts commit 3093dcbf4a5d830207a3a107980bf9ec431a7a04. --- public/locales/de/common.json | 3 ++- public/locales/de/tools.json | 15 +++++---------- public/locales/en/common.json | 3 ++- public/locales/en/tools.json | 15 +++++---------- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/public/locales/de/common.json b/public/locales/de/common.json index 70ec209..1aa64f4 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -255,7 +255,8 @@ "success": "Erfolg", "file": "Datei", "files": "Dateien", - "close": "Schließen" + "close": "Schließen", + "convert": "Konvertieren" }, "about": { "hero": { diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index 0233515..ea93f3a 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -204,28 +204,23 @@ }, "pdfToJpg": { "name": "PDF zu JPG", - "subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren.", - "convertButton": "Konvertieren" + "subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren." }, "pdfToPng": { "name": "PDF zu PNG", - "subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren.", - "convertButton": "Konvertieren" + "subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren." }, "pdfToWebp": { "name": "PDF zu WebP", - "subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren.", - "convertButton": "Konvertieren" + "subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren." }, "pdfToBmp": { "name": "PDF zu BMP", - "subtitle": "Jede PDF-Seite in ein BMP-Bild konvertieren.", - "convertButton": "Konvertieren" + "subtitle": "Jede PDF-Seite in ein BMP-Bild konvertieren." }, "pdfToTiff": { "name": "PDF zu TIFF", - "subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren.", - "convertButton": "Konvertieren" + "subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren." }, "pdfToGreyscale": { "name": "PDF zu Graustufen", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 475ee64..50adeac 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -264,7 +264,8 @@ "success": "Success", "file": "File", "files": "Files", - "close": "Close" + "close": "Close", + "convert": "Convert" }, "about": { "hero": { diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index fc2f1a8..9783bee 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -204,28 +204,23 @@ }, "pdfToJpg": { "name": "PDF to JPG", - "subtitle": "Convert each PDF page into a JPG image.", - "convertButton": "Convert" + "subtitle": "Convert each PDF page into a JPG image." }, "pdfToPng": { "name": "PDF to PNG", - "subtitle": "Convert each PDF page into a PNG image.", - "convertButton": "Convert" + "subtitle": "Convert each PDF page into a PNG image." }, "pdfToWebp": { "name": "PDF to WebP", - "subtitle": "Convert each PDF page into a WebP image.", - "convertButton": "Convert" + "subtitle": "Convert each PDF page into a WebP image." }, "pdfToBmp": { "name": "PDF to BMP", - "subtitle": "Convert each PDF page into a BMP image.", - "convertButton": "Convert" + "subtitle": "Convert each PDF page into a BMP image." }, "pdfToTiff": { "name": "PDF to TIFF", - "subtitle": "Convert each PDF page into a TIFF image.", - "convertButton": "Convert" + "subtitle": "Convert each PDF page into a TIFF image." }, "pdfToGreyscale": { "name": "PDF to Greyscale", From b479abf6cfb08686f85fc4987f094183ce6630f4 Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 17:49:03 +0100 Subject: [PATCH 03/14] Add localization to "download" button --- src/pages/pdf-to-bmp.html | 8 ++++++-- src/pages/pdf-to-jpg.html | 8 ++++++-- src/pages/pdf-to-png.html | 8 ++++++-- src/pages/pdf-to-tiff.html | 8 ++++++-- src/pages/pdf-to-webp.html | 8 ++++++-- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/pages/pdf-to-bmp.html b/src/pages/pdf-to-bmp.html index 31c8c87..75c674c 100644 --- a/src/pages/pdf-to-bmp.html +++ b/src/pages/pdf-to-bmp.html @@ -159,8 +159,12 @@
diff --git a/src/pages/pdf-to-jpg.html b/src/pages/pdf-to-jpg.html index dcee91c..2c8d63b 100644 --- a/src/pages/pdf-to-jpg.html +++ b/src/pages/pdf-to-jpg.html @@ -189,8 +189,12 @@

- diff --git a/src/pages/pdf-to-png.html b/src/pages/pdf-to-png.html index 0853293..c0d9c69 100644 --- a/src/pages/pdf-to-png.html +++ b/src/pages/pdf-to-png.html @@ -186,8 +186,12 @@

- diff --git a/src/pages/pdf-to-tiff.html b/src/pages/pdf-to-tiff.html index f4292dc..8980324 100644 --- a/src/pages/pdf-to-tiff.html +++ b/src/pages/pdf-to-tiff.html @@ -154,8 +154,12 @@
diff --git a/src/pages/pdf-to-webp.html b/src/pages/pdf-to-webp.html index 0501482..a1cc403 100644 --- a/src/pages/pdf-to-webp.html +++ b/src/pages/pdf-to-webp.html @@ -186,8 +186,12 @@

- From 667385ee1c3008e5f876f0bbc62ec322b470ec9b Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 18:24:31 +0100 Subject: [PATCH 04/14] Add localization to fileDisplayArea --- src/js/logic/adjust-colors-page.ts | 3 ++- src/js/logic/pdf-to-bmp-page.ts | 3 ++- src/js/logic/pdf-to-greyscale-page.ts | 3 ++- src/js/logic/pdf-to-jpg-page.ts | 3 ++- src/js/logic/pdf-to-png-page.ts | 3 ++- src/js/logic/pdf-to-tiff-page.ts | 3 ++- src/js/logic/pdf-to-webp-page.ts | 3 ++- src/js/logic/scanner-effect-page.ts | 3 ++- 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/js/logic/adjust-colors-page.ts b/src/js/logic/adjust-colors-page.ts index 59ece2d..3f9ae89 100644 --- a/src/js/logic/adjust-colors-page.ts +++ b/src/js/logic/adjust-colors-page.ts @@ -10,6 +10,7 @@ import { PDFDocument } from 'pdf-lib'; import { applyColorAdjustments } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; import type { AdjustColorsSettings } from '../types/adjust-colors-type.js'; +import { t } from '../i18n/index.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -146,7 +147,7 @@ const updateUI = () => { return getPDFDocument(buffer).promise; }) .then((pdf: pdfjsLib.PDFDocumentProxy) => { - metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`; }) .catch(() => { metaSpan.textContent = formatBytes(file.size); diff --git a/src/js/logic/pdf-to-bmp-page.ts b/src/js/logic/pdf-to-bmp-page.ts index 1f71b77..a16463d 100644 --- a/src/js/logic/pdf-to-bmp-page.ts +++ b/src/js/logic/pdf-to-bmp-page.ts @@ -10,6 +10,7 @@ import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; +import { t } from '../i18n/index.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -67,7 +68,7 @@ const updateUI = () => { return getPDFDocument(buffer).promise; }) .then((pdf) => { - metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`; }) .catch((e) => { console.warn('Error loading PDF page count:', e); diff --git a/src/js/logic/pdf-to-greyscale-page.ts b/src/js/logic/pdf-to-greyscale-page.ts index 64c8df5..3d336fd 100644 --- a/src/js/logic/pdf-to-greyscale-page.ts +++ b/src/js/logic/pdf-to-greyscale-page.ts @@ -9,6 +9,7 @@ import { createIcons, icons } from 'lucide'; import { PDFDocument } from 'pdf-lib'; import { applyGreyscale } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; +import { t } from '../i18n/index.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -66,7 +67,7 @@ const updateUI = () => { return getPDFDocument(buffer).promise; }) .then((pdf) => { - metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`; }) .catch((e) => { console.warn('Error loading PDF page count:', e); diff --git a/src/js/logic/pdf-to-jpg-page.ts b/src/js/logic/pdf-to-jpg-page.ts index dc43566..8ad97a3 100644 --- a/src/js/logic/pdf-to-jpg-page.ts +++ b/src/js/logic/pdf-to-jpg-page.ts @@ -10,6 +10,7 @@ import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; +import { t } from '../i18n/index.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -66,7 +67,7 @@ const updateUI = () => { return getPDFDocument(buffer).promise; }) .then((pdf) => { - metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`; }) .catch((e) => { console.warn('Error loading PDF page count:', e); diff --git a/src/js/logic/pdf-to-png-page.ts b/src/js/logic/pdf-to-png-page.ts index 8f4995d..a079706 100644 --- a/src/js/logic/pdf-to-png-page.ts +++ b/src/js/logic/pdf-to-png-page.ts @@ -10,6 +10,7 @@ import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; +import { t } from '../i18n/index.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -66,7 +67,7 @@ const updateUI = () => { return getPDFDocument(buffer).promise; }) .then((pdf) => { - metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`; }) .catch((e) => { console.warn('Error loading PDF page count:', e); diff --git a/src/js/logic/pdf-to-tiff-page.ts b/src/js/logic/pdf-to-tiff-page.ts index 10ad314..f69f9f0 100644 --- a/src/js/logic/pdf-to-tiff-page.ts +++ b/src/js/logic/pdf-to-tiff-page.ts @@ -11,6 +11,7 @@ import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import UTIF from 'utif'; import { PDFPageProxy } from 'pdfjs-dist'; +import { t } from '../i18n/index.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -67,7 +68,7 @@ const updateUI = () => { return getPDFDocument(buffer).promise; }) .then((pdf) => { - metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`; }) .catch((e) => { console.warn('Error loading PDF page count:', e); diff --git a/src/js/logic/pdf-to-webp-page.ts b/src/js/logic/pdf-to-webp-page.ts index c2c0fec..4f8e59e 100644 --- a/src/js/logic/pdf-to-webp-page.ts +++ b/src/js/logic/pdf-to-webp-page.ts @@ -10,6 +10,7 @@ import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; +import { t } from '../i18n/index.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -66,7 +67,7 @@ const updateUI = () => { return getPDFDocument(buffer).promise; }) .then((pdf) => { - metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`; }) .catch((e) => { console.warn('Error loading PDF page count:', e); diff --git a/src/js/logic/scanner-effect-page.ts b/src/js/logic/scanner-effect-page.ts index a93bb8c..273cf95 100644 --- a/src/js/logic/scanner-effect-page.ts +++ b/src/js/logic/scanner-effect-page.ts @@ -10,6 +10,7 @@ import { PDFDocument } from 'pdf-lib'; import { applyScannerEffect } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; import type { ScanSettings } from '../types/scanner-effect-type.js'; +import { t } from '../i18n/index.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -152,7 +153,7 @@ const updateUI = () => { return getPDFDocument(buffer).promise; }) .then((pdf: pdfjsLib.PDFDocumentProxy) => { - metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`; + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`; }) .catch(() => { metaSpan.textContent = formatBytes(file.size); From f1b03d076cba8c0ebd66a26eafa0ad9ab09ee4bc Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 18:31:27 +0100 Subject: [PATCH 05/14] Add localization for image quality --- public/locales/de/tools.json | 4 +++- public/locales/en/tools.json | 4 +++- src/pages/pdf-to-webp.html | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index ea93f3a..f6d9197 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -212,7 +212,9 @@ }, "pdfToWebp": { "name": "PDF zu WebP", - "subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren.", + "imageQuality": "Bildqualität", + "imageQualityExplanation": "Höhere Qualität = größere Dateigröße" }, "pdfToBmp": { "name": "PDF zu BMP", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index 9783bee..e9485f7 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -212,7 +212,9 @@ }, "pdfToWebp": { "name": "PDF to WebP", - "subtitle": "Convert each PDF page into a WebP image." + "subtitle": "Convert each PDF page into a WebP image.", + "imageQuality": "Image Quality", + "imageQualityExplanation": "Higher quality = larger file size" }, "pdfToBmp": { "name": "PDF to BMP", diff --git a/src/pages/pdf-to-webp.html b/src/pages/pdf-to-webp.html index a1cc403..cb8c870 100644 --- a/src/pages/pdf-to-webp.html +++ b/src/pages/pdf-to-webp.html @@ -163,6 +163,7 @@
@@ -181,7 +182,10 @@ >85%
-

+

Higher quality = larger file size

From 67e38871652ff9274dee74e6a03c2b84b223fb93 Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 18:35:17 +0100 Subject: [PATCH 06/14] Add localization for image quality --- public/locales/de/tools.json | 4 +++- public/locales/en/tools.json | 4 +++- src/pages/pdf-to-jpg.html | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index f6d9197..9b2e1dd 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -204,7 +204,9 @@ }, "pdfToJpg": { "name": "PDF zu JPG", - "subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren.", + "imageQuality": "Bildqualität", + "imageQualityExplanation": "Höhere Qualität = größere Dateigröße" }, "pdfToPng": { "name": "PDF zu PNG", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index e9485f7..ae20264 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -204,7 +204,9 @@ }, "pdfToJpg": { "name": "PDF to JPG", - "subtitle": "Convert each PDF page into a JPG image." + "subtitle": "Convert each PDF page into a JPG image.", + "imageQuality": "Image Quality", + "imageQualityExplanation": "Higher quality = larger file size" }, "pdfToPng": { "name": "PDF to PNG", diff --git a/src/pages/pdf-to-jpg.html b/src/pages/pdf-to-jpg.html index 2c8d63b..64ad002 100644 --- a/src/pages/pdf-to-jpg.html +++ b/src/pages/pdf-to-jpg.html @@ -166,6 +166,7 @@
@@ -184,7 +185,10 @@ >90%
-

+

Higher quality = larger file size

From 4f869602656d24ecb8a0faafbf93227bf1ed513b Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 18:38:09 +0100 Subject: [PATCH 07/14] Add localization for image scale --- public/locales/de/tools.json | 4 +++- public/locales/en/tools.json | 4 +++- src/pages/pdf-to-png.html | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index 9b2e1dd..f89f6cf 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -210,7 +210,9 @@ }, "pdfToPng": { "name": "PDF zu PNG", - "subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren.", + "imageScale": "Bildskalierung", + "imageScaleExplanation": "Höhere Skalierung = bessere Qualität, aber größere Dateigröße" }, "pdfToWebp": { "name": "PDF zu WebP", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index ae20264..16a825b 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -210,7 +210,9 @@ }, "pdfToPng": { "name": "PDF to PNG", - "subtitle": "Convert each PDF page into a PNG image." + "subtitle": "Convert each PDF page into a PNG image.", + "imageScale": "Image Scale", + "imageScaleExplanation": "Higher scale = better quality but larger file size" }, "pdfToWebp": { "name": "PDF to WebP", diff --git a/src/pages/pdf-to-png.html b/src/pages/pdf-to-png.html index c0d9c69..e5c54a5 100644 --- a/src/pages/pdf-to-png.html +++ b/src/pages/pdf-to-png.html @@ -163,6 +163,7 @@
@@ -181,7 +182,10 @@ >2.0x
-

+

Higher scale = better quality but larger file size

From d5244649a332c8efbbab2442fdacf88277afc172 Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 18:40:47 +0100 Subject: [PATCH 08/14] Update imports according to the translation guidelines --- src/js/logic/adjust-colors-page.ts | 2 +- src/js/logic/pdf-to-bmp-page.ts | 2 +- src/js/logic/pdf-to-greyscale-page.ts | 2 +- src/js/logic/pdf-to-jpg-page.ts | 2 +- src/js/logic/pdf-to-png-page.ts | 2 +- src/js/logic/pdf-to-tiff-page.ts | 2 +- src/js/logic/pdf-to-webp-page.ts | 2 +- src/js/logic/scanner-effect-page.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/js/logic/adjust-colors-page.ts b/src/js/logic/adjust-colors-page.ts index 3f9ae89..aeab10f 100644 --- a/src/js/logic/adjust-colors-page.ts +++ b/src/js/logic/adjust-colors-page.ts @@ -10,7 +10,7 @@ import { PDFDocument } from 'pdf-lib'; import { applyColorAdjustments } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; import type { AdjustColorsSettings } from '../types/adjust-colors-type.js'; -import { t } from '../i18n/index.js'; +import { t } from '../i18n/i18n'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', diff --git a/src/js/logic/pdf-to-bmp-page.ts b/src/js/logic/pdf-to-bmp-page.ts index a16463d..e5d41fe 100644 --- a/src/js/logic/pdf-to-bmp-page.ts +++ b/src/js/logic/pdf-to-bmp-page.ts @@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; -import { t } from '../i18n/index.js'; +import { t } from '../i18n/i18n'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', diff --git a/src/js/logic/pdf-to-greyscale-page.ts b/src/js/logic/pdf-to-greyscale-page.ts index 3d336fd..9a4f94a 100644 --- a/src/js/logic/pdf-to-greyscale-page.ts +++ b/src/js/logic/pdf-to-greyscale-page.ts @@ -9,7 +9,7 @@ import { createIcons, icons } from 'lucide'; import { PDFDocument } from 'pdf-lib'; import { applyGreyscale } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; -import { t } from '../i18n/index.js'; +import { t } from '../i18n/i18n'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', diff --git a/src/js/logic/pdf-to-jpg-page.ts b/src/js/logic/pdf-to-jpg-page.ts index 8ad97a3..af73aeb 100644 --- a/src/js/logic/pdf-to-jpg-page.ts +++ b/src/js/logic/pdf-to-jpg-page.ts @@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; -import { t } from '../i18n/index.js'; +import { t } from '../i18n/i18n'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', diff --git a/src/js/logic/pdf-to-png-page.ts b/src/js/logic/pdf-to-png-page.ts index a079706..76a3706 100644 --- a/src/js/logic/pdf-to-png-page.ts +++ b/src/js/logic/pdf-to-png-page.ts @@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; -import { t } from '../i18n/index.js'; +import { t } from '../i18n/i18n'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', diff --git a/src/js/logic/pdf-to-tiff-page.ts b/src/js/logic/pdf-to-tiff-page.ts index f69f9f0..71886af 100644 --- a/src/js/logic/pdf-to-tiff-page.ts +++ b/src/js/logic/pdf-to-tiff-page.ts @@ -11,7 +11,7 @@ import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import UTIF from 'utif'; import { PDFPageProxy } from 'pdfjs-dist'; -import { t } from '../i18n/index.js'; +import { t } from '../i18n/i18n'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', diff --git a/src/js/logic/pdf-to-webp-page.ts b/src/js/logic/pdf-to-webp-page.ts index 4f8e59e..d4c74b1 100644 --- a/src/js/logic/pdf-to-webp-page.ts +++ b/src/js/logic/pdf-to-webp-page.ts @@ -10,7 +10,7 @@ import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; import { PDFPageProxy } from 'pdfjs-dist'; -import { t } from '../i18n/index.js'; +import { t } from '../i18n/i18n'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', diff --git a/src/js/logic/scanner-effect-page.ts b/src/js/logic/scanner-effect-page.ts index 273cf95..2954549 100644 --- a/src/js/logic/scanner-effect-page.ts +++ b/src/js/logic/scanner-effect-page.ts @@ -10,7 +10,7 @@ import { PDFDocument } from 'pdf-lib'; import { applyScannerEffect } from '../utils/image-effects.js'; import * as pdfjsLib from 'pdfjs-dist'; import type { ScanSettings } from '../types/scanner-effect-type.js'; -import { t } from '../i18n/index.js'; +import { t } from '../i18n/i18n'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', From 01c4d3bd7c881079f085f723b7faa72ebc949acd Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 19:17:17 +0100 Subject: [PATCH 09/14] Add localization for PDF to JPG conversion alerts and loader messages --- public/locales/de/tools.json | 13 ++++++++++++- public/locales/en/tools.json | 13 ++++++++++++- src/js/logic/pdf-to-jpg-page.ts | 21 ++++++++++++--------- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index f89f6cf..09f6ebe 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -206,7 +206,18 @@ "name": "PDF zu JPG", "subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren.", "imageQuality": "Bildqualität", - "imageQualityExplanation": "Höhere Qualität = größere Dateigröße" + "imageQualityExplanation": "Höhere Qualität = größere Dateigröße", + "alert": { + "invalidFile": "Ungültige Datei", + "invalidFileExplanation": "Bitte wähle eine PDF Datei aus.", + "noFile": "Keine Datei", + "noFileExplanation": "Bitte lade zuerst eine PDF-Datei hoch.", + "conversionSuccess": "PDF erfolgreich in JPGs konvertiert!", + "conversionError": "Konvertierung in JPG fehlgeschlagen. Die Datei könnte beschädigt sein." + }, + "loader": { + "converting": "Wird in JPG konvertiert..." + } }, "pdfToPng": { "name": "PDF zu PNG", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index 16a825b..f446c8d 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -206,7 +206,18 @@ "name": "PDF to JPG", "subtitle": "Convert each PDF page into a JPG image.", "imageQuality": "Image Quality", - "imageQualityExplanation": "Higher quality = larger file size" + "imageQualityExplanation": "Higher quality = larger file size", + "alert": { + "invalidFile": "Invalid File", + "invalidFileExplanation": "Please upload a PDF file.", + "noFile": "No File", + "noFileExplanation": "Please upload a PDF file first.", + "conversionSuccess": "PDF converted to JPGs successfully!", + "conversionError": "Failed to convert PDF to JPG. The file might be corrupted." + }, + "loader": { + "converting": "Converting to JPG..." + } }, "pdfToPng": { "name": "PDF to PNG", diff --git a/src/js/logic/pdf-to-jpg-page.ts b/src/js/logic/pdf-to-jpg-page.ts index af73aeb..dedebb3 100644 --- a/src/js/logic/pdf-to-jpg-page.ts +++ b/src/js/logic/pdf-to-jpg-page.ts @@ -97,10 +97,13 @@ const resetState = () => { async function convert() { if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); + showAlert( + t('tools:pdfToJpg.alert.noFile'), + t('tools:pdfToJpg.alert.noFileExplanation') + ); return; } - showLoader('Converting to JPG...'); + showLoader(t('tools:pdfToJpg.loader.converting')); try { const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) .promise; @@ -129,8 +132,8 @@ async function convert() { } showAlert( - 'Success', - 'PDF converted to JPGs successfully!', + t('common.success'), + t('tools:pdfToJpg.alert.conversionSuccess'), 'success', () => { resetState(); @@ -138,10 +141,7 @@ async function convert() { ); } catch (e) { console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to JPG. The file might be corrupted.' - ); + showAlert(t('common.error'), t('tools:pdfToJpg.alert.conversionError')); } finally { hideLoader(); } @@ -198,7 +198,10 @@ document.addEventListener('DOMContentLoaded', () => { ); if (validFiles.length === 0) { - showAlert('Invalid File', 'Please upload a PDF file.'); + showAlert( + t('tools:pdfToJpg.alert.invalidFile'), + t('tools:pdfToJpg.alert.invalidFileExplanation') + ); return; } From 37d95e249df6b0f45dda3bc1a6654acb9bce856b Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 19:18:39 +0100 Subject: [PATCH 10/14] Add localization for PDF to PNG conversion alerts and loader messages --- public/locales/de/tools.json | 13 ++++++++++++- public/locales/en/tools.json | 13 ++++++++++++- src/js/logic/pdf-to-png-page.ts | 21 ++++++++++++--------- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index 09f6ebe..eef2bf6 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -223,7 +223,18 @@ "name": "PDF zu PNG", "subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren.", "imageScale": "Bildskalierung", - "imageScaleExplanation": "Höhere Skalierung = bessere Qualität, aber größere Dateigröße" + "imageScaleExplanation": "Höhere Skalierung = bessere Qualität, aber größere Dateigröße", + "alert": { + "invalidFile": "Ungültige Datei", + "invalidFileExplanation": "Bitte wähle eine PDF Datei aus.", + "noFile": "Keine Datei", + "noFileExplanation": "Bitte lade zuerst eine PDF-Datei hoch.", + "conversionSuccess": "PDF erfolgreich in PNGs konvertiert!", + "conversionError": "Konvertierung in PNG fehlgeschlagen. Die Datei könnte beschädigt sein." + }, + "loader": { + "converting": "Wird in PNG konvertiert..." + } }, "pdfToWebp": { "name": "PDF zu WebP", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index f446c8d..043c880 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -223,7 +223,18 @@ "name": "PDF to PNG", "subtitle": "Convert each PDF page into a PNG image.", "imageScale": "Image Scale", - "imageScaleExplanation": "Higher scale = better quality but larger file size" + "imageScaleExplanation": "Higher scale = better quality but larger file size", + "alert": { + "invalidFile": "Invalid File", + "invalidFileExplanation": "Please upload a PDF file.", + "noFile": "No File", + "noFileExplanation": "Please upload a PDF file first.", + "conversionSuccess": "PDF converted to PNGs successfully!", + "conversionError": "Failed to convert PDF to PNG. The file might be corrupted." + }, + "loader": { + "converting": "Converting to PNG..." + } }, "pdfToWebp": { "name": "PDF to WebP", diff --git a/src/js/logic/pdf-to-png-page.ts b/src/js/logic/pdf-to-png-page.ts index 76a3706..84fe5e5 100644 --- a/src/js/logic/pdf-to-png-page.ts +++ b/src/js/logic/pdf-to-png-page.ts @@ -95,10 +95,13 @@ const resetState = () => { async function convert() { if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); + showAlert( + t('tools:pdfToPng.alert.noFile'), + t('tools:pdfToPng.alert.noFileExplanation') + ); return; } - showLoader('Converting to PNG...'); + showLoader(t('tools:pdfToPng.loader.converting')); try { const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) .promise; @@ -125,8 +128,8 @@ async function convert() { } showAlert( - 'Success', - 'PDF converted to PNGs successfully!', + t('common.success'), + t('tools:pdfToPng.alert.conversionSuccess'), 'success', () => { resetState(); @@ -134,10 +137,7 @@ async function convert() { ); } catch (e) { console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to PNG. The file might be corrupted.' - ); + showAlert(t('common.error'), t('tools:pdfToPng.alert.conversionError')); } finally { hideLoader(); } @@ -192,7 +192,10 @@ document.addEventListener('DOMContentLoaded', () => { ); if (validFiles.length === 0) { - showAlert('Invalid File', 'Please upload a PDF file.'); + showAlert( + t('tools:pdfToPng.alert.invalidFile'), + t('tools:pdfToPng.alert.invalidFileExplanation') + ); return; } From 8d263786119c15952a3688c9e9f6dc695bff7936 Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 19:52:38 +0100 Subject: [PATCH 11/14] Add localization for PDF to WebP conversion alerts and loader messages --- public/locales/de/tools.json | 13 ++++++++++++- public/locales/en/tools.json | 13 ++++++++++++- src/js/logic/pdf-to-webp-page.ts | 21 ++++++++++++--------- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index eef2bf6..a137e1a 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -240,7 +240,18 @@ "name": "PDF zu WebP", "subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren.", "imageQuality": "Bildqualität", - "imageQualityExplanation": "Höhere Qualität = größere Dateigröße" + "imageQualityExplanation": "Höhere Qualität = größere Dateigröße", + "alert": { + "invalidFile": "Ungültige Datei", + "invalidFileExplanation": "Bitte wähle eine PDF Datei aus.", + "noFile": "Keine Datei", + "noFileExplanation": "Bitte lade zuerst eine PDF-Datei hoch.", + "conversionSuccess": "PDF erfolgreich in WebPs konvertiert!", + "conversionError": "Konvertierung in WebP fehlgeschlagen. Die Datei könnte beschädigt sein." + }, + "loader": { + "converting": "Wird in WebP konvertiert..." + } }, "pdfToBmp": { "name": "PDF zu BMP", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index 043c880..fcbb462 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -240,7 +240,18 @@ "name": "PDF to WebP", "subtitle": "Convert each PDF page into a WebP image.", "imageQuality": "Image Quality", - "imageQualityExplanation": "Higher quality = larger file size" + "imageQualityExplanation": "Higher quality = larger file size", + "alert": { + "invalidFile": "Invalid File", + "invalidFileExplanation": "Please upload a PDF file.", + "noFile": "No File", + "noFileExplanation": "Please upload a PDF file first.", + "conversionSuccess": "PDF converted to WebPs successfully!", + "conversionError": "Failed to convert PDF to WebP. The file might be corrupted." + }, + "loader": { + "converting": "Converting to WebP..." + } }, "pdfToBmp": { "name": "PDF to BMP", diff --git a/src/js/logic/pdf-to-webp-page.ts b/src/js/logic/pdf-to-webp-page.ts index d4c74b1..57e4d5e 100644 --- a/src/js/logic/pdf-to-webp-page.ts +++ b/src/js/logic/pdf-to-webp-page.ts @@ -97,10 +97,13 @@ const resetState = () => { async function convert() { if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); + showAlert( + t('tools:pdfToWebp.alert.noFile'), + t('tools:pdfToWebp.alert.noFileExplanation') + ); return; } - showLoader('Converting to WebP...'); + showLoader(t('tools:pdfToWebp.loader.converting')); try { const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) .promise; @@ -129,8 +132,8 @@ async function convert() { } showAlert( - 'Success', - 'PDF converted to WebPs successfully!', + t('common.success'), + t('tools:pdfToWebp.alert.conversionSuccess'), 'success', () => { resetState(); @@ -138,10 +141,7 @@ async function convert() { ); } catch (e) { console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to WebP. The file might be corrupted.' - ); + showAlert(t('common.error'), t('tools:pdfToWebp.alert.conversionError')); } finally { hideLoader(); } @@ -198,7 +198,10 @@ document.addEventListener('DOMContentLoaded', () => { ); if (validFiles.length === 0) { - showAlert('Invalid File', 'Please upload a PDF file.'); + showAlert( + t('tools:pdfToWebp.alert.invalidFile'), + t('tools:pdfToWebp.alert.invalidFileExplanation') + ); return; } From 571ce07af66507662193001578a76b3395b29620 Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 19:55:21 +0100 Subject: [PATCH 12/14] Add localization for PDF to BMP and PDF to TIFF conversion alerts and loader messages --- public/locales/de/tools.json | 26 ++++++++++++++++++++++++-- public/locales/en/tools.json | 26 ++++++++++++++++++++++++-- src/js/logic/pdf-to-bmp-page.ts | 21 ++++++++++++--------- src/js/logic/pdf-to-tiff-page.ts | 21 ++++++++++++--------- 4 files changed, 72 insertions(+), 22 deletions(-) diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index a137e1a..ab0988c 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -255,11 +255,33 @@ }, "pdfToBmp": { "name": "PDF zu BMP", - "subtitle": "Jede PDF-Seite in ein BMP-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein BMP-Bild konvertieren.", + "alert": { + "invalidFile": "Ungültige Datei", + "invalidFileExplanation": "Bitte wähle eine PDF Datei aus.", + "noFile": "Keine Datei", + "noFileExplanation": "Bitte lade zuerst eine PDF-Datei hoch.", + "conversionSuccess": "PDF erfolgreich in BMPs konvertiert!", + "conversionError": "Konvertierung in BMP fehlgeschlagen. Die Datei könnte beschädigt sein." + }, + "loader": { + "converting": "Wird in BMP konvertiert..." + } }, "pdfToTiff": { "name": "PDF zu TIFF", - "subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren." + "subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren.", + "alert": { + "invalidFile": "Ungültige Datei", + "invalidFileExplanation": "Bitte wähle eine PDF Datei aus.", + "noFile": "Keine Datei", + "noFileExplanation": "Bitte lade zuerst eine PDF-Datei hoch.", + "conversionSuccess": "PDF erfolgreich in TIFFs konvertiert!", + "conversionError": "Konvertierung in TIFF fehlgeschlagen. Die Datei könnte beschädigt sein." + }, + "loader": { + "converting": "Wird in TIFF konvertiert..." + } }, "pdfToGreyscale": { "name": "PDF zu Graustufen", diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index fcbb462..6fa2476 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -255,11 +255,33 @@ }, "pdfToBmp": { "name": "PDF to BMP", - "subtitle": "Convert each PDF page into a BMP image." + "subtitle": "Convert each PDF page into a BMP image.", + "alert": { + "invalidFile": "Invalid File", + "invalidFileExplanation": "Please upload a PDF file.", + "noFile": "No File", + "noFileExplanation": "Please upload a PDF file first.", + "conversionSuccess": "PDF converted to BMPs successfully!", + "conversionError": "Failed to convert PDF to BMP. The file might be corrupted." + }, + "loader": { + "converting": "Converting to BMP..." + } }, "pdfToTiff": { "name": "PDF to TIFF", - "subtitle": "Convert each PDF page into a TIFF image." + "subtitle": "Convert each PDF page into a TIFF image.", + "alert": { + "invalidFile": "Invalid File", + "invalidFileExplanation": "Please upload a PDF file.", + "noFile": "No File", + "noFileExplanation": "Please upload a PDF file first.", + "conversionSuccess": "PDF converted to TIFFs successfully!", + "conversionError": "Failed to convert PDF to TIFF. The file might be corrupted." + }, + "loader": { + "converting": "Converting to TIFF..." + } }, "pdfToGreyscale": { "name": "PDF to Greyscale", diff --git a/src/js/logic/pdf-to-bmp-page.ts b/src/js/logic/pdf-to-bmp-page.ts index e5d41fe..b38ac4d 100644 --- a/src/js/logic/pdf-to-bmp-page.ts +++ b/src/js/logic/pdf-to-bmp-page.ts @@ -92,10 +92,13 @@ const resetState = () => { async function convert() { if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); + showAlert( + t('tools:pdfToBmp.alert.noFile'), + t('tools:pdfToBmp.alert.noFileExplanation') + ); return; } - showLoader('Converting to BMP...'); + showLoader(t('tools:pdfToBmp.loader.converting')); try { const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) .promise; @@ -119,8 +122,8 @@ async function convert() { } showAlert( - 'Success', - 'PDF converted to BMPs successfully!', + t('common.success'), + t('tools:pdfToBmp.alert.conversionSuccess'), 'success', () => { resetState(); @@ -128,10 +131,7 @@ async function convert() { ); } catch (e) { console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to BMP. The file might be corrupted.' - ); + showAlert(t('common.error'), t('tools:pdfToBmp.alert.conversionError')); } finally { hideLoader(); } @@ -175,7 +175,10 @@ document.addEventListener('DOMContentLoaded', () => { ); if (validFiles.length === 0) { - showAlert('Invalid File', 'Please upload a PDF file.'); + showAlert( + t('tools:pdfToBmp.alert.invalidFile'), + t('tools:pdfToBmp.alert.invalidFileExplanation') + ); return; } diff --git a/src/js/logic/pdf-to-tiff-page.ts b/src/js/logic/pdf-to-tiff-page.ts index 71886af..c463124 100644 --- a/src/js/logic/pdf-to-tiff-page.ts +++ b/src/js/logic/pdf-to-tiff-page.ts @@ -92,10 +92,13 @@ const resetState = () => { async function convert() { if (files.length === 0) { - showAlert('No File', 'Please upload a PDF file first.'); + showAlert( + t('tools:pdfToTiff.alert.noFile'), + t('tools:pdfToTiff.alert.noFileExplanation') + ); return; } - showLoader('Converting to TIFF...'); + showLoader(t('tools:pdfToTiff.loader.converting')); try { const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) .promise; @@ -122,8 +125,8 @@ async function convert() { } showAlert( - 'Success', - 'PDF converted to TIFFs successfully!', + t('common.success'), + t('tools:pdfToTiff.alert.conversionSuccess'), 'success', () => { resetState(); @@ -131,10 +134,7 @@ async function convert() { ); } catch (e) { console.error(e); - showAlert( - 'Error', - 'Failed to convert PDF to TIFF. The file might be corrupted.' - ); + showAlert(t('common.error'), t('tools:pdfToTiff.alert.conversionError')); } finally { hideLoader(); } @@ -212,7 +212,10 @@ document.addEventListener('DOMContentLoaded', () => { ); if (validFiles.length === 0) { - showAlert('Invalid File', 'Please upload a PDF file.'); + showAlert( + t('tools:pdfToTiff.alert.invalidFile'), + t('tools:pdfToTiff.alert.invalidFileExplanation') + ); return; } From 2ede256de2645e8ae9308665b03d43f7b66c3f1b Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 10 Mar 2026 20:13:13 +0100 Subject: [PATCH 13/14] Add localization for loading page count in various PDF processing pages --- public/locales/de/common.json | 1 + public/locales/en/common.json | 1 + src/js/logic/adjust-colors-page.ts | 2 +- src/js/logic/digital-sign-pdf-page.ts | 1168 ++++++++++++----------- src/js/logic/extract-images-page.ts | 3 +- src/js/logic/pdf-layers-page.ts | 3 +- src/js/logic/pdf-to-bmp-page.ts | 2 +- src/js/logic/pdf-to-docx-page.ts | 3 +- src/js/logic/pdf-to-greyscale-page.ts | 2 +- src/js/logic/pdf-to-jpg-page.ts | 2 +- src/js/logic/pdf-to-markdown-page.ts | 3 +- src/js/logic/pdf-to-pdfa-page.ts | 3 +- src/js/logic/pdf-to-png-page.ts | 2 +- src/js/logic/pdf-to-tiff-page.ts | 2 +- src/js/logic/pdf-to-webp-page.ts | 2 +- src/js/logic/prepare-pdf-for-ai-page.ts | 3 +- src/js/logic/rasterize-pdf-page.ts | 3 +- src/js/logic/scanner-effect-page.ts | 2 +- src/js/logic/sign-pdf-page.ts | 540 ++++++----- src/js/logic/split-pdf-page.ts | 3 +- 20 files changed, 944 insertions(+), 806 deletions(-) diff --git a/public/locales/de/common.json b/public/locales/de/common.json index 1aa64f4..ec04c7b 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -251,6 +251,7 @@ "add": "Hinzufügen", "remove": "Entfernen", "loading": "Laden...", + "loadingPageCount": "Seitenanzahl wird geladen...", "error": "Fehler", "success": "Erfolg", "file": "Datei", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 50adeac..c66aebd 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -260,6 +260,7 @@ "add": "Add", "remove": "Remove", "loading": "Loading...", + "loadingPageCount": "Loading pages...", "error": "Error", "success": "Success", "file": "File", diff --git a/src/js/logic/adjust-colors-page.ts b/src/js/logic/adjust-colors-page.ts index aeab10f..e46d4b1 100644 --- a/src/js/logic/adjust-colors-page.ts +++ b/src/js/logic/adjust-colors-page.ts @@ -124,7 +124,7 @@ const updateUI = () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/digital-sign-pdf-page.ts b/src/js/logic/digital-sign-pdf-page.ts index 306b22a..42611d0 100644 --- a/src/js/logic/digital-sign-pdf-page.ts +++ b/src/js/logic/digital-sign-pdf-page.ts @@ -1,689 +1,775 @@ 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, + readFileAsArrayBuffer, + formatBytes, + downloadFile, + getPDFDocument, +} from '../utils/helpers.js'; +import { t } from '../i18n/i18n'; +import { + signPdf, + parsePfxFile, + parseCombinedPem, + getCertificateInfo, } from './digital-sign-pdf.js'; -import { SignatureInfo, VisibleSignatureOptions, DigitalSignState } from '@/types'; +import { + SignatureInfo, + VisibleSignatureOptions, + DigitalSignState, +} from '@/types'; const state: DigitalSignState = { - pdfFile: null, - pdfBytes: null, - certFile: null, - certData: null, - sigImageData: null, - sigImageType: null, + 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; + 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 fileDisplayArea = getElement('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - const certDisplayArea = getElement('cert-display-area'); - if (certDisplayArea) certDisplayArea.innerHTML = ''; + const certDisplayArea = getElement('cert-display-area'); + if (certDisplayArea) certDisplayArea.innerHTML = ''; - const fileInput = getElement('file-input'); - if (fileInput) fileInput.value = ''; + const fileInput = getElement('file-input'); + if (fileInput) fileInput.value = ''; - const certInput = getElement('cert-input'); - if (certInput) certInput.value = ''; + const certInput = getElement('cert-input'); + if (certInput) certInput.value = ''; - const sigImageInput = getElement('sig-image-input'); - if (sigImageInput) sigImageInput.value = ''; + const sigImageInput = getElement('sig-image-input'); + if (sigImageInput) sigImageInput.value = ''; - const sigImagePreview = getElement('sig-image-preview'); - if (sigImagePreview) sigImagePreview.classList.add('hidden'); + const sigImagePreview = getElement('sig-image-preview'); + if (sigImagePreview) sigImagePreview.classList.add('hidden'); - const certSection = getElement('certificate-section'); - if (certSection) certSection.classList.add('hidden'); + const certSection = getElement('certificate-section'); + if (certSection) certSection.classList.add('hidden'); - hidePasswordSection(); - hideSignatureOptions(); - hideCertInfo(); - updateProcessButton(); + hidePasswordSection(); + hideSignatureOptions(); + hideCertInfo(); + updateProcessButton(); } function getElement(id: string): T | null { - return document.getElementById(id) as T | null; + return document.getElementById(id) as T | null; } function initializePage(): void { - createIcons({ icons }); + 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'); + 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 (fileInput) { + fileInput.addEventListener('change', handlePdfUpload); + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); + 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('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]); - } - }); - } + 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 (certInput) { + certInput.addEventListener('change', handleCertUpload); + certInput.addEventListener('click', () => { + certInput.value = ''; + }); + } - if (certDropZone) { - certDropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - certDropZone.classList.add('bg-gray-700'); - }); + 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('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]); - } - }); - } + 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 (certPassword) { + certPassword.addEventListener('input', handlePasswordInput); + certPassword.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + handlePasswordInput(); + } + }); + } - if (processBtn) { - processBtn.addEventListener('click', processSignature); - } + if (processBtn) { + processBtn.addEventListener('click', processSignature); + } - if (backBtn) { - backBtn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; - }); - } + 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'); + 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 (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 (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 (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 (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 (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'); - } - }); - } + 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]); - } + 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; - } + 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); + state.pdfFile = file; + state.pdfBytes = new Uint8Array( + (await readFileAsArrayBuffer(file)) as ArrayBuffer + ); - updatePdfDisplay(); - showCertificateSection(); + updatePdfDisplay(); + showCertificateSection(); } async function updatePdfDisplay(): Promise { - const fileDisplayArea = getElement('file-display-area'); + const fileDisplayArea = getElement('file-display-area'); - if (!fileDisplayArea || !state.pdfFile) return; + 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)} • ${t('common.loadingPageCount')}`; + + 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(); + }; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - 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)}`; + 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'); - } + const certSection = getElement('certificate-section'); + if (certSection) { + certSection.classList.remove('hidden'); + } } function hideCertificateSection(): void { - const certSection = getElement('certificate-section'); - const signatureOptions = getElement('signature-options'); + const certSection = getElement('certificate-section'); + const signatureOptions = getElement('signature-options'); - if (certSection) { - certSection.classList.add('hidden'); - } - if (signatureOptions) { - signatureOptions.classList.add('hidden'); - } + if (certSection) { + certSection.classList.add('hidden'); + } + if (signatureOptions) { + signatureOptions.classList.add('hidden'); + } - state.certFile = null; - state.certData = null; + state.certFile = null; + state.certData = null; - const certDisplayArea = getElement('cert-display-area'); - if (certDisplayArea) { - certDisplayArea.innerHTML = ''; - } + const certDisplayArea = getElement('cert-display-area'); + if (certDisplayArea) { + certDisplayArea.innerHTML = ''; + } - const certInfo = getElement('cert-info'); - if (certInfo) { - certInfo.classList.add('hidden'); - } + const certInfo = getElement('cert-info'); + if (certInfo) { + certInfo.classList.add('hidden'); + } - const certPasswordSection = getElement('cert-password-section'); - if (certPasswordSection) { - certPasswordSection.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]); - } + 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) + 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; + } - if (!hasValidExtension) { - showAlert('Invalid Certificate', 'Please select a .pfx, .p12, or .pem certificate file.'); - return; - } + state.certFile = file; + state.certData = null; - state.certFile = file; - state.certData = null; + updateCertDisplay(); - updateCertDisplay(); + const isPemFile = file.name.toLowerCase().endsWith('.pem'); - const isPemFile = file.name.toLowerCase().endsWith('.pem'); + if (isPemFile) { + try { + const pemContent = await file.text(); + const isEncrypted = pemContent.includes('ENCRYPTED'); - 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 { + if (isEncrypted) { showPasswordSection(); - updatePasswordLabel('Certificate Password'); - } + updatePasswordLabel('Private Key Password'); + } else { + state.certData = parseCombinedPem(pemContent); + updateCertInfo(); + showSignatureOptions(); - hideSignatureOptions(); - updateProcessButton(); + 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'); + const certDisplayArea = getElement('cert-display-area'); - if (!certDisplayArea || !state.certFile) return; + 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(); + }; - 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 }); + 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 certPasswordSection = getElement( + 'cert-password-section' + ); + if (certPasswordSection) { + certPasswordSection.classList.remove('hidden'); + } - const certPassword = getElement('cert-password'); - if (certPassword) { - certPassword.value = ''; - certPassword.focus(); - } + 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; - } + 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'); - } + 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'); - } + 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'); - } + 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'); - } + const certInfo = getElement('cert-info'); + if (certInfo) { + certInfo.classList.add('hidden'); + } } async function handlePasswordInput(): Promise { - const certPassword = getElement('cert-password'); - const password = certPassword?.value ?? ''; + const certPassword = getElement('cert-password'); + const password = certPassword?.value ?? ''; - if (!state.certFile || !password) { - return; + 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); } - try { - const isPemFile = state.certFile.name.toLowerCase().endsWith('.pem'); + updateCertInfo(); + showSignatureOptions(); + updateProcessButton(); - 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'; - } + 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; + if (!state.certData) return; - const certInfo = getElement('cert-info'); - const certSubject = getElement('cert-subject'); - const certIssuer = getElement('cert-issuer'); - const certValidity = getElement('cert-validity'); + const certInfo = getElement('cert-info'); + const certSubject = getElement('cert-subject'); + const certIssuer = getElement('cert-issuer'); + const certValidity = getElement('cert-validity'); - if (!certInfo) return; + if (!certInfo) return; - const info = getCertificateInfo(state.certData.certificate); + 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)}`; - } + 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'); + certInfo.classList.remove('hidden'); } function updateProcessButton(): void { - const processBtn = getElement('process-btn'); - if (!processBtn) return; + const processBtn = getElement('process-btn'); + if (!processBtn) return; - const canProcess = state.pdfBytes !== null && state.certData !== null; + const canProcess = state.pdfBytes !== null && state.certData !== null; - if (canProcess) { - processBtn.style.display = ''; - } else { - processBtn.style.display = 'none'; - } + 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; - } + 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 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; + const signatureInfo: SignatureInfo = {}; + if (reason) signatureInfo.reason = reason; + if (location) signatureInfo.location = location; + if (contactInfo) signatureInfo.contactInfo = contactInfo; - let visibleSignature: VisibleSignatureOptions | undefined; + 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 ?? '70', 10); + 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 ?? '70', + 10 + ); - const sigPageSelect = getElement('sig-page'); - let sigPage: number | string = 0; - let numPages = 1; - - try { - const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }).promise; - numPages = pdfDoc.numPages; - } catch (error) { - console.error('Error getting PDF page count:', error); - } - - if (sigPageSelect) { - if (sigPageSelect.value === 'last') { - sigPage = (numPages - 1).toString(); - } else if (sigPageSelect.value === 'all') { - if (numPages === 1) { - sigPage = '0'; - } else { - sigPage = `0-${numPages - 1}`; - } - } 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}`; - } - - let finalHeight = sigHeight; - if (sigText && !state.sigImageData) { - const lineCount = (sigText.match(/\n/g) || []).length + 1; - const lineHeightFactor = 1.4; - const padding = 16; - const calculatedHeight = Math.ceil(lineCount * sigTextSize * lineHeightFactor + padding); - finalHeight = Math.max(calculatedHeight, sigHeight); - } - - visibleSignature = { - enabled: true, - x: sigX, - y: sigY, - width: sigWidth, - height: finalHeight, - page: sigPage, - imageData: state.sigImageData ?? undefined, - imageType: state.sigImageType ?? undefined, - text: sigText, - textColor: sigTextColor, - textSize: sigTextSize, - }; - } - - showLoader('Applying digital signature...'); + const sigPageSelect = getElement('sig-page'); + let sigPage: number | string = 0; + let numPages = 1; 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(); }); + const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }) + .promise; + numPages = pdfDoc.numPages; } catch (error) { - hideLoader(); - console.error('Signing error:', error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - - // Check if this is a CORS/network error from certificate chain fetching - if (errorMessage.includes('Failed to fetch') || errorMessage.includes('CORS') || errorMessage.includes('NetworkError')) { - showAlert( - 'Signing Failed', - 'Failed to fetch certificate chain. This may be due to network issues or the certificate proxy being unavailable. Please check your internet connection and try again. If the issue persists, contact support.' - ); - } else { - showAlert('Signing Failed', `Failed to sign PDF: ${errorMessage}`); - } + console.error('Error getting PDF page count:', error); } + + if (sigPageSelect) { + if (sigPageSelect.value === 'last') { + sigPage = (numPages - 1).toString(); + } else if (sigPageSelect.value === 'all') { + if (numPages === 1) { + sigPage = '0'; + } else { + sigPage = `0-${numPages - 1}`; + } + } 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}`; + } + + let finalHeight = sigHeight; + if (sigText && !state.sigImageData) { + const lineCount = (sigText.match(/\n/g) || []).length + 1; + const lineHeightFactor = 1.4; + const padding = 16; + const calculatedHeight = Math.ceil( + lineCount * sigTextSize * lineHeightFactor + padding + ); + finalHeight = Math.max(calculatedHeight, sigHeight); + } + + visibleSignature = { + enabled: true, + x: sigX, + y: sigY, + width: sigWidth, + height: finalHeight, + 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'; + + // Check if this is a CORS/network error from certificate chain fetching + if ( + errorMessage.includes('Failed to fetch') || + errorMessage.includes('CORS') || + errorMessage.includes('NetworkError') + ) { + showAlert( + 'Signing Failed', + 'Failed to fetch certificate chain. This may be due to network issues or the certificate proxy being unavailable. Please check your internet connection and try again. If the issue persists, contact support.' + ); + } else { + showAlert('Signing Failed', `Failed to sign PDF: ${errorMessage}`); + } + } } if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); + document.addEventListener('DOMContentLoaded', initializePage); } else { - initializePage(); + initializePage(); } diff --git a/src/js/logic/extract-images-page.ts b/src/js/logic/extract-images-page.ts index 36d7f0b..aaab5d1 100644 --- a/src/js/logic/extract-images-page.ts +++ b/src/js/logic/extract-images-page.ts @@ -1,4 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { t } from '../i18n/i18n'; import { downloadFile, readFileAsArrayBuffer, @@ -66,7 +67,7 @@ document.addEventListener('DOMContentLoaded', () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-layers-page.ts b/src/js/logic/pdf-layers-page.ts index 22ea415..e1a3836 100644 --- a/src/js/logic/pdf-layers-page.ts +++ b/src/js/logic/pdf-layers-page.ts @@ -1,4 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { t } from '../i18n/i18n'; import { downloadFile, readFileAsArrayBuffer, @@ -60,7 +61,7 @@ document.addEventListener('DOMContentLoaded', () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(currentFile.size)} • Loading pages...`; + metaSpan.textContent = `${formatBytes(currentFile.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-to-bmp-page.ts b/src/js/logic/pdf-to-bmp-page.ts index b38ac4d..046eea6 100644 --- a/src/js/logic/pdf-to-bmp-page.ts +++ b/src/js/logic/pdf-to-bmp-page.ts @@ -45,7 +45,7 @@ const updateUI = () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-to-docx-page.ts b/src/js/logic/pdf-to-docx-page.ts index 3850fbc..101f748 100644 --- a/src/js/logic/pdf-to-docx-page.ts +++ b/src/js/logic/pdf-to-docx-page.ts @@ -1,4 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { t } from '../i18n/i18n'; import { downloadFile, readFileAsArrayBuffer, @@ -50,7 +51,7 @@ document.addEventListener('DOMContentLoaded', () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-to-greyscale-page.ts b/src/js/logic/pdf-to-greyscale-page.ts index 9a4f94a..c1e7546 100644 --- a/src/js/logic/pdf-to-greyscale-page.ts +++ b/src/js/logic/pdf-to-greyscale-page.ts @@ -45,7 +45,7 @@ const updateUI = () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-to-jpg-page.ts b/src/js/logic/pdf-to-jpg-page.ts index dedebb3..945d022 100644 --- a/src/js/logic/pdf-to-jpg-page.ts +++ b/src/js/logic/pdf-to-jpg-page.ts @@ -45,7 +45,7 @@ const updateUI = () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-to-markdown-page.ts b/src/js/logic/pdf-to-markdown-page.ts index f99294b..17143c0 100644 --- a/src/js/logic/pdf-to-markdown-page.ts +++ b/src/js/logic/pdf-to-markdown-page.ts @@ -1,4 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { t } from '../i18n/i18n'; import { downloadFile, readFileAsArrayBuffer, @@ -53,7 +54,7 @@ document.addEventListener('DOMContentLoaded', () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-to-pdfa-page.ts b/src/js/logic/pdf-to-pdfa-page.ts index 0de4073..07b3a7d 100644 --- a/src/js/logic/pdf-to-pdfa-page.ts +++ b/src/js/logic/pdf-to-pdfa-page.ts @@ -1,4 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { t } from '../i18n/i18n'; import { downloadFile, readFileAsArrayBuffer, @@ -53,7 +54,7 @@ document.addEventListener('DOMContentLoaded', () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-to-png-page.ts b/src/js/logic/pdf-to-png-page.ts index 84fe5e5..9b33813 100644 --- a/src/js/logic/pdf-to-png-page.ts +++ b/src/js/logic/pdf-to-png-page.ts @@ -45,7 +45,7 @@ const updateUI = () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-to-tiff-page.ts b/src/js/logic/pdf-to-tiff-page.ts index c463124..f247a21 100644 --- a/src/js/logic/pdf-to-tiff-page.ts +++ b/src/js/logic/pdf-to-tiff-page.ts @@ -46,7 +46,7 @@ const updateUI = () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/pdf-to-webp-page.ts b/src/js/logic/pdf-to-webp-page.ts index 57e4d5e..c991752 100644 --- a/src/js/logic/pdf-to-webp-page.ts +++ b/src/js/logic/pdf-to-webp-page.ts @@ -45,7 +45,7 @@ const updateUI = () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/prepare-pdf-for-ai-page.ts b/src/js/logic/prepare-pdf-for-ai-page.ts index 464f522..e65dd48 100644 --- a/src/js/logic/prepare-pdf-for-ai-page.ts +++ b/src/js/logic/prepare-pdf-for-ai-page.ts @@ -1,4 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { t } from '../i18n/i18n'; import { downloadFile, readFileAsArrayBuffer, @@ -50,7 +51,7 @@ document.addEventListener('DOMContentLoaded', () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/rasterize-pdf-page.ts b/src/js/logic/rasterize-pdf-page.ts index fc9960b..8949c7b 100644 --- a/src/js/logic/rasterize-pdf-page.ts +++ b/src/js/logic/rasterize-pdf-page.ts @@ -1,4 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { t } from '../i18n/i18n'; import { downloadFile, readFileAsArrayBuffer, @@ -50,7 +51,7 @@ document.addEventListener('DOMContentLoaded', () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/scanner-effect-page.ts b/src/js/logic/scanner-effect-page.ts index 2954549..bae63b6 100644 --- a/src/js/logic/scanner-effect-page.ts +++ b/src/js/logic/scanner-effect-page.ts @@ -130,7 +130,7 @@ const updateUI = () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); diff --git a/src/js/logic/sign-pdf-page.ts b/src/js/logic/sign-pdf-page.ts index 8f04273..f446f3f 100644 --- a/src/js/logic/sign-pdf-page.ts +++ b/src/js/logic/sign-pdf-page.ts @@ -1,312 +1,354 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js'; +import { + readFileAsArrayBuffer, + formatBytes, + downloadFile, + getPDFDocument, +} from '../utils/helpers.js'; import { PDFDocument } from 'pdf-lib'; +import { t } from '../i18n/i18n'; interface SignState { - file: File | null; - pdfDoc: any; - viewerIframe: HTMLIFrameElement | null; - viewerReady: boolean; - blobUrl: string | null; + file: File | null; + pdfDoc: any; + viewerIframe: HTMLIFrameElement | null; + viewerReady: boolean; + blobUrl: string | null; } const signState: SignState = { - file: null, - pdfDoc: null, - viewerIframe: null, - viewerReady: false, - blobUrl: null, + file: null, + pdfDoc: null, + viewerIframe: null, + viewerReady: false, + blobUrl: null, }; if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); + document.addEventListener('DOMContentLoaded', initializePage); } else { - initializePage(); + initializePage(); } function initializePage() { - createIcons({ icons }); + createIcons({ icons }); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const processBtn = document.getElementById('process-btn'); + 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 (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', applyAndSaveSignatures); - } - - document.getElementById('back-to-tools')?.addEventListener('click', () => { - cleanup(); - window.location.href = import.meta.env.BASE_URL; + 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', applyAndSaveSignatures); + } + + document.getElementById('back-to-tools')?.addEventListener('click', () => { + cleanup(); + 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]); - } + const input = e.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + handleFile(input.files[0]); + } } function handleFile(file: File) { - if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { - showAlert('Invalid File', 'Please select a PDF file.'); - return; - } + if ( + file.type !== 'application/pdf' && + !file.name.toLowerCase().endsWith('.pdf') + ) { + showAlert('Invalid File', 'Please select a PDF file.'); + return; + } - signState.file = file; - updateFileDisplay(); - setupSignTool(); + signState.file = file; + updateFileDisplay(); + setupSignTool(); } async function updateFileDisplay() { - const fileDisplayArea = document.getElementById('file-display-area'); + const fileDisplayArea = document.getElementById('file-display-area'); - if (!fileDisplayArea || !signState.file) return; + if (!fileDisplayArea || !signState.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 = signState.file.name; + + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(signState.file.size)} • ${t('common.loadingPageCount')}`; + + 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 = () => { + signState.file = null; + signState.pdfDoc = null; fileDisplayArea.innerHTML = ''; + document.getElementById('signature-editor')?.classList.add('hidden'); + }; - const fileDiv = document.createElement('div'); - fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); - 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 = signState.file.name; - - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(signState.file.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 = () => { - signState.file = null; - signState.pdfDoc = null; - fileDisplayArea.innerHTML = ''; - document.getElementById('signature-editor')?.classList.add('hidden'); - }; - - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); - - // Load page count - try { - const arrayBuffer = await readFileAsArrayBuffer(signState.file); - const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; - metaSpan.textContent = `${formatBytes(signState.file.size)} • ${pdfDoc.numPages} pages`; - } catch (error) { - console.error('Error loading PDF:', error); - } + // Load page count + try { + const arrayBuffer = await readFileAsArrayBuffer(signState.file); + const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; + metaSpan.textContent = `${formatBytes(signState.file.size)} • ${pdfDoc.numPages} pages`; + } catch (error) { + console.error('Error loading PDF:', error); + } } async function setupSignTool() { - const signatureEditor = document.getElementById('signature-editor'); - if (signatureEditor) { - signatureEditor.classList.remove('hidden'); - } + const signatureEditor = document.getElementById('signature-editor'); + if (signatureEditor) { + signatureEditor.classList.remove('hidden'); + } - showLoader('Loading PDF viewer...'); + showLoader('Loading PDF viewer...'); - const container = document.getElementById('canvas-container-sign'); - if (!container) { - console.error('Sign tool canvas container not found'); - hideLoader(); - return; - } + const container = document.getElementById('canvas-container-sign'); + if (!container) { + console.error('Sign tool canvas container not found'); + hideLoader(); + return; + } - if (!signState.file) { - console.error('No file loaded for signing'); - hideLoader(); - return; - } + if (!signState.file) { + console.error('No file loaded for signing'); + hideLoader(); + return; + } - 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; + 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 readFileAsArrayBuffer(signState.file); - const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); - signState.blobUrl = URL.createObjectURL(blob); + const pdfBytes = await readFileAsArrayBuffer(signState.file); + const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); + signState.blobUrl = URL.createObjectURL(blob); - try { - const existingPrefsRaw = localStorage.getItem('pdfjs.preferences'); - const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}; - delete (existingPrefs as any).annotationEditorMode; - const newPrefs = { - ...existingPrefs, - enableSignatureEditor: true, - enablePermissions: false, - }; - localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)); - } catch { } - - const viewerUrl = new URL(`${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`, window.location.origin); - const query = new URLSearchParams({ file: signState.blobUrl }); - iframe.src = `${viewerUrl.toString()}?${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 eventBus = app.eventBus; - eventBus?._on('annotationeditoruimanager', () => { - 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; - } - try { - const highlightBtn = doc.getElementById('editorHighlightButton') as HTMLButtonElement | null; - highlightBtn?.click(); - } catch { } - }); - } - } catch (e) { - console.error('Could not initialize PDF.js viewer for signing:', e); - } - - const saveBtn = document.getElementById('process-btn') as HTMLButtonElement | null; - if (saveBtn) { - saveBtn.style.display = ''; - } + try { + const existingPrefsRaw = localStorage.getItem('pdfjs.preferences'); + const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}; + delete (existingPrefs as any).annotationEditorMode; + const newPrefs = { + ...existingPrefs, + enableSignatureEditor: true, + enablePermissions: false, }; + localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)); + } catch {} + + const viewerUrl = new URL( + `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`, + window.location.origin + ); + const query = new URLSearchParams({ file: signState.blobUrl }); + iframe.src = `${viewerUrl.toString()}?${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 eventBus = app.eventBus; + eventBus?._on('annotationeditoruimanager', () => { + 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; + } + try { + const highlightBtn = doc.getElementById( + 'editorHighlightButton' + ) as HTMLButtonElement | null; + highlightBtn?.click(); + } catch {} + }); + } + } catch (e) { + console.error('Could not initialize PDF.js viewer for signing:', e); + } + + const saveBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement | null; + if (saveBtn) { + saveBtn.style.display = ''; + } + }; } async function applyAndSaveSignatures() { - if (!signState.viewerReady || !signState.viewerIframe) { - showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.'); - return; + if (!signState.viewerReady || !signState.viewerIframe) { + showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.'); + return; + } + + try { + const viewerWindow: any = signState.viewerIframe.contentWindow; + if (!viewerWindow || !viewerWindow.PDFViewerApplication) { + showAlert('Viewer not ready', 'The PDF viewer is still initializing.'); + return; } - try { - const viewerWindow: any = signState.viewerIframe.contentWindow; - if (!viewerWindow || !viewerWindow.PDFViewerApplication) { - showAlert('Viewer not ready', 'The PDF viewer is still initializing.'); - return; + const app = viewerWindow.PDFViewerApplication; + const flattenCheckbox = document.getElementById( + 'flatten-signature-toggle' + ) as HTMLInputElement | null; + const shouldFlatten = flattenCheckbox?.checked; + + if (shouldFlatten) { + showLoader('Flattening and saving PDF...'); + + const rawPdfBytes = await app.pdfDocument.saveDocument( + app.pdfDocument.annotationStorage + ); + const pdfBytes = new Uint8Array(rawPdfBytes); + const pdfDoc = await PDFDocument.load(pdfBytes); + pdfDoc.getForm().flatten(); + const flattenedPdfBytes = await pdfDoc.save(); + + const blob = new Blob([flattenedPdfBytes as BlobPart], { + type: 'application/pdf', + }); + downloadFile( + blob, + `signed_flattened_${signState.file?.name || 'document.pdf'}` + ); + + hideLoader(); + showAlert('Success', 'Signed PDF saved successfully!', 'success', () => { + resetState(); + }); + } else { + app.eventBus?.dispatch('download', { source: app }); + showAlert( + 'Success', + 'Signed PDF downloaded successfully!', + 'success', + () => { + resetState(); } - - const app = viewerWindow.PDFViewerApplication; - const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null; - const shouldFlatten = flattenCheckbox?.checked; - - if (shouldFlatten) { - showLoader('Flattening and saving PDF...'); - - const rawPdfBytes = await app.pdfDocument.saveDocument(app.pdfDocument.annotationStorage); - const pdfBytes = new Uint8Array(rawPdfBytes); - const pdfDoc = await PDFDocument.load(pdfBytes); - pdfDoc.getForm().flatten(); - const flattenedPdfBytes = await pdfDoc.save(); - - const blob = new Blob([flattenedPdfBytes as BlobPart], { type: 'application/pdf' }); - downloadFile(blob, `signed_flattened_${signState.file?.name || 'document.pdf'}`); - - hideLoader(); - showAlert('Success', 'Signed PDF saved successfully!', 'success', () => { - resetState(); - }); - } else { - app.eventBus?.dispatch('download', { source: app }); - showAlert('Success', 'Signed PDF downloaded successfully!', 'success', () => { - resetState(); - }); - } - } catch (error) { - console.error('Failed to export the signed PDF:', error); - hideLoader(); - showAlert('Export failed', 'Could not export the signed PDF. Please try again.'); + ); } + } catch (error) { + console.error('Failed to export the signed PDF:', error); + hideLoader(); + showAlert( + 'Export failed', + 'Could not export the signed PDF. Please try again.' + ); + } } function resetState() { - cleanup(); - signState.file = null; - signState.viewerIframe = null; - signState.viewerReady = false; + cleanup(); + signState.file = null; + signState.viewerIframe = null; + signState.viewerReady = false; - const signatureEditor = document.getElementById('signature-editor'); - if (signatureEditor) { - signatureEditor.classList.add('hidden'); - } + const signatureEditor = document.getElementById('signature-editor'); + if (signatureEditor) { + signatureEditor.classList.add('hidden'); + } - const container = document.getElementById('canvas-container-sign'); - if (container) { - container.textContent = ''; - } + const container = document.getElementById('canvas-container-sign'); + if (container) { + container.textContent = ''; + } - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) { - fileDisplayArea.innerHTML = ''; - } + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; + } - const processBtn = document.getElementById('process-btn') as HTMLButtonElement | null; - if (processBtn) { - processBtn.style.display = 'none'; - } + const processBtn = document.getElementById( + 'process-btn' + ) as HTMLButtonElement | null; + if (processBtn) { + processBtn.style.display = 'none'; + } - const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null; - if (flattenCheckbox) { - flattenCheckbox.checked = false; - } + const flattenCheckbox = document.getElementById( + 'flatten-signature-toggle' + ) as HTMLInputElement | null; + if (flattenCheckbox) { + flattenCheckbox.checked = false; + } } function cleanup() { - if (signState.blobUrl) { - URL.revokeObjectURL(signState.blobUrl); - signState.blobUrl = null; - } + if (signState.blobUrl) { + URL.revokeObjectURL(signState.blobUrl); + signState.blobUrl = null; + } } diff --git a/src/js/logic/split-pdf-page.ts b/src/js/logic/split-pdf-page.ts index 699c248..a130815 100644 --- a/src/js/logic/split-pdf-page.ts +++ b/src/js/logic/split-pdf-page.ts @@ -1,4 +1,5 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { t } from '../i18n/i18n'; import { createIcons, icons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; import { @@ -71,7 +72,7 @@ document.addEventListener('DOMContentLoaded', () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Placeholder + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Placeholder infoContainer.append(nameSpan, metaSpan); From 6d19c5f2e41b6c0998cfbd7856c8ed0448dcd0c1 Mon Sep 17 00:00:00 2001 From: Sebastian Espei Date: Tue, 24 Mar 2026 01:55:48 +0100 Subject: [PATCH 14/14] Revert formatting churn in signing pages --- src/js/logic/digital-sign-pdf-page.ts | 1167 ++++++++++++------------- src/js/logic/sign-pdf-page.ts | 537 ++++++------ 2 files changed, 789 insertions(+), 915 deletions(-) diff --git a/src/js/logic/digital-sign-pdf-page.ts b/src/js/logic/digital-sign-pdf-page.ts index 42611d0..1b2d607 100644 --- a/src/js/logic/digital-sign-pdf-page.ts +++ b/src/js/logic/digital-sign-pdf-page.ts @@ -1,775 +1,690 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { - readFileAsArrayBuffer, - formatBytes, - downloadFile, - getPDFDocument, -} from '../utils/helpers.js'; +import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js'; import { t } from '../i18n/i18n'; import { - signPdf, - parsePfxFile, - parseCombinedPem, - getCertificateInfo, + signPdf, + parsePfxFile, + parseCombinedPem, + getCertificateInfo, } from './digital-sign-pdf.js'; -import { - SignatureInfo, - VisibleSignatureOptions, - DigitalSignState, -} from '@/types'; +import { SignatureInfo, VisibleSignatureOptions, DigitalSignState } from '@/types'; const state: DigitalSignState = { - pdfFile: null, - pdfBytes: null, - certFile: null, - certData: null, - sigImageData: null, - sigImageType: null, + 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; + 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 fileDisplayArea = getElement('file-display-area'); + if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - const certDisplayArea = getElement('cert-display-area'); - if (certDisplayArea) certDisplayArea.innerHTML = ''; + const certDisplayArea = getElement('cert-display-area'); + if (certDisplayArea) certDisplayArea.innerHTML = ''; - const fileInput = getElement('file-input'); - if (fileInput) fileInput.value = ''; + const fileInput = getElement('file-input'); + if (fileInput) fileInput.value = ''; - const certInput = getElement('cert-input'); - if (certInput) certInput.value = ''; + const certInput = getElement('cert-input'); + if (certInput) certInput.value = ''; - const sigImageInput = getElement('sig-image-input'); - if (sigImageInput) sigImageInput.value = ''; + const sigImageInput = getElement('sig-image-input'); + if (sigImageInput) sigImageInput.value = ''; - const sigImagePreview = getElement('sig-image-preview'); - if (sigImagePreview) sigImagePreview.classList.add('hidden'); + const sigImagePreview = getElement('sig-image-preview'); + if (sigImagePreview) sigImagePreview.classList.add('hidden'); - const certSection = getElement('certificate-section'); - if (certSection) certSection.classList.add('hidden'); + const certSection = getElement('certificate-section'); + if (certSection) certSection.classList.add('hidden'); - hidePasswordSection(); - hideSignatureOptions(); - hideCertInfo(); - updateProcessButton(); + hidePasswordSection(); + hideSignatureOptions(); + hideCertInfo(); + updateProcessButton(); } function getElement(id: string): T | null { - return document.getElementById(id) as T | null; + return document.getElementById(id) as T | null; } function initializePage(): void { - createIcons({ icons }); + 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'); + 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 (fileInput) { + fileInput.addEventListener('change', handlePdfUpload); + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); - }); + 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('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]); - } - }); - } + 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 (certInput) { + certInput.addEventListener('change', handleCertUpload); + certInput.addEventListener('click', () => { + certInput.value = ''; + }); + } - if (certDropZone) { - certDropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - certDropZone.classList.add('bg-gray-700'); - }); + 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('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]); - } - }); - } + 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 (certPassword) { + certPassword.addEventListener('input', handlePasswordInput); + certPassword.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + handlePasswordInput(); + } + }); + } - if (processBtn) { - processBtn.addEventListener('click', processSignature); - } + if (processBtn) { + processBtn.addEventListener('click', processSignature); + } - if (backBtn) { - backBtn.addEventListener('click', () => { - window.location.href = import.meta.env.BASE_URL; - }); - } + 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'); + 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 (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 (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 (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 (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 (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'); - } - }); - } + 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]); - } + 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; - } + 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 - ); + state.pdfFile = file; + state.pdfBytes = new Uint8Array(await readFileAsArrayBuffer(file) as ArrayBuffer); - updatePdfDisplay(); - showCertificateSection(); + updatePdfDisplay(); + showCertificateSection(); } async function updatePdfDisplay(): Promise { - const fileDisplayArea = getElement('file-display-area'); + const fileDisplayArea = getElement('file-display-area'); - if (!fileDisplayArea || !state.pdfFile) return; + 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)} • ${t('common.loadingPageCount')}`; - - 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 }); + const fileDiv = document.createElement('div'); + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; - try { - if (state.pdfBytes) { - const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }) - .promise; - metaSpan.textContent = `${formatBytes(state.pdfFile.size)} • ${pdfDoc.numPages} pages`; + 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)} • ${t('common.loadingPageCount')}`; + + 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)}`; } - } 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'); - } + const certSection = getElement('certificate-section'); + if (certSection) { + certSection.classList.remove('hidden'); + } } function hideCertificateSection(): void { - const certSection = getElement('certificate-section'); - const signatureOptions = getElement('signature-options'); + const certSection = getElement('certificate-section'); + const signatureOptions = getElement('signature-options'); - if (certSection) { - certSection.classList.add('hidden'); - } - if (signatureOptions) { - signatureOptions.classList.add('hidden'); - } + if (certSection) { + certSection.classList.add('hidden'); + } + if (signatureOptions) { + signatureOptions.classList.add('hidden'); + } - state.certFile = null; - state.certData = null; + state.certFile = null; + state.certData = null; - const certDisplayArea = getElement('cert-display-area'); - if (certDisplayArea) { - certDisplayArea.innerHTML = ''; - } + const certDisplayArea = getElement('cert-display-area'); + if (certDisplayArea) { + certDisplayArea.innerHTML = ''; + } - const certInfo = getElement('cert-info'); - if (certInfo) { - certInfo.classList.add('hidden'); - } + const certInfo = getElement('cert-info'); + if (certInfo) { + certInfo.classList.add('hidden'); + } - const certPasswordSection = getElement( - 'cert-password-section' - ); - if (certPasswordSection) { - certPasswordSection.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]); - } + 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.' + const validExtensions = ['.pfx', '.p12', '.pem']; + const hasValidExtension = validExtensions.some(ext => + file.name.toLowerCase().endsWith(ext) ); - 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'; - } + if (!hasValidExtension) { + showAlert('Invalid Certificate', 'Please select a .pfx, .p12, or .pem certificate file.'); + return; } - } else { - showPasswordSection(); - updatePasswordLabel('Certificate Password'); - } - hideSignatureOptions(); - updateProcessButton(); + 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'); + const certDisplayArea = getElement('cert-display-area'); - if (!certDisplayArea || !state.certFile) return; + 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 }); + 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 certPasswordSection = getElement('cert-password-section'); + if (certPasswordSection) { + certPasswordSection.classList.remove('hidden'); + } - const certPassword = getElement('cert-password'); - if (certPassword) { - certPassword.value = ''; - certPassword.focus(); - } + 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; - } + 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'); - } + 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'); - } + 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'); - } + 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'); - } + const certInfo = getElement('cert-info'); + if (certInfo) { + certInfo.classList.add('hidden'); + } } async function handlePasswordInput(): Promise { - const certPassword = getElement('cert-password'); - const password = certPassword?.value ?? ''; + 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); + if (!state.certFile || !password) { + return; } - updateCertInfo(); - showSignatureOptions(); - updateProcessButton(); + try { + const isPemFile = state.certFile.name.toLowerCase().endsWith('.pem'); - 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(); + 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); + } - 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'; + 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; + if (!state.certData) return; - const certInfo = getElement('cert-info'); - const certSubject = getElement('cert-subject'); - const certIssuer = getElement('cert-issuer'); - const certValidity = getElement('cert-validity'); + const certInfo = getElement('cert-info'); + const certSubject = getElement('cert-subject'); + const certIssuer = getElement('cert-issuer'); + const certValidity = getElement('cert-validity'); - if (!certInfo) return; + if (!certInfo) return; - const info = getCertificateInfo(state.certData.certificate); + 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)}`; - } + 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'); + certInfo.classList.remove('hidden'); } function updateProcessButton(): void { - const processBtn = getElement('process-btn'); - if (!processBtn) return; + const processBtn = getElement('process-btn'); + if (!processBtn) return; - const canProcess = state.pdfBytes !== null && state.certData !== null; + const canProcess = state.pdfBytes !== null && state.certData !== null; - if (canProcess) { - processBtn.style.display = ''; - } else { - processBtn.style.display = 'none'; - } + 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; - } + 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 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; + const signatureInfo: SignatureInfo = {}; + if (reason) signatureInfo.reason = reason; + if (location) signatureInfo.location = location; + if (contactInfo) signatureInfo.contactInfo = contactInfo; - let visibleSignature: VisibleSignatureOptions | undefined; + 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 ?? '70', - 10 - ); + 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 ?? '70', 10); - const sigPageSelect = getElement('sig-page'); - let sigPage: number | string = 0; - let numPages = 1; + const sigPageSelect = getElement('sig-page'); + let sigPage: number | string = 0; + let numPages = 1; + + try { + const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }).promise; + numPages = pdfDoc.numPages; + } catch (error) { + console.error('Error getting PDF page count:', error); + } + + if (sigPageSelect) { + if (sigPageSelect.value === 'last') { + sigPage = (numPages - 1).toString(); + } else if (sigPageSelect.value === 'all') { + if (numPages === 1) { + sigPage = '0'; + } else { + sigPage = `0-${numPages - 1}`; + } + } 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}`; + } + + let finalHeight = sigHeight; + if (sigText && !state.sigImageData) { + const lineCount = (sigText.match(/\n/g) || []).length + 1; + const lineHeightFactor = 1.4; + const padding = 16; + const calculatedHeight = Math.ceil(lineCount * sigTextSize * lineHeightFactor + padding); + finalHeight = Math.max(calculatedHeight, sigHeight); + } + + visibleSignature = { + enabled: true, + x: sigX, + y: sigY, + width: sigWidth, + height: finalHeight, + page: sigPage, + imageData: state.sigImageData ?? undefined, + imageType: state.sigImageType ?? undefined, + text: sigText, + textColor: sigTextColor, + textSize: sigTextSize, + }; + } + + showLoader('Applying digital signature...'); try { - const pdfDoc = await getPDFDocument({ data: state.pdfBytes.slice() }) - .promise; - numPages = pdfDoc.numPages; + 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) { - console.error('Error getting PDF page count:', error); - } + hideLoader(); + console.error('Signing error:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - if (sigPageSelect) { - if (sigPageSelect.value === 'last') { - sigPage = (numPages - 1).toString(); - } else if (sigPageSelect.value === 'all') { - if (numPages === 1) { - sigPage = '0'; + // Check if this is a CORS/network error from certificate chain fetching + if (errorMessage.includes('Failed to fetch') || errorMessage.includes('CORS') || errorMessage.includes('NetworkError')) { + showAlert( + 'Signing Failed', + 'Failed to fetch certificate chain. This may be due to network issues or the certificate proxy being unavailable. Please check your internet connection and try again. If the issue persists, contact support.' + ); } else { - sigPage = `0-${numPages - 1}`; + showAlert('Signing Failed', `Failed to sign PDF: ${errorMessage}`); } - } 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}`; - } - - let finalHeight = sigHeight; - if (sigText && !state.sigImageData) { - const lineCount = (sigText.match(/\n/g) || []).length + 1; - const lineHeightFactor = 1.4; - const padding = 16; - const calculatedHeight = Math.ceil( - lineCount * sigTextSize * lineHeightFactor + padding - ); - finalHeight = Math.max(calculatedHeight, sigHeight); - } - - visibleSignature = { - enabled: true, - x: sigX, - y: sigY, - width: sigWidth, - height: finalHeight, - 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'; - - // Check if this is a CORS/network error from certificate chain fetching - if ( - errorMessage.includes('Failed to fetch') || - errorMessage.includes('CORS') || - errorMessage.includes('NetworkError') - ) { - showAlert( - 'Signing Failed', - 'Failed to fetch certificate chain. This may be due to network issues or the certificate proxy being unavailable. Please check your internet connection and try again. If the issue persists, contact support.' - ); - } else { - showAlert('Signing Failed', `Failed to sign PDF: ${errorMessage}`); - } - } } if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); + document.addEventListener('DOMContentLoaded', initializePage); } else { - initializePage(); + initializePage(); } diff --git a/src/js/logic/sign-pdf-page.ts b/src/js/logic/sign-pdf-page.ts index f446f3f..633e8fd 100644 --- a/src/js/logic/sign-pdf-page.ts +++ b/src/js/logic/sign-pdf-page.ts @@ -1,354 +1,313 @@ import { createIcons, icons } from 'lucide'; import { showAlert, showLoader, hideLoader } from '../ui.js'; -import { - readFileAsArrayBuffer, - formatBytes, - downloadFile, - getPDFDocument, -} from '../utils/helpers.js'; +import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js'; import { PDFDocument } from 'pdf-lib'; import { t } from '../i18n/i18n'; interface SignState { - file: File | null; - pdfDoc: any; - viewerIframe: HTMLIFrameElement | null; - viewerReady: boolean; - blobUrl: string | null; + file: File | null; + pdfDoc: any; + viewerIframe: HTMLIFrameElement | null; + viewerReady: boolean; + blobUrl: string | null; } const signState: SignState = { - file: null, - pdfDoc: null, - viewerIframe: null, - viewerReady: false, - blobUrl: null, + file: null, + pdfDoc: null, + viewerIframe: null, + viewerReady: false, + blobUrl: null, }; if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); + document.addEventListener('DOMContentLoaded', initializePage); } else { - initializePage(); + initializePage(); } function initializePage() { - createIcons({ icons }); + createIcons({ icons }); - const fileInput = document.getElementById('file-input') as HTMLInputElement; - const dropZone = document.getElementById('drop-zone'); - const processBtn = document.getElementById('process-btn'); + 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 (fileInput) { + fileInput.addEventListener('change', handleFileUpload); + } - if (dropZone) { - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.classList.add('bg-gray-700'); + 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', applyAndSaveSignatures); + } + + document.getElementById('back-to-tools')?.addEventListener('click', () => { + cleanup(); + window.location.href = import.meta.env.BASE_URL; }); - - 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', applyAndSaveSignatures); - } - - document.getElementById('back-to-tools')?.addEventListener('click', () => { - cleanup(); - 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]); - } + const input = e.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + handleFile(input.files[0]); + } } function handleFile(file: File) { - if ( - file.type !== 'application/pdf' && - !file.name.toLowerCase().endsWith('.pdf') - ) { - showAlert('Invalid File', 'Please select a PDF file.'); - return; - } + if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { + showAlert('Invalid File', 'Please select a PDF file.'); + return; + } - signState.file = file; - updateFileDisplay(); - setupSignTool(); + signState.file = file; + updateFileDisplay(); + setupSignTool(); } async function updateFileDisplay() { - const fileDisplayArea = document.getElementById('file-display-area'); + const fileDisplayArea = document.getElementById('file-display-area'); - if (!fileDisplayArea || !signState.file) return; + if (!fileDisplayArea || !signState.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 = signState.file.name; - - const metaSpan = document.createElement('div'); - metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(signState.file.size)} • ${t('common.loadingPageCount')}`; - - 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 = () => { - signState.file = null; - signState.pdfDoc = null; fileDisplayArea.innerHTML = ''; - document.getElementById('signature-editor')?.classList.add('hidden'); - }; - fileDiv.append(infoContainer, removeBtn); - fileDisplayArea.appendChild(fileDiv); - createIcons({ icons }); + const fileDiv = document.createElement('div'); + fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg'; - // Load page count - try { - const arrayBuffer = await readFileAsArrayBuffer(signState.file); - const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; - metaSpan.textContent = `${formatBytes(signState.file.size)} • ${pdfDoc.numPages} pages`; - } catch (error) { - console.error('Error loading PDF:', error); - } + 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 = signState.file.name; + + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(signState.file.size)} • ${t('common.loadingPageCount')}`; + + 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 = () => { + signState.file = null; + signState.pdfDoc = null; + fileDisplayArea.innerHTML = ''; + document.getElementById('signature-editor')?.classList.add('hidden'); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + createIcons({ icons }); + + // Load page count + try { + const arrayBuffer = await readFileAsArrayBuffer(signState.file); + const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise; + metaSpan.textContent = `${formatBytes(signState.file.size)} • ${pdfDoc.numPages} pages`; + } catch (error) { + console.error('Error loading PDF:', error); + } } async function setupSignTool() { - const signatureEditor = document.getElementById('signature-editor'); - if (signatureEditor) { - signatureEditor.classList.remove('hidden'); - } + const signatureEditor = document.getElementById('signature-editor'); + if (signatureEditor) { + signatureEditor.classList.remove('hidden'); + } - showLoader('Loading PDF viewer...'); + showLoader('Loading PDF viewer...'); - const container = document.getElementById('canvas-container-sign'); - if (!container) { - console.error('Sign tool canvas container not found'); - hideLoader(); - return; - } + const container = document.getElementById('canvas-container-sign'); + if (!container) { + console.error('Sign tool canvas container not found'); + hideLoader(); + return; + } - if (!signState.file) { - console.error('No file loaded for signing'); - hideLoader(); - return; - } + if (!signState.file) { + console.error('No file loaded for signing'); + hideLoader(); + return; + } - 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; + 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 readFileAsArrayBuffer(signState.file); - const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); - signState.blobUrl = URL.createObjectURL(blob); + const pdfBytes = await readFileAsArrayBuffer(signState.file); + const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); + signState.blobUrl = URL.createObjectURL(blob); - try { - const existingPrefsRaw = localStorage.getItem('pdfjs.preferences'); - const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}; - delete (existingPrefs as any).annotationEditorMode; - const newPrefs = { - ...existingPrefs, - enableSignatureEditor: true, - enablePermissions: false, - }; - localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)); - } catch {} - - const viewerUrl = new URL( - `${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`, - window.location.origin - ); - const query = new URLSearchParams({ file: signState.blobUrl }); - iframe.src = `${viewerUrl.toString()}?${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 eventBus = app.eventBus; - eventBus?._on('annotationeditoruimanager', () => { - 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; - } - try { - const highlightBtn = doc.getElementById( - 'editorHighlightButton' - ) as HTMLButtonElement | null; - highlightBtn?.click(); - } catch {} - }); - } - } catch (e) { - console.error('Could not initialize PDF.js viewer for signing:', e); - } + const existingPrefsRaw = localStorage.getItem('pdfjs.preferences'); + const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}; + delete (existingPrefs as any).annotationEditorMode; + const newPrefs = { + ...existingPrefs, + enableSignatureEditor: true, + enablePermissions: false, + }; + localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs)); + } catch { } - const saveBtn = document.getElementById( - 'process-btn' - ) as HTMLButtonElement | null; - if (saveBtn) { - saveBtn.style.display = ''; - } - }; + const viewerUrl = new URL(`${import.meta.env.BASE_URL}pdfjs-viewer/viewer.html`, window.location.origin); + const query = new URLSearchParams({ file: signState.blobUrl }); + iframe.src = `${viewerUrl.toString()}?${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 eventBus = app.eventBus; + eventBus?._on('annotationeditoruimanager', () => { + 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; + } + try { + const highlightBtn = doc.getElementById('editorHighlightButton') as HTMLButtonElement | null; + highlightBtn?.click(); + } catch { } + }); + } + } catch (e) { + console.error('Could not initialize PDF.js viewer for signing:', e); + } + + const saveBtn = document.getElementById('process-btn') as HTMLButtonElement | null; + if (saveBtn) { + saveBtn.style.display = ''; + } + }; } async function applyAndSaveSignatures() { - if (!signState.viewerReady || !signState.viewerIframe) { - showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.'); - return; - } - - try { - const viewerWindow: any = signState.viewerIframe.contentWindow; - if (!viewerWindow || !viewerWindow.PDFViewerApplication) { - showAlert('Viewer not ready', 'The PDF viewer is still initializing.'); - return; + if (!signState.viewerReady || !signState.viewerIframe) { + showAlert('Viewer not ready', 'Please wait for the PDF viewer to load.'); + return; } - const app = viewerWindow.PDFViewerApplication; - const flattenCheckbox = document.getElementById( - 'flatten-signature-toggle' - ) as HTMLInputElement | null; - const shouldFlatten = flattenCheckbox?.checked; - - if (shouldFlatten) { - showLoader('Flattening and saving PDF...'); - - const rawPdfBytes = await app.pdfDocument.saveDocument( - app.pdfDocument.annotationStorage - ); - const pdfBytes = new Uint8Array(rawPdfBytes); - const pdfDoc = await PDFDocument.load(pdfBytes); - pdfDoc.getForm().flatten(); - const flattenedPdfBytes = await pdfDoc.save(); - - const blob = new Blob([flattenedPdfBytes as BlobPart], { - type: 'application/pdf', - }); - downloadFile( - blob, - `signed_flattened_${signState.file?.name || 'document.pdf'}` - ); - - hideLoader(); - showAlert('Success', 'Signed PDF saved successfully!', 'success', () => { - resetState(); - }); - } else { - app.eventBus?.dispatch('download', { source: app }); - showAlert( - 'Success', - 'Signed PDF downloaded successfully!', - 'success', - () => { - resetState(); + try { + const viewerWindow: any = signState.viewerIframe.contentWindow; + if (!viewerWindow || !viewerWindow.PDFViewerApplication) { + showAlert('Viewer not ready', 'The PDF viewer is still initializing.'); + return; } - ); + + const app = viewerWindow.PDFViewerApplication; + const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null; + const shouldFlatten = flattenCheckbox?.checked; + + if (shouldFlatten) { + showLoader('Flattening and saving PDF...'); + + const rawPdfBytes = await app.pdfDocument.saveDocument(app.pdfDocument.annotationStorage); + const pdfBytes = new Uint8Array(rawPdfBytes); + const pdfDoc = await PDFDocument.load(pdfBytes); + pdfDoc.getForm().flatten(); + const flattenedPdfBytes = await pdfDoc.save(); + + const blob = new Blob([flattenedPdfBytes as BlobPart], { type: 'application/pdf' }); + downloadFile(blob, `signed_flattened_${signState.file?.name || 'document.pdf'}`); + + hideLoader(); + showAlert('Success', 'Signed PDF saved successfully!', 'success', () => { + resetState(); + }); + } else { + app.eventBus?.dispatch('download', { source: app }); + showAlert('Success', 'Signed PDF downloaded successfully!', 'success', () => { + resetState(); + }); + } + } catch (error) { + console.error('Failed to export the signed PDF:', error); + hideLoader(); + showAlert('Export failed', 'Could not export the signed PDF. Please try again.'); } - } catch (error) { - console.error('Failed to export the signed PDF:', error); - hideLoader(); - showAlert( - 'Export failed', - 'Could not export the signed PDF. Please try again.' - ); - } } function resetState() { - cleanup(); - signState.file = null; - signState.viewerIframe = null; - signState.viewerReady = false; + cleanup(); + signState.file = null; + signState.viewerIframe = null; + signState.viewerReady = false; - const signatureEditor = document.getElementById('signature-editor'); - if (signatureEditor) { - signatureEditor.classList.add('hidden'); - } + const signatureEditor = document.getElementById('signature-editor'); + if (signatureEditor) { + signatureEditor.classList.add('hidden'); + } - const container = document.getElementById('canvas-container-sign'); - if (container) { - container.textContent = ''; - } + const container = document.getElementById('canvas-container-sign'); + if (container) { + container.textContent = ''; + } - const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) { - fileDisplayArea.innerHTML = ''; - } + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; + } - const processBtn = document.getElementById( - 'process-btn' - ) as HTMLButtonElement | null; - if (processBtn) { - processBtn.style.display = 'none'; - } + const processBtn = document.getElementById('process-btn') as HTMLButtonElement | null; + if (processBtn) { + processBtn.style.display = 'none'; + } - const flattenCheckbox = document.getElementById( - 'flatten-signature-toggle' - ) as HTMLInputElement | null; - if (flattenCheckbox) { - flattenCheckbox.checked = false; - } + const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null; + if (flattenCheckbox) { + flattenCheckbox.checked = false; + } } function cleanup() { - if (signState.blobUrl) { - URL.revokeObjectURL(signState.blobUrl); - signState.blobUrl = null; - } + if (signState.blobUrl) { + URL.revokeObjectURL(signState.blobUrl); + signState.blobUrl = null; + } }