mirror of
https://github.com/gen2brain/cbconvert
synced 2025-10-14 02:28:51 +02:00
Add support for CBT
This commit is contained in:
10
README.md
10
README.md
@@ -9,7 +9,7 @@ It can convert comics to different formats to fit your various devices.
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
* reads RAR, ZIP, 7Z, CBR, CBZ, CB7, CBT, PDF, EPUB, and plain directory
|
* reads RAR, ZIP, 7Z, CBR, CBZ, CB7, CBT, PDF, EPUB, and plain directory
|
||||||
* always saves processed comics in CBZ (ZIP) archive format
|
* saves processed comics in CBZ (ZIP) archive format or CBT (TAR)
|
||||||
* images can be converted to JPEG, PNG, TIFF, WEBP, AVIF, or 4-Bit BMP (16 colors) file format
|
* images can be converted to JPEG, PNG, TIFF, WEBP, AVIF, or 4-Bit BMP (16 colors) file format
|
||||||
* rotate, flip, adjust brightness/contrast, adjust levels (Photoshop-like) or grayscale images
|
* rotate, flip, adjust brightness/contrast, adjust levels (Photoshop-like) or grayscale images
|
||||||
* resize algorithms (NearestNeighbor, Box, Linear, MitchellNetravali, CatmullRom, Gaussian, Lanczos)
|
* resize algorithms (NearestNeighbor, Box, Linear, MitchellNetravali, CatmullRom, Gaussian, Lanczos)
|
||||||
@@ -32,7 +32,7 @@ Copy cbconvert cli binary to your PATH and create file ~/.local/share/thumbnaile
|
|||||||
[Thumbnailer Entry]
|
[Thumbnailer Entry]
|
||||||
TryExec=cbconvert
|
TryExec=cbconvert
|
||||||
Exec=cbconvert thumbnail --quiet --width %s --outfile %o %i
|
Exec=cbconvert thumbnail --quiet --width %s --outfile %o %i
|
||||||
MimeType=application/pdf;application/x-pdf;image/pdf;application/x-cbz;application/x-cbr;application/x-cb7;application/x-cbt;application/epub+zip;application/vnd.comicbook-rar;application/vnd.comicbook+zip;
|
MimeType=application/pdf;application/x-cb7;application/x-cbt;application/epub+zip;application/vnd.comicbook-rar;application/vnd.comicbook+zip;
|
||||||
```
|
```
|
||||||
|
|
||||||
This is what it looks like in the PCManFM file manager:
|
This is what it looks like in the PCManFM file manager:
|
||||||
@@ -59,6 +59,8 @@ This is what it looks like in the PCManFM file manager:
|
|||||||
Best fit for required width and height (default "false")
|
Best fit for required width and height (default "false")
|
||||||
--format
|
--format
|
||||||
Image format, valid values are jpeg, png, tiff, bmp, webp, avif (default "jpeg")
|
Image format, valid values are jpeg, png, tiff, bmp, webp, avif (default "jpeg")
|
||||||
|
--archive
|
||||||
|
Archive format, valid values are zip, tar (default "zip")
|
||||||
--quality
|
--quality
|
||||||
Image quality (default "75")
|
Image quality (default "75")
|
||||||
--lossless
|
--lossless
|
||||||
@@ -156,9 +158,9 @@ This is what it looks like in the PCManFM file manager:
|
|||||||
--cover
|
--cover
|
||||||
Print cover name (default "false")
|
Print cover name (default "false")
|
||||||
--comment
|
--comment
|
||||||
Print comment (default "false")
|
Print zip comment (default "false")
|
||||||
--comment-body
|
--comment-body
|
||||||
Set comment (default "")
|
Set zip comment (default "")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
90
cbconvert.go
90
cbconvert.go
@@ -1,6 +1,7 @@
|
|||||||
package cbconvert
|
package cbconvert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
@@ -68,6 +69,8 @@ var filters = map[int]imaging.ResampleFilter{
|
|||||||
type Options struct {
|
type Options struct {
|
||||||
// Image format, valid values are jpeg, png, tiff, bmp, webp, avif
|
// Image format, valid values are jpeg, png, tiff, bmp, webp, avif
|
||||||
Format string
|
Format string
|
||||||
|
// Archive format, valid values are zip, tar
|
||||||
|
Archive string
|
||||||
// JPEG image quality
|
// JPEG image quality
|
||||||
Quality int
|
Quality int
|
||||||
// Lossless compression (avif)
|
// Lossless compression (avif)
|
||||||
@@ -701,6 +704,17 @@ func (c *Convertor) imEncode(i image.Image, fileName string) error {
|
|||||||
|
|
||||||
// archiveSave saves workdir to CBZ archive.
|
// archiveSave saves workdir to CBZ archive.
|
||||||
func (c *Convertor) archiveSave(fileName string) error {
|
func (c *Convertor) archiveSave(fileName string) error {
|
||||||
|
if c.Opts.Archive == "zip" {
|
||||||
|
return c.archiveSaveZip(fileName)
|
||||||
|
} else if c.Opts.Archive == "tar" {
|
||||||
|
return c.archiveSaveTar(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// archiveSaveZip saves workdir to CBZ archive.
|
||||||
|
func (c *Convertor) archiveSaveZip(fileName string) error {
|
||||||
if c.OnCompress != nil {
|
if c.OnCompress != nil {
|
||||||
c.OnCompress()
|
c.OnCompress()
|
||||||
}
|
}
|
||||||
@@ -709,50 +723,108 @@ func (c *Convertor) archiveSave(fileName string) error {
|
|||||||
|
|
||||||
zipFile, err := os.Create(zipName)
|
zipFile, err := os.Create(zipName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archiveSave: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
z := zip.NewWriter(zipFile)
|
z := zip.NewWriter(zipFile)
|
||||||
|
|
||||||
files, err := os.ReadDir(c.Workdir)
|
files, err := os.ReadDir(c.Workdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archiveSave: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
r, err := os.ReadFile(filepath.Join(c.Workdir, file.Name()))
|
r, err := os.ReadFile(filepath.Join(c.Workdir, file.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archiveSave: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := file.Info()
|
info, err := file.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archiveSave: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
zipInfo, err := zip.FileInfoHeader(info)
|
zipInfo, err := zip.FileInfoHeader(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archiveSave: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
zipInfo.Method = zip.Deflate
|
zipInfo.Method = zip.Deflate
|
||||||
w, err := z.CreateHeader(zipInfo)
|
w, err := z.CreateHeader(zipInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archiveSave: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = w.Write(r)
|
_, err = w.Write(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archiveSave: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = z.Close(); err != nil {
|
if err = z.Close(); err != nil {
|
||||||
return fmt.Errorf("archiveSave: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = zipFile.Close(); err != nil {
|
if err = zipFile.Close(); err != nil {
|
||||||
return fmt.Errorf("archiveSave: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.RemoveAll(c.Workdir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// archiveSaveTar saves workdir to CBT archive.
|
||||||
|
func (c *Convertor) archiveSaveTar(fileName string) error {
|
||||||
|
if c.OnCompress != nil {
|
||||||
|
c.OnCompress()
|
||||||
|
}
|
||||||
|
|
||||||
|
tarName := filepath.Join(c.Opts.Outdir, fmt.Sprintf("%s%s.cbt", c.baseNoExt(fileName), c.Opts.Suffix))
|
||||||
|
|
||||||
|
tarFile, err := os.Create(tarName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tw := tar.NewWriter(tarFile)
|
||||||
|
|
||||||
|
files, err := os.ReadDir(c.Workdir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
r, err := os.ReadFile(filepath.Join(c.Workdir, file.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := file.Info()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := tar.FileInfoHeader(info, info.Name())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tw.WriteHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tw.Write(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tw.Close(); err != nil {
|
||||||
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tarFile.Close(); err != nil {
|
||||||
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.RemoveAll(c.Workdir)
|
return os.RemoveAll(c.Workdir)
|
||||||
|
@@ -135,6 +135,7 @@ func parseFlags() (cbconvert.Options, []string) {
|
|||||||
convert.IntVar(&opts.Height, "height", 0, "Image height")
|
convert.IntVar(&opts.Height, "height", 0, "Image height")
|
||||||
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
|
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
|
||||||
convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif")
|
convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif")
|
||||||
|
convert.StringVar(&opts.Archive, "archive", "zip", "Archive format, valid values are zip, tar")
|
||||||
convert.IntVar(&opts.Quality, "quality", 75, "Image quality")
|
convert.IntVar(&opts.Quality, "quality", 75, "Image quality")
|
||||||
convert.BoolVar(&opts.Lossless, "lossless", false, "Lossless compression (avif)")
|
convert.BoolVar(&opts.Lossless, "lossless", false, "Lossless compression (avif)")
|
||||||
convert.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
|
convert.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
|
||||||
@@ -186,8 +187,8 @@ func parseFlags() (cbconvert.Options, []string) {
|
|||||||
meta := flag.NewFlagSet("meta", flag.ExitOnError)
|
meta := flag.NewFlagSet("meta", flag.ExitOnError)
|
||||||
meta.SortFlags = false
|
meta.SortFlags = false
|
||||||
meta.BoolVar(&opts.Cover, "cover", false, "Print cover name")
|
meta.BoolVar(&opts.Cover, "cover", false, "Print cover name")
|
||||||
meta.BoolVar(&opts.Comment, "comment", false, "Print comment")
|
meta.BoolVar(&opts.Comment, "comment", false, "Print zip comment")
|
||||||
meta.StringVar(&opts.CommentBody, "comment-body", "", "Set comment")
|
meta.StringVar(&opts.CommentBody, "comment-body", "", "Set zip comment")
|
||||||
|
|
||||||
convert.Usage = func() {
|
convert.Usage = func() {
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
|
_, _ = fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
|
||||||
|
Reference in New Issue
Block a user