diff --git a/public/locales/ar/common.json b/public/locales/ar/common.json
index 8732c29..0b2872a 100644
--- a/public/locales/ar/common.json
+++ b/public/locales/ar/common.json
@@ -350,9 +350,12 @@
"download": "تنزيل:",
"downloadSelected": "تنزيل المحدد",
"exportPdf": "تصدير PDF",
- "uploadPdfFiles": "اختر ملفات PDF",
- "dragAndDrop": "اسحب وأفلت ملفات PDF هنا، أو انقر للاختيار",
+ "uploadPdfFiles": "اختر ملفات PDF أو صور",
+ "dragAndDrop": "اسحب وأفلت ملفات PDF أو الصور هنا، أو انقر للاختيار",
"selectFiles": "اختر الملفات",
+ "dropToAdd": "أفلت ملفات PDF أو الصور للإضافة",
+ "convertingImages": "جارٍ تحويل الصور إلى PDF...",
+ "failedToConvertImages": "فشل تحويل الصور إلى PDF.",
"renderingPages": "جارٍ عرض الصفحات...",
"actions": {
"duplicatePage": "تكرار هذه الصفحة",
diff --git a/public/locales/be/common.json b/public/locales/be/common.json
index e622435..4df0b65 100644
--- a/public/locales/be/common.json
+++ b/public/locales/be/common.json
@@ -350,9 +350,12 @@
"download": "Спампаваць:",
"downloadSelected": "Спампаваць выбранае",
"exportPdf": "Экспартаваць PDF",
- "uploadPdfFiles": "Выбраць PDF файлы",
- "dragAndDrop": "Перацягніце PDF файлы сюды або націсніце, каб выбраць",
+ "uploadPdfFiles": "Выбраць PDF або выявы",
+ "dragAndDrop": "Перацягніце PDF або выявы сюды або націсніце, каб выбраць",
"selectFiles": "Выбраць файлы",
+ "dropToAdd": "Адпусціце PDF або выявы для дабаўлення",
+ "convertingImages": "Канвертацыя выяў у PDF...",
+ "failedToConvertImages": "Не ўдалося канвертаваць выявы ў PDF.",
"renderingPages": "Апрацоўка старонак...",
"actions": {
"duplicatePage": "Дубляваць гэту старонку",
diff --git a/public/locales/da/common.json b/public/locales/da/common.json
index ade2e22..fd9cf46 100644
--- a/public/locales/da/common.json
+++ b/public/locales/da/common.json
@@ -350,9 +350,12 @@
"download": "Download:",
"downloadSelected": "Download valgte",
"exportPdf": "Eksportér PDF",
- "uploadPdfFiles": "Vælg PDF-filer",
- "dragAndDrop": "Træk og slip PDF-filer her, eller klik for at vælge",
+ "uploadPdfFiles": "Vælg PDF- eller billedfiler",
+ "dragAndDrop": "Træk og slip PDF- eller billedfiler her, eller klik for at vælge",
"selectFiles": "Vælg filer",
+ "dropToAdd": "Slip PDF- eller billedfiler for at tilføje",
+ "convertingImages": "Konverterer billeder til PDF...",
+ "failedToConvertImages": "Kunne ikke konvertere billeder til PDF.",
"renderingPages": "Renderer sider...",
"actions": {
"duplicatePage": "Duplikér denne side",
diff --git a/public/locales/de/common.json b/public/locales/de/common.json
index 836bd79..446893a 100644
--- a/public/locales/de/common.json
+++ b/public/locales/de/common.json
@@ -350,9 +350,12 @@
"download": "Download:",
"downloadSelected": "Auswahl herunterladen",
"exportPdf": "PDF exportieren",
- "uploadPdfFiles": "PDF-Dateien auswählen",
- "dragAndDrop": "PDF-Dateien hierhin ziehen oder klicken zum Auswählen",
+ "uploadPdfFiles": "PDF- oder Bilddateien auswählen",
+ "dragAndDrop": "PDF- oder Bilddateien hierhin ziehen oder klicken zum Auswählen",
"selectFiles": "Dateien auswählen",
+ "dropToAdd": "PDF- oder Bilddateien zum Hinzufügen ablegen",
+ "convertingImages": "Bilder werden in PDF konvertiert...",
+ "failedToConvertImages": "Bilder konnten nicht in PDF konvertiert werden.",
"renderingPages": "Seiten werden gerendert...",
"actions": {
"duplicatePage": "Diese Seite duplizieren",
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index da9d6f1..17af04d 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -350,9 +350,12 @@
"download": "Download:",
"downloadSelected": "Download Selected",
"exportPdf": "Export PDF",
- "uploadPdfFiles": "Select PDF Files",
- "dragAndDrop": "Drag and drop PDF files here, or click to select",
+ "uploadPdfFiles": "Select PDF or image files",
+ "dragAndDrop": "Drag and drop PDF or image files here, or click to select",
"selectFiles": "Select Files",
+ "dropToAdd": "Drop PDF or image files to add",
+ "convertingImages": "Converting images to PDF...",
+ "failedToConvertImages": "Failed to convert images to PDF.",
"renderingPages": "Rendering pages...",
"actions": {
"duplicatePage": "Duplicate this page",
diff --git a/public/locales/es/common.json b/public/locales/es/common.json
index cff8cbd..3f0a6a5 100644
--- a/public/locales/es/common.json
+++ b/public/locales/es/common.json
@@ -350,9 +350,12 @@
"download": "Descargar:",
"downloadSelected": "Descargar Seleccionados",
"exportPdf": "Exportar PDF",
- "uploadPdfFiles": "Seleccionar Archivos PDF",
- "dragAndDrop": "Arrastra y suelta archivos PDF aquí, o haz clic para seleccionar",
+ "uploadPdfFiles": "Seleccionar archivos PDF o imágenes",
+ "dragAndDrop": "Arrastra y suelta archivos PDF o imágenes aquí, o haz clic para seleccionar",
"selectFiles": "Seleccionar Archivos",
+ "dropToAdd": "Suelta archivos PDF o imágenes para añadir",
+ "convertingImages": "Convirtiendo imágenes a PDF...",
+ "failedToConvertImages": "No se pudieron convertir las imágenes a PDF.",
"renderingPages": "Renderizando páginas...",
"actions": {
"duplicatePage": "Duplicar esta página",
diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json
index 996c4fd..57dcb80 100644
--- a/public/locales/fr/common.json
+++ b/public/locales/fr/common.json
@@ -350,9 +350,12 @@
"download": "Téléchargement :",
"downloadSelected": "Télécharger la sélection",
"exportPdf": "Exporter en PDF",
- "uploadPdfFiles": "Sélectionner des fichiers PDF",
- "dragAndDrop": "Glissez-déposez vos fichiers PDF ici ou cliquez pour sélectionner",
+ "uploadPdfFiles": "Sélectionner des fichiers PDF ou des images",
+ "dragAndDrop": "Glissez-déposez vos fichiers PDF ou images ici, ou cliquez pour sélectionner",
"selectFiles": "Sélectionner des fichiers",
+ "dropToAdd": "Déposez des fichiers PDF ou images à ajouter",
+ "convertingImages": "Conversion des images en PDF...",
+ "failedToConvertImages": "Échec de la conversion des images en PDF.",
"renderingPages": "Rendu des pages...",
"actions": {
"duplicatePage": "Dupliquer cette page",
diff --git a/public/locales/id/common.json b/public/locales/id/common.json
index c51669b..bd52853 100644
--- a/public/locales/id/common.json
+++ b/public/locales/id/common.json
@@ -350,9 +350,12 @@
"download": "Unduh:",
"downloadSelected": "Unduh yang Dipilih",
"exportPdf": "Ekspor PDF",
- "uploadPdfFiles": "Pilih File PDF",
- "dragAndDrop": "Seret dan jatuhkan file PDF di sini, atau klik untuk memilih",
+ "uploadPdfFiles": "Pilih File PDF atau Gambar",
+ "dragAndDrop": "Seret dan jatuhkan file PDF atau gambar di sini, atau klik untuk memilih",
"selectFiles": "Pilih File",
+ "dropToAdd": "Jatuhkan file PDF atau gambar untuk menambahkan",
+ "convertingImages": "Mengonversi gambar ke PDF...",
+ "failedToConvertImages": "Gagal mengonversi gambar ke PDF.",
"renderingPages": "Merender halaman...",
"actions": {
"duplicatePage": "Duplikat halaman ini",
diff --git a/public/locales/it/common.json b/public/locales/it/common.json
index e3b6a42..dfe5b60 100644
--- a/public/locales/it/common.json
+++ b/public/locales/it/common.json
@@ -350,9 +350,12 @@
"download": "Scarica:",
"downloadSelected": "Scarica selezionati",
"exportPdf": "Esporta PDF",
- "uploadPdfFiles": "Seleziona file PDF",
- "dragAndDrop": "Trascina e rilascia i file PDF qui, oppure clicca per selezionare",
+ "uploadPdfFiles": "Seleziona file PDF o immagini",
+ "dragAndDrop": "Trascina e rilascia file PDF o immagini qui, oppure clicca per selezionare",
"selectFiles": "Seleziona file",
+ "dropToAdd": "Rilascia file PDF o immagini per aggiungere",
+ "convertingImages": "Conversione delle immagini in PDF...",
+ "failedToConvertImages": "Impossibile convertire le immagini in PDF.",
"renderingPages": "Generazione anteprime pagine...",
"actions": {
"duplicatePage": "Duplica questa pagina",
diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json
index 6832db6..ed3583b 100644
--- a/public/locales/ja/common.json
+++ b/public/locales/ja/common.json
@@ -350,9 +350,12 @@
"download": "ダウンロード:",
"downloadSelected": "選択したものをダウンロード",
"exportPdf": "PDFをエクスポート",
- "uploadPdfFiles": "PDFファイルを選択",
- "dragAndDrop": "ここにPDFファイルをドラッグ&ドロップするかクリックして選択します",
+ "uploadPdfFiles": "PDFまたは画像ファイルを選択",
+ "dragAndDrop": "ここにPDFまたは画像ファイルをドラッグ&ドロップするかクリックして選択します",
"selectFiles": "ファイルの選択",
+ "dropToAdd": "PDFまたは画像ファイルをドロップして追加",
+ "convertingImages": "画像をPDFに変換中...",
+ "failedToConvertImages": "画像をPDFに変換できませんでした。",
"renderingPages": "ページをレンダリング中...",
"actions": {
"duplicatePage": "このページを複製",
diff --git a/public/locales/ko/common.json b/public/locales/ko/common.json
index 24724f9..a0652a0 100644
--- a/public/locales/ko/common.json
+++ b/public/locales/ko/common.json
@@ -350,9 +350,12 @@
"download": "다운로드:",
"downloadSelected": "선택 항목 다운로드",
"exportPdf": "PDF 내보내기",
- "uploadPdfFiles": "PDF 파일 선택",
- "dragAndDrop": "PDF 파일을 여기에 끌어다 놓거나 클릭하여 선택하세요",
+ "uploadPdfFiles": "PDF 또는 이미지 파일 선택",
+ "dragAndDrop": "PDF 또는 이미지 파일을 여기에 끌어다 놓거나 클릭하여 선택하세요",
"selectFiles": "파일 선택",
+ "dropToAdd": "PDF 또는 이미지 파일을 놓아 추가",
+ "convertingImages": "이미지를 PDF로 변환 중...",
+ "failedToConvertImages": "이미지를 PDF로 변환하지 못했습니다.",
"renderingPages": "페이지 렌더링 중...",
"actions": {
"duplicatePage": "이 페이지 복제",
diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json
index 4661102..0cf2153 100644
--- a/public/locales/nl/common.json
+++ b/public/locales/nl/common.json
@@ -350,9 +350,12 @@
"download": "Laden:",
"downloadSelected": "Selectie laden",
"exportPdf": "PDF exporteren",
- "uploadPdfFiles": "PDF-bestanden selecteren",
- "dragAndDrop": "Klik om een bestand te selecteren, of sleep er een hierheen",
+ "uploadPdfFiles": "PDF- of afbeeldingsbestanden selecteren",
+ "dragAndDrop": "Klik om een bestand te selecteren, of sleep een PDF of afbeelding hierheen",
"selectFiles": "Bestanden selecteren",
+ "dropToAdd": "Sleep PDF- of afbeeldingsbestanden om toe te voegen",
+ "convertingImages": "Afbeeldingen omzetten naar PDF...",
+ "failedToConvertImages": "Afbeeldingen omzetten naar PDF mislukt.",
"renderingPages": "Pagina's verwerken…",
"actions": {
"duplicatePage": "Deze pagina dupliceren",
diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json
index b469bc1..eee7a88 100644
--- a/public/locales/pt/common.json
+++ b/public/locales/pt/common.json
@@ -350,9 +350,12 @@
"download": "Baixar:",
"downloadSelected": "Baixar Selecionadas",
"exportPdf": "Exportar PDF",
- "uploadPdfFiles": "Selecionar Arquivos PDF",
- "dragAndDrop": "Arraste arquivos PDF aqui ou clique para selecionar",
+ "uploadPdfFiles": "Selecionar Arquivos PDF ou Imagens",
+ "dragAndDrop": "Arraste arquivos PDF ou imagens aqui ou clique para selecionar",
"selectFiles": "Selecionar Arquivos",
+ "dropToAdd": "Solte arquivos PDF ou imagens para adicionar",
+ "convertingImages": "Convertendo imagens em PDF...",
+ "failedToConvertImages": "Falha ao converter imagens em PDF.",
"renderingPages": "Renderizando páginas...",
"actions": {
"duplicatePage": "Duplicar esta página",
diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json
index beb23b4..fd32f7b 100644
--- a/public/locales/ru/common.json
+++ b/public/locales/ru/common.json
@@ -350,9 +350,12 @@
"download": "Скачать:",
"downloadSelected": "Скачать выбранное",
"exportPdf": "Экспортировать PDF",
- "uploadPdfFiles": "Выбрать PDF-файлы",
- "dragAndDrop": "Перетащите PDF-файлы сюда или нажмите, чтобы выбрать",
+ "uploadPdfFiles": "Выбрать PDF-файлы или изображения",
+ "dragAndDrop": "Перетащите PDF-файлы или изображения сюда или нажмите, чтобы выбрать",
"selectFiles": "Выбрать файлы",
+ "dropToAdd": "Отпустите PDF-файлы или изображения, чтобы добавить",
+ "convertingImages": "Преобразование изображений в PDF...",
+ "failedToConvertImages": "Не удалось преобразовать изображения в PDF.",
"renderingPages": "Отрисовка страниц...",
"actions": {
"duplicatePage": "Дублировать эту страницу",
diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json
index e184c79..9a960b8 100644
--- a/public/locales/sv/common.json
+++ b/public/locales/sv/common.json
@@ -350,9 +350,12 @@
"download": "Ladda ner:",
"downloadSelected": "Ladda ner markerade",
"exportPdf": "Exportera PDF",
- "uploadPdfFiles": "Välj PDF-filer",
- "dragAndDrop": "Dra och släpp PDF-filer här, eller klicka för att välja",
+ "uploadPdfFiles": "Välj PDF- eller bildfiler",
+ "dragAndDrop": "Dra och släpp PDF- eller bildfiler här, eller klicka för att välja",
"selectFiles": "Välj filer",
+ "dropToAdd": "Släpp PDF- eller bildfiler för att lägga till",
+ "convertingImages": "Konverterar bilder till PDF...",
+ "failedToConvertImages": "Kunde inte konvertera bilder till PDF.",
"renderingPages": "Renderar sidor...",
"actions": {
"duplicatePage": "Duplicera denna sida",
diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json
index e795284..9c9b4ff 100644
--- a/public/locales/tr/common.json
+++ b/public/locales/tr/common.json
@@ -350,9 +350,12 @@
"download": "İndir:",
"downloadSelected": "Seçilenleri İndir",
"exportPdf": "PDF Olarak Dışa Aktar",
- "uploadPdfFiles": "PDF Dosyalarını Seçin",
- "dragAndDrop": "PDF dosyalarını buraya sürükleyip bırakın veya seçmek için tıklayın",
+ "uploadPdfFiles": "PDF veya Görsel Dosyalarını Seçin",
+ "dragAndDrop": "PDF veya görsel dosyalarını buraya sürükleyip bırakın veya seçmek için tıklayın",
"selectFiles": "Dosya Seç",
+ "dropToAdd": "Eklemek için PDF veya görsel dosyalarını bırakın",
+ "convertingImages": "Görseller PDF'e dönüştürülüyor...",
+ "failedToConvertImages": "Görseller PDF'e dönüştürülemedi.",
"renderingPages": "Sayfalar oluşturuluyor...",
"actions": {
"duplicatePage": "Bu sayfayı çoğalt",
diff --git a/public/locales/uk/common.json b/public/locales/uk/common.json
index 173685d..12d3179 100644
--- a/public/locales/uk/common.json
+++ b/public/locales/uk/common.json
@@ -350,9 +350,12 @@
"download": "Завантажити:",
"downloadSelected": "Завантажити вибране",
"exportPdf": "Експортувати PDF",
- "uploadPdfFiles": "Виберіть PDF-файли",
- "dragAndDrop": "Перетягніть PDF або натисніть для вибору",
+ "uploadPdfFiles": "Виберіть PDF-файли або зображення",
+ "dragAndDrop": "Перетягніть PDF або зображення сюди, або натисніть для вибору",
"selectFiles": "Вибрати файли",
+ "dropToAdd": "Відпустіть PDF або зображення для додавання",
+ "convertingImages": "Перетворення зображень на PDF...",
+ "failedToConvertImages": "Не вдалося перетворити зображення на PDF.",
"renderingPages": "Рендеринг сторінок...",
"actions": {
"duplicatePage": "Дублювати сторінку",
diff --git a/public/locales/vi/common.json b/public/locales/vi/common.json
index 822439a..04ddc0d 100644
--- a/public/locales/vi/common.json
+++ b/public/locales/vi/common.json
@@ -350,9 +350,12 @@
"download": "Tải xuống:",
"downloadSelected": "Tải xuống đã chọn",
"exportPdf": "Xuất PDF",
- "uploadPdfFiles": "Chọn tệp PDF",
- "dragAndDrop": "Kéo và thả tệp PDF vào đây, hoặc nhấp để chọn",
+ "uploadPdfFiles": "Chọn tệp PDF hoặc hình ảnh",
+ "dragAndDrop": "Kéo và thả tệp PDF hoặc hình ảnh vào đây, hoặc nhấp để chọn",
"selectFiles": "Chọn tệp",
+ "dropToAdd": "Thả tệp PDF hoặc hình ảnh để thêm",
+ "convertingImages": "Đang chuyển đổi hình ảnh sang PDF...",
+ "failedToConvertImages": "Không thể chuyển đổi hình ảnh sang PDF.",
"renderingPages": "Đang kết xuất trang...",
"actions": {
"duplicatePage": "Nhân bản trang này",
diff --git a/public/locales/zh-TW/common.json b/public/locales/zh-TW/common.json
index a564a50..4dbb13e 100644
--- a/public/locales/zh-TW/common.json
+++ b/public/locales/zh-TW/common.json
@@ -350,9 +350,12 @@
"download": "下載:",
"downloadSelected": "下載選取的項目",
"exportPdf": "匯出 PDF",
- "uploadPdfFiles": "選擇 PDF 檔案",
- "dragAndDrop": "拖放 PDF 檔案至此處,或是點擊以選取",
+ "uploadPdfFiles": "選擇 PDF 或圖片檔案",
+ "dragAndDrop": "拖放 PDF 或圖片檔案至此處,或是點擊以選取",
"selectFiles": "選擇檔案",
+ "dropToAdd": "拖放 PDF 或圖片檔案以新增",
+ "convertingImages": "正在將圖片轉換為 PDF...",
+ "failedToConvertImages": "圖片轉換為 PDF 失敗。",
"renderingPages": "渲染頁面...",
"actions": {
"duplicatePage": "複製此頁",
diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json
index c8c5467..f8c225d 100644
--- a/public/locales/zh/common.json
+++ b/public/locales/zh/common.json
@@ -350,9 +350,12 @@
"download": "下载:",
"downloadSelected": "下载选中",
"exportPdf": "导出 PDF",
- "uploadPdfFiles": "选择 PDF 文件",
- "dragAndDrop": "将 PDF 文件拖放到此处,或点击选择",
+ "uploadPdfFiles": "选择 PDF 或图片文件",
+ "dragAndDrop": "将 PDF 或图片文件拖放到此处,或点击选择",
"selectFiles": "选择文件",
+ "dropToAdd": "拖放 PDF 或图片文件以添加",
+ "convertingImages": "正在将图片转换为 PDF...",
+ "failedToConvertImages": "图片转换为 PDF 失败。",
"renderingPages": "正在渲染页面...",
"actions": {
"duplicatePage": "复制此页",
diff --git a/scripts/generate-security-headers.mjs b/scripts/generate-security-headers.mjs
index 9ba93b3..f832956 100644
--- a/scripts/generate-security-headers.mjs
+++ b/scripts/generate-security-headers.mjs
@@ -27,6 +27,7 @@ const DEFAULT_WASM_ORIGINS = {
};
const DEFAULT_CORS_PROXY_ORIGIN =
'https://bentopdf-cors-proxy.bentopdf.workers.dev';
+const DEFAULT_OCR_FONT_CDN_ORIGIN = 'https://rawcdn.githack.com';
const wasmOrigins = [
originOf(process.env.VITE_WASM_PYMUPDF_URL) || DEFAULT_WASM_ORIGINS.pymupdf,
@@ -43,13 +44,15 @@ const tesseractOrigins = uniq([
const corsProxyOrigin =
originOf(process.env.VITE_CORS_PROXY_URL) || DEFAULT_CORS_PROXY_ORIGIN;
-const ocrFontOrigin = originOf(process.env.VITE_OCR_FONT_BASE_URL);
+const ocrFontOrigin =
+ originOf(process.env.VITE_OCR_FONT_BASE_URL) || DEFAULT_OCR_FONT_CDN_ORIGIN;
const scriptOrigins = uniq([...wasmOrigins, ...tesseractOrigins]);
const connectOrigins = uniq([
...wasmOrigins,
...tesseractOrigins,
corsProxyOrigin,
+ ocrFontOrigin,
]);
const fontOrigins = uniq([ocrFontOrigin].filter(Boolean));
diff --git a/security-headers-docs.conf b/security-headers-docs.conf
index 758ff89..33f5b37 100644
--- a/security-headers-docs.conf
+++ b/security-headers-docs.conf
@@ -1,4 +1,4 @@
-add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://api.github.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://bentopdf-cors-proxy.bentopdf.workers.dev; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'" always;
+add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com https://rawcdn.githack.com; connect-src 'self' https://api.github.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://bentopdf-cors-proxy.bentopdf.workers.dev https://rawcdn.githack.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
diff --git a/src/js/config/font-mappings.ts b/src/js/config/font-mappings.ts
index 29d9da1..a1af1bc 100644
--- a/src/js/config/font-mappings.ts
+++ b/src/js/config/font-mappings.ts
@@ -160,59 +160,59 @@ export const languageToFontFamily: Record
- Drag and drop PDF files here, or click to select + Drag and drop PDF or image files here, or click to select
diff --git a/src/tests/font-loader.test.ts b/src/tests/font-loader.test.ts index 3cfdf5e..585d6f2 100644 --- a/src/tests/font-loader.test.ts +++ b/src/tests/font-loader.test.ts @@ -6,7 +6,7 @@ import { resolveFontUrl } from '../js/utils/font-loader'; describe('font-loader', () => { it('uses the default public font URL when no offline font base URL is configured', () => { expect(resolveFontUrl('Noto Sans', {})).toBe( - 'https://raw.githack.com/googlefonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Regular.ttf' + 'https://rawcdn.githack.com/googlefonts/noto-fonts/ffebf8c1ee449e544955a7e813c54f9b73848eac/hinted/ttf/NotoSans/NotoSans-Regular.ttf' ); }); diff --git a/src/tests/images-to-pdf-lib.test.ts b/src/tests/images-to-pdf-lib.test.ts new file mode 100644 index 0000000..f572e2b --- /dev/null +++ b/src/tests/images-to-pdf-lib.test.ts @@ -0,0 +1,270 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +vi.mock('heic2any', () => ({ + default: vi.fn(async ({ blob }: { blob: Blob }) => { + const bytes = new Uint8Array(await blob.arrayBuffer()); + return new Blob([bytes], { type: 'image/png' }); + }), +})); + +const mockEmbedJpg = vi.fn(); +const mockEmbedPng = vi.fn(); +const mockDrawImage = vi.fn(); +const mockAddPage = vi.fn(); +const mockSave = vi.fn(); + +vi.mock('pdf-lib', () => { + return { + PDFDocument: { + create: vi.fn(async () => ({ + embedJpg: mockEmbedJpg, + embedPng: mockEmbedPng, + addPage: mockAddPage, + save: mockSave, + })), + }, + }; +}); + +import { + normalizeImageToEmbeddable, + convertImagesToPdfFile, +} from '../js/utils/images-to-pdf-lib'; + +function makeFile(name: string, type: string, bytes = [1, 2, 3]): File { + return new File([new Uint8Array(bytes)], name, { type }); +} + +describe('normalizeImageToEmbeddable', () => { + beforeEach(() => { + mockEmbedJpg.mockReset(); + mockEmbedPng.mockReset(); + mockDrawImage.mockReset(); + mockAddPage.mockReset(); + mockSave.mockReset(); + }); + + it('passes through JPG bytes unchanged when MIME is image/jpeg', async () => { + const file = makeFile('photo.jpg', 'image/jpeg', [0xff, 0xd8, 0xff, 0xe0]); + const result = await normalizeImageToEmbeddable(file); + expect(result.format).toBe('jpg'); + expect(Array.from(result.bytes)).toEqual([0xff, 0xd8, 0xff, 0xe0]); + }); + + it('passes through JPG by extension when MIME is missing', async () => { + const file = makeFile('photo.jpeg', '', [0xff, 0xd8]); + const result = await normalizeImageToEmbeddable(file); + expect(result.format).toBe('jpg'); + }); + + it('passes through PNG bytes unchanged when MIME is image/png', async () => { + const file = makeFile('scan.png', 'image/png', [0x89, 0x50, 0x4e, 0x47]); + const result = await normalizeImageToEmbeddable(file); + expect(result.format).toBe('png'); + expect(Array.from(result.bytes)).toEqual([0x89, 0x50, 0x4e, 0x47]); + }); + + it('passes through PNG by uppercase extension when MIME is missing', async () => { + const file = makeFile('SCAN.PNG', '', [0x89, 0x50]); + const result = await normalizeImageToEmbeddable(file); + expect(result.format).toBe('png'); + }); + + describe('canvas-decode path (non-JPG/PNG formats)', () => { + const originalCreateElement = document.createElement.bind(document); + const originalImage = globalThis.Image; + const originalCreateObjectURL = URL.createObjectURL; + const originalRevokeObjectURL = URL.revokeObjectURL; + const revokeSpy = vi.fn(); + + beforeEach(() => { + revokeSpy.mockReset(); + + URL.createObjectURL = vi.fn(() => 'blob:test'); + URL.revokeObjectURL = revokeSpy; + + globalThis.Image = class { + naturalWidth = 100; + naturalHeight = 50; + onload: (() => void) | null = null; + onerror: (() => void) | null = null; + set src(_: string) { + setTimeout(() => this.onload?.(), 0); + } + } as unknown as typeof Image; + + document.createElement = ((tagName: string) => { + if (tagName !== 'canvas') { + return originalCreateElement(tagName); + } + return { + width: 0, + height: 0, + getContext: vi.fn(() => ({ drawImage: vi.fn() })), + toBlob: vi.fn((cb: (b: Blob) => void) => { + cb(new Blob([new Uint8Array([0x89, 0x50])], { type: 'image/png' })); + }), + } as unknown as HTMLCanvasElement; + }) as typeof document.createElement; + }); + + afterEach(() => { + document.createElement = originalCreateElement; + globalThis.Image = originalImage; + URL.createObjectURL = originalCreateObjectURL; + URL.revokeObjectURL = originalRevokeObjectURL; + }); + + it('decodes a BMP through canvas and returns PNG bytes', async () => { + const file = makeFile('scan.bmp', 'image/bmp'); + const result = await normalizeImageToEmbeddable(file); + expect(result.format).toBe('png'); + expect(Array.from(result.bytes)).toEqual([0x89, 0x50]); + }); + + it('decodes a TIFF through canvas', async () => { + const file = makeFile('page.tiff', 'image/tiff'); + const result = await normalizeImageToEmbeddable(file); + expect(result.format).toBe('png'); + }); + + it('revokes the object URL after success', async () => { + const file = makeFile('scan.bmp', 'image/bmp'); + await normalizeImageToEmbeddable(file); + expect(revokeSpy).toHaveBeenCalledWith('blob:test'); + }); + + it('revokes the object URL even on Image decode error', async () => { + globalThis.Image = class { + naturalWidth = 0; + naturalHeight = 0; + onload: (() => void) | null = null; + onerror: (() => void) | null = null; + set src(_: string) { + setTimeout(() => this.onerror?.(), 0); + } + } as unknown as typeof Image; + + const file = makeFile('broken.gif', 'image/gif'); + await expect(normalizeImageToEmbeddable(file)).rejects.toThrow( + /Failed to decode image/ + ); + expect(revokeSpy).toHaveBeenCalledWith('blob:test'); + }); + + it('rejects when canvas getContext returns null', async () => { + document.createElement = ((tagName: string) => { + if (tagName !== 'canvas') { + return originalCreateElement(tagName); + } + return { + width: 0, + height: 0, + getContext: vi.fn(() => null), + toBlob: vi.fn(), + } as unknown as HTMLCanvasElement; + }) as typeof document.createElement; + + const file = makeFile('scan.bmp', 'image/bmp'); + await expect(normalizeImageToEmbeddable(file)).rejects.toThrow( + /Canvas context unavailable/ + ); + expect(revokeSpy).toHaveBeenCalled(); + }); + + it('rejects when canvas.toBlob returns null', async () => { + document.createElement = ((tagName: string) => { + if (tagName !== 'canvas') { + return originalCreateElement(tagName); + } + return { + width: 0, + height: 0, + getContext: vi.fn(() => ({ drawImage: vi.fn() })), + toBlob: vi.fn((cb: (b: Blob | null) => void) => cb(null)), + } as unknown as HTMLCanvasElement; + }) as typeof document.createElement; + + const file = makeFile('scan.bmp', 'image/bmp'); + await expect(normalizeImageToEmbeddable(file)).rejects.toThrow( + /Canvas toBlob failed/ + ); + expect(revokeSpy).toHaveBeenCalled(); + }); + }); +}); + +describe('convertImagesToPdfFile', () => { + beforeEach(() => { + mockEmbedJpg.mockReset(); + mockEmbedPng.mockReset(); + mockDrawImage.mockReset(); + mockAddPage.mockReset(); + mockSave.mockReset(); + + mockEmbedJpg.mockResolvedValue({ width: 300, height: 200 }); + mockEmbedPng.mockResolvedValue({ width: 600, height: 400 }); + mockAddPage.mockReturnValue({ drawImage: mockDrawImage }); + mockSave.mockResolvedValue(new Uint8Array([0x25, 0x50, 0x44, 0x46])); + }); + + it('embeds a single JPG as one page and names file after image', async () => { + const file = makeFile('vacation.jpg', 'image/jpeg', [0xff, 0xd8]); + const result = await convertImagesToPdfFile([file]); + + expect(mockEmbedJpg).toHaveBeenCalledTimes(1); + expect(mockEmbedPng).not.toHaveBeenCalled(); + expect(mockAddPage).toHaveBeenCalledWith([300, 200]); + expect(mockDrawImage).toHaveBeenCalledWith( + expect.objectContaining({ width: 300, height: 200 }), + { x: 0, y: 0, width: 300, height: 200 } + ); + expect(result.name).toBe('vacation.pdf'); + expect(result.type).toBe('application/pdf'); + }); + + it('embeds a single PNG and names file after image', async () => { + const file = makeFile('scan.png', 'image/png', [0x89, 0x50]); + const result = await convertImagesToPdfFile([file]); + + expect(mockEmbedPng).toHaveBeenCalledTimes(1); + expect(mockEmbedJpg).not.toHaveBeenCalled(); + expect(mockAddPage).toHaveBeenCalledWith([600, 400]); + expect(result.name).toBe('scan.pdf'); + }); + + it('embeds multiple images and uses generic filename', async () => { + const files = [ + makeFile('a.jpg', 'image/jpeg'), + makeFile('b.png', 'image/png'), + makeFile('c.jpg', 'image/jpeg'), + ]; + const result = await convertImagesToPdfFile(files); + + expect(mockEmbedJpg).toHaveBeenCalledTimes(2); + expect(mockEmbedPng).toHaveBeenCalledTimes(1); + expect(mockAddPage).toHaveBeenCalledTimes(3); + expect(result.name).toMatch(/^images-\d+\.pdf$/); + }); + + it('produces a PDF File with correct MIME type', async () => { + const file = makeFile('a.jpg', 'image/jpeg'); + const result = await convertImagesToPdfFile([file]); + expect(result.type).toBe('application/pdf'); + expect(result).toBeInstanceOf(File); + }); + + it('strips the original extension when generating single-file name', async () => { + const file = makeFile('photo.JPEG', 'image/jpeg'); + const result = await convertImagesToPdfFile([file]); + expect(result.name).toBe('photo.pdf'); + }); + + it('propagates errors from pdf-lib embed step', async () => { + mockEmbedJpg.mockRejectedValueOnce(new Error('embed failed')); + const file = makeFile('bad.jpg', 'image/jpeg'); + await expect(convertImagesToPdfFile([file])).rejects.toThrow( + /embed failed/ + ); + }); +}); diff --git a/src/tests/multi-tool-file-input.test.ts b/src/tests/multi-tool-file-input.test.ts new file mode 100644 index 0000000..e68b986 --- /dev/null +++ b/src/tests/multi-tool-file-input.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, vi } from 'vitest'; + +vi.mock('heic2any', () => ({ + default: vi.fn(), +})); + +import { partitionIncomingFiles } from '../js/utils/multi-tool-file-input'; + +function makeFile(name: string, type: string): File { + return new File([new Uint8Array([1, 2, 3])], name, { type }); +} + +describe('partitionIncomingFiles', () => { + it('routes PDFs by MIME type', () => { + const result = partitionIncomingFiles([ + makeFile('a.pdf', 'application/pdf'), + makeFile('b.pdf', 'application/pdf'), + ]); + expect(result.pdfFiles).toHaveLength(2); + expect(result.imageFiles).toHaveLength(0); + expect(result.skipped).toHaveLength(0); + }); + + it('routes PDFs by extension when MIME is missing', () => { + const result = partitionIncomingFiles([makeFile('doc.pdf', '')]); + expect(result.pdfFiles).toHaveLength(1); + expect(result.imageFiles).toHaveLength(0); + }); + + it('routes PDFs with uppercase extension', () => { + const result = partitionIncomingFiles([makeFile('SCAN.PDF', '')]); + expect(result.pdfFiles).toHaveLength(1); + }); + + it('routes images by MIME type', () => { + const result = partitionIncomingFiles([ + makeFile('a.png', 'image/png'), + makeFile('b.jpg', 'image/jpeg'), + makeFile('c.webp', 'image/webp'), + ]); + expect(result.imageFiles).toHaveLength(3); + expect(result.pdfFiles).toHaveLength(0); + }); + + it('routes images by extension when MIME is missing (HEIC, BMP, TIFF)', () => { + const result = partitionIncomingFiles([ + makeFile('photo.heic', ''), + makeFile('scan.bmp', ''), + makeFile('page.tiff', ''), + ]); + expect(result.imageFiles).toHaveLength(3); + expect(result.skipped).toHaveLength(0); + }); + + it('splits a mixed batch into PDFs, images, and skipped', () => { + const result = partitionIncomingFiles([ + makeFile('doc.pdf', 'application/pdf'), + makeFile('photo.png', 'image/png'), + makeFile('readme.txt', 'text/plain'), + makeFile('archive.zip', 'application/zip'), + ]); + expect(result.pdfFiles).toHaveLength(1); + expect(result.imageFiles).toHaveLength(1); + expect(result.skipped).toEqual(['readme.txt', 'archive.zip']); + }); + + it('preserves original File order within each bucket', () => { + const a = makeFile('a.pdf', 'application/pdf'); + const b = makeFile('b.png', 'image/png'); + const c = makeFile('c.pdf', 'application/pdf'); + const d = makeFile('d.jpg', 'image/jpeg'); + + const result = partitionIncomingFiles([a, b, c, d]); + expect(result.pdfFiles).toEqual([a, c]); + expect(result.imageFiles).toEqual([b, d]); + }); + + it('returns empty buckets for empty input', () => { + const result = partitionIncomingFiles([]); + expect(result.pdfFiles).toHaveLength(0); + expect(result.imageFiles).toHaveLength(0); + expect(result.skipped).toHaveLength(0); + }); + + it('skips files with neither matching MIME nor extension', () => { + const result = partitionIncomingFiles([ + makeFile('script.js', 'application/javascript'), + makeFile('data.csv', 'text/csv'), + ]); + expect(result.skipped).toHaveLength(2); + expect(result.pdfFiles).toHaveLength(0); + expect(result.imageFiles).toHaveLength(0); + }); +});