From e3216dddc510139c3e96848e111d1f4de139424a Mon Sep 17 00:00:00 2001 From: alam00000 Date: Tue, 24 Mar 2026 14:55:51 +0530 Subject: [PATCH] feat: add PDF to CBZ conversion tool with metadata support - Updated main.ts to include 'PDF to CBZ' in the tools list. - Added new types for CBZ options and comic metadata in pdf-to-cbz-type.ts. - Implemented comic-info utility functions for generating ComicInfo.xml and metadata OPF files. - Created pdf-to-cbz.html page with UI for PDF to CBZ conversion, including options for image format, quality, and metadata. - Updated vite.config.ts to route to the new PDF to CBZ page. --- README.md | 1 + docs/.vitepress/config.mts | 1 + docs/tools/index.md | 1 + docs/tools/pdf-to-cbz.md | 77 ++++ public/locales/ar/tools.json | 30 ++ public/locales/be/tools.json | 30 ++ public/locales/da/tools.json | 30 ++ public/locales/de/tools.json | 30 ++ public/locales/en/tools.json | 30 ++ public/locales/es/tools.json | 30 ++ public/locales/fr/tools.json | 30 ++ public/locales/id/tools.json | 30 ++ public/locales/it/tools.json | 30 ++ public/locales/ko/tools.json | 30 ++ public/locales/nl/tools.json | 30 ++ public/locales/pt/tools.json | 30 ++ public/locales/ru/tools.json | 30 ++ public/locales/sv/tools.json | 30 ++ public/locales/tr/tools.json | 30 ++ public/locales/vi/tools.json | 30 ++ public/locales/zh-TW/tools.json | 30 ++ public/locales/zh/tools.json | 30 ++ src/js/config/tools.ts | 7 + src/js/logic/pdf-to-cbz-page.ts | 500 ++++++++++++++++++++++++ src/js/main.ts | 1 + src/js/types/index.ts | 1 + src/js/types/pdf-to-cbz-type.ts | 32 ++ src/js/utils/comic-info.ts | 190 ++++++++++ src/pages/pdf-to-cbz.html | 652 ++++++++++++++++++++++++++++++++ vite.config.ts | 1 + 30 files changed, 2004 insertions(+) create mode 100644 docs/tools/pdf-to-cbz.md create mode 100644 src/js/logic/pdf-to-cbz-page.ts create mode 100644 src/js/types/pdf-to-cbz-type.ts create mode 100644 src/js/utils/comic-info.ts create mode 100644 src/pages/pdf-to-cbz.html diff --git a/README.md b/README.md index 1754fb8..cf25b4b 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,7 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs. | **PDF to WebP** | Convert each PDF page into a WebP image. | | **PDF to BMP** | Convert each PDF page into a BMP image. | | **PDF to TIFF** | Convert each PDF page into a TIFF image. | +| **PDF to CBZ** | Convert a PDF into a CBZ (Comic Book Archive) for comic readers and Calibre. | | **PDF to SVG** | Convert each page into a scalable vector graphic (SVG) for perfect quality. | | **PDF to Greyscale** | Convert a color PDF into a black-and-white version. | | **PDF to Text** | Extract text from PDF files and save as plain text (.txt). | diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 9613f48..4d8f9fd 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -98,6 +98,7 @@ export default defineConfig({ { text: 'PDF to WebP', link: '/tools/pdf-to-webp' }, { text: 'PDF to BMP', link: '/tools/pdf-to-bmp' }, { text: 'PDF to TIFF', link: '/tools/pdf-to-tiff' }, + { text: 'PDF to CBZ', link: '/tools/pdf-to-cbz' }, { text: 'PDF to SVG', link: '/tools/pdf-to-svg' }, { text: 'PDF to CSV', link: '/tools/pdf-to-csv' }, { text: 'PDF to Excel', link: '/tools/pdf-to-excel' }, diff --git a/docs/tools/index.md b/docs/tools/index.md index 884dc56..2b507dd 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -89,6 +89,7 @@ Extract content from PDFs into images, documents, and data formats. - [**PDF to WebP**](./pdf-to-webp) — Convert each PDF page into a WebP image. - [**PDF to BMP**](./pdf-to-bmp) — Convert each PDF page into a BMP image. - [**PDF to TIFF**](./pdf-to-tiff) — Convert each PDF page into a TIFF image. +- [**PDF to CBZ**](./pdf-to-cbz) — Convert a PDF into a CBZ (Comic Book Archive) for comic readers and Calibre. - [**PDF to SVG**](./pdf-to-svg) — Convert each PDF page into a scalable vector graphic. - [**PDF to CSV**](./pdf-to-csv) — Extract tables from PDF and convert to CSV format. - [**PDF to Excel**](./pdf-to-excel) — Extract tables from PDF and convert to Excel (XLSX). diff --git a/docs/tools/pdf-to-cbz.md b/docs/tools/pdf-to-cbz.md new file mode 100644 index 0000000..b79ecd2 --- /dev/null +++ b/docs/tools/pdf-to-cbz.md @@ -0,0 +1,77 @@ +--- +title: PDF to CBZ +description: Convert a PDF into a CBZ (Comic Book Archive) file for comic readers like Komga, Kavita, CDisplayEx, and Calibre. +--- + +# PDF to CBZ + +Converts a PDF into a CBZ (Comic Book ZIP) file — the standard format for digital comics and manga. Each PDF page becomes an image inside the archive. The tool generates metadata in three formats for maximum compatibility: ComicInfo.xml, metadata.opf, and ComicBookInfo JSON. + +## How It Works + +1. Upload a PDF by clicking the drop zone or dragging a file onto it. +2. Choose image format, quality, scale, and optional metadata. +3. Click **Convert** to generate the CBZ file. +4. The `.cbz` file downloads automatically. + +## Options + +| Option | Values | Default | Description | +| ---------------- | --------------- | ------- | ----------------------------------------------------------------------------- | +| Image Format | JPEG, PNG, WebP | JPEG | JPEG for color comics, PNG for lossless B&W manga, WebP for best compression. | +| Quality | 50–100% | 85% | Controls JPEG/WebP compression. Hidden for PNG (always lossless). | +| Scale | 1.0x–4.0x | 2.0x | Higher scale produces sharper images for high-res screens. | +| Grayscale | On/Off | Off | Converts pages to grayscale. Reduces file size for B&W content. | +| Manga mode | On/Off | Off | Sets right-to-left reading direction in metadata. | +| Include metadata | On/Off | On | Embeds ComicInfo.xml, metadata.opf, and ComicBookInfo JSON. | + +## Metadata Fields + +When metadata is enabled, you can fill in: + +- **Title** — Auto-detected from the PDF filename. +- **Series** — The series name (e.g., "Naruto"). +- **Number (#)** — Issue number within the series. +- **Volume (Vol.)** — Volume number. +- **Author(s)** — Writer or creator name. +- **Publisher** — Publishing company. +- **Tags / Genre** — Comma-separated tags (e.g., "Action, Adventure"). +- **Published Year** — Year of publication (1900–2100). +- **Rating** — Community rating from 0 to 5. + +## Metadata Compatibility + +The tool writes metadata in three formats so every reader can find it: + +| Format | Location | Supported by | +| ------------------ | ----------------- | ------------------------------------------- | +| ComicInfo.xml | File inside ZIP | Komga, Kavita, CDisplayEx, Mylar, ComicRack | +| metadata.opf | File inside ZIP | Calibre | +| ComicBookInfo JSON | ZIP comment field | Calibre (fallback) | + +## Output Format + +- `filename.cbz` — A ZIP archive containing numbered page images and metadata files. + +Page images are named with zero-padded numbers (`01.jpg`, `02.jpg`, etc.) so readers display them in the correct order. + +## Use Cases + +- Converting manga or comic PDFs for use with comic book readers. +- Building a digital comic library in Komga, Kavita, or Calibre. +- Converting scanned comic books to a reader-friendly format. +- Sharing comics in a format that preserves reading direction and metadata. + +## Tips + +- Use **JPEG** for color comics and **PNG** for black-and-white manga — PNG compresses B&W content very efficiently. +- Enable **Grayscale** for manga to significantly reduce file size. +- Fill in the **Series** and **Number** fields so library managers (Komga, Calibre) can organize your collection automatically. +- **WebP** offers the best compression but older comic readers may not support it. + +## Related Tools + +- [PDF to JPG](./pdf-to-jpg) +- [PDF to PNG](./pdf-to-png) +- [PDF to TIFF](./pdf-to-tiff) +- [Extract Images](./extract-images) diff --git a/public/locales/ar/tools.json b/public/locales/ar/tools.json index 2502c5f..a4a3795 100644 --- a/public/locales/ar/tools.json +++ b/public/locales/ar/tools.json @@ -289,6 +289,36 @@ "loadingVips": "جارٍ تحميل معالج الصور...", "converting": "جارٍ التحويل إلى TIFF..." }, + "pdfToCbz": { + "name": "PDF إلى CBZ", + "subtitle": "تحويل ملف PDF إلى ملف CBZ (أرشيف الكتب المصورة) لقارئات القصص المصورة.", + "imageFormat": "تنسيق الصورة", + "quality": "جودة الصورة", + "qualityExplanation": "جودة أعلى = حجم ملف أكبر", + "scale": "المقياس", + "scaleExplanation": "مقياس أعلى = جودة أفضل للشاشات عالية الدقة", + "grayscale": "تحويل إلى تدرج الرمادي", + "manga": "وضع المانغا (من اليمين إلى اليسار)", + "includeMetadata": "تضمين بيانات ComicInfo.xml الوصفية", + "titleLabel": "العنوان", + "seriesLabel": "السلسلة", + "authorLabel": "المؤلف(ون)", + "numberLabel": "#", + "volumeLabel": "المجلد", + "publisherLabel": "الناشر", + "tagsLabel": "الوسوم / النوع", + "yearLabel": "سنة النشر", + "ratingLabel": "التقييم (0-5)", + "converting": "جارٍ التحويل إلى CBZ...", + "alert": { + "invalidFile": "ملف غير صالح", + "invalidFileExplanation": "يرجى تحميل ملف PDF.", + "noFile": "لا يوجد ملف", + "noFileExplanation": "يرجى تحميل ملف PDF أولاً.", + "conversionSuccess": "تم تحويل PDF إلى CBZ بنجاح!", + "conversionError": "فشل تحويل PDF إلى CBZ. قد يكون الملف تالفاً." + } + }, "pdfToGreyscale": { "name": "PDF إلى تدرج الرمادي", "subtitle": "تحويل جميع الألوان إلى أبيض وأسود." diff --git a/public/locales/be/tools.json b/public/locales/be/tools.json index 8b8bf22..ebfb9e8 100644 --- a/public/locales/be/tools.json +++ b/public/locales/be/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Загрузка апрацоўшчыка відарысаў...", "converting": "Канвертаванне ў TIFF..." }, + "pdfToCbz": { + "name": "PDF у CBZ", + "subtitle": "Канвертаваць PDF у файл CBZ (архіў коміксаў) для чытачоў коміксаў.", + "imageFormat": "Фармат выявы", + "quality": "Якасць выявы", + "qualityExplanation": "Вышэйшая якасць = большы памер файла", + "scale": "Маштаб", + "scaleExplanation": "Вышэйшы маштаб = лепшая якасць для экранаў высокай раздзяляльнасці", + "grayscale": "Канвертаваць у градацыі шэрага", + "manga": "Рэжым манга (справа налева)", + "includeMetadata": "Уключыць метаданыя ComicInfo.xml", + "titleLabel": "Назва", + "seriesLabel": "Серыя", + "authorLabel": "Аўтар(ы)", + "numberLabel": "#", + "volumeLabel": "Том", + "publisherLabel": "Выдавец", + "tagsLabel": "Тэгі / Жанр", + "yearLabel": "Год выдання", + "ratingLabel": "Рэйтынг (0-5)", + "converting": "Канвертаванне ў CBZ...", + "alert": { + "invalidFile": "Няправільны файл", + "invalidFileExplanation": "Калі ласка, загрузіце файл PDF.", + "noFile": "Няма файла", + "noFileExplanation": "Калі ласка, спачатку загрузіце файл PDF.", + "conversionSuccess": "PDF паспяхова канвертаваны ў CBZ!", + "conversionError": "Не ўдалося канвертаваць PDF у CBZ. Файл можа быць пашкоджаны." + } + }, "pdfToGreyscale": { "name": "PDF у градацыі шэрага", "subtitle": "Канвертаваць усе колеры ў чорна-белыя." diff --git a/public/locales/da/tools.json b/public/locales/da/tools.json index f37af3c..1f46545 100644 --- a/public/locales/da/tools.json +++ b/public/locales/da/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Indlæser billedprocessor...", "converting": "Konverterer til TIFF..." }, + "pdfToCbz": { + "name": "PDF til CBZ", + "subtitle": "Konverter en PDF til en CBZ-fil (tegneseriearkiv) til tegneserielæsere.", + "imageFormat": "Billedformat", + "quality": "Billedkvalitet", + "qualityExplanation": "Højere kvalitet = større filstørrelse", + "scale": "Skalering", + "scaleExplanation": "Højere skalering = bedre kvalitet til højopløsningsskærme", + "grayscale": "Konverter til gråtoner", + "manga": "Manga-tilstand (højre-til-venstre)", + "includeMetadata": "Inkluder ComicInfo.xml-metadata", + "titleLabel": "Titel", + "seriesLabel": "Serie", + "authorLabel": "Forfatter(e)", + "numberLabel": "#", + "volumeLabel": "Bind", + "publisherLabel": "Udgiver", + "tagsLabel": "Tags / Genre", + "yearLabel": "Udgivelsesår", + "ratingLabel": "Bedømmelse (0-5)", + "converting": "Konverterer til CBZ...", + "alert": { + "invalidFile": "Ugyldig fil", + "invalidFileExplanation": "Upload venligst en PDF-fil.", + "noFile": "Ingen fil", + "noFileExplanation": "Upload venligst en PDF-fil først.", + "conversionSuccess": "PDF konverteret til CBZ!", + "conversionError": "Kunne ikke konvertere PDF til CBZ. Filen kan være beskadiget." + } + }, "pdfToGreyscale": { "name": "PDF til gråtoner", "subtitle": "Konverter alle farver til sort/hvid." diff --git a/public/locales/de/tools.json b/public/locales/de/tools.json index b6aa6f1..a63bfd8 100644 --- a/public/locales/de/tools.json +++ b/public/locales/de/tools.json @@ -339,6 +339,36 @@ "loadingVips": "Bildprozessor wird geladen...", "converting": "Wird in TIFF konvertiert..." }, + "pdfToCbz": { + "name": "PDF zu CBZ", + "subtitle": "Konvertieren Sie ein PDF in eine CBZ-Datei (Comic-Bucharchiv) für Comic-Reader.", + "imageFormat": "Bildformat", + "quality": "Bildqualität", + "qualityExplanation": "Höhere Qualität = größere Dateigröße", + "scale": "Skalierung", + "scaleExplanation": "Höhere Skalierung = bessere Qualität für hochauflösende Bildschirme", + "grayscale": "In Graustufen konvertieren", + "manga": "Manga-Modus (rechts nach links)", + "includeMetadata": "ComicInfo.xml-Metadaten einschließen", + "titleLabel": "Titel", + "seriesLabel": "Serie", + "authorLabel": "Autor(en)", + "numberLabel": "#", + "volumeLabel": "Bd.", + "publisherLabel": "Verlag", + "tagsLabel": "Tags / Genre", + "yearLabel": "Erscheinungsjahr", + "ratingLabel": "Bewertung (0-5)", + "converting": "Wird in CBZ konvertiert...", + "alert": { + "invalidFile": "Ungültige Datei", + "invalidFileExplanation": "Bitte laden Sie eine PDF-Datei hoch.", + "noFile": "Keine Datei", + "noFileExplanation": "Bitte laden Sie zuerst eine PDF-Datei hoch.", + "conversionSuccess": "PDF erfolgreich in CBZ konvertiert!", + "conversionError": "PDF konnte nicht in CBZ konvertiert werden. Die Datei ist möglicherweise beschädigt." + } + }, "pdfToGreyscale": { "name": "PDF zu Graustufen", "subtitle": "Alle Farben in Schwarz-Weiß konvertieren." diff --git a/public/locales/en/tools.json b/public/locales/en/tools.json index 310f8a5..380ca65 100644 --- a/public/locales/en/tools.json +++ b/public/locales/en/tools.json @@ -347,6 +347,36 @@ "conversionError": "Failed to convert PDF to TIFF. The file might be corrupted." } }, + "pdfToCbz": { + "name": "PDF to CBZ", + "subtitle": "Convert a PDF into a CBZ (Comic Book Archive) file for comic readers.", + "imageFormat": "Image Format", + "quality": "Image Quality", + "qualityExplanation": "Higher quality = larger file size", + "scale": "Scale", + "scaleExplanation": "Higher scale = better quality for high-res screens", + "grayscale": "Convert to grayscale", + "manga": "Manga mode (right-to-left)", + "includeMetadata": "Include ComicInfo.xml metadata", + "titleLabel": "Title", + "seriesLabel": "Series", + "authorLabel": "Author(s)", + "numberLabel": "#", + "volumeLabel": "Vol.", + "publisherLabel": "Publisher", + "tagsLabel": "Tags / Genre", + "yearLabel": "Published Year", + "ratingLabel": "Rating (0-5)", + "converting": "Converting to CBZ...", + "alert": { + "invalidFile": "Invalid File", + "invalidFileExplanation": "Please upload a PDF file.", + "noFile": "No File", + "noFileExplanation": "Please upload a PDF file first.", + "conversionSuccess": "PDF converted to CBZ successfully!", + "conversionError": "Failed to convert PDF to CBZ. The file might be corrupted." + } + }, "pdfToGreyscale": { "name": "PDF to Greyscale", "subtitle": "Convert all colors to black and white." diff --git a/public/locales/es/tools.json b/public/locales/es/tools.json index f028195..3c49e98 100644 --- a/public/locales/es/tools.json +++ b/public/locales/es/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Cargando procesador de imágenes...", "converting": "Convirtiendo a TIFF..." }, + "pdfToCbz": { + "name": "PDF a CBZ", + "subtitle": "Convierte un PDF en un archivo CBZ (archivo de cómics) para lectores de cómics.", + "imageFormat": "Formato de imagen", + "quality": "Calidad de imagen", + "qualityExplanation": "Mayor calidad = mayor tamaño de archivo", + "scale": "Escala", + "scaleExplanation": "Mayor escala = mejor calidad para pantallas de alta resolución", + "grayscale": "Convertir a escala de grises", + "manga": "Modo manga (derecha a izquierda)", + "includeMetadata": "Incluir metadatos ComicInfo.xml", + "titleLabel": "Título", + "seriesLabel": "Serie", + "authorLabel": "Autor(es)", + "numberLabel": "#", + "volumeLabel": "Vol.", + "publisherLabel": "Editorial", + "tagsLabel": "Etiquetas / Género", + "yearLabel": "Año de publicación", + "ratingLabel": "Calificación (0-5)", + "converting": "Convirtiendo a CBZ...", + "alert": { + "invalidFile": "Archivo no válido", + "invalidFileExplanation": "Por favor, suba un archivo PDF.", + "noFile": "Sin archivo", + "noFileExplanation": "Por favor, suba un archivo PDF primero.", + "conversionSuccess": "¡PDF convertido a CBZ exitosamente!", + "conversionError": "Error al convertir PDF a CBZ. El archivo podría estar dañado." + } + }, "pdfToGreyscale": { "name": "PDF a Escala de Grises", "subtitle": "Convierte todos los colores a blanco y negro." diff --git a/public/locales/fr/tools.json b/public/locales/fr/tools.json index e5154e9..2d27d37 100644 --- a/public/locales/fr/tools.json +++ b/public/locales/fr/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Chargement du processeur d'images...", "converting": "Conversion en TIFF..." }, + "pdfToCbz": { + "name": "PDF en CBZ", + "subtitle": "Convertir un PDF en fichier CBZ (archive de bande dessinée) pour les lecteurs de comics.", + "imageFormat": "Format d'image", + "quality": "Qualité d'image", + "qualityExplanation": "Qualité supérieure = taille de fichier plus grande", + "scale": "Échelle", + "scaleExplanation": "Échelle supérieure = meilleure qualité pour les écrans haute résolution", + "grayscale": "Convertir en niveaux de gris", + "manga": "Mode manga (droite à gauche)", + "includeMetadata": "Inclure les métadonnées ComicInfo.xml", + "titleLabel": "Titre", + "seriesLabel": "Série", + "authorLabel": "Auteur(s)", + "numberLabel": "#", + "volumeLabel": "Vol.", + "publisherLabel": "Éditeur", + "tagsLabel": "Tags / Genre", + "yearLabel": "Année de publication", + "ratingLabel": "Note (0-5)", + "converting": "Conversion en CBZ...", + "alert": { + "invalidFile": "Fichier invalide", + "invalidFileExplanation": "Veuillez télécharger un fichier PDF.", + "noFile": "Aucun fichier", + "noFileExplanation": "Veuillez d'abord télécharger un fichier PDF.", + "conversionSuccess": "PDF converti en CBZ avec succès !", + "conversionError": "Échec de la conversion du PDF en CBZ. Le fichier est peut-être corrompu." + } + }, "pdfToGreyscale": { "name": "PDF en niveaux de gris", "subtitle": "Convertir toutes les couleurs en noir et blanc." diff --git a/public/locales/id/tools.json b/public/locales/id/tools.json index c97cad1..43e9245 100644 --- a/public/locales/id/tools.json +++ b/public/locales/id/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Memuat prosesor gambar...", "converting": "Mengonversi ke TIFF..." }, + "pdfToCbz": { + "name": "PDF ke CBZ", + "subtitle": "Konversi PDF menjadi file CBZ (Arsip Buku Komik) untuk pembaca komik.", + "imageFormat": "Format Gambar", + "quality": "Kualitas Gambar", + "qualityExplanation": "Kualitas lebih tinggi = ukuran file lebih besar", + "scale": "Skala", + "scaleExplanation": "Skala lebih tinggi = kualitas lebih baik untuk layar resolusi tinggi", + "grayscale": "Konversi ke skala abu-abu", + "manga": "Mode manga (kanan-ke-kiri)", + "includeMetadata": "Sertakan metadata ComicInfo.xml", + "titleLabel": "Judul", + "seriesLabel": "Seri", + "authorLabel": "Penulis", + "numberLabel": "#", + "volumeLabel": "Vol.", + "publisherLabel": "Penerbit", + "tagsLabel": "Tag / Genre", + "yearLabel": "Tahun Terbit", + "ratingLabel": "Peringkat (0-5)", + "converting": "Mengonversi ke CBZ...", + "alert": { + "invalidFile": "File Tidak Valid", + "invalidFileExplanation": "Silakan unggah file PDF.", + "noFile": "Tidak Ada File", + "noFileExplanation": "Silakan unggah file PDF terlebih dahulu.", + "conversionSuccess": "PDF berhasil dikonversi ke CBZ!", + "conversionError": "Gagal mengonversi PDF ke CBZ. File mungkin rusak." + } + }, "pdfToGreyscale": { "name": "PDF ke Skala Abu-abu", "subtitle": "Konversi semua warna ke hitam dan putih." diff --git a/public/locales/it/tools.json b/public/locales/it/tools.json index 3b3320d..2e5cbda 100644 --- a/public/locales/it/tools.json +++ b/public/locales/it/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Caricamento processore immagini...", "converting": "Conversione in TIFF..." }, + "pdfToCbz": { + "name": "PDF in CBZ", + "subtitle": "Converti un PDF in un file CBZ (Archivio Fumetti) per lettori di fumetti.", + "imageFormat": "Formato Immagine", + "quality": "Qualità Immagine", + "qualityExplanation": "Qualità più alta = dimensione file più grande", + "scale": "Scala", + "scaleExplanation": "Scala più alta = migliore qualità per schermi ad alta risoluzione", + "grayscale": "Converti in scala di grigi", + "manga": "Modalità manga (da destra a sinistra)", + "includeMetadata": "Includi metadati ComicInfo.xml", + "titleLabel": "Titolo", + "seriesLabel": "Serie", + "authorLabel": "Autore/i", + "numberLabel": "#", + "volumeLabel": "Vol.", + "publisherLabel": "Editore", + "tagsLabel": "Tag / Genere", + "yearLabel": "Anno di pubblicazione", + "ratingLabel": "Valutazione (0-5)", + "converting": "Conversione in CBZ...", + "alert": { + "invalidFile": "File Non Valido", + "invalidFileExplanation": "Carica un file PDF.", + "noFile": "Nessun File", + "noFileExplanation": "Carica prima un file PDF.", + "conversionSuccess": "PDF convertito in CBZ con successo!", + "conversionError": "Impossibile convertire il PDF in CBZ. Il file potrebbe essere danneggiato." + } + }, "pdfToGreyscale": { "name": "PDF in Scala di Grigi", "subtitle": "Converti tutti i colori in scala di grigi." diff --git a/public/locales/ko/tools.json b/public/locales/ko/tools.json index 8f156d2..b259d84 100644 --- a/public/locales/ko/tools.json +++ b/public/locales/ko/tools.json @@ -289,6 +289,36 @@ "loadingVips": "이미지 프로세서 로드 중...", "converting": "TIFF로 변환 중..." }, + "pdfToCbz": { + "name": "PDF를 CBZ로", + "subtitle": "PDF를 만화 리더용 CBZ(만화책 아카이브) 파일로 변환합니다.", + "imageFormat": "이미지 형식", + "quality": "이미지 품질", + "qualityExplanation": "품질이 높을수록 파일 크기가 커집니다", + "scale": "배율", + "scaleExplanation": "배율이 높을수록 고해상도 화면에서 품질이 좋아집니다", + "grayscale": "흑백으로 변환", + "manga": "만화 모드 (오른쪽에서 왼쪽으로)", + "includeMetadata": "ComicInfo.xml 메타데이터 포함", + "titleLabel": "제목", + "seriesLabel": "시리즈", + "authorLabel": "저자", + "numberLabel": "#", + "volumeLabel": "권", + "publisherLabel": "출판사", + "tagsLabel": "태그 / 장르", + "yearLabel": "출판 연도", + "ratingLabel": "평점 (0-5)", + "converting": "CBZ로 변환 중...", + "alert": { + "invalidFile": "잘못된 파일", + "invalidFileExplanation": "PDF 파일을 업로드해 주세요.", + "noFile": "파일 없음", + "noFileExplanation": "먼저 PDF 파일을 업로드해 주세요.", + "conversionSuccess": "PDF가 CBZ로 성공적으로 변환되었습니다!", + "conversionError": "PDF를 CBZ로 변환하지 못했습니다. 파일이 손상되었을 수 있습니다." + } + }, "pdfToGreyscale": { "name": "PDF 흑백 변환", "subtitle": "모든 색상을 흑백으로 변환합니다." diff --git a/public/locales/nl/tools.json b/public/locales/nl/tools.json index fea76c5..08709a9 100644 --- a/public/locales/nl/tools.json +++ b/public/locales/nl/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Beeldprocessor laden...", "converting": "Converteren naar TIFF..." }, + "pdfToCbz": { + "name": "PDF naar CBZ", + "subtitle": "Converteer een PDF naar een CBZ (Comic Book Archive) bestand voor striplezers.", + "imageFormat": "Afbeeldingsformaat", + "quality": "Beeldkwaliteit", + "qualityExplanation": "Hogere kwaliteit = groter bestandsformaat", + "scale": "Schaal", + "scaleExplanation": "Hogere schaal = betere kwaliteit voor hoge-resolutieschermen", + "grayscale": "Converteren naar grijswaarden", + "manga": "Mangamodus (rechts-naar-links)", + "includeMetadata": "ComicInfo.xml-metadata opnemen", + "titleLabel": "Titel", + "seriesLabel": "Serie", + "authorLabel": "Auteur(s)", + "numberLabel": "#", + "volumeLabel": "Vol.", + "publisherLabel": "Uitgever", + "tagsLabel": "Tags / Genre", + "yearLabel": "Publicatiejaar", + "ratingLabel": "Beoordeling (0-5)", + "converting": "Converteren naar CBZ...", + "alert": { + "invalidFile": "Ongeldig bestand", + "invalidFileExplanation": "Upload een PDF-bestand.", + "noFile": "Geen bestand", + "noFileExplanation": "Upload eerst een PDF-bestand.", + "conversionSuccess": "PDF succesvol geconverteerd naar CBZ!", + "conversionError": "Kan PDF niet converteren naar CBZ. Het bestand is mogelijk beschadigd." + } + }, "pdfToGreyscale": { "name": "PDF naar Grijswaarden", "subtitle": "Converteer alle kleuren naar zwart-wit." diff --git a/public/locales/pt/tools.json b/public/locales/pt/tools.json index bc415b1..b9151eb 100644 --- a/public/locales/pt/tools.json +++ b/public/locales/pt/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Carregando processador de imagem...", "converting": "Convertendo para TIFF..." }, + "pdfToCbz": { + "name": "PDF para CBZ", + "subtitle": "Converta um PDF em um arquivo CBZ (Comic Book Archive) para leitores de quadrinhos.", + "imageFormat": "Formato de Imagem", + "quality": "Qualidade da Imagem", + "qualityExplanation": "Maior qualidade = maior tamanho de arquivo", + "scale": "Escala", + "scaleExplanation": "Maior escala = melhor qualidade para telas de alta resolução", + "grayscale": "Converter para tons de cinza", + "manga": "Modo mangá (direita para esquerda)", + "includeMetadata": "Incluir metadados ComicInfo.xml", + "titleLabel": "Título", + "seriesLabel": "Série", + "authorLabel": "Autor(es)", + "numberLabel": "#", + "volumeLabel": "Vol.", + "publisherLabel": "Editora", + "tagsLabel": "Tags / Gênero", + "yearLabel": "Ano de publicação", + "ratingLabel": "Avaliação (0-5)", + "converting": "Convertendo para CBZ...", + "alert": { + "invalidFile": "Arquivo Inválido", + "invalidFileExplanation": "Envie um arquivo PDF.", + "noFile": "Nenhum Arquivo", + "noFileExplanation": "Envie um arquivo PDF primeiro.", + "conversionSuccess": "PDF convertido para CBZ com sucesso!", + "conversionError": "Falha ao converter PDF para CBZ. O arquivo pode estar corrompido." + } + }, "pdfToGreyscale": { "name": "PDF para Tons de Cinza", "subtitle": "Converta todas as cores para preto e branco." diff --git a/public/locales/ru/tools.json b/public/locales/ru/tools.json index b1b44e4..dd24c3e 100644 --- a/public/locales/ru/tools.json +++ b/public/locales/ru/tools.json @@ -283,6 +283,36 @@ "loadingVips": "Загрузка обработчика изображений...", "converting": "Конвертация в TIFF..." }, + "pdfToCbz": { + "name": "PDF в CBZ", + "subtitle": "Конвертируйте PDF в файл CBZ (Comic Book Archive) для чтения комиксов.", + "imageFormat": "Формат изображения", + "quality": "Качество изображения", + "qualityExplanation": "Выше качество = больше размер файла", + "scale": "Масштаб", + "scaleExplanation": "Больше масштаб = лучшее качество для экранов с высоким разрешением", + "grayscale": "Конвертировать в оттенки серого", + "manga": "Режим манги (справа налево)", + "includeMetadata": "Включить метаданные ComicInfo.xml", + "titleLabel": "Название", + "seriesLabel": "Серия", + "authorLabel": "Автор(ы)", + "numberLabel": "#", + "volumeLabel": "Том", + "publisherLabel": "Издатель", + "tagsLabel": "Теги / Жанр", + "yearLabel": "Год издания", + "ratingLabel": "Рейтинг (0-5)", + "converting": "Конвертация в CBZ...", + "alert": { + "invalidFile": "Недопустимый файл", + "invalidFileExplanation": "Пожалуйста, загрузите PDF-файл.", + "noFile": "Нет файла", + "noFileExplanation": "Сначала загрузите PDF-файл.", + "conversionSuccess": "PDF успешно конвертирован в CBZ!", + "conversionError": "Не удалось конвертировать PDF в CBZ. Файл может быть повреждён." + } + }, "pdfToGreyscale": { "name": "Градации серого", "subtitle": "Преобразовать все цвета в градации серого." diff --git a/public/locales/sv/tools.json b/public/locales/sv/tools.json index 0e32a22..2e09e46 100644 --- a/public/locales/sv/tools.json +++ b/public/locales/sv/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Laddar bildprocessor...", "converting": "Konverterar till TIFF..." }, + "pdfToCbz": { + "name": "PDF till CBZ", + "subtitle": "Konvertera en PDF till en CBZ-fil (serietidningsarkiv) för serieläsare.", + "imageFormat": "Bildformat", + "quality": "Bildkvalitet", + "qualityExplanation": "Högre kvalitet = större filstorlek", + "scale": "Skala", + "scaleExplanation": "Högre skala = bättre kvalitet för högupplösta skärmar", + "grayscale": "Konvertera till gråskala", + "manga": "Manga-läge (höger till vänster)", + "includeMetadata": "Inkludera ComicInfo.xml-metadata", + "titleLabel": "Titel", + "seriesLabel": "Serie", + "authorLabel": "Författare", + "numberLabel": "#", + "volumeLabel": "Vol.", + "publisherLabel": "Förlag", + "tagsLabel": "Taggar / Genre", + "yearLabel": "Utgivningsår", + "ratingLabel": "Betyg (0-5)", + "converting": "Konverterar till CBZ...", + "alert": { + "invalidFile": "Ogiltig fil", + "invalidFileExplanation": "Vänligen ladda upp en PDF-fil.", + "noFile": "Ingen fil", + "noFileExplanation": "Vänligen ladda upp en PDF-fil först.", + "conversionSuccess": "PDF konverterad till CBZ framgångsrikt!", + "conversionError": "Kunde inte konvertera PDF till CBZ. Filen kan vara skadad." + } + }, "pdfToGreyscale": { "name": "PDF till gråskala", "subtitle": "Konvertera alla färger till svart och vitt." diff --git a/public/locales/tr/tools.json b/public/locales/tr/tools.json index fdcdf95..839bcb5 100644 --- a/public/locales/tr/tools.json +++ b/public/locales/tr/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Görüntü işlemci yükleniyor...", "converting": "TIFF'e dönüştürülüyor..." }, + "pdfToCbz": { + "name": "PDF'den CBZ'ye", + "subtitle": "Çizgi roman okuyucuları için bir PDF'yi CBZ (Çizgi Roman Arşivi) dosyasına dönüştürün.", + "imageFormat": "Görüntü Formatı", + "quality": "Görüntü Kalitesi", + "qualityExplanation": "Daha yüksek kalite = daha büyük dosya boyutu", + "scale": "Ölçek", + "scaleExplanation": "Daha yüksek ölçek = yüksek çözünürlüklü ekranlar için daha iyi kalite", + "grayscale": "Gri tonlamaya dönüştür", + "manga": "Manga modu (sağdan sola)", + "includeMetadata": "ComicInfo.xml meta verilerini dahil et", + "titleLabel": "Başlık", + "seriesLabel": "Seri", + "authorLabel": "Yazar(lar)", + "numberLabel": "#", + "volumeLabel": "Cilt", + "publisherLabel": "Yayıncı", + "tagsLabel": "Etiketler / Tür", + "yearLabel": "Yayın Yılı", + "ratingLabel": "Puan (0-5)", + "converting": "CBZ'ye dönüştürülüyor...", + "alert": { + "invalidFile": "Geçersiz Dosya", + "invalidFileExplanation": "Lütfen bir PDF dosyası yükleyin.", + "noFile": "Dosya Yok", + "noFileExplanation": "Lütfen önce bir PDF dosyası yükleyin.", + "conversionSuccess": "PDF başarıyla CBZ'ye dönüştürüldü!", + "conversionError": "PDF, CBZ'ye dönüştürülemedi. Dosya bozuk olabilir." + } + }, "pdfToGreyscale": { "name": "PDF'yi Gri Tonlamaya Çevir", "subtitle": "Tüm renkleri siyah beyaza çevirin." diff --git a/public/locales/vi/tools.json b/public/locales/vi/tools.json index 2bf06aa..94f0ced 100644 --- a/public/locales/vi/tools.json +++ b/public/locales/vi/tools.json @@ -289,6 +289,36 @@ "loadingVips": "Đang tải bộ xử lý hình ảnh...", "converting": "Đang chuyển đổi sang TIFF..." }, + "pdfToCbz": { + "name": "PDF sang CBZ", + "subtitle": "Chuyển đổi PDF thành tệp CBZ (Kho lưu trữ truyện tranh) cho trình đọc truyện tranh.", + "imageFormat": "Định dạng hình ảnh", + "quality": "Chất lượng hình ảnh", + "qualityExplanation": "Chất lượng cao hơn = kích thước tệp lớn hơn", + "scale": "Tỷ lệ", + "scaleExplanation": "Tỷ lệ cao hơn = chất lượng tốt hơn cho màn hình độ phân giải cao", + "grayscale": "Chuyển sang thang xám", + "manga": "Chế độ manga (phải sang trái)", + "includeMetadata": "Bao gồm siêu dữ liệu ComicInfo.xml", + "titleLabel": "Tiêu đề", + "seriesLabel": "Bộ truyện", + "authorLabel": "Tác giả", + "numberLabel": "#", + "volumeLabel": "Tập", + "publisherLabel": "Nhà xuất bản", + "tagsLabel": "Thẻ / Thể loại", + "yearLabel": "Năm xuất bản", + "ratingLabel": "Đánh giá (0-5)", + "converting": "Đang chuyển đổi sang CBZ...", + "alert": { + "invalidFile": "Tệp không hợp lệ", + "invalidFileExplanation": "Vui lòng tải lên tệp PDF.", + "noFile": "Không có tệp", + "noFileExplanation": "Vui lòng tải lên tệp PDF trước.", + "conversionSuccess": "Chuyển đổi PDF sang CBZ thành công!", + "conversionError": "Không thể chuyển đổi PDF sang CBZ. Tệp có thể bị hỏng." + } + }, "pdfToGreyscale": { "name": "PDF sang thang xám", "subtitle": "Chuyển đổi tất cả màu sắc sang đen trắng." diff --git a/public/locales/zh-TW/tools.json b/public/locales/zh-TW/tools.json index 307442b..e078887 100644 --- a/public/locales/zh-TW/tools.json +++ b/public/locales/zh-TW/tools.json @@ -289,6 +289,36 @@ "loadingVips": "正在載入影像處理器...", "converting": "正在轉換為 TIFF..." }, + "pdfToCbz": { + "name": "PDF 轉 CBZ", + "subtitle": "將 PDF 轉換為 CBZ (漫畫書封存) 檔案,適用於漫畫閱讀器。", + "imageFormat": "圖像格式", + "quality": "圖像品質", + "qualityExplanation": "品質越高 = 檔案越大", + "scale": "縮放", + "scaleExplanation": "縮放越高 = 高解析度螢幕上品質越好", + "grayscale": "轉換為灰階", + "manga": "漫畫模式 (從右到左)", + "includeMetadata": "包含 ComicInfo.xml 中繼資料", + "titleLabel": "標題", + "seriesLabel": "系列", + "authorLabel": "作者", + "numberLabel": "#", + "volumeLabel": "卷", + "publisherLabel": "出版社", + "tagsLabel": "標籤 / 類型", + "yearLabel": "出版年份", + "ratingLabel": "評分 (0-5)", + "converting": "正在轉換為 CBZ...", + "alert": { + "invalidFile": "無效檔案", + "invalidFileExplanation": "請上傳 PDF 檔案。", + "noFile": "沒有檔案", + "noFileExplanation": "請先上傳 PDF 檔案。", + "conversionSuccess": "PDF 已成功轉換為 CBZ!", + "conversionError": "無法將 PDF 轉換為 CBZ。檔案可能已損壞。" + } + }, "pdfToGreyscale": { "name": "PDF 轉灰階", "subtitle": "將所有顏色轉換為黑白。" diff --git a/public/locales/zh/tools.json b/public/locales/zh/tools.json index e2fdbb3..5dece16 100644 --- a/public/locales/zh/tools.json +++ b/public/locales/zh/tools.json @@ -289,6 +289,36 @@ "loadingVips": "正在加载图像处理器...", "converting": "正在转换为 TIFF..." }, + "pdfToCbz": { + "name": "PDF 转 CBZ", + "subtitle": "将 PDF 转换为 CBZ (漫画书归档) 文件,适用于漫画阅读器。", + "imageFormat": "图像格式", + "quality": "图像质量", + "qualityExplanation": "质量越高 = 文件越大", + "scale": "缩放", + "scaleExplanation": "缩放越高 = 高分辨率屏幕上质量越好", + "grayscale": "转换为灰度", + "manga": "漫画模式 (从右到左)", + "includeMetadata": "包含 ComicInfo.xml 元数据", + "titleLabel": "标题", + "seriesLabel": "系列", + "authorLabel": "作者", + "numberLabel": "#", + "volumeLabel": "卷", + "publisherLabel": "出版社", + "tagsLabel": "标签 / 类型", + "yearLabel": "出版年份", + "ratingLabel": "评分 (0-5)", + "converting": "正在转换为 CBZ...", + "alert": { + "invalidFile": "无效文件", + "invalidFileExplanation": "请上传 PDF 文件。", + "noFile": "没有文件", + "noFileExplanation": "请先上传 PDF 文件。", + "conversionSuccess": "PDF 已成功转换为 CBZ!", + "conversionError": "无法将 PDF 转换为 CBZ。文件可能已损坏。" + } + }, "pdfToGreyscale": { "name": "PDF 转 灰度", "subtitle": "将所有颜色转换为黑白。" diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 528d61c..21f4369 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -447,6 +447,13 @@ const baseCategories = [ icon: 'ph-file-image', subtitle: 'Convert each PDF page into a TIFF image.', }, + { + href: import.meta.env.BASE_URL + 'pdf-to-cbz.html', + name: 'PDF to CBZ', + icon: 'ph-book-open', + subtitle: + 'Convert a PDF into a CBZ (Comic Book Archive) file for comic readers.', + }, { href: import.meta.env.BASE_URL + 'pdf-to-svg.html', name: 'PDF to SVG', diff --git a/src/js/logic/pdf-to-cbz-page.ts b/src/js/logic/pdf-to-cbz-page.ts new file mode 100644 index 0000000..0f4cd97 --- /dev/null +++ b/src/js/logic/pdf-to-cbz-page.ts @@ -0,0 +1,500 @@ +import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { + downloadFile, + formatBytes, + readFileAsArrayBuffer, + getPDFDocument, + getCleanPdfFilename, +} from '../utils/helpers.js'; +import { createIcons, icons } from 'lucide'; +import JSZip from 'jszip'; +import * as pdfjsLib from 'pdfjs-dist'; +import { PDFPageProxy } from 'pdfjs-dist'; +import { t } from '../i18n/i18n'; +import { + generateComicInfoXml, + generateMetadataOpf, + generateComicBookInfoJson, +} from '../utils/comic-info.js'; +import type { CbzOptions, ComicMetadata } from '@/types'; + +pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/build/pdf.worker.min.mjs', + import.meta.url +).toString(); + +let files: File[] = []; + +function getOptions(): CbzOptions { + const formatInput = document.getElementById( + 'cbz-format' + ) as HTMLSelectElement; + const qualityInput = document.getElementById( + 'cbz-quality' + ) as HTMLInputElement; + const scaleInput = document.getElementById('cbz-scale') as HTMLInputElement; + const grayscaleInput = document.getElementById( + 'cbz-grayscale' + ) as HTMLInputElement; + const mangaInput = document.getElementById('cbz-manga') as HTMLInputElement; + const metadataInput = document.getElementById( + 'cbz-metadata' + ) as HTMLInputElement; + const titleInput = document.getElementById('cbz-title') as HTMLInputElement; + const seriesInput = document.getElementById('cbz-series') as HTMLInputElement; + const numberInput = document.getElementById('cbz-number') as HTMLInputElement; + const volumeInput = document.getElementById('cbz-volume') as HTMLInputElement; + const authorInput = document.getElementById('cbz-author') as HTMLInputElement; + const publisherInput = document.getElementById( + 'cbz-publisher' + ) as HTMLInputElement; + const tagsInput = document.getElementById('cbz-tags') as HTMLInputElement; + const yearInput = document.getElementById('cbz-year') as HTMLInputElement; + const ratingInput = document.getElementById('cbz-rating') as HTMLInputElement; + + return { + imageFormat: (formatInput?.value as CbzOptions['imageFormat']) || 'jpeg', + quality: qualityInput ? parseInt(qualityInput.value, 10) / 100 : 0.85, + scale: scaleInput ? parseFloat(scaleInput.value) : 2.0, + grayscale: grayscaleInput?.checked ?? false, + manga: mangaInput?.checked ?? false, + includeMetadata: metadataInput?.checked ?? true, + title: titleInput?.value?.trim() ?? '', + series: seriesInput?.value?.trim() ?? '', + number: numberInput?.value?.trim() ?? '', + volume: volumeInput?.value?.trim() ?? '', + author: authorInput?.value?.trim() ?? '', + publisher: publisherInput?.value?.trim() ?? '', + tags: tagsInput?.value?.trim() ?? '', + year: yearInput?.value?.trim() ?? '', + rating: ratingInput?.value?.trim() ?? '', + }; +} + +function getMimeType(format: CbzOptions['imageFormat']): string { + const mimeTypes: Record = { + jpeg: 'image/jpeg', + png: 'image/png', + webp: 'image/webp', + }; + return mimeTypes[format]; +} + +function getExtension(format: CbzOptions['imageFormat']): string { + const extensions: Record = { + jpeg: 'jpg', + png: 'png', + webp: 'webp', + }; + return extensions[format]; +} + +const updateUI = () => { + const fileDisplayArea = document.getElementById('file-display-area'); + const optionsPanel = document.getElementById('options-panel'); + const dropZone = document.getElementById('drop-zone'); + + if (!fileDisplayArea || !optionsPanel || !dropZone) return; + + fileDisplayArea.innerHTML = ''; + + if (files.length > 0) { + optionsPanel.classList.remove('hidden'); + + files.forEach((file) => { + const fileDiv = document.createElement('div'); + fileDiv.className = + 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm'; + + const infoContainer = document.createElement('div'); + infoContainer.className = 'flex flex-col overflow-hidden'; + + const nameSpan = document.createElement('div'); + nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1'; + nameSpan.textContent = file.name; + + const metaSpan = document.createElement('div'); + metaSpan.className = 'text-xs text-gray-400'; + metaSpan.textContent = `${formatBytes(file.size)} • ${t('common.loadingPageCount')}`; + + infoContainer.append(nameSpan, metaSpan); + + const removeBtn = document.createElement('button'); + removeBtn.className = + 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => { + files = []; + updateUI(); + }; + + fileDiv.append(infoContainer, removeBtn); + fileDisplayArea.appendChild(fileDiv); + + readFileAsArrayBuffer(file) + .then((buffer) => getPDFDocument(buffer).promise) + .then((pdf) => { + metaSpan.textContent = `${formatBytes(file.size)} • ${pdf.numPages} ${pdf.numPages !== 1 ? t('common.pages') : t('common.page')}`; + }) + .catch(() => { + metaSpan.textContent = formatBytes(file.size); + }); + }); + + createIcons({ icons }); + + const titleInput = document.getElementById('cbz-title') as HTMLInputElement; + if (titleInput && !titleInput.value) { + titleInput.value = getCleanPdfFilename(files[0].name); + } + } else { + optionsPanel.classList.add('hidden'); + } +}; + +const resetState = () => { + files = []; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) fileInput.value = ''; + + const textInputIds = [ + 'cbz-title', + 'cbz-series', + 'cbz-number', + 'cbz-volume', + 'cbz-author', + 'cbz-publisher', + 'cbz-tags', + 'cbz-year', + 'cbz-rating', + ]; + for (const id of textInputIds) { + const input = document.getElementById(id) as HTMLInputElement; + if (input) input.value = ''; + } + + updateUI(); +}; + +async function renderPage( + page: PDFPageProxy, + options: CbzOptions +): Promise { + const viewport = page.getViewport({ scale: options.scale }); + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + if (!context) throw new Error('Failed to acquire 2D canvas context'); + canvas.height = viewport.height; + canvas.width = viewport.width; + + await page.render({ + canvasContext: context, + viewport: viewport, + canvas, + }).promise; + + if (options.grayscale) { + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + for (let i = 0; i < data.length; i += 4) { + const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]; + data[i] = gray; + data[i + 1] = gray; + data[i + 2] = gray; + } + context.putImageData(imageData, 0, 0); + } + + const mimeType = getMimeType(options.imageFormat); + const quality = options.imageFormat === 'png' ? undefined : options.quality; + + const blob = await new Promise((resolve) => + canvas.toBlob(resolve, mimeType, quality) + ); + canvas.width = 0; + canvas.height = 0; + return blob; +} + +async function convert() { + if (files.length === 0) { + showAlert( + t('tools:pdfToCbz.alert.noFile'), + t('tools:pdfToCbz.alert.noFileExplanation') + ); + return; + } + + showLoader(t('tools:pdfToCbz.converting')); + + try { + const options = getOptions(); + const pdf = await getPDFDocument(await readFileAsArrayBuffer(files[0])) + .promise; + + if (pdf.numPages === 0) { + throw new Error('PDF has no pages'); + } + + const zip = new JSZip(); + const ext = getExtension(options.imageFormat); + const padLength = String(pdf.numPages).length; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const blob = await renderPage(page, options); + if (blob) { + const pageNum = String(i).padStart(padLength, '0'); + zip.file(`${pageNum}.${ext}`, blob); + } + } + + let zipComment = ''; + + if (options.includeMetadata) { + const meta: ComicMetadata = { + title: options.title || getCleanPdfFilename(files[0].name), + series: options.series || undefined, + number: options.number || undefined, + volume: options.volume || undefined, + writer: options.author || undefined, + publisher: options.publisher || undefined, + genre: options.tags || undefined, + year: options.year || undefined, + communityRating: options.rating || undefined, + pageCount: pdf.numPages, + manga: options.manga, + blackAndWhite: options.grayscale, + }; + + zip.file('ComicInfo.xml', generateComicInfoXml(meta)); + zip.file('metadata.opf', generateMetadataOpf(meta)); + zipComment = generateComicBookInfoJson(meta); + } + + const cbzBlob = await zip.generateAsync({ + type: 'blob', + comment: zipComment || undefined, + }); + downloadFile(cbzBlob, getCleanPdfFilename(files[0].name) + '.cbz'); + + showAlert( + t('common.success'), + t('tools:pdfToCbz.alert.conversionSuccess'), + 'success', + () => resetState() + ); + } catch (e) { + console.error(e); + showAlert(t('common.error'), t('tools:pdfToCbz.alert.conversionError')); + } finally { + hideLoader(); + } +} + +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 qualitySlider = document.getElementById( + 'cbz-quality' + ) as HTMLInputElement; + const qualityValue = document.getElementById('cbz-quality-value'); + const scaleSlider = document.getElementById('cbz-scale') as HTMLInputElement; + const scaleValue = document.getElementById('cbz-scale-value'); + const formatSelect = document.getElementById( + 'cbz-format' + ) as HTMLSelectElement; + const qualitySection = document.getElementById('quality-section'); + const metadataCheckbox = document.getElementById( + 'cbz-metadata' + ) as HTMLInputElement; + const metadataSection = document.getElementById('metadata-section'); + + if (backBtn) { + backBtn.addEventListener('click', () => { + window.location.href = import.meta.env.BASE_URL; + }); + } + + if (qualitySlider && qualityValue) { + qualitySlider.addEventListener('input', () => { + qualityValue.textContent = `${qualitySlider.value}%`; + }); + } + + if (scaleSlider && scaleValue) { + scaleSlider.addEventListener('input', () => { + scaleValue.textContent = `${parseFloat(scaleSlider.value).toFixed(1)}x`; + }); + } + + if (formatSelect && qualitySection) { + formatSelect.addEventListener('change', () => { + if (formatSelect.value === 'png') { + qualitySection.classList.add('hidden'); + } else { + qualitySection.classList.remove('hidden'); + } + }); + } + + if (metadataCheckbox && metadataSection) { + metadataCheckbox.addEventListener('change', () => { + if (metadataCheckbox.checked) { + metadataSection.classList.remove('hidden'); + } else { + metadataSection.classList.add('hidden'); + } + }); + } + + if (formatSelect?.value === 'png' && qualitySection) { + qualitySection.classList.add('hidden'); + } + if (metadataCheckbox && !metadataCheckbox.checked && metadataSection) { + metadataSection.classList.add('hidden'); + } + + function setInputValidity( + input: HTMLInputElement, + valid: boolean, + errorMsg: string + ): void { + const errorId = `${input.id}-error`; + let errorEl = document.getElementById(errorId); + + if (valid) { + input.classList.remove('border-red-500', 'focus:ring-red-500'); + input.classList.add('border-gray-600', 'focus:ring-indigo-500'); + if (errorEl) errorEl.remove(); + } else { + input.classList.remove('border-gray-600', 'focus:ring-indigo-500'); + input.classList.add('border-red-500', 'focus:ring-red-500'); + if (!errorEl) { + errorEl = document.createElement('p'); + errorEl.id = errorId; + errorEl.className = 'text-xs text-red-400 mt-1'; + input.parentElement?.appendChild(errorEl); + } + errorEl.textContent = errorMsg; + } + } + + const yearInput = document.getElementById('cbz-year') as HTMLInputElement; + const ratingInput = document.getElementById('cbz-rating') as HTMLInputElement; + const numberInput = document.getElementById('cbz-number') as HTMLInputElement; + const volumeInput = document.getElementById('cbz-volume') as HTMLInputElement; + + if (yearInput) { + yearInput.addEventListener('input', () => { + const val = yearInput.value.trim(); + if (val === '') { + setInputValidity(yearInput, true, ''); + return; + } + const year = parseInt(val, 10); + setInputValidity( + yearInput, + !isNaN(year) && year >= 1900 && year <= 2100, + 'Year must be between 1900 and 2100' + ); + }); + } + + if (ratingInput) { + ratingInput.addEventListener('input', () => { + const val = ratingInput.value.trim(); + if (val === '') { + setInputValidity(ratingInput, true, ''); + return; + } + const rating = parseFloat(val); + setInputValidity( + ratingInput, + !isNaN(rating) && rating >= 0 && rating <= 5, + 'Rating must be between 0 and 5' + ); + }); + } + + if (numberInput) { + numberInput.addEventListener('input', () => { + const val = numberInput.value.trim(); + if (val === '') { + setInputValidity(numberInput, true, ''); + return; + } + setInputValidity( + numberInput, + !isNaN(parseFloat(val)) && parseFloat(val) >= 0, + 'Must be a positive number' + ); + }); + } + + if (volumeInput) { + volumeInput.addEventListener('input', () => { + const val = volumeInput.value.trim(); + if (val === '') { + setInputValidity(volumeInput, true, ''); + return; + } + const vol = parseInt(val, 10); + setInputValidity( + volumeInput, + !isNaN(vol) && vol >= 1, + 'Must be a positive integer' + ); + }); + } + + const handleFileSelect = (newFiles: FileList | null) => { + if (!newFiles || newFiles.length === 0) return; + const validFiles = Array.from(newFiles).filter( + (file) => file.type === 'application/pdf' + ); + + if (validFiles.length === 0) { + showAlert( + t('tools:pdfToCbz.alert.invalidFile'), + t('tools:pdfToCbz.alert.invalidFileExplanation') + ); + return; + } + + files = [validFiles[0]]; + updateUI(); + }; + + if (fileInput && dropZone) { + fileInput.addEventListener('change', (e) => { + handleFileSelect((e.target as HTMLInputElement).files); + }); + + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.classList.add('bg-gray-700'); + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + dropZone.classList.remove('bg-gray-700'); + handleFileSelect(e.dataTransfer?.files ?? null); + }); + + fileInput.addEventListener('click', () => { + fileInput.value = ''; + }); + } + + if (processBtn) { + processBtn.addEventListener('click', convert); + } +}); diff --git a/src/js/main.ts b/src/js/main.ts index f27c050..4e686d9 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -179,6 +179,7 @@ const init = async () => { 'PDF to WebP': 'tools:pdfToWebp', 'PDF to BMP': 'tools:pdfToBmp', 'PDF to TIFF': 'tools:pdfToTiff', + 'PDF to CBZ': 'tools:pdfToCbz', 'PDF to Greyscale': 'tools:pdfToGreyscale', 'PDF to JSON': 'tools:pdfToJson', 'OCR PDF': 'tools:ocrPdf', diff --git a/src/js/types/index.ts b/src/js/types/index.ts index 9b8b0cd..3840c91 100644 --- a/src/js/types/index.ts +++ b/src/js/types/index.ts @@ -53,3 +53,4 @@ 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'; +export * from './pdf-to-cbz-type.ts'; diff --git a/src/js/types/pdf-to-cbz-type.ts b/src/js/types/pdf-to-cbz-type.ts new file mode 100644 index 0000000..2b91eb5 --- /dev/null +++ b/src/js/types/pdf-to-cbz-type.ts @@ -0,0 +1,32 @@ +export interface CbzOptions { + imageFormat: 'jpeg' | 'png' | 'webp'; + quality: number; + scale: number; + grayscale: boolean; + manga: boolean; + includeMetadata: boolean; + title: string; + series: string; + number: string; + volume: string; + author: string; + publisher: string; + tags: string; + year: string; + rating: string; +} + +export interface ComicMetadata { + title?: string; + series?: string; + number?: string; + volume?: string; + writer?: string; + publisher?: string; + genre?: string; + year?: string; + communityRating?: string; + pageCount: number; + manga?: boolean; + blackAndWhite?: boolean; +} diff --git a/src/js/utils/comic-info.ts b/src/js/utils/comic-info.ts new file mode 100644 index 0000000..ef0a600 --- /dev/null +++ b/src/js/utils/comic-info.ts @@ -0,0 +1,190 @@ +import type { ComicMetadata } from '@/types'; + +function escapeXml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +export function generateComicInfoXml(params: ComicMetadata): string { + const lines: string[] = [ + '', + '', + ]; + + if (params.title) { + lines.push(` ${escapeXml(params.title)}`); + } + + if (params.series) { + lines.push(` ${escapeXml(params.series)}`); + } + + if (params.number) { + lines.push(` ${escapeXml(params.number)}`); + } + + if (params.volume) { + lines.push(` ${escapeXml(params.volume)}`); + } + + if (params.writer) { + lines.push(` ${escapeXml(params.writer)}`); + } + + if (params.publisher) { + lines.push(` ${escapeXml(params.publisher)}`); + } + + if (params.genre) { + lines.push(` ${escapeXml(params.genre)}`); + } + + if (params.year) { + const yearNum = parseInt(params.year, 10); + if (!isNaN(yearNum)) { + lines.push(` ${yearNum}`); + } + } + + if (params.communityRating) { + const rating = parseFloat(params.communityRating); + if (!isNaN(rating) && rating >= 0 && rating <= 5) { + lines.push(` ${rating}`); + } + } + + lines.push(` ${params.pageCount}`); + + if (params.blackAndWhite) { + lines.push(' Yes'); + } + + if (params.manga) { + lines.push(' YesAndRightToLeft'); + } + + lines.push(''); + + return lines.join('\n'); +} + +export function generateMetadataOpf(params: ComicMetadata): string { + const lines: string[] = [ + '', + '', + ' ', + ]; + + if (params.title) { + lines.push(` ${escapeXml(params.title)}`); + } + + if (params.writer) { + lines.push( + ` ${escapeXml(params.writer)}` + ); + } + + if (params.publisher) { + lines.push( + ` ${escapeXml(params.publisher)}` + ); + } + + if (params.year) { + const yearNum = parseInt(params.year, 10); + if (!isNaN(yearNum)) { + lines.push(` ${yearNum}-01-01`); + } + } + + if (params.genre) { + lines.push(` ${escapeXml(params.genre)}`); + } + + if (params.series) { + lines.push( + ` ` + ); + if (params.number) { + lines.push( + ` ` + ); + } + } + + if (params.communityRating) { + const rating = parseFloat(params.communityRating); + if (!isNaN(rating) && rating >= 0 && rating <= 5) { + lines.push( + ` ` + ); + } + } + + lines.push(' '); + lines.push(''); + + return lines.join('\n'); +} + +export function generateComicBookInfoJson(params: ComicMetadata): string { + const info: Record = {}; + + if (params.title) { + info.title = params.title; + } + + if (params.series) { + info.series = params.series; + } + + if (params.number) { + info.issue = params.number; + } + + if (params.volume) { + info.volume = parseInt(params.volume, 10) || undefined; + } + + if (params.publisher) { + info.publisher = params.publisher; + } + + if (params.year) { + const yearNum = parseInt(params.year, 10); + if (!isNaN(yearNum)) { + info.publicationYear = yearNum; + } + } + + if (params.genre) { + info.tags = params.genre + .split(',') + .map((t) => t.trim()) + .filter(Boolean); + } + + if (params.writer) { + info.credits = [{ person: params.writer, role: 'Writer' }]; + } + + if (params.communityRating) { + const rating = parseFloat(params.communityRating); + if (!isNaN(rating) && rating >= 0 && rating <= 5) { + info.rating = rating; + } + } + + const wrapper = { + appID: 'BentoPDF/1.0', + lastModified: new Date().toISOString(), + 'ComicBookInfo/1.0': info, + }; + + return JSON.stringify(wrapper); +} diff --git a/src/pages/pdf-to-cbz.html b/src/pages/pdf-to-cbz.html new file mode 100644 index 0000000..7aae84a --- /dev/null +++ b/src/pages/pdf-to-cbz.html @@ -0,0 +1,652 @@ + + + + + + + + PDF to CBZ Converter Free Online - Comic Book Archive | BentoPDF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{> navbar }} + +
+
+ + +

+ PDF to CBZ +

+

+ Convert a PDF into a CBZ (Comic Book Archive) file for comic readers. +

+ +
+
+ +

+ Click to select a file + or drag and drop +

+

+ A single PDF file +

+

+ Your files never leave your device. +

+
+ +
+ +
+ + +
+
+ + + + + +
+

+ How It Works +

+
+
+
+ 1 +
+
+

Upload PDF

+

+ Select the PDF you want to convert to a comic book archive +

+
+
+
+
+ 2 +
+
+

+ Choose Settings +

+

+ Pick image format, quality, and optional metadata +

+
+
+
+
+ 3 +
+
+

Download CBZ

+

+ Get your .cbz file ready for any comic book reader +

+
+
+
+
+ +
+

+ Related PDF Tools +

+ +
+ +
+

+ Frequently Asked Questions +

+
+
+ + What is a CBZ file? + + +

+ CBZ (Comic Book ZIP) is a popular format for digital comics and + manga. It's simply a ZIP archive containing page images, supported + by most comic book readers like Komga, Kavita, and CDisplayEx. +

+
+
+ + Which image format should I use? + + +

+ JPEG is best for color comics (smaller files). PNG is ideal for + black-and-white manga (lossless quality). WebP offers the best + compression but may not work in older readers. +

+
+
+ + What is ComicInfo.xml? + + +

+ ComicInfo.xml is a metadata file embedded in the CBZ that stores + information like title, author, and reading direction. Comic library + managers like Komga and Kavita use it to organize your collection. +

+
+
+
+ + {{> footer }} + + + + + + + + + + + + + diff --git a/vite.config.ts b/vite.config.ts index 8c156a8..6a1d497 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -508,6 +508,7 @@ export default defineConfig(() => { 'pdf-to-jpg': resolve(__dirname, 'src/pages/pdf-to-jpg.html'), 'pdf-to-png': resolve(__dirname, 'src/pages/pdf-to-png.html'), 'pdf-to-tiff': resolve(__dirname, 'src/pages/pdf-to-tiff.html'), + 'pdf-to-cbz': resolve(__dirname, 'src/pages/pdf-to-cbz.html'), 'pdf-to-webp': resolve(__dirname, 'src/pages/pdf-to-webp.html'), 'pdf-to-docx': resolve(__dirname, 'src/pages/pdf-to-docx.html'), 'extract-images': resolve(__dirname, 'src/pages/extract-images.html'),