22 Commits
Author SHA1 Message Date
Milan Nikolic a8b2f65ca5 Update README.md 2026-06-24 21:40:44 +02:00
Milan Nikolic 8f75ffc43b Split GUI files 2026-06-24 21:36:32 +02:00
Milan Nikolic aca062a3f2 Organize Input tab, add more tooltips 2026-06-24 21:18:19 +02:00
Milan Nikolic 9de98599a9 Add DPI option, issue #52 2026-06-24 20:44:29 +02:00
Milan Nikolic f65fc4bafa Turn Convert into Cancel button 2026-06-24 19:53:27 +02:00
Milan Nikolic 26a320ef9d Handle SORT_CB 2026-06-24 18:39:16 +02:00
Milan Nikolic c4f236bdf9 Add Command button, issue #33 2026-06-24 13:16:09 +02:00
Milan Nikolic 23ac71ee0c Add settings profiles, issue #34 and issue #58 2026-06-24 11:20:12 +02:00
Milan Nikolic 3f0ae41456 Add ZipLevel, issue #48 2026-06-24 07:55:16 +02:00
Milan Nikolic 04a047aa2e Add more tests 2026-06-24 07:10:02 +02:00
Milan Nikolic 1284b9ded7 Fix outdir and recursive, issue #41 2026-06-24 06:42:48 +02:00
Milan Nikolic b8d82e920c Fix comic chapters, issue #50 2026-06-24 04:50:10 +02:00
Milan Nikolic 3c81595e08 Add Combine option, issue #21 2026-06-24 04:37:38 +02:00
Milan Nikolic 0f6fbba1d7 Use Table instead of List 2026-06-23 19:42:06 +02:00
Milan Nikolic 47a10e9185 Update modules 2026-06-23 18:25:02 +02:00
Milan Nikolic a5817c3ba5 Add Lossless option 2026-06-23 18:16:11 +02:00
Milan Nikolic b7c422fe33 Add Effort option 2026-06-23 05:34:18 +02:00
Milan Nikolic 09630243fb Fix 4-bit 16 color palette 2026-06-23 04:39:03 +02:00
Milan Nikolic 59bc2b7a1e Remove dbus portal 2026-06-22 22:46:28 +02:00
Milan Nikolic 3a627d92e1 Use archives instead of go-unarr 2026-06-22 22:07:40 +02:00
Milan Nikolic f555f09e59 Update modules 2026-06-22 20:33:51 +02:00
Milan Nikolic ad4f522b1f Update README.md 2024-11-08 16:01:33 +01:00
24 changed files with 3289 additions and 1427 deletions
+141 -114
View File
@@ -26,6 +26,23 @@ Download the latest binaries from the [releases](https://github.com/gen2brain/cb
Linux Flatpak is available at [Flathub](https://flathub.org/apps/io.github.gen2brain.cbconvert).
### Compile
You must have `CGO_ENABLED=1`. Note that `Go` will disable cgo when cross-compiling.
Install to `GOBIN` (you can point `GOBIN` to e.g. `/usr/local/bin` or `~/.local/bin`):
`go install github.com/gen2brain/cbconvert/cmd/cbconvert@latest`
For GUI app, check [IUP](https://github.com/gen2brain/iup-go) requirements, and then install:
`go install github.com/gen2brain/cbconvert/cmd/cbconvert-gui@latest`
### Build tags
* `extlib` - use external `libmupdf` and `libunarr` libraries
* `pkgconfig` - enable pkg-config (used with `extlib`)
### Using cbconvert in file managers to generate FreeDesktop thumbnails
Copy/install `cbconvert` cli binary to your `PATH`, create file `~/.local/share/thumbnailers/cbconvert.thumbnailer`
@@ -39,115 +56,137 @@ This is what it looks like in the `PCManFM` file manager:
### Using command line app
```
    Usage: cbconvert <command> [<flags>] [file1 dir1 ... fileOrDirN]
Usage: cbconvert <command> [<flags>] [file1 dir1 ... fileOrDirN]
    Commands:
Commands:
      convert
            Convert archive or document
convert
Convert archive or document
        --width
            Image width (default "0")
        --height
            Image height (default "0")
        --fit
            Best fit for required width and height (default "false")
        --format
            Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg")
        --archive
            Archive format, valid values are zip, tar (default "zip")
        --quality
            Image quality (default "75")
        --filter
            0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
        --no-cover
            Do not convert the cover image (default "false")
        --no-rgb
            Do not convert images that have RGB colorspace (default "false")
        --no-nonimage
            Remove non-image files from the archive (default "false")
        --no-convert
            Do not transform or convert images (default "false")
        --grayscale
            Convert images to grayscale (monochromatic) (default "false")
        --rotate
            Rotate images, valid values are 0, 90, 180, 270 (default "0")
        --brightness
            Adjust the brightness of the images, must be in the range (-100, 100) (default "0")
        --contrast
            Adjust the contrast of the images, must be in the range (-100, 100) (default "0")
        --suffix
            Add suffix to file basename (default "")
        --outdir
            Output directory (default ".")
        --size
            Process only files larger than size (in MB) (default "0")
        --recursive
            Process subdirectories recursively (default "false")
        --quiet
            Hide console output (default "false")
--width
Image width (default "0")
--height
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--format
Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg")
--archive
Archive format, valid values are zip, tar (default "zip")
--zip-level
ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default (default "-1")
--quality
Image quality (default "75")
--effort
Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default (default "-1")
--lossless
Lossless compression (webp, avif, jxl), ignores quality (default "false")
--combine
Combine all inputs into a single archive (default "false")
--outfile
Output file name for --combine (default: first input + -combined) (default "")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
--no-cover
Do not convert the cover image (default "false")
--no-rgb
Do not convert images that have RGB colorspace (default "false")
--no-nonimage
Remove non-image files from the archive (default "false")
--no-convert
Do not transform or convert images (default "false")
--grayscale
Convert images to grayscale (monochromatic) (default "false")
--rotate
Rotate images, valid values are 0, 90, 180, 270 (default "0")
--brightness
Adjust the brightness of the images, must be in the range (-100, 100) (default "0")
--contrast
Adjust the contrast of the images, must be in the range (-100, 100) (default "0")
--suffix
Add suffix to file basename (default "")
--outdir
Output directory (default ".")
--size
Process only files larger than size (in MB) (default "0")
--recursive
Process subdirectories recursively (default "false")
--quiet
Hide console output (default "false")
      cover
            Extract cover
cover
Extract cover
        --width
            Image width (default "0")
        --height
            Image height (default "0")
        --fit
            Best fit for required width and height (default "false")
        --format
            Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg")
        --quality
            Image quality (default "75")
        --filter
            0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
        --outdir
            Output directory (default ".")
        --size
            Process only files larger than size (in MB) (default "0")
        --recursive
            Process subdirectories recursively (default "false")
        --quiet
            Hide console output (default "false")
--width
Image width (default "0")
--height
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--format
Image format, valid values are jpeg, png, tiff, bmp, webp, avif (default "jpeg")
--quality
Image quality (default "75")
--effort
Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default (default "-1")
--lossless
Lossless compression (webp, avif, jxl), ignores quality (default "false")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
--outdir
Output directory (default ".")
--size
Process only files larger than size (in MB) (default "0")
--recursive
Process subdirectories recursively (default "false")
--quiet
Hide console output (default "false")
      thumbnail
            Extract cover thumbnail (freedesktop spec.)
thumbnail
Extract cover thumbnail (freedesktop spec.)
        --width
            Image width (default "0")
        --height
            Image height (default "0")
        --fit
            Best fit for required width and height (default "false")
        --filter
            0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
        --outdir
            Output directory (default ".")
        --outfile
            Output file (default "")
        --size
            Process only files larger than size (in MB) (default "0")
        --recursive
            Process subdirectories recursively (default "false")
        --quiet
            Hide console output (default "false")
--width
Image width (default "0")
--height
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
--outdir
Output directory (default ".")
--outfile
Output file (default "")
--size
Process only files larger than size (in MB) (default "0")
--recursive
Process subdirectories recursively (default "false")
--quiet
Hide console output (default "false")
      meta
            CBZ metadata
meta
CBZ metadata
        --cover
            Print cover name (default "false")
        --comment
            Print zip comment (default "false")
        --comment-body
            Set zip comment (default "")
        --file-add
            Add file to archive (default "")
        --file-remove
            Remove file(s) from archive (glob pattern, i.e. *.xml) (default "")
--cover
Print cover name (default "false")
--comment
Print zip comment (default "false")
--comment-body
Set zip comment (default "")
--file-add
Add file to archive (default "")
--file-remove
Remove file from archive (glob pattern, i.e. *.xml) (default "")
version
Print version
```
### Examples
@@ -170,6 +209,10 @@ This is what it looks like in the `PCManFM` file manager:
`cbconvert --format avif --quality 50 --width 1280 --outdir ~/comics /media/comics/Misc/`
* Combine several issues into a single archive:
`cbconvert convert --combine --outfile Series.cbz --outdir ~/comics issue1.cbz issue2.cbr issue3.cb7`
### Quality settings
This table maps quality settings for JPEG to the respective AVIF and WEBP quality settings:
@@ -180,19 +223,3 @@ This table maps quality settings for JPEG to the respective AVIF and WEBP qualit
| AVIF quality | 48 | 51 | 56 | 64 |
| WEBP quality | 55 | 64 | 72 | 82 |
### Compile
You must have `CGO_ENABLED=1`. Note that `Go` will disable cgo when cross-compiling.
Install to `GOBIN` (you can point `GOBIN` to e.g. `/usr/local/bin` or `~/.local/bin`):
`go install github.com/gen2brain/cbconvert/cmd/cbconvert@latest`
For GUI app, check [IUP](https://github.com/gen2brain/iup-go) requirements, and then install:
`go install github.com/gen2brain/cbconvert/cmd/cbconvert-gui@latest`
### Build tags
* `extlib` - use external `libmupdf` and `libunarr` libraries
* `pkgconfig` - enable pkg-config (used with `extlib`)
+171 -13
View File
@@ -23,14 +23,24 @@ type Options struct {
Format string
// Archive format, valid values are zip, tar
Archive string
// ZIP compression level: -1 default, 0 store (no compression), 1-9 deflate (1 fastest, 9 smallest)
ZipLevel int
// JPEG image quality
Quality int
// Encoder speed/effort, format-specific: webp method 0-6, avif speed 0-10, jxl effort 1-10; -1 uses the format default
Effort int
// Combine all inputs into a single archive
Combine bool
// Lossless enables lossless compression (webp, avif, jxl), ignores quality
Lossless bool
// Image width
Width int
// Image height
Height int
// Best fit for required width and height
Fit bool
// Document rendering resolution in DPI (PDF, EPUB, etc.); 0 uses the default
DPI int
// 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
Filter int
// Do not convert the cover image
@@ -85,9 +95,13 @@ type Converter struct {
Opts Options
// Current working directory
Workdir string
// Page name prefix, set per input when combining
prefix string
// Input root for the current file, used to build recursive output paths
root string
// Number of files
Nfiles int
// Index of current file
// Index of the current file
CurrFile int
// Number of contents in archive/document
Ncontents int
@@ -107,6 +121,7 @@ type Converter struct {
type File struct {
Name string
Path string
Root string
Stat os.FileInfo
SizeHuman string
}
@@ -125,6 +140,8 @@ func NewOptions() Options {
o.Format = "jpeg"
o.Archive = "zip"
o.Quality = 75
o.Effort = -1
o.ZipLevel = -1
o.Filter = 2
return o
@@ -138,6 +155,66 @@ func New(o Options) *Converter {
return c
}
// Args returns the non-default options as cbconvert convert command-line flags.
func (o Options) Args() []string {
def := NewOptions()
var args []string
str := func(name, val, dflt string) {
if val != dflt {
args = append(args, "--"+name, val)
}
}
num := func(name string, val, dflt int) {
if val != dflt {
args = append(args, "--"+name, strconv.Itoa(val))
}
}
flag := func(name string, val bool) {
if val {
args = append(args, "--"+name)
}
}
num("width", o.Width, def.Width)
num("height", o.Height, def.Height)
flag("fit", o.Fit)
num("dpi", o.DPI, def.DPI)
str("format", o.Format, def.Format)
str("archive", o.Archive, def.Archive)
num("zip-level", o.ZipLevel, def.ZipLevel)
num("quality", o.Quality, def.Quality)
num("effort", o.Effort, def.Effort)
flag("lossless", o.Lossless)
flag("combine", o.Combine)
str("outfile", o.OutFile, def.OutFile)
num("filter", o.Filter, def.Filter)
flag("no-cover", o.NoCover)
flag("no-rgb", o.NoRGB)
flag("no-nonimage", o.NoNonImage)
flag("no-convert", o.NoConvert)
str("suffix", o.Suffix, def.Suffix)
flag("grayscale", o.Grayscale)
num("rotate", o.Rotate, def.Rotate)
num("brightness", o.Brightness, def.Brightness)
num("contrast", o.Contrast, def.Contrast)
flag("recursive", o.Recursive)
str("outdir", o.OutDir, def.OutDir)
num("size", o.Size, def.Size)
return args
}
// renderDPI returns the document rendering resolution, falling back to 300 when unset.
func (c *Converter) renderDPI() float64 {
if c.Opts.DPI > 0 {
return float64(c.Opts.DPI)
}
return 300
}
// Cancel cancels the operation.
func (c *Converter) Cancel() {
if c.OnCancel != nil {
@@ -148,11 +225,13 @@ func (c *Converter) Cancel() {
// Files returns list of found comic files.
func (c *Converter) Files(args []string) ([]File, error) {
var files []File
var root string
toFile := func(fp string, f os.FileInfo) File {
var file File
file.Name = filepath.Base(fp)
file.Path = fp
file.Root = root
file.Stat = f
file.SizeHuman = humanize.IBytes(uint64(f.Size()))
return file
@@ -205,12 +284,14 @@ func (c *Converter) Files(args []string) ([]File, error) {
}
if !stat.IsDir() {
root = filepath.Dir(path)
if isArchive(path) || isDocument(path) {
if isSize(int64(c.Opts.Size), stat.Size()) {
files = append(files, toFile(path, stat))
}
}
} else {
root = path
if c.Opts.Recursive {
if err := filepath.Walk(path, walkFiles); err != nil {
return files, fmt.Errorf("%s: %w", arg, err)
@@ -252,9 +333,26 @@ func (c *Converter) Files(args []string) ([]File, error) {
return files, nil
}
// recursiveDir mirrors the source path under OutDir, relative to the input root.
func (c *Converter) recursiveDir(fileName string) string {
dir := filepath.Dir(fileName)
if c.root != "" {
if rel, err := filepath.Rel(c.root, dir); err == nil {
return filepath.Join(c.Opts.OutDir, rel)
}
}
dir = strings.TrimPrefix(dir[len(filepath.VolumeName(dir)):], string(os.PathSeparator))
return filepath.Join(c.Opts.OutDir, dir)
}
// Cover extracts cover.
func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error {
func (c *Converter) Cover(file File) error {
c.CurrFile++
c.root = file.Root
fileName, fileInfo := file.Path, file.Stat
cover, err := c.coverImage(fileName, fileInfo)
if err != nil {
@@ -276,13 +374,12 @@ func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error {
var fName string
if c.Opts.Recursive {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
if err != nil {
outDir := c.recursiveDir(fileName)
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("%s: %w", fileName, err)
}
fName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%s.%s", baseNoExt(fileName), ext))
fName = filepath.Join(outDir, fmt.Sprintf("%s.%s", baseNoExt(fileName), ext))
} else {
fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s.%s", baseNoExt(fileName), ext))
}
@@ -301,8 +398,10 @@ func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error {
}
// Thumbnail extracts thumbnail.
func (c *Converter) Thumbnail(fileName string, fileInfo os.FileInfo) error {
func (c *Converter) Thumbnail(file File) error {
c.CurrFile++
c.root = file.Root
fileName, fileInfo := file.Path, file.Stat
cover, err := c.coverImage(fileName, fileInfo)
if err != nil {
@@ -343,13 +442,12 @@ func (c *Converter) Thumbnail(fileName string, fileInfo os.FileInfo) error {
fURI = "file://" + fileName
if c.Opts.Recursive {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
if err != nil {
outDir := c.recursiveDir(fileName)
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("%s: %w", fileName, err)
}
fName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%x.png", md5.Sum([]byte(fURI))))
fName = filepath.Join(outDir, fmt.Sprintf("%x.png", md5.Sum([]byte(fURI))))
} else {
fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%x.png", md5.Sum([]byte(fURI))))
}
@@ -486,14 +584,23 @@ func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height
return img, nil
}
// Convert converts comic book.
func (c *Converter) Convert(fileName string, fileInfo os.FileInfo) error {
// Convert converts a comic book.
func (c *Converter) Convert(file File) error {
c.CurrFile++
c.root = file.Root
fileName, fileInfo := file.Path, file.Stat
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c.OnCancel = cancel
c.prefix = ""
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("%s: %w", fileName, err)
}
switch {
case fileInfo.IsDir():
@@ -518,3 +625,54 @@ func (c *Converter) Convert(fileName string, fileInfo os.FileInfo) error {
return nil
}
// Combine merges multiple comic books into a single archive.
func (c *Converter) Combine(files []File) error {
if len(files) == 0 {
return nil
}
c.root = ""
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c.OnCancel = cancel
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("Combine: %w", err)
}
for i, file := range files {
c.CurrFile++
c.prefix = fmt.Sprintf("%04d_", i+1)
switch {
case file.Stat.IsDir():
err = c.convertDirectory(ctx, file.Path)
case isDocument(file.Path):
err = c.convertDocument(ctx, file.Path)
case isArchive(file.Path):
err = c.convertArchive(ctx, file.Path)
}
if err != nil {
return fmt.Errorf("%s: %w", file.Path, err)
}
}
out := c.Opts.OutFile
if out == "" {
out = baseNoExt(files[0].Path) + "-combined"
}
if err := c.archiveSave(out); err != nil {
return fmt.Errorf("Combine: %w", err)
}
c.OnCancel = nil
return nil
}
+114 -13
View File
@@ -3,13 +3,16 @@ package cbconvert
import (
"archive/tar"
"archive/zip"
"compress/flate"
"context"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/gen2brain/go-unarr"
"github.com/mholt/archives"
)
// archiveSave saves workdir to CBZ archive.
@@ -31,13 +34,12 @@ func (c *Converter) archiveSaveZip(fileName string) error {
var zipName string
if c.Opts.Recursive {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
if err != nil {
outDir := c.recursiveDir(fileName)
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
}
zipName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix))
zipName = filepath.Join(outDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix))
} else {
zipName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix))
}
@@ -49,6 +51,13 @@ func (c *Converter) archiveSaveZip(fileName string) error {
z := zip.NewWriter(zipFile)
if c.Opts.ZipLevel >= 1 {
level := c.Opts.ZipLevel
z.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
return flate.NewWriter(out, level)
})
}
files, err := os.ReadDir(c.Workdir)
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
@@ -71,6 +80,9 @@ func (c *Converter) archiveSaveZip(fileName string) error {
}
zipInfo.Method = zip.Deflate
if c.Opts.ZipLevel == 0 {
zipInfo.Method = zip.Store
}
w, err := z.CreateHeader(zipInfo)
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err)
@@ -106,13 +118,12 @@ func (c *Converter) archiveSaveTar(fileName string) error {
var tarName string
if c.Opts.Recursive {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
if err != nil {
outDir := c.recursiveDir(fileName)
if err := os.MkdirAll(outDir, 0755); err != nil {
return fmt.Errorf("archiveSaveTar: %w", err)
}
tarName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix))
tarName = filepath.Join(outDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix))
} else {
tarName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix))
}
@@ -172,17 +183,70 @@ func (c *Converter) archiveSaveTar(fileName string) error {
return nil
}
// archiveOpen identifies the archive and returns its extractor and a reader positioned at the start.
func archiveOpen(ctx context.Context, fileName string) (io.ReadCloser, archives.Extractor, io.Reader, error) {
file, err := os.Open(fileName)
if err != nil {
return nil, nil, nil, err
}
format, input, err := archives.Identify(ctx, fileName, file)
if err != nil {
file.Close()
return nil, nil, nil, err
}
ex, ok := format.(archives.Extractor)
if !ok {
file.Close()
return nil, nil, nil, fmt.Errorf("%s: unsupported archive format", fileName)
}
return file, ex, input, nil
}
// FileType returns the detected archive container, document extension, or "DIR".
func FileType(path string) string {
if isArchive(path) {
file, err := os.Open(path)
if err == nil {
defer file.Close()
format, _, err := archives.Identify(context.Background(), path, file)
if err == nil {
return strings.ToUpper(strings.TrimPrefix(format.Extension(), "."))
}
}
}
if info, err := os.Stat(path); err == nil && info.IsDir() {
return "DIR"
}
return strings.ToUpper(strings.TrimPrefix(filepath.Ext(path), "."))
}
// archiveList lists contents of archive.
func (c *Converter) archiveList(fileName string) ([]string, error) {
var contents []string
archive, err := unarr.NewArchive(fileName)
ctx := context.Background()
file, ex, input, err := archiveOpen(ctx, fileName)
if err != nil {
return contents, fmt.Errorf("archiveList: %w", err)
}
defer archive.Close()
defer file.Close()
contents, err = archive.List()
err = ex.Extract(ctx, input, func(ctx context.Context, f archives.FileInfo) error {
if f.IsDir() {
return nil
}
contents = append(contents, f.NameInArchive)
return nil
})
if err != nil {
return contents, fmt.Errorf("archiveList: %w", err)
}
@@ -190,6 +254,43 @@ func (c *Converter) archiveList(fileName string) ([]string, error) {
return contents, nil
}
// archiveFile returns the contents of a single named file from the archive.
func (c *Converter) archiveFile(fileName, name string) ([]byte, error) {
var data []byte
ctx := context.Background()
file, ex, input, err := archiveOpen(ctx, fileName)
if err != nil {
return nil, fmt.Errorf("archiveFile: %w", err)
}
defer file.Close()
err = ex.Extract(ctx, input, func(ctx context.Context, f archives.FileInfo) error {
if f.NameInArchive != name {
return nil
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
data, err = io.ReadAll(rc)
if err != nil {
return err
}
return fs.SkipAll
})
if err != nil {
return nil, fmt.Errorf("archiveFile: %w", err)
}
return data, nil
}
// archiveComment returns ZIP comment.
func (c *Converter) archiveComment(fileName string) (string, error) {
zr, err := zip.OpenReader(fileName)
@@ -265,7 +366,7 @@ func (c *Converter) archiveSetComment(fileName, commentBody string) error {
return nil
}
// archiveFileAdd adds file to archive.
// archiveFileAdd adds a file to the archive.
func (c *Converter) archiveFileAdd(fileName, newFileName string) error {
zr, err := zip.OpenReader(fileName)
if err != nil {
+72 -62
View File
@@ -3,7 +3,6 @@ package cbconvert
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/png"
@@ -16,24 +15,17 @@ import (
"github.com/gen2brain/avif"
"github.com/gen2brain/go-fitz"
"github.com/gen2brain/go-unarr"
"github.com/gen2brain/jpegli"
"github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp"
"github.com/jsummers/gobmp"
"github.com/mholt/archives"
"golang.org/x/image/tiff"
"golang.org/x/sync/errgroup"
)
// convertDocument converts PDF/EPUB document to CBZ.
func (c *Converter) convertDocument(ctx context.Context, fileName string) error {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
doc, err := fitz.New(fileName)
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
@@ -56,7 +48,7 @@ func (c *Converter) convertDocument(ctx context.Context, fileName string) error
return fmt.Errorf("convertDocument: %w", ctx.Err())
}
img, err := doc.Image(n)
img, err := doc.ImageDPI(n, c.renderDPI())
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
@@ -78,13 +70,6 @@ func (c *Converter) convertDocument(ctx context.Context, fileName string) error
// convertArchive converts archive to CBZ.
func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
contents, err := c.archiveList(fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
@@ -101,51 +86,56 @@ func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
cover := c.coverName(images)
archive, err := unarr.NewArchive(fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
defer archive.Close()
eg, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1)
for {
if ctx.Err() != nil {
return fmt.Errorf("convertArchive: %w", ctx.Err())
file, ex, input, err := archiveOpen(ctx, fileName)
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
defer file.Close()
err = ex.Extract(ctx, input, func(ctx context.Context, f archives.FileInfo) error {
if err := ctx.Err(); err != nil {
return err
}
err := archive.Entry()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return fmt.Errorf("convertArchive: %w", err)
if f.IsDir() {
return nil
}
data, err := archive.ReadAll()
pathName := f.NameInArchive
rc, err := f.Open()
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
pathName := archive.Name()
data, err := io.ReadAll(rc)
if err != nil {
rc.Close()
return fmt.Errorf("convertArchive: %w", err)
}
if err = rc.Close(); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
if isImage(pathName) {
if c.Opts.NoConvert {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
if err = copyFile(bytes.NewReader(data), c.workPath(flatName(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
return nil
}
if cover == pathName && c.Opts.NoCover {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
if err = copyFile(bytes.NewReader(data), c.workPath(flatName(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
return nil
}
var img image.Image
@@ -155,11 +145,11 @@ func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
}
if c.Opts.NoRGB && !isGrayScale(img) {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
if err = copyFile(bytes.NewReader(data), c.workPath(flatName(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
continue
return nil
}
if img != nil {
@@ -169,18 +159,23 @@ func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
}
} else {
if filepath.Ext(pathName) == ".DS_Store" || strings.Contains(pathName, "__MACOSX") {
continue
return nil
}
if !c.Opts.NoNonImage {
if err = copyFile(bytes.NewReader(data), filepath.Join(c.Workdir, filepath.Base(pathName))); err != nil {
if c.prefix == "" && !c.Opts.NoNonImage {
if err = copyFile(bytes.NewReader(data), c.workPath(flatName(pathName))); err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
}
}
return nil
})
if werr := eg.Wait(); werr != nil {
return fmt.Errorf("convertArchive: %w", werr)
}
err = eg.Wait()
if err != nil {
return fmt.Errorf("convertArchive: %w", err)
}
@@ -190,13 +185,6 @@ func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
// convertDirectory converts directory to CBZ.
func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error {
var err error
c.Workdir, err = os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
contents, err := imagesFromPath(dirPath)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
@@ -218,13 +206,18 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
return fmt.Errorf("convertDirectory: %w", ctx.Err())
}
rel, rerr := filepath.Rel(dirPath, img)
if rerr != nil {
rel = filepath.Base(img)
}
file, err := os.Open(img)
if err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
if isNonImage(img) && !c.Opts.NoNonImage {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
if isNonImage(img) && c.prefix == "" && !c.Opts.NoNonImage {
if err = copyFile(file, c.workPath(flatName(rel))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
@@ -235,7 +228,7 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
continue
} else if isImage(img) {
if c.Opts.NoConvert {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
if err = copyFile(file, c.workPath(flatName(rel))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
@@ -253,7 +246,7 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
}
if c.Opts.NoRGB && !isGrayScale(i) {
if err = copyFile(file, filepath.Join(c.Workdir, filepath.Base(img))); err != nil {
if err = copyFile(file, c.workPath(flatName(rel))); err != nil {
return fmt.Errorf("convertDirectory: %w", err)
}
@@ -270,7 +263,7 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
if i != nil {
eg.Go(func() error {
return c.imageConvert(ctx, i, index, img)
return c.imageConvert(ctx, i, index, rel)
})
}
}
@@ -284,6 +277,11 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
return nil
}
// workPath returns the path of name inside the workdir, with the combine prefix applied.
func (c *Converter) workPath(name string) string {
return filepath.Join(c.Workdir, c.prefix+name)
}
// imageConvert converts image.Image.
func (c *Converter) imageConvert(ctx context.Context, img image.Image, index int, pathName string) error {
err := ctx.Err()
@@ -303,9 +301,9 @@ func (c *Converter) imageConvert(ctx context.Context, img image.Image, index int
var fileName string
if pathName != "" {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%s.%s", baseNoExt(pathName), ext))
fileName = c.workPath(fmt.Sprintf("%s.%s", flatName(strings.TrimSuffix(pathName, filepath.Ext(pathName))), ext))
} else {
fileName = filepath.Join(c.Workdir, fmt.Sprintf("%03d.%s", index, ext))
fileName = c.workPath(fmt.Sprintf("%03d.%s", index, ext))
}
img = c.imageTransform(img)
@@ -389,11 +387,23 @@ func (c *Converter) imageEncode(img image.Image, w io.Writer) error {
opts.DCTMethod = jpegli.DefaultDCTMethod
err = jpegli.Encode(w, img, opts)
case "webp":
err = webp.Encode(w, img, webp.Options{Quality: c.Opts.Quality, Method: webp.DefaultMethod})
method := webp.DefaultMethod
if c.Opts.Effort >= 0 {
method = min(max(c.Opts.Effort, 0), 6)
}
err = webp.Encode(w, img, webp.Options{Quality: c.Opts.Quality, Method: method, Lossless: c.Opts.Lossless})
case "avif":
err = avif.Encode(w, img, avif.Options{Quality: c.Opts.Quality, Speed: avif.DefaultSpeed})
speed := avif.DefaultSpeed
if c.Opts.Effort >= 0 {
speed = min(max(c.Opts.Effort, 0), 10)
}
err = avif.Encode(w, img, avif.Options{Quality: c.Opts.Quality, Speed: speed, Lossless: c.Opts.Lossless})
case "jxl":
err = jpegxl.Encode(w, img, jpegxl.Options{Quality: c.Opts.Quality, Effort: jpegxl.DefaultEffort})
effort := jpegxl.DefaultEffort
if c.Opts.Effort >= 0 {
effort = min(max(c.Opts.Effort, 1), 10)
}
err = jpegxl.Encode(w, img, jpegxl.Options{Quality: c.Opts.Quality, Effort: effort, Lossless: c.Opts.Lossless})
case "bmp":
opts := &gobmp.EncoderOptions{}
opts.SupportTransparency(false)
+2 -13
View File
@@ -11,7 +11,6 @@ import (
"github.com/fvbommel/sortorder"
"github.com/gen2brain/go-fitz"
"github.com/gen2brain/go-unarr"
)
// coverArchive extracts cover from archive.
@@ -31,17 +30,7 @@ func (c *Converter) coverArchive(fileName string) (image.Image, error) {
cover := c.coverName(images)
archive, err := unarr.NewArchive(fileName)
if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
defer archive.Close()
if err = archive.EntryFor(cover); err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
data, err := archive.ReadAll()
data, err := c.archiveFile(fileName, cover)
if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err)
}
@@ -63,7 +52,7 @@ func (c *Converter) coverDocument(fileName string) (image.Image, error) {
}
defer doc.Close()
img, err := doc.Image(0)
img, err := doc.ImageDPI(0, c.renderDPI())
if err != nil {
return nil, fmt.Errorf("coverDocument: %w", err)
}
+7 -1
View File
@@ -59,7 +59,7 @@ func imagesFromSlice(files []string) []string {
return images
}
// isArchive checks if file is archive.
// isArchive checks if a file is archive.
func isArchive(f string) bool {
var types = []string{".rar", ".zip", ".7z", ".tar", ".cbr", ".cbz", ".cb7", ".cbt"}
for _, t := range types {
@@ -123,6 +123,12 @@ func baseNoExt(filename string) string {
return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
}
// flatName flattens a path into a single collision-free name by replacing separators.
func flatName(name string) string {
name = strings.ReplaceAll(name, "\\", "/")
return strings.ReplaceAll(name, "/", "_")
}
// copyFile copies reader to file.
func copyFile(reader io.Reader, filename string) error {
err := os.MkdirAll(filepath.Dir(filename), 0755)
+19 -19
View File
@@ -22,7 +22,7 @@ const (
mitchellNetravali
// CatmullRom is a sharp bicubic filter.
catmullRom
// Gaussian is a blurring filter that uses gaussian function, useful for noise removal.
// Gaussian is a blurring filter, which uses gaussian function, useful for noise removal.
gaussian
// Lanczos is a high-quality resampling filter, it's slower than cubic filters.
lanczos
@@ -124,7 +124,7 @@ func imageToGray(src image.Image) *image.Gray {
return dst
}
// isGrayScale checks if image is grayscale.
// isGrayScale checks if the image is grayscale.
func isGrayScale(img image.Image) bool {
model := img.ColorModel()
if model == color.GrayModel || model == color.Gray16Model {
@@ -135,29 +135,29 @@ func isGrayScale(img image.Image) bool {
}
var colors16 = []color.Color{
color.RGBA{0, 0, 0, 255},
color.RGBA{17, 17, 17, 255},
color.RGBA{34, 34, 34, 255},
color.RGBA{51, 51, 51, 255},
color.RGBA{68, 68, 68, 255},
color.RGBA{85, 85, 85, 255},
color.RGBA{102, 102, 102, 255},
color.RGBA{119, 119, 119, 255},
color.RGBA{136, 136, 136, 255},
color.RGBA{153, 153, 153, 255},
color.RGBA{170, 170, 170, 255},
color.RGBA{187, 187, 187, 255},
color.RGBA{204, 204, 204, 255},
color.RGBA{221, 221, 221, 255},
color.RGBA{238, 238, 238, 255},
color.RGBA{255, 255, 255, 255},
color.RGBA{A: 255},
color.RGBA{R: 17, G: 17, B: 17, A: 255},
color.RGBA{R: 34, G: 34, B: 34, A: 255},
color.RGBA{R: 51, G: 51, B: 51, A: 255},
color.RGBA{R: 68, G: 68, B: 68, A: 255},
color.RGBA{R: 85, G: 85, B: 85, A: 255},
color.RGBA{R: 102, G: 102, B: 102, A: 255},
color.RGBA{R: 119, G: 119, B: 119, A: 255},
color.RGBA{R: 136, G: 136, B: 136, A: 255},
color.RGBA{R: 153, G: 153, B: 153, A: 255},
color.RGBA{R: 170, G: 170, B: 170, A: 255},
color.RGBA{R: 187, G: 187, B: 187, A: 255},
color.RGBA{R: 204, G: 204, B: 204, A: 255},
color.RGBA{R: 221, G: 221, B: 221, A: 255},
color.RGBA{R: 238, G: 238, B: 238, A: 255},
color.RGBA{R: 255, G: 255, B: 255, A: 255},
}
// imageToPaletted converts an image.Image to *image.Paletted using 16-color palette.
func imageToPaletted(src image.Image) *image.Paletted {
b := src.Bounds()
dst := image.NewPaletted(b, colors16)
draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src)
draw.FloydSteinberg.Draw(dst, b, imageToGray(src), b.Min)
return dst
}
+631 -3
View File
@@ -1,9 +1,12 @@
package cbconvert
import (
"archive/zip"
"fmt"
"image"
"os"
"path/filepath"
"strings"
"testing"
)
@@ -29,7 +32,7 @@ func TestConvert(t *testing.T) {
for _, file := range files {
conv.Opts.Suffix = fmt.Sprintf("_%s%s", format, filepath.Ext(file.Path))
err = conv.Convert(file.Path, file.Stat)
err = conv.Convert(file)
if err != nil {
t.Errorf("format %s: file %s: %v", format, file.Name, err)
}
@@ -59,7 +62,7 @@ func TestCover(t *testing.T) {
}
for _, file := range files {
err = conv.Cover(file.Path, file.Stat)
err = conv.Cover(file)
if err != nil {
t.Error(err)
}
@@ -88,7 +91,7 @@ func TestThumbnail(t *testing.T) {
}
for _, file := range files {
err = conv.Thumbnail(file.Path, file.Stat)
err = conv.Thumbnail(file)
if err != nil {
t.Error(err)
}
@@ -99,3 +102,628 @@ func TestThumbnail(t *testing.T) {
t.Error(err)
}
}
func TestArgs(t *testing.T) {
opts := NewOptions()
if got := opts.Args(); len(got) != 0 {
t.Errorf("defaults should emit no flags, got %v", got)
}
opts.Format = "webp"
opts.Quality = 90
opts.Effort = 4
opts.Lossless = true
opts.Width = 1200
opts.DPI = 150
opts.Grayscale = true
opts.OutDir = "/out"
got := strings.Join(opts.Args(), " ")
want := "--width 1200 --dpi 150 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out"
if got != want {
t.Errorf("Args() = %q, want %q", got, want)
}
}
func TestConvertDPI(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
dims := func(dpi int) int {
opts := NewOptions()
opts.OutDir = tmpDir
opts.DPI = dpi
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.pdf"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
return firstPage(t, conv, filepath.Join(tmpDir, "test.cbz")).Bounds().Dx()
}
low := dims(150)
high := dims(600)
if low >= high {
t.Errorf("higher DPI should render larger pages: 150dpi=%d, 600dpi=%d", low, high)
}
}
func TestConvertResize(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
opts.Width = 100
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
img := firstPage(t, conv, filepath.Join(tmpDir, "test.cbz"))
if got := img.Bounds().Dx(); got != 100 {
t.Errorf("resized width: got %d, want 100", got)
}
}
func TestConvertFit(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
opts.Width = 120
opts.Height = 120
opts.Fit = true
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
img := firstPage(t, conv, filepath.Join(tmpDir, "test.cbz"))
w, h := img.Bounds().Dx(), img.Bounds().Dy()
if w > 120 || h > 120 {
t.Errorf("fit exceeded bounds: got %dx%d, want <= 120x120", w, h)
}
if w != 120 && h != 120 {
t.Errorf("fit did not touch a bound: got %dx%d, want one side == 120", w, h)
}
}
func TestConvertTar(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
opts.Archive = "tar"
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
out := filepath.Join(tmpDir, "test.cbt")
list, err := conv.archiveList(out)
if err != nil {
t.Fatalf("read tar output: %v", err)
}
if len(list) != 2 {
t.Errorf("expected 2 pages in tar output, got %d: %v", len(list), list)
}
}
func TestZipLevel(t *testing.T) {
convertWith := func(level int) *zip.ReadCloser {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.RemoveAll(tmpDir) })
opts := NewOptions()
opts.OutDir = tmpDir
opts.ZipLevel = level
opts.NoConvert = true
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
zr, err := zip.OpenReader(filepath.Join(tmpDir, "test.cbz"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { zr.Close() })
return zr
}
store := convertWith(0)
for _, f := range store.File {
if f.Method != zip.Store {
t.Errorf("level 0: %s stored with method %d, want Store", f.Name, f.Method)
}
if f.CompressedSize64 != f.UncompressedSize64 {
t.Errorf("level 0: %s is compressed (%d < %d)", f.Name, f.CompressedSize64, f.UncompressedSize64)
}
}
deflate := convertWith(9)
for _, f := range deflate.File {
if f.Method != zip.Deflate {
t.Errorf("level 9: %s method %d, want Deflate", f.Name, f.Method)
}
}
}
func TestImageTransforms(t *testing.T) {
conv := New(NewOptions())
f, err := os.Open("testdata/test/00.jpg")
if err != nil {
t.Fatal(err)
}
defer f.Close()
src, err := conv.imageDecode(f)
if err != nil {
t.Fatal(err)
}
srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy()
conv.Opts.Rotate = 90
rotated := conv.imageTransform(src)
if rotated.Bounds().Dx() != srcH || rotated.Bounds().Dy() != srcW {
t.Errorf("rotate 90: got %dx%d, want %dx%d", rotated.Bounds().Dx(), rotated.Bounds().Dy(), srcH, srcW)
}
conv.Opts = NewOptions()
conv.Opts.Grayscale = true
gray := conv.imageTransform(src)
if !isGrayScale(gray) {
t.Errorf("grayscale: result is not grayscale")
}
conv.Opts = NewOptions()
conv.Opts.Brightness = 20
conv.Opts.Contrast = 20
adjusted := conv.imageTransform(src)
if adjusted.Bounds().Dx() != srcW || adjusted.Bounds().Dy() != srcH {
t.Errorf("brightness/contrast changed dimensions: got %dx%d, want %dx%d",
adjusted.Bounds().Dx(), adjusted.Bounds().Dy(), srcW, srcH)
}
}
func TestCoverName(t *testing.T) {
conv := New(NewOptions())
tests := []struct {
name string
images []string
want string
}{
{"empty", nil, ""},
{"natural sort", []string{"10.jpg", "2.jpg", "1.jpg"}, "1.jpg"},
{"cover prefix wins", []string{"01.jpg", "cover.jpg", "02.jpg"}, "cover.jpg"},
{"front prefix wins", []string{"01.jpg", "front.png", "00.jpg"}, "front.png"},
{"cover suffix wins", []string{"01.jpg", "page_cover.jpg"}, "page_cover.jpg"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := conv.coverName(tt.images); got != tt.want {
t.Errorf("coverName(%v) = %q, want %q", tt.images, got, tt.want)
}
})
}
}
func TestCoverDirectory(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
conv := New(opts)
files, err := conv.Files([]string{"testdata/test"})
if err != nil {
t.Fatal(err)
}
if len(files) != 1 {
t.Fatalf("expected 1 directory file, got %d", len(files))
}
for _, file := range files {
if err := conv.Cover(file); err != nil {
t.Fatal(err)
}
}
if _, err := os.Stat(filepath.Join(tmpDir, "test.jpg")); err != nil {
t.Errorf("directory cover not written: %v", err)
}
}
func TestFileType(t *testing.T) {
tests := []struct {
path string
want string
}{
{"testdata/test.cbz", "ZIP"},
{"testdata/test.cbr", "RAR"},
{"testdata/test.cb7", "7Z"},
{"testdata/test.cbt", "TAR"},
{"testdata/test.pdf", "PDF"},
{"testdata/test", "DIR"},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
if got := FileType(tt.path); got != tt.want {
t.Errorf("FileType(%q) = %q, want %q", tt.path, got, tt.want)
}
})
}
}
func TestCombine(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
opts.OutFile = "merged"
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.cbz", "testdata/test.cbt"})
if err != nil {
t.Fatal(err)
}
if len(files) != 2 {
t.Fatalf("expected 2 input files, got %d", len(files))
}
if err := conv.Combine(files); err != nil {
t.Fatal(err)
}
zr, err := zip.OpenReader(filepath.Join(tmpDir, "merged.cbz"))
if err != nil {
t.Fatalf("open combined archive: %v", err)
}
defer zr.Close()
var names []string
for _, f := range zr.File {
names = append(names, f.Name)
}
if len(names) != 4 {
t.Fatalf("expected 4 pages in combined archive, got %d: %v", len(names), names)
}
// each input is prefixed so identically named pages do not collide
var first, second int
for _, n := range names {
switch {
case strings.HasPrefix(n, "0001_"):
first++
case strings.HasPrefix(n, "0002_"):
second++
}
}
if first != 2 || second != 2 {
t.Errorf("expected 2 pages from each input, got 0001_=%d 0002_=%d: %v", first, second, names)
}
}
func TestSubfolders(t *testing.T) {
page0, err := os.ReadFile("testdata/test/00.jpg")
if err != nil {
t.Fatal(err)
}
page1, err := os.ReadFile("testdata/test/01.jpg")
if err != nil {
t.Fatal(err)
}
inDir, err := os.MkdirTemp(os.TempDir(), "cbc-in")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(inDir)
src := filepath.Join(inDir, "chapters.cbz")
buildZip(t, src, []zipEntry{
{"chapter1/00.jpg", page0},
{"chapter1/01.jpg", page1},
{"chapter2/00.jpg", page0},
{"chapter2/01.jpg", page1},
})
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc-out")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
opts := NewOptions()
opts.OutDir = tmpDir
conv := New(opts)
files, err := conv.Files([]string{src})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
zr, err := zip.OpenReader(filepath.Join(tmpDir, "chapters.cbz"))
if err != nil {
t.Fatalf("open output archive: %v", err)
}
defer zr.Close()
// without subfolder preservation chapter2/00 overwrites chapter1/00 and only 2 pages survive
if len(zr.File) != 4 {
var names []string
for _, f := range zr.File {
names = append(names, f.Name)
}
t.Fatalf("expected 4 pages from numbered subfolders, got %d: %v", len(zr.File), names)
}
}
func TestMeta(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// operate on a copy so the fixture stays intact
data, err := os.ReadFile("testdata/test.cbz")
if err != nil {
t.Fatal(err)
}
archive := filepath.Join(tmpDir, "meta.cbz")
if err := os.WriteFile(archive, data, 0644); err != nil {
t.Fatal(err)
}
conv := New(NewOptions())
conv.Opts = NewOptions()
conv.Opts.CommentBody = "hello world"
if _, err := conv.Meta(archive); err != nil {
t.Fatalf("set comment: %v", err)
}
conv.Opts = NewOptions()
conv.Opts.Comment = true
got, err := conv.Meta(archive)
if err != nil {
t.Fatalf("get comment: %v", err)
}
if got != "hello world" {
t.Errorf("comment roundtrip: got %q, want %q", got, "hello world")
}
extra := filepath.Join(tmpDir, "ComicInfo.xml")
if err := os.WriteFile(extra, []byte("<ComicInfo/>"), 0644); err != nil {
t.Fatal(err)
}
conv.Opts = NewOptions()
conv.Opts.FileAdd = extra
if _, err := conv.Meta(archive); err != nil {
t.Fatalf("add file: %v", err)
}
if !archiveHas(t, conv, archive, "ComicInfo.xml") {
t.Errorf("added file not found in archive")
}
conv.Opts = NewOptions()
conv.Opts.FileRemove = "ComicInfo.xml"
if _, err := conv.Meta(archive); err != nil {
t.Fatalf("remove file: %v", err)
}
if archiveHas(t, conv, archive, "ComicInfo.xml") {
t.Errorf("removed file still present in archive")
}
}
func TestRecursive(t *testing.T) {
inDir, err := os.MkdirTemp(os.TempDir(), "cbc-in")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(inDir)
sub := filepath.Join(inDir, "chapter1")
if err := os.MkdirAll(sub, 0755); err != nil {
t.Fatal(err)
}
src, err := os.ReadFile("testdata/test.cbz")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sub, "test.cbz"), src, 0644); err != nil {
t.Fatal(err)
}
outDir, err := os.MkdirTemp(os.TempDir(), "cbc-out")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(outDir)
opts := NewOptions()
opts.OutDir = outDir
opts.Recursive = true
conv := New(opts)
files, err := conv.Files([]string{inDir})
if err != nil {
t.Fatal(err)
}
if len(files) != 1 {
t.Fatalf("expected 1 file, got %d", len(files))
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Error(err)
}
}
// output must mirror the input subtree relative to the input root, not the absolute path
want := filepath.Join(outDir, "chapter1", "test.cbz")
if _, err := os.Stat(want); err != nil {
t.Errorf("expected output relative to input root at %s: %v", want, err)
}
}
type zipEntry struct {
name string
data []byte
}
func buildZip(t *testing.T, path string, entries []zipEntry) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
zw := zip.NewWriter(f)
for _, e := range entries {
w, err := zw.Create(e.name)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(e.data); err != nil {
t.Fatal(err)
}
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
}
func firstPage(t *testing.T, conv *Converter, archive string) image.Image {
t.Helper()
zr, err := zip.OpenReader(archive)
if err != nil {
t.Fatal(err)
}
defer zr.Close()
if len(zr.File) == 0 {
t.Fatalf("archive %s has no entries", archive)
}
rc, err := zr.File[0].Open()
if err != nil {
t.Fatal(err)
}
defer rc.Close()
img, err := conv.imageDecode(rc)
if err != nil {
t.Fatal(err)
}
return img
}
func archiveHas(t *testing.T, conv *Converter, archive, name string) bool {
t.Helper()
list, err := conv.archiveList(archive)
if err != nil {
t.Fatal(err)
}
for _, n := range list {
if n == name {
return true
}
}
return false
}
+80
View File
@@ -0,0 +1,80 @@
package main
import "github.com/gen2brain/iup-go/iup"
func fileDlg(title string, multiple, directory bool, dirKey string) ([]string, error) {
ret := make([]string, 0)
dlg := iup.FileDlg()
defer dlg.Destroy()
if !directory {
mf := "YES"
if !multiple {
mf = "NO"
}
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "OPEN",
"MULTIPLEFILES": mf,
"MULTIVALUEPATH": "YES",
"EXTFILTER": "Comic Files|*.rar;*.zip;*.7z;*.tar;*.cbr;*.cbz;*.cb7;*.cbt;*.pdf;*.epub;*.mobi;*.docx;*.pptx|",
"FILTER": "*.cb*", // for Motif
"TITLE": title,
})
} else {
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "DIR",
"TITLE": title,
})
}
setStartDir(dlg, dirKey)
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
if dlg.GetInt("STATUS") == 0 {
switch {
case multiple:
// MULTIVALUEPATH makes each MULTIVALUE a full path (id 0 is the path), so a folder-spanning selection works.
count := dlg.GetInt("MULTIVALUECOUNT")
if count > 1 {
for i := 1; i < count; i++ {
ret = append(ret, iup.GetAttributeId(dlg, "MULTIVALUE", i))
}
} else if value := dlg.GetAttribute("VALUE"); value != "" {
ret = append(ret, value)
}
default:
ret = append(ret, dlg.GetAttribute("VALUE"))
}
rememberDir(dlg, dirKey)
}
return ret, nil
}
func saveDlg(title, dirKey string) string {
dlg := iup.FileDlg()
defer dlg.Destroy()
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "SAVE",
"EXTFILTER": "Comic Files|*.cbz;*.cbt|",
"FILTER": "*.cb*", // for Motif
"TITLE": title,
})
setStartDir(dlg, dirKey)
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
if dlg.GetInt("STATUS") == -1 {
return ""
}
rememberDir(dlg, dirKey)
return dlg.GetAttribute("VALUE")
}
+39 -19
View File
@@ -1,36 +1,56 @@
module github.com/gen2brain/cbconvert/cmd/cbconvert-gui
go 1.23
go 1.26
require (
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca
github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4
github.com/godbus/dbus/v5 v5.1.0
github.com/gen2brain/cbconvert v1.0.5-0.20260623161611-a5817c3ba5de
github.com/gen2brain/iup-go/iup v0.32.1-0.20260604112206-a19da6654449
)
require (
github.com/anthonynsimon/bild v0.14.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/anthonynsimon/bild v0.15.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.4 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gen2brain/avif v0.4.1 // indirect
github.com/gen2brain/go-fitz v1.24.14 // indirect
github.com/gen2brain/go-unarr v0.2.4 // indirect
github.com/gen2brain/jpegli v0.3.3 // indirect
github.com/gen2brain/jpegxl v0.4.2 // indirect
github.com/gen2brain/webp v0.5.1 // indirect
github.com/gen2brain/avif v0.5.1 // indirect
github.com/gen2brain/go-fitz v1.24.15 // indirect
github.com/gen2brain/jpegli v0.4.0 // indirect
github.com/gen2brain/jpegxl v0.5.1 // indirect
github.com/gen2brain/webp v0.6.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/jupiterrider/ffi v0.2.1 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
github.com/jupiterrider/ffi v0.7.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mholt/archives v0.1.5 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.1.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.5 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/image v0.43.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/gen2brain/cbconvert => ../..
+98 -36
View File
@@ -1,5 +1,21 @@
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anthonynsimon/bild v0.15.0 h1:FzvaNLuNlAPKw1Xz7V2WYOcGIEBMj8Y6ZyAk7CI+HzA=
github.com/anthonynsimon/bild v0.15.0/go.mod h1:qIgJ9FldkCn0iy5Ad24fzUkz5R+iJ0WfhiV+6FeCB5A=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.4 h1:iHiVJfxbrB6RF4X+snI2MpVgNBKmVfGaTqZGNlMQIU0=
github.com/bodgit/sevenzip v1.6.4/go.mod h1:ZtNi5KNgHXeXg1G7WiF0IWSuFE2eG6lt/cTGlvuirO0=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
@@ -18,64 +34,110 @@ github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gen2brain/avif v0.4.1 h1:fjwv5SDNYHdI1gbW6MJn3Yaxs1ldUEfAIAH8Ahee538=
github.com/gen2brain/avif v0.4.1/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca h1:qYA+cXWUG/Dx+wG2yQ4iuw/s5ROGdXAdo5aIUHrOq4U=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca/go.mod h1:pQ9kQXsCZQqy7LraruUkBl7CpKNHFpWayxqQ+qliE8Y=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4=
github.com/gen2brain/go-unarr v0.2.4 h1:Iu2kqtGfkLBSQoTFwMkSCmp0g3GrEM/XMVWzo9TQr/Y=
github.com/gen2brain/go-unarr v0.2.4/go.mod h1:0kdy3HtjKBcEaewifXZguHCvt4qD9V8iJCx4FPEOWT8=
github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4 h1:ElkeiufJ5qqFbhxLH0TP7FhLOm37mXezIOk5iJgV27Q=
github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4/go.mod h1:F026AOq3tZrMCqTMa3K32xLh7fyTdc9+KMTMUX6EtX0=
github.com/gen2brain/jpegli v0.3.3 h1:ryCOQpmGuVk6FA+QBe9st6cW48jsRdVOPiNrAJ50m+k=
github.com/gen2brain/jpegli v0.3.3/go.mod h1:6Dbgr+ni1IUBqGVOKHn8lY+6DvwSGfAfC7pPQiSK6uA=
github.com/gen2brain/jpegxl v0.4.2 h1:Ff0jAWtCRdc9yjPc9jkyak6Ji/A89Jg0KI+D7qOEtRI=
github.com/gen2brain/jpegxl v0.4.2/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o=
github.com/gen2brain/webp v0.5.1 h1:ly9olTGveZEpq3soJuCmex9fxLJ0ipHcQRRSRit5EUE=
github.com/gen2brain/webp v0.5.1/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw=
github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/gen2brain/iup-go/iup v0.32.1-0.20260604112206-a19da6654449 h1:HPqLUcspf4aPnyQIgeFKlLmphFVpFP34fzr1ZdnNyfc=
github.com/gen2brain/iup-go/iup v0.32.1-0.20260604112206-a19da6654449/go.mod h1:V4f7tHOJAeHtjQ+ju795QKv6DGdLEb4L5cmWB1sjSzU=
github.com/gen2brain/jpegli v0.4.0 h1:TnmAO8EcekS/XCboyLPjP6cqz025tkhdaFkiYNd3wH8=
github.com/gen2brain/jpegli v0.4.0/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE=
github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 h1:q100v18oYIaE4p5myLFqDyfXTOxivzwGyLjGCEfv8x4=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jupiterrider/ffi v0.2.1 h1:08GJVDqz4eoQq7cKT1T0kwb9MB58XEAGjgxDvz80yBs=
github.com/jupiterrider/ffi v0.2.1/go.mod h1:tJ7Q8p/3blFjdWt5qJU4W5oDE0xloImvrViE+0td0Rk=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/nwaples/rardecode/v2 v2.2.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
github.com/nwaples/rardecode/v2 v2.2.5/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+442
View File
@@ -0,0 +1,442 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
// selectRow focuses and selects the given 0-based row in the table.
func selectRow(i int) {
if i < 0 || i >= len(files) {
return
}
index = i
iup.GetHandle("Table").SetAttribute("FOCUSCELL", fmt.Sprintf("%d:1", i+1))
}
// onSort re-syncs the files slice to the table's displayed order after a sort, so rows keep mapping to the right file.
func onSort(ih iup.Ihandle, col int) int {
n := len(files)
if n < 2 {
return iup.DEFAULT
}
rowKey := func(name, size string) string {
return name + "\x00" + size
}
buckets := make(map[string][]int, n)
for i, f := range files {
size := strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64)
k := rowKey(f.Name, size)
buckets[k] = append(buckets[k], i)
}
var selPath string
if index >= 0 && index < len(files) {
selPath = files[index].Path
}
reordered := make([]cbconvert.File, 0, n)
for lin := 1; lin <= n; lin++ {
k := rowKey(iup.GetAttributeId2(ih, "", lin, 1), iup.GetAttributeId2(ih, "", lin, 3))
idxs := buckets[k]
if len(idxs) == 0 {
return iup.DEFAULT
}
reordered = append(reordered, files[idxs[0]])
buckets[k] = idxs[1:]
}
files = reordered
index = -1
if selPath != "" {
for i, f := range files {
if f.Path == selPath {
selectRow(i)
break
}
}
}
return iup.DEFAULT
}
// appendFile adds a file as a new row to the table and the files slice.
func appendFile(file cbconvert.File) {
files = append(files, file)
t := iup.GetHandle("Table")
lin := len(files)
t.SetAttribute("NUMLIN", strconv.Itoa(lin))
iup.SetAttributeId2(t, "", lin, 1, file.Name)
iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(file.Path))
iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(file.Stat.Size())/(1024*1024), 'f', 2, 64))
}
func previewPost() {
if index == -1 || len(files) == 0 {
return
}
width, height := previewSize()
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" {
iup.GetHandle("Preview").SetAttribute("IMAGE", "")
}
opts := options()
go func(opts cbconvert.Options) {
conv := cbconvert.New(opts)
var s string
file := files[index]
img, err := conv.Preview(file.Path, file.Stat, width, height)
if err != nil {
s = err.Error()
fmt.Println(err)
}
iup.PostMessage(iup.GetHandle("Preview"), s, 0, img)
}(opts)
}
func onAddFiles(ih iup.Ihandle) int {
args, err := fileDlg("Add Files", true, false, inputDirKey)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) > 0 {
conv := cbconvert.New(options())
fs, err := conv.Files(args)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
wasEmpty := len(files) == 0
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
}
return iup.DEFAULT
}
func onAddDir(ih iup.Ihandle) int {
args, err := fileDlg("Add Directory", false, true, inputDirKey)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) > 0 {
conv := cbconvert.New(options())
fs, err := conv.Files(args)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
wasEmpty := len(files) == 0
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
}
return iup.DEFAULT
}
func onRemove(ih iup.Ihandle) int {
if index < 0 || index >= len(files) {
return iup.IGNORE
}
iup.GetHandle("Table").SetAttribute("DELLIN", strconv.Itoa(index+1))
files = slices.Delete(files, index, index+1)
if index >= len(files) {
index = len(files) - 1
}
setActive()
previewPost()
return iup.DEFAULT
}
func onRemoveAll(ih iup.Ihandle) int {
index = -1
files = make([]cbconvert.File, 0)
iup.GetHandle("Table").SetAttribute("NUMLIN", "0")
setActive()
return iup.DEFAULT
}
func onThumbnail(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
activeConv = conv
setBusy(true)
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
}
var canceled = false
conv.OnCancel = func() {
canceled = true
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
}))
iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)
go func(c *cbconvert.Converter) {
for _, file := range files {
if canceled {
break
}
if err := c.Thumbnail(file); err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
continue
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onCover(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
activeConv = conv
setBusy(true)
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
}
var canceled = false
conv.OnCancel = func() {
canceled = true
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
}))
iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)
go func(c *cbconvert.Converter) {
for _, file := range files {
if canceled {
break
}
if err := c.Cover(file); err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
continue
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onConvert(ih iup.Ihandle) int {
if busy {
if activeConv != nil {
activeConv.Cancel()
}
return iup.DEFAULT
}
conv := cbconvert.New(options())
conv.Nfiles = len(files)
activeConv = conv
setBusy(true)
conv.OnStart = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "convert", 0, conv)
}
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress", 0, conv)
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
})).SetCallback("CLOSE_CB", iup.CloseFunc(func(ih iup.Ihandle) int {
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
return iup.DEFAULT
}))
convertErr := func(err error) {
if errors.Is(err, context.Canceled) {
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
return
}
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
}
go func(c *cbconvert.Converter) {
if c.Opts.Combine {
if err := c.Combine(files); err != nil {
convertErr(err)
}
} else {
for _, file := range files {
if err := c.Convert(file); err != nil {
convertErr(err)
if errors.Is(err, context.Canceled) {
break
}
continue
}
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onOutputDirectory(ih iup.Ihandle) int {
args, err := fileDlg("Output Directory", false, true, outputDirKey)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) == 1 {
iup.GetHandle("OutDir").SetAttribute("VALUE", args[0])
}
setActive()
return iup.DEFAULT
}
func onOutputFile(ih iup.Ihandle) int {
name := saveDlg("Output File", outputDirKey)
if name != "" {
iup.GetHandle("OutFile").SetAttribute("VALUE", filepath.Base(name))
iup.GetHandle("OutDir").SetAttribute("VALUE", filepath.Dir(name))
setActive()
}
return iup.DEFAULT
}
func onFilterChanged(ih iup.Ihandle) int {
switch ih.GetInt("VALUE") {
case 1:
ih.SetAttribute("TIP", "NearestNeighbor is the fastest resampling filter, no antialiasing")
case 2:
ih.SetAttribute("TIP", "Box filter (averaging pixels)")
case 3:
ih.SetAttribute("TIP", "Linear is the bilinear filter, smooth and reasonably fast")
case 4:
ih.SetAttribute("TIP", "MitchellNetravali is a smooth bicubic filter")
case 5:
ih.SetAttribute("TIP", "CatmullRom is a sharp bicubic filter")
case 6:
ih.SetAttribute("TIP", "Gaussian is a blurring filter that uses gaussian function, useful for noise removal")
case 7:
ih.SetAttribute("TIP", "Lanczos is a high-quality resampling filter, it's slower than cubic filters")
}
previewPost()
return iup.DEFAULT
}
+21 -827
View File
@@ -2,19 +2,13 @@ package main
import (
"bytes"
"context"
_ "embed"
"errors"
"flag"
"fmt"
"image/gif"
"image/png"
"net/url"
"os"
"path/filepath"
"runtime/debug"
"slices"
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
@@ -34,6 +28,11 @@ var appVersion string
var (
index = -1
files []cbconvert.File
config iup.Ihandle
activeConv *cbconvert.Converter
busy bool
)
func init() {
@@ -69,13 +68,17 @@ func main() {
iup.Open()
defer iup.Close()
iup.SetGlobal("UTF8MODE", "YES")
iup.SetGlobal("UTF8MODE_FILE", "YES")
iup.SetGlobal("APPNAME", "cbconvert")
iup.SetGlobal("APPID", "io.github.gen2brain.cbconvert")
iup.SetGlobal("AUTODARKMODE", "YES")
config = iup.Config()
iup.ConfigLoad(config)
img, _ := png.Decode(bytes.NewReader(appLogo))
iup.ImageFromImage(img).SetHandle("logo")
dlg := iup.Dialog(layout()).SetAttributes(fmt.Sprintf(`TITLE="CBconvert %s", ICON=logo`, appVersion)).SetHandle("dlg")
dlg := iup.Dialog(layout()).SetAttributes(fmt.Sprintf(`TITLE="CBconvert %s", ICON=logo, SHRINK=YES`, appVersion)).SetHandle("dlg")
dlg.SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
sp := strings.Split(s, ": ")
@@ -95,8 +98,16 @@ func main() {
return iup.DEFAULT
}))
dlg.SetCallback("THEMECHANGED_CB", iup.ThemeChangedFunc(func(ih iup.Ihandle, darkMode int) int {
t := iup.GetHandle("Table")
tableRowColors(t, darkMode == 1)
t.SetAttribute("REDRAW", "YES")
return iup.DEFAULT
}))
iup.Map(dlg)
setActive()
profilesInit()
iup.ShowXY(dlg, iup.CENTER, iup.CENTER)
iup.MainLoop()
@@ -123,90 +134,6 @@ func parseFlags() {
}
}
func options() cbconvert.Options {
var opts cbconvert.Options
opts.Recursive = iup.GetHandle("Recursive").GetAttribute("VALUE") == "ON"
opts.NoRGB = iup.GetHandle("NoRGB").GetAttribute("VALUE") == "ON"
opts.NoCover = iup.GetHandle("NoCover").GetAttribute("VALUE") == "ON"
opts.Size = iup.GetHandle("Size").GetInt("VALUE")
opts.OutDir = iup.GetHandle("OutDir").GetAttribute("VALUE")
opts.Suffix = iup.GetHandle("Suffix").GetAttribute("VALUE")
opts.NoConvert = iup.GetHandle("NoConvert").GetAttribute("VALUE") == "ON"
opts.NoNonImage = iup.GetHandle("NoNonImage").GetAttribute("VALUE") == "ON"
opts.Archive = strings.ToLower(iup.GetHandle("Archive").GetAttribute("VALUESTRING"))
opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
opts.Width = iup.GetHandle("Width").GetInt("VALUE")
opts.Height = iup.GetHandle("Height").GetInt("VALUE")
opts.Fit = iup.GetHandle("Fit").GetAttribute("VALUE") == "ON"
opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1
opts.Quality = iup.GetHandle("Quality").GetInt("VALUE")
opts.Grayscale = iup.GetHandle("Grayscale").GetAttribute("VALUE") == "ON"
opts.Brightness = iup.GetHandle("Brightness").GetInt("VALUE")
opts.Contrast = iup.GetHandle("Contrast").GetInt("VALUE")
opts.Rotate = iup.GetHandle("Rotate").GetInt("VALUESTRING")
return opts
}
func setActive() {
opts := options()
count := iup.GetHandle("List").GetInt("COUNT")
if count == 0 {
iup.GetHandle("Remove").SetAttribute("ACTIVE", "NO")
iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "NO")
iup.GetHandle("Preview").SetAttribute("IMAGE", "logo")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
} else {
if index != -1 {
iup.GetHandle("Remove").SetAttribute("ACTIVE", "YES")
}
iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "YES")
}
if opts.OutDir == "" {
iup.GetHandle("Thumbnail").SetAttributes(`ACTIVE=NO`)
iup.GetHandle("Cover").SetAttributes(`ACTIVE=NO`)
iup.GetHandle("Convert").SetAttributes(`ACTIVE=NO`)
if count > 0 {
iup.GetHandle("Thumbnail").SetAttributes(`ACTIVE=NO, TIP="Set Output Directory"`)
iup.GetHandle("Cover").SetAttributes(`ACTIVE=NO, TIP="Set Output Directory"`)
iup.GetHandle("Convert").SetAttributes(`ACTIVE=NO, TIP="Set Output Directory"`)
}
} else {
if count > 0 {
iup.GetHandle("Thumbnail").SetAttributes(`ACTIVE=YES, TIP=""`)
iup.GetHandle("Cover").SetAttributes(`ACTIVE=YES, TIP=""`)
iup.GetHandle("Convert").SetAttributes(`ACTIVE=YES, TIP=""`)
} else {
iup.GetHandle("Thumbnail").SetAttributes(`ACTIVE=NO`)
iup.GetHandle("Cover").SetAttributes(`ACTIVE=NO`)
iup.GetHandle("Convert").SetAttributes(`ACTIVE=NO`)
}
}
if opts.NoConvert {
iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "NO")
iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "NO")
} else {
iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "YES")
iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "YES")
}
if (opts.Format == "jpeg" || opts.Format == "webp" || opts.Format == "avif" || opts.Format == "jxl") && !opts.NoConvert {
iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "NO")
}
if opts.Width != 0 && opts.Height != 0 && !opts.NoConvert {
iup.GetHandle("Fit").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("Fit").SetAttribute("ACTIVE", "NO")
}
}
func layout() iup.Ihandle {
return iup.Vbox(
iup.Hbox(
@@ -224,736 +151,3 @@ func layout() iup.Ihandle {
status(),
)
}
func list() iup.Ihandle {
return iup.Vbox(
iup.List().SetAttributes("EXPAND=YES, VISIBLECOLUMNS=16, VISIBLELINES=5").SetHandle("List").
SetCallback("ACTION", iup.ListActionFunc(func(ih iup.Ihandle, text string, item int, state int) int {
if state == 1 {
index = item - 1
setActive()
previewPost()
}
return iup.DEFAULT
})).
SetCallback("DROPFILES_CB", iup.DropFilesFunc(func(ih iup.Ihandle, fileName string, num, x, y int) int {
dec, err := url.QueryUnescape(fileName)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
conv := cbconvert.New(options())
fs, err := conv.Files([]string{dec})
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
for _, file := range fs {
iup.SetAttribute(iup.GetHandle("List"), "APPENDITEM", fmt.Sprintf("%s (%s)", file.Name, file.SizeHuman))
files = append(files, file)
}
setActive()
return iup.DEFAULT
})),
)
}
func previewPost() {
if index == -1 || len(files) == 0 {
return
}
width, height := previewSize()
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" {
iup.GetHandle("Preview").SetAttribute("IMAGE", "")
}
opts := options()
go func(opts cbconvert.Options) {
conv := cbconvert.New(opts)
var s string
file := files[index]
img, err := conv.Preview(file.Path, file.Stat, width, height)
if err != nil {
s = err.Error()
fmt.Println(err)
}
iup.PostMessage(iup.GetHandle("Preview"), s, 0, img)
}(opts)
}
func previewSize() (int, int) {
var width, height int
sp := strings.Split(iup.GetHandle("Preview").GetAttribute("RASTERSIZE"), "x")
if len(sp) == 2 {
width, _ = strconv.Atoi(sp[0])
height, _ = strconv.Atoi(sp[1])
}
return width, height
}
func preview() iup.Ihandle {
return iup.Frame(
iup.Vbox(
iup.Label("").SetAttributes("EXPAND=YES, ALIGNMENT=ACENTER, MINSIZE=400x, IMAGE=cover").SetHandle("Preview").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
img := p.(cbconvert.Image)
iup.GetHandle("Loading").SetAttributes("VISIBLE=NO, STOP=YES")
if img.Image != nil && len(s) == 0 {
iup.Destroy(iup.GetHandle("cover"))
iup.ImageFromImage(img.Image).SetHandle("cover")
ih.SetAttribute("IMAGE", "cover")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
} else {
ih.SetAttribute("IMAGE", "logo")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
sp := strings.Split(s, ": ")
if len(sp) > 1 {
iup.MessageError(ih, fmt.Sprintf("%s\n\n%s", sp[0], strings.Join(sp[1:], ": ")))
}
}
return iup.DEFAULT
})),
iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"),
),
)
}
func tabs() iup.Ihandle {
vboxInput := iup.Vbox(
iup.Toggle(" Recurse SubDirectories").SetHandle("Recursive").
SetAttributes(`TIP="Process subdirectories recursively"`),
iup.Toggle(" Only Grayscale Images").SetHandle("NoRGB").
SetAttributes(`TIP="Do not convert images that have RGB colorspace"`),
iup.Toggle(" Exclude Cover").SetHandle("NoCover").
SetAttributes(`TIP="Do not convert the cover image"`),
iup.Toggle(" Remove Non-Image Files from the Archive").SetHandle("NoNonImage").
SetAttribute("TIP", "Remove .nfo, .xml, .txt files from the archive"),
iup.Toggle(" Do not Transform or Convert Images").SetHandle("NoConvert").
SetAttributes(`TIP="Copy images from archive or directory without modifications"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
iup.Vbox(
iup.Label("Minimum Size (MiB):"),
iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size").
SetAttributes(`TIP="Process only files larger than minimum size"`),
),
iup.Space().SetAttributes("EXPAND=HORIZONTAL"),
).SetHandle("VboxInput").SetAttributes("MARGIN=5x5, GAP=5")
vboxOutput := iup.Vbox(
iup.Vbox(
iup.Label("Output Directory:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutDir").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputDirectory)),
),
iup.Vbox(
iup.Label("Add Suffix to Output File:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("Suffix").
SetAttribute("TIP", "Add suffix to filename, i.e. filename_suffix.cbz"),
),
iup.Vbox(
iup.Label("Archive Format:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "ZIP",
"2": "TAR",
}).SetHandle("Archive"),
),
).SetHandle("VboxOutput").SetAttributes("MARGIN=5x5, GAP=5")
vboxImage := iup.Vbox(
iup.Vbox(
iup.Label("Format:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "JPEG",
"2": "PNG",
"3": "TIFF",
"4": "BMP",
"5": "WEBP",
"6": "AVIF",
"7": "JXL",
}).SetHandle("Format").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
previewPost()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Size:"),
iup.Hbox(
iup.Text().SetAttributes(`CUEBANNER=" width", VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Width").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
iup.Label("x"),
iup.Text().SetAttributes(`CUEBANNER=" height", VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Height").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetAttributes("ALIGNMENT=ACENTER, MARGIN=0"),
),
iup.Vbox(
iup.Toggle(" Best Fit").SetHandle("Fit").
SetAttributes(`TIP="Best fit for required width and height"`),
),
iup.Vbox(
iup.Label("Resize Filter:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "3",
"TIP": "Linear is the bilinear filter, smooth and reasonably fast",
"1": "NearestNeighbor",
"2": "Box",
"3": "Linear",
"4": "MitchellNetravali",
"5": "CatmullRom",
"6": "Gaussian",
"7": "Lanczos",
}).SetHandle("Filter").SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onFilterChanged)),
),
iup.Vbox(
iup.Hbox(
iup.Label("Quality: "),
iup.Label("75").SetHandle("LabelQuality"),
).SetAttributes("MARGIN=0"),
iup.Val("").SetAttributes(`MIN=0, MAX=100, VALUE=75, SHOWTICKS=10`).SetHandle("Quality").
SetAttribute("TIP", "Quality affects JPEG, WEBP, AVIF and JXL").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelQuality").SetAttribute("TITLE", ih.GetInt("VALUE"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetHandle("VboxQuality"),
iup.Vbox(
iup.Toggle(" Grayscale").SetHandle("Grayscale").
SetAttributes(`TIP="Convert images to grayscale (monochromatic)"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
return iup.DEFAULT
})),
),
).SetHandle("VboxImage").SetAttributes("MARGIN=5x5, GAP=5")
vboxTransform := iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Brightness: "),
iup.Label("0").SetHandle("LabelBrightness"),
).SetAttributes("ALIGNMENT=ACENTER, MARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Brightness").
SetAttributes(`TIP="Adjust the brightness of the images"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Hbox(
iup.Label("Contrast: "),
iup.Label("0").SetHandle("LabelContrast"),
).SetAttributes("ALIGNMENT=ACENTER, MARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Contrast").
SetAttributes(`TIP="Adjust the contrast of the images"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Rotate:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "0",
"2": "90",
"3": "180",
"4": "270",
}).SetHandle("Rotate").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
return iup.DEFAULT
})),
),
).SetHandle("VboxTransform").SetAttributes("MARGIN=5x5, GAP=5")
return iup.Tabs(
vboxInput.SetAttributes("TABTITLE=Input"),
vboxOutput.SetAttributes("TABTITLE=Output"),
vboxImage.SetAttributes("TABTITLE=Image"),
vboxTransform.SetAttributes("TABTITLE=Transform"),
).SetHandle("Tabs").SetAttributes("MINSIZE=320x400, EXPAND=HORIZONTAL, MULTILINE=YES")
}
func buttons() iup.Ihandle {
return iup.Vbox(
iup.Frame(
iup.Vbox(
iup.Button("Add &Files...").SetHandle("AddFiles").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddFiles)),
iup.Button("Add &Dir...").SetHandle("AddDir").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddDir)),
iup.Button("Remove").SetHandle("Remove").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemove)),
iup.Button("Remove All").SetHandle("RemoveAll").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemoveAll)),
).SetAttributes("NGAP=5"),
),
iup.Frame(
iup.Vbox(
iup.Button("Thumbnail").SetHandle("Thumbnail").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onThumbnail)),
iup.Button("Cover").SetHandle("Cover").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onCover)),
).SetAttributes("NGAP=5"),
),
iup.Frame(
iup.Vbox(
iup.Button("&Convert").SetHandle("Convert").SetAttributes("EXPAND=HORIZONTAL, PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onConvert)),
),
),
).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER, NGAP=10")
}
func status() iup.Ihandle {
return iup.Hbox(
loading(),
iup.Fill(),
iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Label("(000/000)").SetHandle("LabelStatus2").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.ProgressBar().SetAttributes("RASTERSIZE=200x15, VISIBLE=NO").SetHandle("ProgressBar").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
switch s {
case "convert":
conv := p.(*cbconvert.Converter)
ih.SetAttributes("VALUE=0, VISIBLE=YES")
ih.SetAttribute("MAX", conv.Ncontents)
iup.GetHandle("List").SetAttributes("ACTIVE=NO")
iup.GetHandle("Tabs").SetAttributes("ACTIVE=NO")
iup.GetHandle("Buttons").SetAttributes("ACTIVE=NO")
iup.GetHandle("LabelStatus1").SetAttribute("TITLE", fmt.Sprintf("File %d of %d", conv.CurrFile, conv.Nfiles))
iup.GetHandle("LabelStatus1").SetAttributes("VISIBLE=YES")
iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES")
iup.Refresh(iup.GetHandle("StatusBar"))
case "start":
conv := p.(*cbconvert.Converter)
ih.SetAttributes("VALUE=0, VISIBLE=YES")
ih.SetAttribute("MAX", conv.Nfiles)
iup.GetHandle("List").SetAttributes("ACTIVE=NO")
iup.GetHandle("Tabs").SetAttributes("ACTIVE=NO")
iup.GetHandle("Buttons").SetAttributes("ACTIVE=NO")
iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES")
case "progress":
conv := p.(*cbconvert.Converter)
ih.SetAttribute("VALUE", conv.CurrContent)
iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrContent, conv.Ncontents))
iup.Refresh(iup.GetHandle("StatusBar"))
case "progress2":
conv := p.(*cbconvert.Converter)
ih.SetAttribute("VALUE", conv.CurrFile)
iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrFile, conv.Nfiles))
iup.Refresh(iup.GetHandle("StatusBar"))
case "finish":
iup.GetHandle("List").SetAttributes("ACTIVE=YES")
iup.GetHandle("Tabs").SetAttributes("ACTIVE=YES")
iup.GetHandle("Buttons").SetAttributes("ACTIVE=YES")
iup.GetHandle("LabelStatus1").SetAttributes(`TITLE="", VISIBLE=NO`)
iup.GetHandle("LabelStatus2").SetAttributes(`TITLE="", VISIBLE=NO`)
ih.SetAttributes("VALUE=0, VISIBLE=NO")
iup.Refresh(iup.GetHandle("StatusBar"))
iup.GetHandle("dlg").SetCallback("K_ANY", nil)
iup.GetHandle("dlg").SetCallback("CLOSE_CB", nil)
}
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "5x0"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=5x5").SetHandle("StatusBar")
}
func loading() iup.Ihandle {
img, _ := gif.DecodeAll(bytes.NewReader(appLoading))
animation := iup.User()
for idx, i := range img.Image {
name := fmt.Sprintf("Loading%d", idx)
iup.ImageFromImage(i).SetHandle(name)
iup.Append(animation, iup.GetHandle(name))
}
return iup.AnimatedLabel(animation).SetAttributes("VISIBLE=NO").SetHandle("Loading")
}
func onAddFiles(ih iup.Ihandle) int {
args, err := fileDlg("Add Files", true, false)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) > 0 {
conv := cbconvert.New(options())
fs, err := conv.Files(args)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
for _, file := range fs {
iup.SetAttribute(iup.GetHandle("List"), "APPENDITEM", fmt.Sprintf("%s (%s)", file.Name, file.SizeHuman))
files = append(files, file)
}
setActive()
}
return iup.DEFAULT
}
func onAddDir(ih iup.Ihandle) int {
args, err := fileDlg("Add Directory", false, true)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) > 0 {
conv := cbconvert.New(options())
fs, err := conv.Files(args)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
for _, file := range fs {
iup.SetAttribute(iup.GetHandle("List"), "APPENDITEM", fmt.Sprintf("%s (%s)", file.Name, file.SizeHuman))
files = append(files, file)
}
setActive()
}
return iup.DEFAULT
}
func onRemove(ih iup.Ihandle) int {
if index == -1 || len(files) == 0 {
return iup.IGNORE
}
if len(files) == 1 {
files = make([]cbconvert.File, 0)
} else {
files = slices.Delete(files, index, index)
}
iup.GetHandle("List").SetAttribute("REMOVEITEM", iup.GetHandle("List").GetAttribute("VALUE"))
setActive()
return iup.DEFAULT
}
func onRemoveAll(ih iup.Ihandle) int {
index = -1
files = make([]cbconvert.File, 0)
iup.GetHandle("List").SetAttribute("REMOVEITEM", "ALL")
setActive()
return iup.DEFAULT
}
func onThumbnail(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
}
var canceled = false
conv.OnCancel = func() {
canceled = true
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
}))
iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)
go func(c *cbconvert.Converter) {
for _, file := range files {
if canceled {
break
}
if err := c.Thumbnail(file.Path, file.Stat); err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
continue
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onCover(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
}
var canceled = false
conv.OnCancel = func() {
canceled = true
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
}))
iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)
go func(c *cbconvert.Converter) {
for _, file := range files {
if canceled {
break
}
if err := c.Cover(file.Path, file.Stat); err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
continue
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onConvert(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
conv.OnStart = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "convert", 0, conv)
}
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress", 0, conv)
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
})).SetCallback("CLOSE_CB", iup.CloseFunc(func(ih iup.Ihandle) int {
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
return iup.DEFAULT
}))
go func(c *cbconvert.Converter) {
for _, file := range files {
if err := c.Convert(file.Path, file.Stat); err != nil {
if errors.Is(err, context.Canceled) {
if err := os.RemoveAll(c.Workdir); err != nil {
fmt.Println(err)
}
break
}
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
if err := os.RemoveAll(c.Workdir); err != nil {
fmt.Println(err)
}
continue
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onOutputDirectory(ih iup.Ihandle) int {
args, err := fileDlg("Output Directory", false, true)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) == 1 {
iup.GetHandle("OutDir").SetAttribute("VALUE", args[0])
}
setActive()
return iup.DEFAULT
}
func onFilterChanged(ih iup.Ihandle) int {
switch ih.GetInt("VALUE") {
case 1:
ih.SetAttribute("TIP", "NearestNeighbor is the fastest resampling filter, no antialiasing")
case 2:
ih.SetAttribute("TIP", "Box filter (averaging pixels)")
case 3:
ih.SetAttribute("TIP", "Linear is the bilinear filter, smooth and reasonably fast")
case 4:
ih.SetAttribute("TIP", "MitchellNetravali is a smooth bicubic filter")
case 5:
ih.SetAttribute("TIP", "CatmullRom is a sharp bicubic filter")
case 6:
ih.SetAttribute("TIP", "Gaussian is a blurring filter that uses gaussian function, useful for noise removal")
case 7:
ih.SetAttribute("TIP", "Lanczos is a high-quality resampling filter, it's slower than cubic filters")
}
previewPost()
return iup.DEFAULT
}
-65
View File
@@ -1,65 +0,0 @@
//go:build !portal
package main
import (
"path/filepath"
"strings"
"github.com/gen2brain/iup-go/iup"
)
func fileDlg(title string, multiple, directory bool) ([]string, error) {
ret := make([]string, 0)
dlg := iup.FileDlg()
defer dlg.Destroy()
if !directory {
mf := "YES"
if !multiple {
mf = "NO"
}
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "OPEN",
"MULTIPLEFILES": mf,
"EXTFILTER": "Comic Files|*.rar;*.zip;*.7z;*.tar;*.cbr;*.cbz;*.cb7;*.cbt;*.pdf;*.epub;*.mobi;*.docx;*.pptx|",
"FILTER": "*.cb*", // for Motif
"TITLE": title,
})
} else {
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "DIR",
"TITLE": title,
})
}
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
if dlg.GetInt("STATUS") == 0 {
if !directory {
value := dlg.GetAttribute("VALUE")
sp := strings.Split(value, "|")
if strings.ToLower(iup.GetGlobal("DRIVER")) == "cocoa" {
for _, file := range sp {
ret = append(ret, file)
}
} else {
if len(sp) > 1 {
for _, file := range sp[1 : len(sp)-1] {
ret = append(ret, filepath.Join(sp[0], file))
}
} else {
ret = append(ret, value)
}
}
} else {
value := dlg.GetAttribute("VALUE")
ret = append(ret, value)
}
}
return ret, nil
}
-106
View File
@@ -1,106 +0,0 @@
//go:build portal
package main
import (
"net/url"
"github.com/godbus/dbus/v5"
)
func fileDlg(title string, multiple, directory bool) ([]string, error) {
ret := make([]string, 0)
conn, err := dbus.ConnectSessionBus()
if err != nil {
return ret, err
}
defer conn.Close()
dest := "org.freedesktop.portal.Desktop"
path := "/org/freedesktop/portal/desktop"
resp := "org.freedesktop.portal.Request.Response"
if err = conn.AddMatchSignal(
dbus.WithMatchInterface(dest),
dbus.WithMatchObjectPath(dbus.ObjectPath(path)),
dbus.WithMatchSender(conn.Names()[0]),
); err != nil {
return ret, err
}
c := make(chan *dbus.Signal, 10)
conn.Signal(c)
type Item struct {
Index uint32
Filter string
}
type Filter struct {
Title string
Filters []Item
}
filters := []Filter{
{
"Comic Files",
[]Item{
Item{0, "*.rar"},
Item{0, "*.zip"},
Item{0, "*.7z"},
Item{0, "*.tar"},
Item{0, "*.cbr"},
Item{0, "*.cbz"},
Item{0, "*.cb7"},
Item{0, "*.cbt"},
Item{0, "*.pdf"},
Item{0, "*.epub"},
Item{0, "*.mobi"},
Item{0, "*.docx"},
Item{0, "*.pptx"},
},
},
}
opts := map[string]any{
"multiple": multiple,
"directory": directory,
}
if !directory {
opts["filters"] = filters
}
obj := conn.Object(dest, dbus.ObjectPath(path))
call := obj.Call("org.freedesktop.portal.FileChooser.OpenFile", 0, "", title, opts)
if call.Err != nil {
return ret, call.Err
}
for v := range c {
if v.Name != resp {
continue
}
status := v.Body[0].(uint32)
if status == 0 {
m := v.Body[1].(map[string]dbus.Variant)
uris := m["uris"].Value().([]string)
for _, uri := range uris {
u, err := url.ParseRequestURI(uri)
if err != nil {
return ret, err
}
ret = append(ret, u.Path)
}
}
break
}
return ret, nil
}
+101
View File
@@ -0,0 +1,101 @@
package main
import (
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
func options() cbconvert.Options {
var opts cbconvert.Options
opts.Recursive = iup.GetHandle("Recursive").GetAttribute("VALUE") == "ON"
opts.NoRGB = iup.GetHandle("NoRGB").GetAttribute("VALUE") == "ON"
opts.NoCover = iup.GetHandle("NoCover").GetAttribute("VALUE") == "ON"
opts.Size = iup.GetHandle("Size").GetInt("VALUE")
opts.OutDir = iup.GetHandle("OutDir").GetAttribute("VALUE")
opts.Suffix = iup.GetHandle("Suffix").GetAttribute("VALUE")
opts.NoConvert = iup.GetHandle("NoConvert").GetAttribute("VALUE") == "ON"
opts.NoNonImage = iup.GetHandle("NoNonImage").GetAttribute("VALUE") == "ON"
opts.Archive = strings.ToLower(iup.GetHandle("Archive").GetAttribute("VALUESTRING"))
opts.ZipLevel = zipLevel(iup.GetHandle("ZipLevel").GetAttribute("VALUESTRING"))
opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
opts.Width = iup.GetHandle("Width").GetInt("VALUE")
opts.Height = iup.GetHandle("Height").GetInt("VALUE")
opts.DPI = dpiValue(iup.GetHandle("DPI").GetAttribute("VALUE"))
opts.Fit = iup.GetHandle("Fit").GetAttribute("VALUE") == "ON"
opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1
opts.Quality = iup.GetHandle("Quality").GetInt("VALUE")
switch opts.Format {
case "webp", "avif", "jxl":
opts.Effort = iup.GetHandle("Effort").GetInt("VALUE")
default:
opts.Effort = -1
}
opts.Lossless = iup.GetHandle("Lossless").GetAttribute("VALUE") == "ON"
opts.Combine = iup.GetHandle("Combine").GetAttribute("VALUE") == "ON"
if opts.Combine {
opts.OutFile = iup.GetHandle("OutFile").GetAttribute("VALUE")
}
opts.Grayscale = iup.GetHandle("Grayscale").GetAttribute("VALUE") == "ON"
opts.Brightness = iup.GetHandle("Brightness").GetInt("VALUE")
opts.Contrast = iup.GetHandle("Contrast").GetInt("VALUE")
opts.Rotate = iup.GetHandle("Rotate").GetInt("VALUESTRING")
return opts
}
// shellArg quotes a command-line argument that contains whitespace.
func shellArg(s string) string {
if strings.ContainsAny(s, " \t") {
return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"`
}
return s
}
func commandLine() string {
parts := append([]string{"cbconvert", "convert"}, options().Args()...)
for _, file := range files {
parts = append(parts, file.Path)
}
for i, p := range parts {
parts[i] = shellArg(p)
}
return strings.Join(parts, " ")
}
func onCommand(iup.Ihandle) int {
iup.GetText("Command Line", commandLine(), -1)
return iup.DEFAULT
}
// zipLevel maps the compression dropdown selection to Options.ZipLevel.
func zipLevel(value string) int {
switch value {
case "Default":
return -1
case "Store (none)":
return 0
default:
level, err := strconv.Atoi(value)
if err != nil {
return -1
}
return level
}
}
func dpiValue(value string) int {
dpi, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil {
return 0
}
return dpi
}
+248
View File
@@ -0,0 +1,248 @@
package main
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/gen2brain/iup-go/iup"
)
const (
pathsGroup = "Paths"
profilesGroup = "Profiles"
inputDirKey = "InputDir"
outputDirKey = "OutputDir"
)
type settingKind int
const (
kindBool settingKind = iota
kindInt
kindStr
)
type setting struct {
handle string
kind settingKind
def string
}
var settings = []setting{
{"Recursive", kindBool, "OFF"},
{"NoRGB", kindBool, "OFF"},
{"NoCover", kindBool, "OFF"},
{"NoConvert", kindBool, "OFF"},
{"NoNonImage", kindBool, "OFF"},
{"Combine", kindBool, "OFF"},
{"Fit", kindBool, "OFF"},
{"Lossless", kindBool, "OFF"},
{"Grayscale", kindBool, "OFF"},
{"OutDir", kindStr, ""},
{"Suffix", kindStr, ""},
{"Width", kindStr, ""},
{"Height", kindStr, ""},
{"DPI", kindStr, "Default"},
{"Size", kindInt, "0"},
{"Quality", kindInt, "75"},
{"Effort", kindInt, "0"},
{"Brightness", kindInt, "0"},
{"Contrast", kindInt, "0"},
{"Format", kindInt, "1"},
{"Archive", kindInt, "1"},
{"ZipLevel", kindInt, "1"},
{"Filter", kindInt, "3"},
{"Rotate", kindInt, "1"},
}
func profileGroup(name string) string {
return "Profile:" + name
}
func profileNames() []string {
s := iup.ConfigGetVariableStr(config, profilesGroup, "Names")
if s == "" {
return nil
}
return strings.Split(s, ";")
}
func currentProfile() string {
return iup.ConfigGetVariableStrDef(config, profilesGroup, "Current", "Default")
}
func setStartDir(dlg iup.Ihandle, key string) {
if dir := iup.ConfigGetVariableStr(config, pathsGroup, key); dir != "" {
dlg.SetAttribute("DIRECTORY", dir)
}
}
func rememberDir(dlg iup.Ihandle, key string) {
dir := dlg.GetAttribute("DIRECTORY")
if dir == "" {
return
}
iup.ConfigSetVariableStr(config, pathsGroup, key, dir)
iup.ConfigSave(config)
}
func settingsSave(group string) {
for _, s := range settings {
h := iup.GetHandle(s.handle)
switch s.kind {
case kindBool:
v := 0
if h.GetAttribute("VALUE") == "ON" {
v = 1
}
iup.ConfigSetVariableInt(config, group, s.handle, v)
case kindInt:
iup.ConfigSetVariableInt(config, group, s.handle, h.GetInt("VALUE"))
case kindStr:
iup.ConfigSetVariableStr(config, group, s.handle, h.GetAttribute("VALUE"))
}
}
iup.ConfigSave(config)
}
// settingsApply sets every control from the given profile group, or from defaults when a group is empty.
func settingsApply(group string) {
for _, s := range settings {
h := iup.GetHandle(s.handle)
switch s.kind {
case kindBool:
def := 0
if s.def == "ON" {
def = 1
}
v := def
if group != "" {
v = iup.ConfigGetVariableIntDef(config, group, s.handle, def)
}
if v != 0 {
h.SetAttribute("VALUE", "ON")
} else {
h.SetAttribute("VALUE", "OFF")
}
case kindInt:
def, _ := strconv.Atoi(s.def)
v := def
if group != "" {
v = iup.ConfigGetVariableIntDef(config, group, s.handle, def)
}
h.SetAttribute("VALUE", strconv.Itoa(v))
case kindStr:
v := s.def
if group != "" {
v = iup.ConfigGetVariableStrDef(config, group, s.handle, s.def)
}
h.SetAttribute("VALUE", v)
}
}
syncLabels()
setActive()
previewPost()
}
// syncLabels mirrors slider values into their value labels and retunes the effort slider for the current format.
func syncLabels() {
iup.GetHandle("LabelQuality").SetAttribute("TITLE", iup.GetHandle("Quality").GetInt("VALUE"))
iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE"))
iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE"))
format := strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
eff := iup.GetHandle("Effort").GetInt("VALUE")
setEffort(format)
switch format {
case "webp", "avif", "jxl":
val := iup.GetHandle("Effort")
val.SetAttribute("VALUE", strconv.Itoa(eff))
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", val.GetAttribute("EFFORTNAME"), eff))
}
iup.Refresh(iup.GetHandle("Tabs"))
}
func fillProfileList() {
list := iup.GetHandle("Profile")
list.SetAttribute("REMOVEITEM", "ALL")
cur := currentProfile()
sel := 1
for i, n := range profileNames() {
list.SetAttribute(strconv.Itoa(i+1), n)
if n == cur {
sel = i + 1
}
}
list.SetAttribute("VALUE", strconv.Itoa(sel))
}
// profilesInit loads the current profile on startup, creating a default one on the first run.
func profilesInit() {
if len(profileNames()) == 0 {
iup.ConfigSetVariableStr(config, profilesGroup, "Names", "Default")
iup.ConfigSetVariableStr(config, profilesGroup, "Current", "Default")
settingsSave(profileGroup("Default"))
}
fillProfileList()
settingsApply(profileGroup(currentProfile()))
}
func onProfileSelect(ih iup.Ihandle) int {
name := ih.GetAttribute("VALUESTRING")
if name == "" {
return iup.DEFAULT
}
iup.ConfigSetVariableStr(config, profilesGroup, "Current", name)
iup.ConfigSave(config)
settingsApply(profileGroup(name))
return iup.DEFAULT
}
func onSave(iup.Ihandle) int {
name := currentProfile()
if iup.GetParam("Save Profile", nil, "Name: %s\n", &name) != 1 {
return iup.DEFAULT
}
name = strings.TrimSpace(name)
if name == "" || strings.ContainsAny(name, ".;") {
iup.Message("Invalid Name", "Profile name must not be empty or contain '.' or ';'.")
return iup.DEFAULT
}
settingsSave(profileGroup(name))
names := profileNames()
if !slices.Contains(names, name) {
names = append(names, name)
iup.ConfigSetVariableStr(config, profilesGroup, "Names", strings.Join(names, ";"))
}
iup.ConfigSetVariableStr(config, profilesGroup, "Current", name)
iup.ConfigSave(config)
fillProfileList()
return iup.DEFAULT
}
func onReset(iup.Ihandle) int {
settingsApply("")
return iup.DEFAULT
}
+124
View File
@@ -0,0 +1,124 @@
package main
import "github.com/gen2brain/iup-go/iup"
func setActive() {
if busy {
return
}
opts := options()
count := iup.GetHandle("Table").GetInt("NUMLIN")
if count == 0 {
iup.GetHandle("Remove").SetAttribute("ACTIVE", "NO")
iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "NO")
iup.GetHandle("Preview").SetAttribute("IMAGE", "logo")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
} else {
if index != -1 {
iup.GetHandle("Remove").SetAttribute("ACTIVE", "YES")
}
iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "YES")
}
active := "YES"
var tip string
switch {
case count == 0 && opts.OutDir == "":
active, tip = "NO", "Add files and set output directory"
case count == 0:
active, tip = "NO", "Add files"
case opts.OutDir == "":
active, tip = "NO", "Set output directory"
}
enabledTip := map[string]string{
"Thumbnail": "Extract cover thumbnails",
"Cover": "Extract covers",
"Convert": "Convert files to the selected format",
}
for _, h := range []string{"Thumbnail", "Cover", "Convert"} {
b := iup.GetHandle(h)
b.SetAttribute("ACTIVE", active)
if active == "YES" {
b.SetAttribute("TIP", enabledTip[h])
} else {
b.SetAttribute("TIP", tip)
}
}
if opts.NoConvert {
iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "NO")
iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "NO")
} else {
iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "YES")
iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "YES")
}
canLossless := opts.Format == "webp" || opts.Format == "avif" || opts.Format == "jxl"
losslessOn := canLossless && opts.Lossless
if (opts.Format == "jpeg" || canLossless) && !opts.NoConvert && !losslessOn {
iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "NO")
}
if canLossless && !opts.NoConvert {
iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "YES")
iup.GetHandle("Lossless").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "NO")
iup.GetHandle("Lossless").SetAttribute("ACTIVE", "NO")
}
if opts.Width != 0 && opts.Height != 0 && !opts.NoConvert {
iup.GetHandle("Fit").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("Fit").SetAttribute("ACTIVE", "NO")
}
if opts.Combine {
iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "NO")
}
if opts.Archive == "zip" {
iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "NO")
}
}
// setBusy locks the UI while an operation runs and turns Convert into a Cancel button.
func setBusy(on bool) {
busy = on
// Controls not governed by setActive; setActive owns the rest.
always := "YES"
if on {
always = "NO"
}
for _, h := range []string{"AddFiles", "AddDir", "Profile", "Reset", "Save", "Command", "Tabs", "Table"} {
iup.GetHandle(h).SetAttribute("ACTIVE", always)
}
convert := iup.GetHandle("Convert")
if on {
for _, h := range []string{"Remove", "RemoveAll", "Thumbnail", "Cover"} {
iup.GetHandle(h).SetAttribute("ACTIVE", "NO")
}
convert.SetAttribute("ACTIVE", "YES")
convert.SetAttribute("TITLE", "Cancel")
convert.SetAttribute("TIP", "Cancel the running operation (or press Esc)")
} else {
activeConv = nil
convert.SetAttribute("TITLE", "&Convert")
convert.SetAttribute("TIP", "Convert files to the selected format")
setActive() // restores the conditional buttons and option boxes
}
}
+653
View File
@@ -0,0 +1,653 @@
package main
import (
"bytes"
"fmt"
"image/gif"
"net/url"
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
func setEffort(format string) {
val := iup.GetHandle("Effort")
var name string
switch format {
case "webp":
val.SetAttributes("MIN=0, MAX=6, SHOWTICKS=7, VALUE=4")
val.SetAttribute("TIP", "WEBP method, higher is better/slower (0-6, default 4)")
name = "Method"
case "avif":
val.SetAttributes("MIN=0, MAX=10, SHOWTICKS=11, VALUE=10")
val.SetAttribute("TIP", "AVIF speed, higher is faster/worse (0-10, default 10)")
name = "Speed"
case "jxl":
val.SetAttributes("MIN=1, MAX=10, SHOWTICKS=10, VALUE=7")
val.SetAttribute("TIP", "JXL effort, higher is better/slower (1-10, default 7)")
name = "Effort"
default:
return
}
val.SetAttribute("EFFORTNAME", name)
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", name, val.GetInt("VALUE")))
iup.Refresh(iup.GetHandle("LabelEffort"))
}
// tableRowColors sets the alternating row colors for dark or light mode.
func tableRowColors(t iup.Ihandle, dark bool) {
even, odd := "#F0F0F0", "#FFFFFF"
if dark {
even, odd = "#3A3A3A", "#2D2D2D"
}
t.SetAttribute("EVENROWCOLOR", even)
t.SetAttribute("ODDROWCOLOR", odd)
}
func list() iup.Ihandle {
t := iup.Table().SetHandle("Table")
t.SetAttributes(map[string]string{
"EXPAND": "YES",
"NUMCOL": "3",
"NUMLIN": "0",
"TITLE1": "Title",
"TITLE2": "Type",
"TITLE3": "Size (MiB)",
"WIDTH1": "150",
"WIDTH2": "50",
"WIDTH3": "100",
"ALIGNMENT2": "ACENTER",
"ALIGNMENT3": "ARIGHT",
"SELECTIONMODE": "SINGLE",
"USERRESIZE": "YES",
"STRETCHLAST": "NO",
"FOCUSRECT": "NO",
"SORTABLE": "YES",
"ALTERNATECOLOR": "YES",
})
tableRowColors(t, iup.GetGlobal("DARKMODE") == "YES" && iup.GetGlobal("AUTODARKMODE") == "YES")
t.SetCallback("ENTERITEM_CB", iup.EnterItemFunc(func(ih iup.Ihandle, lin, col int) int {
index = lin - 1
setActive()
previewPost()
return iup.DEFAULT
}))
t.SetCallback("SORT_CB", iup.TableSortFunc(onSort))
t.SetCallback("DROPFILES_CB", iup.DropFilesFunc(func(ih iup.Ihandle, fileName string, num, x, y int) int {
dec, err := url.QueryUnescape(fileName)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
conv := cbconvert.New(options())
fs, err := conv.Files([]string{dec})
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
wasEmpty := len(files) == 0
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
return iup.DEFAULT
}))
return iup.Vbox(t)
}
func previewSize() (int, int) {
var width, height int
sp := strings.Split(iup.GetHandle("Preview").GetAttribute("RASTERSIZE"), "x")
if len(sp) == 2 {
width, _ = strconv.Atoi(sp[0])
height, _ = strconv.Atoi(sp[1])
}
return width, height
}
func preview() iup.Ihandle {
return iup.Frame(
iup.Vbox(
iup.Label("").SetAttributes("EXPAND=YES, ALIGNMENT=ACENTER, MINSIZE=400x, IMAGE=cover").SetHandle("Preview").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
img := p.(cbconvert.Image)
iup.GetHandle("Loading").SetAttributes("VISIBLE=NO, STOP=YES")
if img.Image != nil && len(s) == 0 {
iup.Destroy(iup.GetHandle("cover"))
iup.ImageFromImage(img.Image).SetHandle("cover")
ih.SetAttribute("IMAGE", "cover")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
} else {
ih.SetAttribute("IMAGE", "logo")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
sp := strings.Split(s, ": ")
if len(sp) > 1 {
iup.MessageError(ih, fmt.Sprintf("%s\n\n%s", sp[0], strings.Join(sp[1:], ": ")))
}
}
return iup.DEFAULT
})),
iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"),
),
)
}
func tabInput() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Toggle(" Recurse SubDirectories").SetHandle("Recursive").
SetAttributes(`TIP="Process subdirectories recursively"`),
iup.Toggle(" Only Grayscale Images").SetHandle("NoRGB").
SetAttributes(`TIP="Do not convert images that have RGB colorspace"`),
iup.Toggle(" Exclude Cover").SetHandle("NoCover").
SetAttributes(`TIP="Do not convert the cover image"`),
iup.Toggle(" Remove Non-Image Files from the Archive").SetHandle("NoNonImage").
SetAttribute("TIP", "Remove .nfo, .xml, .txt files from the archive"),
iup.Toggle(" Do not Transform or Convert Images").SetHandle("NoConvert").
SetAttributes(`TIP="Copy images from archive or directory without modifications"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Label("Minimum Size (MiB):"),
iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size").
SetAttributes(`TIP="Process only files larger than minimum size"`),
),
iup.Vbox(
iup.Label("Document DPI:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"EDITBOX": "YES",
"VISIBLECOLUMNS": "6",
"VALUE": "Default",
"1": "Default",
"2": "150",
"3": "300",
"4": "600",
"5": "1200",
}).SetHandle("DPI").
SetAttribute("TIP", "Resolution for rendering documents (PDF, EPUB, etc.); Default is 300"),
),
).SetAttributes("NGAP=10"),
).SetHandle("VboxInput")
}
func tabOutput() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Vbox(
iup.Label("Output Directory:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutDir").
SetAttribute("TIP", "Directory where converted files are written (required)").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputDirectory)),
),
iup.Vbox(
iup.Label("Add Suffix to Output File:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("Suffix").
SetAttribute("TIP", "Add suffix to filename, i.e. filename_suffix.cbz"),
),
iup.Vbox(
iup.Label("Archive Format:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "ZIP",
"2": "TAR",
}).SetHandle("Archive").
SetAttribute("TIP", "Output container: ZIP (.cbz) or uncompressed TAR (.cbt)").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Compression:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "Default",
"2": "Store (none)",
"3": "1",
"4": "2",
"5": "3",
"6": "4",
"7": "5",
"8": "6",
"9": "7",
"10": "8",
"11": "9",
}).SetHandle("ZipLevel").
SetAttribute("TIP", "ZIP compression: Store disables it, 1 is fastest, 9 is smallest"),
).SetHandle("VboxZipLevel"),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Toggle(" Combine into single file").SetHandle("Combine").
SetAttributes(`TIP="Merge all listed files into one archive"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Output File:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutFile").
SetAttribute("TIP", "Combined file name (default: first input + -combined)"),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputFile)),
).SetHandle("VboxOutFile"),
).SetAttributes("NGAP=10"),
).SetHandle("VboxOutput")
}
func tabImage() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Vbox(
iup.Label("Format:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "JPEG",
"2": "PNG",
"3": "TIFF",
"4": "BMP",
"5": "WEBP",
"6": "AVIF",
"7": "JXL",
}).SetHandle("Format").
SetAttribute("TIP", "Output image format for the converted pages").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setEffort(strings.ToLower(ih.GetAttribute("VALUESTRING")))
setActive()
previewPost()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Size:"),
iup.Hbox(
iup.Text().SetAttributes(`CUEBANNER="width", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Width").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "2"),
iup.Label("x"),
iup.Space().SetAttribute("SIZE", "2"),
iup.Text().SetAttributes(`CUEBANNER="height", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Height").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
),
iup.Vbox(
iup.Toggle(" Best Fit").SetHandle("Fit").
SetAttributes(`TIP="Best fit for required width and height"`),
),
iup.Vbox(
iup.Label("Resize Filter:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "3",
"TIP": "Linear is the bilinear filter, smooth and reasonably fast",
"1": "NearestNeighbor",
"2": "Box",
"3": "Linear",
"4": "MitchellNetravali",
"5": "CatmullRom",
"6": "Gaussian",
"7": "Lanczos",
}).SetHandle("Filter").SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onFilterChanged)),
),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Quality: "),
iup.Label("75").SetHandle("LabelQuality"),
).SetAttributes("NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=0, MAX=100, VALUE=75, SHOWTICKS=10`).SetHandle("Quality").
SetAttribute("TIP", "Quality affects JPEG, WEBP, AVIF and JXL").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelQuality").SetAttribute("TITLE", ih.GetInt("VALUE"))
iup.Refresh(iup.GetHandle("LabelQuality"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetHandle("VboxQuality"),
iup.Vbox(
iup.Label("Effort:").SetHandle("LabelEffort"),
iup.Val("").SetAttributes(`MIN=0, MAX=10, VALUE=0, SHOWTICKS=11`).SetHandle("Effort").
SetAttribute("TIP", "Encoder speed/effort (WEBP, AVIF, JXL)").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", ih.GetAttribute("EFFORTNAME"), ih.GetInt("VALUE")))
iup.Refresh(iup.GetHandle("LabelEffort"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetHandle("VboxEffort"),
iup.Vbox(
iup.Toggle(" Lossless").SetHandle("Lossless").
SetAttributes(`TIP="Lossless compression (WEBP, AVIF, JXL), ignores quality"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
previewPost()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Toggle(" Grayscale").SetHandle("Grayscale").
SetAttributes(`TIP="Convert images to grayscale (monochromatic)"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
return iup.DEFAULT
})),
),
).SetAttributes("NGAP=10"),
).SetHandle("VboxImage")
}
func tabTransform() iup.Ihandle {
return iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Brightness: "),
iup.Label("0").SetHandle("LabelBrightness"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Brightness").
SetAttributes(`TIP="Adjust the brightness of the images"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE"))
iup.Refresh(iup.GetHandle("LabelBrightness"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Hbox(
iup.Label("Contrast: "),
iup.Label("0").SetHandle("LabelContrast"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Contrast").
SetAttributes(`TIP="Adjust the contrast of the images"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE"))
iup.Refresh(iup.GetHandle("LabelContrast"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Rotate:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "0",
"2": "90",
"3": "180",
"4": "270",
}).SetHandle("Rotate").
SetAttribute("TIP", "Rotate every page clockwise by the given angle in degrees").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
return iup.DEFAULT
})),
),
).SetHandle("VboxTransform").SetAttributes("NGAP=10")
}
func tabs() iup.Ihandle {
return iup.Tabs(
tabInput().SetAttributes("TABTITLE=Input, NMARGIN=10x10"),
tabOutput().SetAttributes("TABTITLE=Output, NMARGIN=10x10"),
tabImage().SetAttributes("TABTITLE=Image, NMARGIN=10x10"),
tabTransform().SetAttributes("TABTITLE=Transform, NMARGIN=10x10"),
).SetHandle("Tabs")
}
func buttons() iup.Ihandle {
addFiles := iup.Button("Add &Files...").SetHandle("AddFiles").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddFiles))
addDir := iup.Button("Add &Dir...").SetHandle("AddDir").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddDir))
remove := iup.Button("Remove").SetHandle("Remove").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemove))
removeAll := iup.Button("Remove All").SetHandle("RemoveAll").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemoveAll))
thumbnail := iup.Button("Thumbnail").SetHandle("Thumbnail").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onThumbnail))
cover := iup.Button("Cover").SetHandle("Cover").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onCover))
convert := iup.Button("&Convert").SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onConvert))
reset := iup.Button("Reset").SetHandle("Reset").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Restore all settings to their defaults").
SetCallback("ACTION", iup.ActionFunc(onReset))
save := iup.Button("Save").SetHandle("Save").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Save current settings to a profile").
SetCallback("ACTION", iup.ActionFunc(onSave))
command := iup.Button("Command").SetHandle("Command").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Show the equivalent command line").
SetCallback("ACTION", iup.ActionFunc(onCommand))
profile := iup.List().SetAttributes("DROPDOWN=YES").SetHandle("Profile").
SetAttribute("TIP", "Select a settings profile").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onProfileSelect))
iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command).SetAttribute("NORMALIZE", "BOTH")
iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command, profile).SetAttribute("NORMALIZE", "HORIZONTAL")
return iup.Vbox(
iup.Vbox(
addFiles,
addDir,
remove,
removeAll,
).SetAttribute("NGAP", "2"),
iup.Space().SetAttribute("SIZE", "x5"),
iup.Vbox(
thumbnail,
cover,
).SetAttribute("NGAP", "2"),
iup.Space().SetAttribute("SIZE", "x5"),
iup.Vbox(
convert,
),
iup.Fill(),
iup.Vbox(
iup.Label("Profile:"),
profile,
reset,
save,
command,
).SetAttribute("NGAP", "2"),
).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER")
}
func status() iup.Ihandle {
return iup.Hbox(
loading(),
iup.Fill(),
iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5"),
iup.Label("(000/000)").SetHandle("LabelStatus2").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5"),
iup.ProgressBar().SetAttributes("VISIBLE=NO").SetHandle("ProgressBar").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
switch s {
case "convert":
conv := p.(*cbconvert.Converter)
ih.SetAttributes("VALUE=0, VISIBLE=YES")
ih.SetAttribute("MAX", conv.Ncontents)
iup.GetHandle("LabelStatus1").SetAttribute("TITLE", fmt.Sprintf("File %d of %d", conv.CurrFile, conv.Nfiles))
iup.GetHandle("LabelStatus1").SetAttributes("VISIBLE=YES")
iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES")
iup.Refresh(iup.GetHandle("StatusBar"))
case "start":
conv := p.(*cbconvert.Converter)
ih.SetAttributes("VALUE=0, VISIBLE=YES")
ih.SetAttribute("MAX", conv.Nfiles)
iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES")
case "progress":
conv := p.(*cbconvert.Converter)
ih.SetAttribute("VALUE", conv.CurrContent)
iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrContent, conv.Ncontents))
iup.Refresh(iup.GetHandle("StatusBar"))
case "progress2":
conv := p.(*cbconvert.Converter)
ih.SetAttribute("VALUE", conv.CurrFile)
iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrFile, conv.Nfiles))
iup.Refresh(iup.GetHandle("StatusBar"))
case "finish":
setBusy(false)
iup.GetHandle("LabelStatus1").SetAttributes(`TITLE="", VISIBLE=NO`)
iup.GetHandle("LabelStatus2").SetAttributes(`TITLE="", VISIBLE=NO`)
ih.SetAttributes("VALUE=0, VISIBLE=NO")
iup.Refresh(iup.GetHandle("StatusBar"))
iup.GetHandle("dlg").SetCallback("K_ANY", nil)
iup.GetHandle("dlg").SetCallback("CLOSE_CB", nil)
}
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "5x0"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=5x5").SetHandle("StatusBar")
}
func loading() iup.Ihandle {
img, _ := gif.DecodeAll(bytes.NewReader(appLoading))
animation := iup.User()
for idx, i := range img.Image {
name := fmt.Sprintf("Loading%d", idx)
iup.ImageFromImage(i).SetHandle(name)
iup.Append(animation, iup.GetHandle(name))
}
return iup.AnimatedLabel(animation).SetAttributes("VISIBLE=NO").SetHandle("Loading")
}
+42 -21
View File
@@ -1,39 +1,60 @@
module github.com/gen2brain/cbconvert/cmd/cbconvert
go 1.23
go 1.26
require (
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca
github.com/schollz/progressbar/v3 v3.13.1
github.com/gen2brain/cbconvert v1.0.5-0.20260623161611-a5817c3ba5de
github.com/schollz/progressbar/v3 v3.19.0
)
require (
github.com/anthonynsimon/bild v0.14.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/anthonynsimon/bild v0.15.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.4 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gen2brain/avif v0.4.1 // indirect
github.com/gen2brain/go-fitz v1.24.14 // indirect
github.com/gen2brain/go-unarr v0.2.4 // indirect
github.com/gen2brain/jpegli v0.3.3 // indirect
github.com/gen2brain/jpegxl v0.4.2 // indirect
github.com/gen2brain/webp v0.5.1 // indirect
github.com/gen2brain/avif v0.5.1 // indirect
github.com/gen2brain/go-fitz v1.24.15 // indirect
github.com/gen2brain/jpegli v0.4.0 // indirect
github.com/gen2brain/jpegxl v0.5.1 // indirect
github.com/gen2brain/webp v0.6.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/jupiterrider/ffi v0.2.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/jupiterrider/ffi v0.7.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mholt/archives v0.1.5 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.1.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.3.4 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
github.com/nwaples/rardecode/v2 v2.2.5 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/image v0.43.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/gen2brain/cbconvert => ../..
+102 -49
View File
@@ -1,8 +1,25 @@
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anthonynsimon/bild v0.15.0 h1:FzvaNLuNlAPKw1Xz7V2WYOcGIEBMj8Y6ZyAk7CI+HzA=
github.com/anthonynsimon/bild v0.15.0/go.mod h1:qIgJ9FldkCn0iy5Ad24fzUkz5R+iJ0WfhiV+6FeCB5A=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.4 h1:iHiVJfxbrB6RF4X+snI2MpVgNBKmVfGaTqZGNlMQIU0=
github.com/bodgit/sevenzip v1.6.4/go.mod h1:ZtNi5KNgHXeXg1G7WiF0IWSuFE2eG6lt/cTGlvuirO0=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
@@ -21,24 +38,20 @@ github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gen2brain/avif v0.4.1 h1:fjwv5SDNYHdI1gbW6MJn3Yaxs1ldUEfAIAH8Ahee538=
github.com/gen2brain/avif v0.4.1/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca h1:qYA+cXWUG/Dx+wG2yQ4iuw/s5ROGdXAdo5aIUHrOq4U=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca/go.mod h1:pQ9kQXsCZQqy7LraruUkBl7CpKNHFpWayxqQ+qliE8Y=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4=
github.com/gen2brain/go-unarr v0.2.4 h1:Iu2kqtGfkLBSQoTFwMkSCmp0g3GrEM/XMVWzo9TQr/Y=
github.com/gen2brain/go-unarr v0.2.4/go.mod h1:0kdy3HtjKBcEaewifXZguHCvt4qD9V8iJCx4FPEOWT8=
github.com/gen2brain/jpegli v0.3.3 h1:ryCOQpmGuVk6FA+QBe9st6cW48jsRdVOPiNrAJ50m+k=
github.com/gen2brain/jpegli v0.3.3/go.mod h1:6Dbgr+ni1IUBqGVOKHn8lY+6DvwSGfAfC7pPQiSK6uA=
github.com/gen2brain/jpegxl v0.4.2 h1:Ff0jAWtCRdc9yjPc9jkyak6Ji/A89Jg0KI+D7qOEtRI=
github.com/gen2brain/jpegxl v0.4.2/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o=
github.com/gen2brain/webp v0.5.1 h1:ly9olTGveZEpq3soJuCmex9fxLJ0ipHcQRRSRit5EUE=
github.com/gen2brain/webp v0.5.1/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw=
github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/gen2brain/jpegli v0.4.0 h1:TnmAO8EcekS/XCboyLPjP6cqz025tkhdaFkiYNd3wH8=
github.com/gen2brain/jpegli v0.4.0/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE=
github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -46,57 +59,97 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 h1:q100v18oYIaE4p5myLFqDyfXTOxivzwGyLjGCEfv8x4=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jupiterrider/ffi v0.2.1 h1:08GJVDqz4eoQq7cKT1T0kwb9MB58XEAGjgxDvz80yBs=
github.com/jupiterrider/ffi v0.2.1/go.mod h1:tJ7Q8p/3blFjdWt5qJU4W5oDE0xloImvrViE+0td0Rk=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/nwaples/rardecode/v2 v2.2.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
github.com/nwaples/rardecode/v2 v2.2.5/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw=
github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+51 -17
View File
@@ -68,8 +68,8 @@ func main() {
if _, err := os.Stat(opts.OutDir); err != nil {
if err := os.MkdirAll(opts.OutDir, 0775); err != nil {
fmt.Println(err)
os.Exit(1)
}
os.Exit(1)
}
files, err := conv.Files(args)
@@ -114,6 +114,17 @@ func main() {
}
}
if opts.Combine {
if err := conv.Combine(files); err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "\r")
return
}
for _, file := range files {
switch {
case opts.Meta:
@@ -131,14 +142,14 @@ func main() {
continue
case opts.Cover:
if err := conv.Cover(file.Path, file.Stat); err != nil {
if err := conv.Cover(file); err != nil {
fmt.Println(err)
os.Exit(1)
}
continue
case opts.Thumbnail:
if err = conv.Thumbnail(file.Path, file.Stat); err != nil {
if err = conv.Thumbnail(file); err != nil {
fmt.Println(err)
os.Exit(1)
}
@@ -146,7 +157,7 @@ func main() {
continue
}
if err := conv.Convert(file.Path, file.Stat); err != nil {
if err := conv.Convert(file); err != nil {
fmt.Println(err)
os.Exit(1)
}
@@ -164,9 +175,15 @@ func parseFlags() (cbconvert.Options, []string) {
convert.IntVar(&opts.Width, "width", 0, "Image width")
convert.IntVar(&opts.Height, "height", 0, "Image height")
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
convert.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl")
convert.StringVar(&opts.Archive, "archive", "zip", "Archive format, valid values are zip, tar")
convert.IntVar(&opts.ZipLevel, "zip-level", -1, "ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default")
convert.IntVar(&opts.Quality, "quality", 75, "Image quality")
convert.IntVar(&opts.Effort, "effort", -1, "Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default")
convert.BoolVar(&opts.Lossless, "lossless", false, "Lossless compression (webp, avif, jxl), ignores quality")
convert.BoolVar(&opts.Combine, "combine", false, "Combine all inputs into a single archive")
convert.StringVar(&opts.OutFile, "outfile", "", "Output file name for --combine (default: first input + -combined)")
convert.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
convert.BoolVar(&opts.NoCover, "no-cover", false, "Do not convert the cover image")
convert.BoolVar(&opts.NoRGB, "no-rgb", false, "Do not convert images that have RGB colorspace")
@@ -186,8 +203,11 @@ func parseFlags() (cbconvert.Options, []string) {
cover.IntVar(&opts.Width, "width", 0, "Image width")
cover.IntVar(&opts.Height, "height", 0, "Image height")
cover.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
cover.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
cover.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif")
cover.IntVar(&opts.Quality, "quality", 75, "Image quality")
cover.IntVar(&opts.Effort, "effort", -1, "Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default")
cover.BoolVar(&opts.Lossless, "lossless", false, "Lossless compression (webp, avif, jxl), ignores quality")
cover.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
cover.StringVar(&opts.OutDir, "outdir", ".", "Output directory")
cover.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)")
@@ -198,6 +218,7 @@ func parseFlags() (cbconvert.Options, []string) {
thumbnail.IntVar(&opts.Width, "width", 0, "Image width")
thumbnail.IntVar(&opts.Height, "height", 0, "Image height")
thumbnail.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
thumbnail.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
thumbnail.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
thumbnail.StringVar(&opts.OutDir, "outdir", ".", "Output directory")
thumbnail.StringVar(&opts.OutFile, "outfile", "", "Output file")
@@ -218,7 +239,7 @@ func parseFlags() (cbconvert.Options, []string) {
fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
fmt.Fprintf(os.Stderr, "\nCommands:\n")
fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n")
order := []string{"width", "height", "fit", "format", "archive", "quality", "filter", "no-cover", "no-rgb",
order := []string{"width", "height", "fit", "dpi", "format", "archive", "zip-level", "quality", "effort", "lossless", "combine", "outfile", "filter", "no-cover", "no-rgb",
"no-nonimage", "no-convert", "grayscale", "rotate", "brightness", "contrast", "suffix", "outdir", "size", "recursive", "quiet"}
for _, name := range order {
f := convert.Lookup(name)
@@ -226,14 +247,14 @@ func parseFlags() (cbconvert.Options, []string) {
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n")
order = []string{"width", "height", "fit", "format", "quality", "filter", "outdir", "size", "recursive", "quiet"}
order = []string{"width", "height", "fit", "dpi", "format", "quality", "effort", "lossless", "filter", "outdir", "size", "recursive", "quiet"}
for _, name := range order {
f := cover.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n")
order = []string{"width", "height", "fit", "filter", "outdir", "outfile", "size", "recursive", "quiet"}
order = []string{"width", "height", "fit", "dpi", "filter", "outdir", "outfile", "size", "recursive", "quiet"}
for _, name := range order {
f := thumbnail.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
@@ -262,27 +283,27 @@ func parseFlags() (cbconvert.Options, []string) {
switch os.Args[1] {
case "convert":
_ = convert.Parse(os.Args[2:])
operands := parseArgs(convert, os.Args[2:])
if !pipe {
args = convert.Args()
args = operands
}
case "cover":
opts.Cover = true
_ = cover.Parse(os.Args[2:])
operands := parseArgs(cover, os.Args[2:])
if !pipe {
args = cover.Args()
args = operands
}
case "thumbnail":
opts.Thumbnail = true
_ = thumbnail.Parse(os.Args[2:])
operands := parseArgs(thumbnail, os.Args[2:])
if !pipe {
args = thumbnail.Args()
args = operands
}
case "meta":
opts.Meta = true
_ = meta.Parse(os.Args[2:])
operands := parseArgs(meta, os.Args[2:])
if !pipe {
args = meta.Args()
args = operands
}
case "version":
opts.Version = true
@@ -297,7 +318,20 @@ func parseFlags() (cbconvert.Options, []string) {
return opts, args
}
// piped checks if we have a piped stdin.
// parseArgs parses flags interspersed with file/dir operands.
func parseArgs(fs *flag.FlagSet, args []string) []string {
var operands []string
_ = fs.Parse(args)
for fs.NArg() > 0 {
operands = append(operands, fs.Arg(0))
_ = fs.Parse(fs.Args()[1:])
}
return operands
}
// piped checks if we have piped stdin.
func piped() bool {
f, err := os.Stdin.Stat()
if err != nil {
@@ -311,7 +345,7 @@ func piped() bool {
return true
}
// lines returns slice of lines from reader.
// lines returns slice of lines from the reader.
func lines(r io.Reader) []string {
data := make([]string, 0)
scanner := bufio.NewScanner(r)
+35 -15
View File
@@ -1,32 +1,52 @@
module github.com/gen2brain/cbconvert
go 1.23
go 1.26
require (
github.com/anthonynsimon/bild v0.14.0
github.com/anthonynsimon/bild v0.15.0
github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d
github.com/dustin/go-humanize v1.0.1
github.com/fvbommel/sortorder v1.1.0
github.com/gen2brain/avif v0.4.1
github.com/gen2brain/go-fitz v1.24.14
github.com/gen2brain/go-unarr v0.2.3
github.com/gen2brain/jpegli v0.3.3
github.com/gen2brain/jpegxl v0.4.2
github.com/gen2brain/webp v0.5.1
github.com/gen2brain/avif v0.5.1
github.com/gen2brain/go-fitz v1.24.15
github.com/gen2brain/jpegli v0.4.0
github.com/gen2brain/jpegxl v0.5.1
github.com/gen2brain/webp v0.6.1
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25
golang.org/x/image v0.21.0
golang.org/x/sync v0.8.0
github.com/mholt/archives v0.1.5
golang.org/x/image v0.43.0
golang.org/x/sync v0.21.0
)
require (
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.4 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/ebitengine/purego v0.8.1 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/jupiterrider/ffi v0.2.1 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect
golang.org/x/net v0.30.0 // indirect
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jupiterrider/ffi v0.7.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.1.1 // indirect
github.com/nwaples/rardecode/v2 v2.2.5 // indirect
github.com/pierrec/lz4/v4 v4.1.27 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/stangelandcl/ppmd v0.1.1 // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
+96 -34
View File
@@ -1,5 +1,21 @@
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anthonynsimon/bild v0.15.0 h1:FzvaNLuNlAPKw1Xz7V2WYOcGIEBMj8Y6ZyAk7CI+HzA=
github.com/anthonynsimon/bild v0.15.0/go.mod h1:qIgJ9FldkCn0iy5Ad24fzUkz5R+iJ0WfhiV+6FeCB5A=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.4 h1:iHiVJfxbrB6RF4X+snI2MpVgNBKmVfGaTqZGNlMQIU0=
github.com/bodgit/sevenzip v1.6.4/go.mod h1:ZtNi5KNgHXeXg1G7WiF0IWSuFE2eG6lt/cTGlvuirO0=
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
@@ -18,28 +34,20 @@ github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gen2brain/avif v0.4.0 h1:JuwAX2rVrkAzQrZx9lpIKx/ovCO35gCUquarfJ6uhHc=
github.com/gen2brain/avif v0.4.0/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs=
github.com/gen2brain/avif v0.4.1 h1:fjwv5SDNYHdI1gbW6MJn3Yaxs1ldUEfAIAH8Ahee538=
github.com/gen2brain/avif v0.4.1/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4=
github.com/gen2brain/go-unarr v0.2.3 h1:VwZg0P6Dc/8Uh51McjVhzUMg4wHwwbiyqjEFsFELc0c=
github.com/gen2brain/go-unarr v0.2.3/go.mod h1:hoHheVuf0KT8/hfvkEL7GMwj2h7fq0lF72NdyySdr3c=
github.com/gen2brain/jpegli v0.3.3 h1:ryCOQpmGuVk6FA+QBe9st6cW48jsRdVOPiNrAJ50m+k=
github.com/gen2brain/jpegli v0.3.3/go.mod h1:6Dbgr+ni1IUBqGVOKHn8lY+6DvwSGfAfC7pPQiSK6uA=
github.com/gen2brain/jpegxl v0.4.1 h1:jWaVp5GkXLJXjRvC+g0R9+uo+xoWiS7DYKxJ3n5gHL4=
github.com/gen2brain/jpegxl v0.4.1/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o=
github.com/gen2brain/jpegxl v0.4.2 h1:Ff0jAWtCRdc9yjPc9jkyak6Ji/A89Jg0KI+D7qOEtRI=
github.com/gen2brain/jpegxl v0.4.2/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o=
github.com/gen2brain/webp v0.5.0 h1:nn3o0BtKltoFKX9rlDZG/Y/aWqNzUZVyXdB815yVNfU=
github.com/gen2brain/webp v0.5.0/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw=
github.com/gen2brain/webp v0.5.1 h1:ly9olTGveZEpq3soJuCmex9fxLJ0ipHcQRRSRit5EUE=
github.com/gen2brain/webp v0.5.1/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw=
github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/gen2brain/jpegli v0.4.0 h1:TnmAO8EcekS/XCboyLPjP6cqz025tkhdaFkiYNd3wH8=
github.com/gen2brain/jpegli v0.4.0/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE=
github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -47,33 +55,87 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 h1:q100v18oYIaE4p5myLFqDyfXTOxivzwGyLjGCEfv8x4=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jupiterrider/ffi v0.2.1 h1:08GJVDqz4eoQq7cKT1T0kwb9MB58XEAGjgxDvz80yBs=
github.com/jupiterrider/ffi v0.2.1/go.mod h1:tJ7Q8p/3blFjdWt5qJU4W5oDE0xloImvrViE+0td0Rk=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.1.1 h1:OGmft1V6AnI/Wme332U6bhG54nxEan+VFgkD7lat4KM=
github.com/minio/minlz v1.1.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/nwaples/rardecode/v2 v2.2.5 h1:L5doqgGfQwI7qADJMqnkrSB86rpPsqQDrHeO0HWa5JY=
github.com/nwaples/rardecode/v2 v2.2.5/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stangelandcl/ppmd v0.1.1 h1:c25QazhlWUn5nmR1QOzafKhQxBicAr7GGCKER2aJ8H8=
github.com/stangelandcl/ppmd v0.1.1/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=