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.
This commit is contained in:
@@ -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.
|
- **[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.
|
- **[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.
|
- **[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):**
|
**AGPL Libraries (Pre-configured via CDN):**
|
||||||
|
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -76,6 +76,7 @@
|
|||||||
"tiff": "^7.1.2",
|
"tiff": "^7.1.2",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"vite-plugin-static-copy": "^3.2.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",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"zgapdfsigner": "^2.7.5"
|
"zgapdfsigner": "^2.7.5"
|
||||||
},
|
},
|
||||||
@@ -13400,6 +13401,15 @@
|
|||||||
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
|
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
|
||||||
"license": "Apache-2.0"
|
"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": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
"tiff": "^7.1.2",
|
"tiff": "^7.1.2",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
"vite-plugin-static-copy": "^3.2.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",
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"zgapdfsigner": "^2.7.5"
|
"zgapdfsigner": "^2.7.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF إلى TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF إلى تدرج الرمادي",
|
"name": "PDF إلى تدرج الرمادي",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF у TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF у градацыі шэрага",
|
"name": "PDF у градацыі шэрага",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF til TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF til gråtoner",
|
"name": "PDF til gråtoner",
|
||||||
|
|||||||
@@ -330,17 +330,14 @@
|
|||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF zu TIFF",
|
"name": "PDF zu TIFF",
|
||||||
"subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren.",
|
"subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren.",
|
||||||
"alert": {
|
"dpi": "DPI (Auflösung)",
|
||||||
"invalidFile": "Ungültige Datei",
|
"dpiExplanation": "Höhere DPI = bessere Druckqualität, größere Dateigröße",
|
||||||
"invalidFileExplanation": "Bitte wähle eine PDF Datei aus.",
|
"compression": "Komprimierung",
|
||||||
"noFile": "Keine Datei",
|
"compressionExplanation": "LZW und Deflate sind verlustfrei. CCITT Group 4 eignet sich am besten für S/W-Scandokumente.",
|
||||||
"noFileExplanation": "Bitte lade zuerst eine PDF-Datei hoch.",
|
"colorMode": "Farbmodus",
|
||||||
"conversionSuccess": "PDF erfolgreich in TIFFs konvertiert!",
|
"multiPage": "Als mehrseitiges TIFF speichern (einzelne Datei)",
|
||||||
"conversionError": "Konvertierung in TIFF fehlgeschlagen. Die Datei könnte beschädigt sein."
|
"loadingVips": "Bildprozessor wird geladen...",
|
||||||
},
|
|
||||||
"loader": {
|
|
||||||
"converting": "Wird in TIFF konvertiert..."
|
"converting": "Wird in TIFF konvertiert..."
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"pdfToGreyscale": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF zu Graustufen",
|
"name": "PDF zu Graustufen",
|
||||||
|
|||||||
@@ -330,6 +330,14 @@
|
|||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF to TIFF",
|
"name": "PDF to TIFF",
|
||||||
"subtitle": "Convert each PDF page into a TIFF image.",
|
"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": {
|
"alert": {
|
||||||
"invalidFile": "Invalid File",
|
"invalidFile": "Invalid File",
|
||||||
"invalidFileExplanation": "Please upload a PDF file.",
|
"invalidFileExplanation": "Please upload a PDF file.",
|
||||||
@@ -337,9 +345,6 @@
|
|||||||
"noFileExplanation": "Please upload a PDF file first.",
|
"noFileExplanation": "Please upload a PDF file first.",
|
||||||
"conversionSuccess": "PDF converted to TIFFs successfully!",
|
"conversionSuccess": "PDF converted to TIFFs successfully!",
|
||||||
"conversionError": "Failed to convert PDF to TIFF. The file might be corrupted."
|
"conversionError": "Failed to convert PDF to TIFF. The file might be corrupted."
|
||||||
},
|
|
||||||
"loader": {
|
|
||||||
"converting": "Converting to TIFF..."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pdfToGreyscale": {
|
"pdfToGreyscale": {
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF a TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF a Escala de Grises",
|
"name": "PDF a Escala de Grises",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF vers TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF en niveaux de gris",
|
"name": "PDF en niveaux de gris",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF ke TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF ke Skala Abu-abu",
|
"name": "PDF ke Skala Abu-abu",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF in TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF in Scala di Grigi",
|
"name": "PDF in Scala di Grigi",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF를 TIFF로",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF 흑백 변환",
|
"name": "PDF 흑백 변환",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF naar TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF naar Grijswaarden",
|
"name": "PDF naar Grijswaarden",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF para TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF para Tons de Cinza",
|
"name": "PDF para Tons de Cinza",
|
||||||
|
|||||||
@@ -273,7 +273,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF в TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "Градации серого",
|
"name": "Градации серого",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF till TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF till gråskala",
|
"name": "PDF till gråskala",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF'den TIFF'e",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF'yi Gri Tonlamaya Çevir",
|
"name": "PDF'yi Gri Tonlamaya Çevir",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF sang TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF sang thang xám",
|
"name": "PDF sang thang xám",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF 轉 TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF 轉灰階",
|
"name": "PDF 轉灰階",
|
||||||
|
|||||||
@@ -279,7 +279,15 @@
|
|||||||
},
|
},
|
||||||
"pdfToTiff": {
|
"pdfToTiff": {
|
||||||
"name": "PDF 转 TIFF",
|
"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": {
|
"pdfToGreyscale": {
|
||||||
"name": "PDF 转 灰度",
|
"name": "PDF 转 灰度",
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import {
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import UTIF from 'utif';
|
|
||||||
import { PDFPageProxy } from 'pdfjs-dist';
|
import { PDFPageProxy } from 'pdfjs-dist';
|
||||||
import { t } from '../i18n/i18n';
|
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(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -19,6 +21,42 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
|||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
let files: File[] = [];
|
let files: File[] = [];
|
||||||
|
let vipsInstance: typeof Vips | null = null;
|
||||||
|
|
||||||
|
async function getVips(): Promise<typeof Vips> {
|
||||||
|
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 updateUI = () => {
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
@@ -46,7 +84,7 @@ const updateUI = () => {
|
|||||||
|
|
||||||
const metaSpan = document.createElement('div');
|
const metaSpan = document.createElement('div');
|
||||||
metaSpan.className = 'text-xs text-gray-400';
|
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);
|
infoContainer.append(nameSpan, metaSpan);
|
||||||
|
|
||||||
@@ -62,7 +100,6 @@ const updateUI = () => {
|
|||||||
fileDiv.append(infoContainer, removeBtn);
|
fileDiv.append(infoContainer, removeBtn);
|
||||||
fileDisplayArea.appendChild(fileDiv);
|
fileDisplayArea.appendChild(fileDiv);
|
||||||
|
|
||||||
// Fetch page count asynchronously
|
|
||||||
readFileAsArrayBuffer(file)
|
readFileAsArrayBuffer(file)
|
||||||
.then((buffer) => {
|
.then((buffer) => {
|
||||||
return getPDFDocument(buffer).promise;
|
return getPDFDocument(buffer).promise;
|
||||||
@@ -76,7 +113,6 @@ const updateUI = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize icons immediately after synchronous render
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
} else {
|
} else {
|
||||||
optionsPanel.classList.add('hidden');
|
optionsPanel.classList.add('hidden');
|
||||||
@@ -90,6 +126,82 @@ const resetState = () => {
|
|||||||
updateUI();
|
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<typeof image.tiffsaveBuffer>[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() {
|
async function convert() {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
showAlert(
|
showAlert(
|
||||||
@@ -98,26 +210,110 @@ async function convert() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader(t('tools:pdfToTiff.loader.converting'));
|
showLoader(t('tools:pdfToTiff.loadingVips'));
|
||||||
|
|
||||||
|
let vips: typeof Vips;
|
||||||
try {
|
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]))
|
const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0]))
|
||||||
.promise;
|
.promise;
|
||||||
|
|
||||||
if (pdf.numPages === 1) {
|
if (options.multiPage && pdf.numPages > 1) {
|
||||||
const page = await pdf.getPage(1);
|
const pages: Vips.Image[] = [];
|
||||||
const blob = await renderPage(page, 1);
|
|
||||||
downloadFile(
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
blob.blobData,
|
const page = await pdf.getPage(i);
|
||||||
getCleanPdfFilename(files[0].name) + '.' + blob.ending
|
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<typeof joined.tiffsaveBuffer>[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 { 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 {
|
} else {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const blob = await renderPage(page, i);
|
const { rgba, width, height } = await renderPageToRgba(
|
||||||
if (blob.blobData) {
|
page,
|
||||||
zip.file(`page_${i}.` + blob.ending, blob.blobData);
|
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' });
|
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<Blob | null>((resolve) =>
|
|
||||||
canvas.toBlob(resolve, 'image/png')
|
|
||||||
);
|
|
||||||
if (pngBlob) {
|
|
||||||
return {
|
|
||||||
blobData: pngBlob,
|
|
||||||
ending: 'png',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
blobData: null,
|
|
||||||
ending: 'tiff',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
const dropZone = document.getElementById('drop-zone');
|
const dropZone = document.getElementById('drop-zone');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
const backBtn = document.getElementById('back-to-tools');
|
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) {
|
if (backBtn) {
|
||||||
backBtn.addEventListener('click', () => {
|
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) => {
|
const handleFileSelect = (newFiles: FileList | null) => {
|
||||||
if (!newFiles || newFiles.length === 0) return;
|
if (!newFiles || newFiles.length === 0) return;
|
||||||
const validFiles = Array.from(newFiles).filter(
|
const validFiles = Array.from(newFiles).filter(
|
||||||
|
|||||||
@@ -52,3 +52,4 @@ export * from './adjust-colors-type.ts';
|
|||||||
export * from './bates-numbering-type.ts';
|
export * from './bates-numbering-type.ts';
|
||||||
export * from './page-preview-type.ts';
|
export * from './page-preview-type.ts';
|
||||||
export * from './add-page-labels-type.ts';
|
export * from './add-page-labels-type.ts';
|
||||||
|
export * from './pdf-to-tiff-type.ts';
|
||||||
|
|||||||
6
src/js/types/pdf-to-tiff-type.ts
Normal file
6
src/js/types/pdf-to-tiff-type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface TiffOptions {
|
||||||
|
dpi: number;
|
||||||
|
compression: string;
|
||||||
|
colorMode: string;
|
||||||
|
multiPage: boolean;
|
||||||
|
}
|
||||||
@@ -153,7 +153,96 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
<div id="options-panel" class="hidden mt-6">
|
<div id="options-panel" class="hidden mt-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="tiff-dpi"
|
||||||
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
|
data-i18n="tools:pdfToTiff.dpi"
|
||||||
|
>DPI (Resolution)</label
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="tiff-dpi"
|
||||||
|
min="72"
|
||||||
|
max="600"
|
||||||
|
step="1"
|
||||||
|
value="300"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
id="tiff-dpi-value"
|
||||||
|
class="text-white font-medium w-16 text-right"
|
||||||
|
>300</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="mt-1 text-xs text-gray-400"
|
||||||
|
data-i18n="tools:pdfToTiff.dpiExplanation"
|
||||||
|
>
|
||||||
|
Higher DPI = better quality for printing, larger file size
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="tiff-compression"
|
||||||
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
|
data-i18n="tools:pdfToTiff.compression"
|
||||||
|
>Compression</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="tiff-compression"
|
||||||
|
class="w-full bg-gray-700 border border-gray-600 text-gray-200 rounded-lg px-3 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="lzw">LZW (Lossless)</option>
|
||||||
|
<option value="deflate">Deflate / ZIP (Lossless)</option>
|
||||||
|
<option value="ccittfax4">CCITT Group 4 (B&W documents)</option>
|
||||||
|
<option value="jpeg">JPEG (Lossy)</option>
|
||||||
|
<option value="packbits">PackBits</option>
|
||||||
|
<option value="none">None (Uncompressed)</option>
|
||||||
|
</select>
|
||||||
|
<p
|
||||||
|
class="mt-1 text-xs text-gray-400"
|
||||||
|
data-i18n="tools:pdfToTiff.compressionExplanation"
|
||||||
|
>
|
||||||
|
LZW and Deflate are lossless. CCITT Group 4 is best for B&W
|
||||||
|
scanned documents.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
for="tiff-color-mode"
|
||||||
|
class="block mb-2 text-sm font-medium text-gray-300"
|
||||||
|
data-i18n="tools:pdfToTiff.colorMode"
|
||||||
|
>Color Mode</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="tiff-color-mode"
|
||||||
|
class="w-full bg-gray-700 border border-gray-600 text-gray-200 rounded-lg px-3 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="rgb">Color (RGB)</option>
|
||||||
|
<option value="greyscale">Greyscale</option>
|
||||||
|
<option value="bw">Black & White (1-bit)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="tiff-multipage"
|
||||||
|
class="w-4 h-4 rounded bg-gray-700 border-gray-600 text-indigo-600 focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
for="tiff-multipage"
|
||||||
|
class="text-sm font-medium text-gray-300"
|
||||||
|
data-i18n="tools:pdfToTiff.multiPage"
|
||||||
|
>Save as multi-page TIFF (single file)</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="process-btn"
|
id="process-btn"
|
||||||
class="btn-gradient w-full"
|
class="btn-gradient w-full"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig, Plugin } from 'vitest/config';
|
import { defineConfig, Plugin } from 'vitest/config';
|
||||||
import type { IncomingMessage, ServerResponse } from 'http';
|
import type { IncomingMessage, ServerResponse } from 'http';
|
||||||
import type { Connect } from 'vite';
|
import type { Connect } from 'vite';
|
||||||
import basicSsl from '@vitejs/plugin-basic-ssl';
|
// import basicSsl from '@vitejs/plugin-basic-ssl';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||||
@@ -352,7 +352,7 @@ export default defineConfig(() => {
|
|||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['pdfkit', 'blob-stream'],
|
include: ['pdfkit', 'blob-stream'],
|
||||||
exclude: ['coherentpdf'],
|
exclude: ['coherentpdf', 'wasm-vips'],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user