From 3ca19af354211ca86909168a81efa7cacc785cf1 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Tue, 24 Mar 2026 13:20:50 +0530 Subject: [PATCH] feat: add TIFF conversion options and integrate wasm-vips for image processing - Updated README.md to include new dependencies: wasm-vips, pixelmatch, diff, and microdiff. - Added wasm-vips to package.json and package-lock.json for advanced TIFF encoding. - Enhanced localization files with new options for DPI, compression, color mode, and multi-page TIFF saving. - Implemented UI changes in pdf-to-tiff.html to allow users to select DPI, compression type, color mode, and multi-page options. - Refactored pdf-to-tiff-page.ts to utilize wasm-vips for TIFF encoding, replacing previous UTIF implementation. - Introduced TiffOptions interface in pdf-to-tiff-type.ts for better type management. - Updated Vite configuration to exclude wasm-vips from dependency optimization. --- README.md | 4 + package-lock.json | 10 + package.json | 1 + public/locales/ar/tools.json | 10 +- public/locales/be/tools.json | 10 +- public/locales/da/tools.json | 10 +- public/locales/de/tools.json | 19 +- public/locales/en/tools.json | 11 +- public/locales/es/tools.json | 10 +- public/locales/fr/tools.json | 10 +- public/locales/id/tools.json | 10 +- public/locales/it/tools.json | 10 +- public/locales/ko/tools.json | 10 +- public/locales/nl/tools.json | 10 +- public/locales/pt/tools.json | 10 +- public/locales/ru/tools.json | 10 +- public/locales/sv/tools.json | 10 +- public/locales/tr/tools.json | 10 +- public/locales/vi/tools.json | 10 +- public/locales/zh-TW/tools.json | 10 +- public/locales/zh/tools.json | 10 +- src/js/logic/pdf-to-tiff-page.ts | 301 ++++++++++++++++++++++++------- src/js/types/index.ts | 1 + src/js/types/pdf-to-tiff-type.ts | 6 + src/pages/pdf-to-tiff.html | 91 +++++++++- vite.config.ts | 4 +- 26 files changed, 507 insertions(+), 101 deletions(-) create mode 100644 src/js/types/pdf-to-tiff-type.ts diff --git a/README.md b/README.md index 09aef6e..1754fb8 100644 --- a/README.md +++ b/README.md @@ -1160,6 +1160,10 @@ BentoPDF wouldn't be possible without the amazing open-source tools and librarie - **[Tailwind CSS](https://tailwindcss.com/)** – For rapid, flexible, and beautiful UI styling. - **[qpdf](https://github.com/qpdf/qpdf)** and **[qpdf-wasm](https://github.com/neslinesli93/qpdf-wasm)** – For inspecting, repairing, and transforming PDF files. - **[LibreOffice](https://www.libreoffice.org/)** – For powerful document conversion capabilities. +- **[wasm-vips](https://github.com/kleisauke/wasm-vips)** – For advanced TIFF encoding with compression (LZW, Deflate, CCITT Group 4). +- **[pixelmatch](https://github.com/mapbox/pixelmatch)** – For fast, accurate image comparison and diff detection. +- **[diff](https://github.com/kpdecker/jsdiff)** – For computing text differences. +- **[microdiff](https://github.com/AsyncBanana/microdiff)** – For lightweight, fast object diffing. **AGPL Libraries (Pre-configured via CDN):** diff --git a/package-lock.json b/package-lock.json index b387419..c7d3017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "tiff": "^7.1.2", "utif": "^3.1.0", "vite-plugin-static-copy": "^3.2.0", + "wasm-vips": "^0.0.17", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zgapdfsigner": "^2.7.5" }, @@ -13400,6 +13401,15 @@ "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", "license": "Apache-2.0" }, + "node_modules/wasm-vips": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/wasm-vips/-/wasm-vips-0.0.17.tgz", + "integrity": "sha512-nhkqUNJDUymImoXGrVfImC4wzIFTb9KfBpAngb7dcEQNPP1gVTx4+WL3VVVDSXQpMsyeacsQDOx0+DM33Rpurg==", + "license": "MIT", + "engines": { + "node": ">=16.4.0" + } + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", diff --git a/package.json b/package.json index 7b488d8..eadfc27 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "tiff": "^7.1.2", "utif": "^3.1.0", "vite-plugin-static-copy": "^3.2.0", + "wasm-vips": "^0.0.17", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "zgapdfsigner": "^2.7.5" }, diff --git a/public/locales/ar/tools.json b/public/locales/ar/tools.json index 73f3209..2502c5f 100644 --- a/public/locales/ar/tools.json +++ b/public/locales/ar/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF إلى TIFF", - "subtitle": "تحويل كل صفحة PDF إلى صورة TIFF." + "subtitle": "تحويل كل صفحة PDF إلى صورة TIFF.", + "dpi": "DPI (الدقة)", + "dpiExplanation": "DPI أعلى = جودة أفضل للطباعة، حجم ملف أكبر", + "compression": "الضغط", + "compressionExplanation": "LZW و Deflate بدون فقدان. CCITT Group 4 هو الأفضل للمستندات الممسوحة ضوئيًا بالأبيض والأسود.", + "colorMode": "وضع الألوان", + "multiPage": "حفظ كملف TIFF متعدد الصفحات (ملف واحد)", + "loadingVips": "جارٍ تحميل معالج الصور...", + "converting": "جارٍ التحويل إلى TIFF..." }, "pdfToGreyscale": { "name": "PDF إلى تدرج الرمادي", diff --git a/public/locales/be/tools.json b/public/locales/be/tools.json index 536647a..8b8bf22 100644 --- a/public/locales/be/tools.json +++ b/public/locales/be/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF у TIFF", - "subtitle": "Канвертаваць кожную старонку PDF у відарыс TIFF." + "subtitle": "Канвертаваць кожную старонку PDF у відарыс TIFF.", + "dpi": "DPI (Разрашэнне)", + "dpiExplanation": "Вышэйшы DPI = лепшая якасць для друку, большы памер файла", + "compression": "Сціск", + "compressionExplanation": "LZW і Deflate — без страт. CCITT Group 4 лепш за ўсё падыходзіць для чорна-белых сканаваных дакументаў.", + "colorMode": "Каляровы рэжым", + "multiPage": "Захаваць як шматстаронкавы TIFF (адзін файл)", + "loadingVips": "Загрузка апрацоўшчыка відарысаў...", + "converting": "Канвертаванне ў TIFF..." }, "pdfToGreyscale": { "name": "PDF у градацыі шэрага", diff --git a/public/locales/da/tools.json b/public/locales/da/tools.json index 357973a..f37af3c 100644 --- a/public/locales/da/tools.json +++ b/public/locales/da/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF til TIFF", - "subtitle": "Konverter hver PDF-side til en TIFF-billedfil." + "subtitle": "Konverter hver PDF-side til en TIFF-billedfil.", + "dpi": "DPI (Opløsning)", + "dpiExplanation": "Højere DPI = bedre kvalitet til print, større filstørrelse", + "compression": "Komprimering", + "compressionExplanation": "LZW og Deflate er tabsfri. CCITT Group 4 er bedst til sort/hvide skannede dokumenter.", + "colorMode": "Farvetilstand", + "multiPage": "Gem som flersidet TIFF (én fil)", + "loadingVips": "Indlæser billedprocessor...", + "converting": "Konverterer til TIFF..." }, "pdfToGreyscale": { "name": "PDF til gråtoner", diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index ca05f8a..b6aa6f1 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -330,17 +330,14 @@ "pdfToTiff": { "name": "PDF zu TIFF", "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..." - } + "dpi": "DPI (Auflösung)", + "dpiExplanation": "Höhere DPI = bessere Druckqualität, größere Dateigröße", + "compression": "Komprimierung", + "compressionExplanation": "LZW und Deflate sind verlustfrei. CCITT Group 4 eignet sich am besten für S/W-Scandokumente.", + "colorMode": "Farbmodus", + "multiPage": "Als mehrseitiges TIFF speichern (einzelne Datei)", + "loadingVips": "Bildprozessor wird geladen...", + "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 097bb96..310f8a5 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -330,6 +330,14 @@ "pdfToTiff": { "name": "PDF to TIFF", "subtitle": "Convert each PDF page into a TIFF image.", + "dpi": "DPI (Resolution)", + "dpiExplanation": "Higher DPI = better quality for printing, larger file size", + "compression": "Compression", + "compressionExplanation": "LZW and Deflate are lossless. CCITT Group 4 is best for B&W scanned documents.", + "colorMode": "Color Mode", + "multiPage": "Save as multi-page TIFF (single file)", + "loadingVips": "Loading image processor...", + "converting": "Converting to TIFF...", "alert": { "invalidFile": "Invalid File", "invalidFileExplanation": "Please upload a PDF file.", @@ -337,9 +345,6 @@ "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": { diff --git a/public/locales/es/tools.json b/public/locales/es/tools.json index d2ba726..f028195 100644 --- a/public/locales/es/tools.json +++ b/public/locales/es/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF a TIFF", - "subtitle": "Convierte cada página PDF en una imagen TIFF." + "subtitle": "Convierte cada página PDF en una imagen TIFF.", + "dpi": "PPP (Resolución)", + "dpiExplanation": "Mayor PPP = mejor calidad para impresión, mayor tamaño de archivo", + "compression": "Compresión", + "compressionExplanation": "LZW y Deflate son sin pérdida. CCITT Grupo 4 es ideal para documentos escaneados en B/N.", + "colorMode": "Modo de color", + "multiPage": "Guardar como TIFF multipágina (archivo único)", + "loadingVips": "Cargando procesador de imágenes...", + "converting": "Convirtiendo a TIFF..." }, "pdfToGreyscale": { "name": "PDF a Escala de Grises", diff --git a/public/locales/fr/tools.json b/public/locales/fr/tools.json index d5531d0..e5154e9 100644 --- a/public/locales/fr/tools.json +++ b/public/locales/fr/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF vers TIFF", - "subtitle": "Convertir chaque page du PDF en image TIFF." + "subtitle": "Convertir chaque page du PDF en image TIFF.", + "dpi": "PPP (Résolution)", + "dpiExplanation": "PPP plus élevé = meilleure qualité pour l'impression, taille de fichier plus grande", + "compression": "Compression", + "compressionExplanation": "LZW et Deflate sont sans perte. CCITT Groupe 4 est idéal pour les documents numérisés en N/B.", + "colorMode": "Mode couleur", + "multiPage": "Enregistrer en TIFF multipage (fichier unique)", + "loadingVips": "Chargement du processeur d'images...", + "converting": "Conversion en TIFF..." }, "pdfToGreyscale": { "name": "PDF en niveaux de gris", diff --git a/public/locales/id/tools.json b/public/locales/id/tools.json index dbef1d4..c97cad1 100644 --- a/public/locales/id/tools.json +++ b/public/locales/id/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF ke TIFF", - "subtitle": "Konversi setiap halaman PDF menjadi gambar TIFF." + "subtitle": "Konversi setiap halaman PDF menjadi gambar TIFF.", + "dpi": "DPI (Resolusi)", + "dpiExplanation": "DPI lebih tinggi = kualitas lebih baik untuk cetak, ukuran file lebih besar", + "compression": "Kompresi", + "compressionExplanation": "LZW dan Deflate adalah lossless. CCITT Group 4 paling cocok untuk dokumen pindaian hitam putih.", + "colorMode": "Mode Warna", + "multiPage": "Simpan sebagai TIFF multi-halaman (satu file)", + "loadingVips": "Memuat prosesor gambar...", + "converting": "Mengonversi ke TIFF..." }, "pdfToGreyscale": { "name": "PDF ke Skala Abu-abu", diff --git a/public/locales/it/tools.json b/public/locales/it/tools.json index 02ff854..3b3320d 100644 --- a/public/locales/it/tools.json +++ b/public/locales/it/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF in TIFF", - "subtitle": "Converti ogni pagina del PDF in un'immagine TIFF." + "subtitle": "Converti ogni pagina del PDF in un'immagine TIFF.", + "dpi": "DPI (Risoluzione)", + "dpiExplanation": "DPI più alto = qualità migliore per la stampa, dimensione file maggiore", + "compression": "Compressione", + "compressionExplanation": "LZW e Deflate sono senza perdita. CCITT Group 4 è ideale per documenti scansionati in bianco e nero.", + "colorMode": "Modalità colore", + "multiPage": "Salva come TIFF multipagina (file singolo)", + "loadingVips": "Caricamento processore immagini...", + "converting": "Conversione in TIFF..." }, "pdfToGreyscale": { "name": "PDF in Scala di Grigi", diff --git a/public/locales/ko/tools.json b/public/locales/ko/tools.json index beeb7c6..8f156d2 100644 --- a/public/locales/ko/tools.json +++ b/public/locales/ko/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF를 TIFF로", - "subtitle": "PDF의 각 페이지를 TIFF 이미지로 변환합니다." + "subtitle": "PDF의 각 페이지를 TIFF 이미지로 변환합니다.", + "dpi": "DPI (해상도)", + "dpiExplanation": "DPI가 높을수록 인쇄 품질이 좋아지지만 파일 크기가 커집니다", + "compression": "압축", + "compressionExplanation": "LZW와 Deflate는 무손실 압축입니다. CCITT Group 4는 흑백 스캔 문서에 가장 적합합니다.", + "colorMode": "색상 모드", + "multiPage": "다중 페이지 TIFF로 저장 (단일 파일)", + "loadingVips": "이미지 프로세서 로드 중...", + "converting": "TIFF로 변환 중..." }, "pdfToGreyscale": { "name": "PDF 흑백 변환", diff --git a/public/locales/nl/tools.json b/public/locales/nl/tools.json index d446cd4..fea76c5 100644 --- a/public/locales/nl/tools.json +++ b/public/locales/nl/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF naar TIFF", - "subtitle": "Converteer elke PDF-pagina naar een TIFF-afbeelding." + "subtitle": "Converteer elke PDF-pagina naar een TIFF-afbeelding.", + "dpi": "DPI (Resolutie)", + "dpiExplanation": "Hogere DPI = betere kwaliteit voor afdrukken, groter bestand", + "compression": "Compressie", + "compressionExplanation": "LZW en Deflate zijn verliesvrij. CCITT Groep 4 is het beste voor zwart-wit gescande documenten.", + "colorMode": "Kleurmodus", + "multiPage": "Opslaan als meerpagina-TIFF (één bestand)", + "loadingVips": "Beeldprocessor laden...", + "converting": "Converteren naar TIFF..." }, "pdfToGreyscale": { "name": "PDF naar Grijswaarden", diff --git a/public/locales/pt/tools.json b/public/locales/pt/tools.json index a02cc12..bc415b1 100644 --- a/public/locales/pt/tools.json +++ b/public/locales/pt/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF para TIFF", - "subtitle": "Converta cada página do PDF em uma imagem TIFF." + "subtitle": "Converta cada página do PDF em uma imagem TIFF.", + "dpi": "DPI (Resolução)", + "dpiExplanation": "Maior DPI = melhor qualidade para impressão, arquivo maior", + "compression": "Compressão", + "compressionExplanation": "LZW e Deflate são sem perdas. CCITT Grupo 4 é ideal para documentos digitalizados em preto e branco.", + "colorMode": "Modo de Cor", + "multiPage": "Salvar como TIFF de várias páginas (arquivo único)", + "loadingVips": "Carregando processador de imagem...", + "converting": "Convertendo para TIFF..." }, "pdfToGreyscale": { "name": "PDF para Tons de Cinza", diff --git a/public/locales/ru/tools.json b/public/locales/ru/tools.json index 331ce4c..b1b44e4 100644 --- a/public/locales/ru/tools.json +++ b/public/locales/ru/tools.json @@ -273,7 +273,15 @@ }, "pdfToTiff": { "name": "PDF в TIFF", - "subtitle": "Преобразовать каждую страницу PDF в изображение TIFF." + "subtitle": "Преобразовать каждую страницу PDF в изображение TIFF.", + "dpi": "DPI (Разрешение)", + "dpiExplanation": "Более высокий DPI = лучшее качество печати, больший размер файла", + "compression": "Сжатие", + "compressionExplanation": "LZW и Deflate — сжатие без потерь. CCITT Group 4 лучше всего подходит для чёрно-белых отсканированных документов.", + "colorMode": "Цветовой режим", + "multiPage": "Сохранить как многостраничный TIFF (один файл)", + "loadingVips": "Загрузка обработчика изображений...", + "converting": "Конвертация в TIFF..." }, "pdfToGreyscale": { "name": "Градации серого", diff --git a/public/locales/sv/tools.json b/public/locales/sv/tools.json index ff693f3..0e32a22 100644 --- a/public/locales/sv/tools.json +++ b/public/locales/sv/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF till TIFF", - "subtitle": "Konvertera varje PDF-sida till en TIFF-bild." + "subtitle": "Konvertera varje PDF-sida till en TIFF-bild.", + "dpi": "DPI (Upplösning)", + "dpiExplanation": "Högre DPI = bättre kvalitet för utskrift, större filstorlek", + "compression": "Komprimering", + "compressionExplanation": "LZW och Deflate är förlustfria. CCITT Grupp 4 är bäst för svartvita skannade dokument.", + "colorMode": "Färgläge", + "multiPage": "Spara som flersidig TIFF (en fil)", + "loadingVips": "Laddar bildprocessor...", + "converting": "Konverterar till TIFF..." }, "pdfToGreyscale": { "name": "PDF till gråskala", diff --git a/public/locales/tr/tools.json b/public/locales/tr/tools.json index 7151ec9..fdcdf95 100644 --- a/public/locales/tr/tools.json +++ b/public/locales/tr/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF'den TIFF'e", - "subtitle": "Her PDF sayfasını TIFF görseline dönüştürün." + "subtitle": "Her PDF sayfasını TIFF görseline dönüştürün.", + "dpi": "DPI (Çözünürlük)", + "dpiExplanation": "Daha yüksek DPI = baskı için daha iyi kalite, daha büyük dosya boyutu", + "compression": "Sıkıştırma", + "compressionExplanation": "LZW ve Deflate kayıpsızdır. CCITT Grup 4, siyah beyaz taranmış belgeler için en iyisidir.", + "colorMode": "Renk Modu", + "multiPage": "Çok sayfalı TIFF olarak kaydet (tek dosya)", + "loadingVips": "Görüntü işlemci yükleniyor...", + "converting": "TIFF'e dönüştürülüyor..." }, "pdfToGreyscale": { "name": "PDF'yi Gri Tonlamaya Çevir", diff --git a/public/locales/vi/tools.json b/public/locales/vi/tools.json index 1e47984..2bf06aa 100644 --- a/public/locales/vi/tools.json +++ b/public/locales/vi/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF sang TIFF", - "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh TIFF." + "subtitle": "Chuyển đổi mỗi trang PDF thành hình ảnh TIFF.", + "dpi": "DPI (Độ phân giải)", + "dpiExplanation": "DPI cao hơn = chất lượng in tốt hơn, kích thước tệp lớn hơn", + "compression": "Nén", + "compressionExplanation": "LZW và Deflate là nén không mất dữ liệu. CCITT Nhóm 4 phù hợp nhất cho tài liệu quét đen trắng.", + "colorMode": "Chế độ màu", + "multiPage": "Lưu dưới dạng TIFF nhiều trang (một tệp)", + "loadingVips": "Đang tải bộ xử lý hình ảnh...", + "converting": "Đang chuyển đổi sang TIFF..." }, "pdfToGreyscale": { "name": "PDF sang thang xám", diff --git a/public/locales/zh-TW/tools.json b/public/locales/zh-TW/tools.json index c8bfe84..307442b 100644 --- a/public/locales/zh-TW/tools.json +++ b/public/locales/zh-TW/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF 轉 TIFF", - "subtitle": "將每個 PDF 頁面轉換為 TIFF 圖片。" + "subtitle": "將每個 PDF 頁面轉換為 TIFF 圖片。", + "dpi": "DPI (解析度)", + "dpiExplanation": "DPI 越高 = 列印品質越好,檔案越大", + "compression": "壓縮方式", + "compressionExplanation": "LZW 和 Deflate 為無損壓縮。CCITT Group 4 最適合黑白掃描文件。", + "colorMode": "色彩模式", + "multiPage": "儲存為多頁 TIFF (單一檔案)", + "loadingVips": "正在載入影像處理器...", + "converting": "正在轉換為 TIFF..." }, "pdfToGreyscale": { "name": "PDF 轉灰階", diff --git a/public/locales/zh/tools.json b/public/locales/zh/tools.json index ee0969d..e2fdbb3 100644 --- a/public/locales/zh/tools.json +++ b/public/locales/zh/tools.json @@ -279,7 +279,15 @@ }, "pdfToTiff": { "name": "PDF 转 TIFF", - "subtitle": "将每一页 PDF 转换为 TIFF 图片。" + "subtitle": "将每一页 PDF 转换为 TIFF 图片。", + "dpi": "DPI (分辨率)", + "dpiExplanation": "DPI 越高 = 打印质量越好,文件越大", + "compression": "压缩方式", + "compressionExplanation": "LZW 和 Deflate 为无损压缩。CCITT Group 4 最适合黑白扫描文档。", + "colorMode": "色彩模式", + "multiPage": "保存为多页 TIFF (单个文件)", + "loadingVips": "正在加载图像处理器...", + "converting": "正在转换为 TIFF..." }, "pdfToGreyscale": { "name": "PDF 转 灰度", diff --git a/src/js/logic/pdf-to-tiff-page.ts b/src/js/logic/pdf-to-tiff-page.ts index f247a21..6bd75ee 100644 --- a/src/js/logic/pdf-to-tiff-page.ts +++ b/src/js/logic/pdf-to-tiff-page.ts @@ -9,9 +9,11 @@ import { import { createIcons, icons } from 'lucide'; import JSZip from 'jszip'; import * as pdfjsLib from 'pdfjs-dist'; -import UTIF from 'utif'; import { PDFPageProxy } from 'pdfjs-dist'; import { t } from '../i18n/i18n'; +import type Vips from 'wasm-vips'; +import wasmUrl from 'wasm-vips/vips.wasm?url'; +import type { TiffOptions } from '@/types'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -19,6 +21,42 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( ).toString(); let files: File[] = []; +let vipsInstance: typeof Vips | null = null; + +async function getVips(): Promise { + if (vipsInstance) return vipsInstance; + const VipsInit = (await import('wasm-vips')).default; + vipsInstance = await VipsInit({ + dynamicLibraries: [], + locateFile: (fileName: string) => { + if (fileName.endsWith('.wasm')) { + return wasmUrl; + } + return fileName; + }, + }); + return vipsInstance; +} + +function getOptions(): TiffOptions { + const dpiInput = document.getElementById('tiff-dpi') as HTMLInputElement; + const compressionInput = document.getElementById( + 'tiff-compression' + ) as HTMLSelectElement; + const colorModeInput = document.getElementById( + 'tiff-color-mode' + ) as HTMLSelectElement; + const multiPageInput = document.getElementById( + 'tiff-multipage' + ) as HTMLInputElement; + + return { + dpi: dpiInput ? parseInt(dpiInput.value, 10) : 300, + compression: compressionInput ? compressionInput.value : 'lzw', + colorMode: colorModeInput ? colorModeInput.value : 'rgb', + multiPage: multiPageInput ? multiPageInput.checked : false, + }; +} const updateUI = () => { const fileDisplayArea = document.getElementById('file-display-area'); @@ -46,7 +84,7 @@ const updateUI = () => { const metaSpan = document.createElement('div'); metaSpan.className = 'text-xs text-gray-400'; - metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; // Initial state + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; infoContainer.append(nameSpan, metaSpan); @@ -62,7 +100,6 @@ const updateUI = () => { fileDiv.append(infoContainer, removeBtn); fileDisplayArea.appendChild(fileDiv); - // Fetch page count asynchronously readFileAsArrayBuffer(file) .then((buffer) => { return getPDFDocument(buffer).promise; @@ -76,7 +113,6 @@ const updateUI = () => { }); }); - // Initialize icons immediately after synchronous render createIcons({ icons }); } else { optionsPanel.classList.add('hidden'); @@ -90,6 +126,82 @@ const resetState = () => { updateUI(); }; +async function renderPageToRgba( + page: PDFPageProxy, + dpi: number +): Promise<{ rgba: Uint8ClampedArray; width: number; height: number }> { + const scale = dpi / 72; + const viewport = page.getViewport({ scale }); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d')!; + canvas.height = viewport.height; + canvas.width = viewport.width; + + await page.render({ + canvasContext: context, + viewport: viewport, + canvas, + }).promise; + + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + return { rgba: imageData.data, width: canvas.width, height: canvas.height }; +} + +function encodePageToTiff( + vips: typeof Vips, + rgba: Uint8ClampedArray, + width: number, + height: number, + options: TiffOptions +): Uint8Array { + let image = vips.Image.newFromMemory( + new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength), + width, + height, + 4, + vips.BandFormat.uchar + ); + + image = image.copy(); + const pixelsPerMm = options.dpi / 25.4; + image.setDouble('xres', pixelsPerMm); + image.setDouble('yres', pixelsPerMm); + + if (options.colorMode === 'greyscale' || options.colorMode === 'bw') { + if (image.bands === 4) { + image = image.flatten({ background: [255, 255, 255] }); + } + image = image.colourspace(vips.Interpretation.b_w); + } else { + if (image.bands === 4) { + image = image.flatten({ background: [255, 255, 255] }); + } + } + + const tiffOptions: Parameters[0] = { + compression: options.compression as Vips.Enum, + resunit: vips.ForeignTiffResunit.inch, + xres: options.dpi / 25.4, + yres: options.dpi / 25.4, + predictor: + options.compression === 'lzw' || options.compression === 'deflate' + ? vips.ForeignTiffPredictor.horizontal + : vips.ForeignTiffPredictor.none, + }; + + if (options.colorMode === 'bw') { + tiffOptions.bitdepth = 1; + } + + if (options.compression === 'jpeg') { + tiffOptions.Q = 85; + } + + const buffer = image.tiffsaveBuffer(tiffOptions); + image.delete(); + return buffer; +} + async function convert() { if (files.length === 0) { showAlert( @@ -98,26 +210,110 @@ async function convert() { ); return; } - showLoader(t('tools:pdfToTiff.loader.converting')); + showLoader(t('tools:pdfToTiff.loadingVips')); + + let vips: typeof Vips; try { + vips = await getVips(); + } catch (e) { + console.error('Failed to load wasm-vips:', e); + hideLoader(); + showAlert( + 'Error', + 'Failed to load the image processor. Please ensure your browser supports SharedArrayBuffer (requires HTTPS or localhost).' + ); + return; + } + + showLoader(t('tools:pdfToTiff.converting')); + + try { + const options = getOptions(); const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) .promise; - if (pdf.numPages === 1) { + if (options.multiPage && pdf.numPages > 1) { + const pages: Vips.Image[] = []; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const { rgba, width, height } = await renderPageToRgba( + page, + options.dpi + ); + + let img = vips.Image.newFromMemory( + new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength), + width, + height, + 4, + vips.BandFormat.uchar + ); + + if (options.colorMode === 'greyscale' || options.colorMode === 'bw') { + if (img.bands === 4) { + img = img.flatten({ background: [255, 255, 255] }); + } + img = img.colourspace(vips.Interpretation.b_w); + } else { + if (img.bands === 4) { + img = img.flatten({ background: [255, 255, 255] }); + } + } + + pages.push(img); + } + + const firstPage = pages[0]; + let joined = firstPage; + if (pages.length > 1) { + joined = vips.Image.arrayjoin(pages, { across: 1 }); + } + + const tiffOptions: Parameters[0] = { + compression: options.compression as Vips.Enum, + resunit: vips.ForeignTiffResunit.inch, + xres: options.dpi / 25.4, + yres: options.dpi / 25.4, + page_height: firstPage.height, + predictor: + options.compression === 'lzw' || options.compression === 'deflate' + ? vips.ForeignTiffPredictor.horizontal + : vips.ForeignTiffPredictor.none, + }; + + if (options.colorMode === 'bw') { + tiffOptions.bitdepth = 1; + } + + if (options.compression === 'jpeg') { + tiffOptions.Q = 85; + } + + const buffer = joined.tiffsaveBuffer(tiffOptions); + const blob = new Blob([new Uint8Array(buffer)], { type: 'image/tiff' }); + downloadFile(blob, getCleanPdfFilename(files[0].name) + '.tiff'); + + joined.delete(); + for (const p of pages) { + if (!p.isDeleted()) p.delete(); + } + } else if (pdf.numPages === 1) { const page = await pdf.getPage(1); - const blob = await renderPage(page, 1); - downloadFile( - blob.blobData, - getCleanPdfFilename(files[0].name) + '.' + blob.ending - ); + const { rgba, width, height } = await renderPageToRgba(page, options.dpi); + const buffer = encodePageToTiff(vips, rgba, width, height, options); + const blob = new Blob([new Uint8Array(buffer)], { type: 'image/tiff' }); + downloadFile(blob, getCleanPdfFilename(files[0].name) + '.tiff'); } else { const zip = new JSZip(); for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); - const blob = await renderPage(page, i); - if (blob.blobData) { - zip.file(`page_${i}.` + blob.ending, blob.blobData); - } + const { rgba, width, height } = await renderPageToRgba( + page, + options.dpi + ); + const buffer = encodePageToTiff(vips, rgba, width, height, options); + zip.file(`page_${i}.tiff`, new Uint8Array(buffer)); } const zipBlob = await zip.generateAsync({ type: 'blob' }); @@ -140,64 +336,19 @@ async function convert() { } } -async function renderPage( - page: PDFPageProxy, - pageNumber: number -): Promise<{ blobData: Blob | null; ending: string }> { - const viewport = page.getViewport({ scale: 2.0 }); - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - canvas.height = viewport.height; - canvas.width = viewport.width; - - await page.render({ - canvasContext: context!, - viewport: viewport, - canvas, - }).promise; - - const imageData = context!.getImageData(0, 0, canvas.width, canvas.height); - const rgba = imageData.data; - - try { - const tiffData = UTIF.encodeImage( - new Uint8Array(rgba), - canvas.width, - canvas.height - ); - const tiffBlob = new Blob([tiffData], { type: 'image/tiff' }); - return { - blobData: tiffBlob, - ending: 'tiff', - }; - } catch (encodeError: any) { - console.warn( - `TIFF encoding failed for page ${pageNumber}, using PNG fallback:`, - encodeError - ); - // Fallback to PNG if TIFF encoding fails (e.g., PackBits compression issues) - const pngBlob = await new Promise((resolve) => - canvas.toBlob(resolve, 'image/png') - ); - if (pngBlob) { - return { - blobData: pngBlob, - ending: 'png', - }; - } - } - - return { - blobData: null, - ending: 'tiff', - }; -} - document.addEventListener('DOMContentLoaded', () => { const fileInput = document.getElementById('file-input') as HTMLInputElement; const dropZone = document.getElementById('drop-zone'); const processBtn = document.getElementById('process-btn'); const backBtn = document.getElementById('back-to-tools'); + const dpiSlider = document.getElementById('tiff-dpi') as HTMLInputElement; + const dpiValue = document.getElementById('tiff-dpi-value'); + const compressionSelect = document.getElementById( + 'tiff-compression' + ) as HTMLSelectElement; + const colorModeSelect = document.getElementById( + 'tiff-color-mode' + ) as HTMLSelectElement; if (backBtn) { backBtn.addEventListener('click', () => { @@ -205,6 +356,20 @@ document.addEventListener('DOMContentLoaded', () => { }); } + if (dpiSlider && dpiValue) { + dpiSlider.addEventListener('input', () => { + dpiValue.textContent = dpiSlider.value; + }); + } + + if (compressionSelect && colorModeSelect) { + compressionSelect.addEventListener('change', () => { + if (compressionSelect.value === 'ccittfax4') { + colorModeSelect.value = 'bw'; + } + }); + } + const handleFileSelect = (newFiles: FileList | null) => { if (!newFiles || newFiles.length === 0) return; const validFiles = Array.from(newFiles).filter( diff --git a/src/js/types/index.ts b/src/js/types/index.ts index cc7ec65..9b8b0cd 100644 --- a/src/js/types/index.ts +++ b/src/js/types/index.ts @@ -52,3 +52,4 @@ export * from './adjust-colors-type.ts'; export * from './bates-numbering-type.ts'; export * from './page-preview-type.ts'; export * from './add-page-labels-type.ts'; +export * from './pdf-to-tiff-type.ts'; diff --git a/src/js/types/pdf-to-tiff-type.ts b/src/js/types/pdf-to-tiff-type.ts new file mode 100644 index 0000000..467d609 --- /dev/null +++ b/src/js/types/pdf-to-tiff-type.ts @@ -0,0 +1,6 @@ +export interface TiffOptions { + dpi: number; + compression: string; + colorMode: string; + multiPage: boolean; +} diff --git a/src/pages/pdf-to-tiff.html b/src/pages/pdf-to-tiff.html index 8980324..361e3d6 100644 --- a/src/pages/pdf-to-tiff.html +++ b/src/pages/pdf-to-tiff.html @@ -153,7 +153,96 @@ />
-