43 Commits

Author SHA1 Message Date
Milan Nikolic 2b85ae5540 Update metadata 2026-06-27 21:36:10 +02:00
Milan Nikolic 3fac75f088 Update modules 2026-06-27 19:30:21 +02:00
Milan Nikolic 0f6e32c177 Update modules 2026-06-27 19:28:25 +02:00
Milan Nikolic 5344970a55 Update modules 2026-06-27 19:12:27 +02:00
Milan Nikolic 6a54e4d5e8 Optimize cover preview 2026-06-27 15:59:49 +02:00
Milan Nikolic 629d569667 Handle Lossless 2026-06-27 14:36:25 +02:00
Milan Nikolic 7b71fdae99 Use document native resolution, issue #55 2026-06-26 22:26:28 +02:00
Milan Nikolic 97554ae16c Update actions 2026-06-26 14:07:14 +02:00
Milan Nikolic e6e1dd33f0 Add go.work 2026-06-26 13:49:21 +02:00
Milan Nikolic e5ea20e51f Update screenshots and modules 2026-06-26 12:36:42 +02:00
Milan Nikolic cfaf016986 Update modules and README 2026-06-26 09:33:30 +02:00
Milan Nikolic 8155626dbb Draw gradient background for preview 2026-06-26 09:16:31 +02:00
Milan Nikolic f1c57c32dd Generate Windows icon for arm64 2026-06-25 22:01:02 +02:00
Milan Nikolic dd332dc282 Remove unused dist files 2026-06-25 21:55:42 +02:00
Milan Nikolic 18e9adabaa Add i18n 2026-06-25 21:52:23 +02:00
Milan Nikolic 4e2f78026d Destroy current image 2026-06-25 11:26:03 +02:00
Milan Nikolic e1134cd902 Add page spin to preview other pages 2026-06-25 11:03:27 +02:00
Milan Nikolic f289c9cd06 Add profiles support to cli app 2026-06-25 10:59:12 +02:00
Milan Nikolic 0439a2edde Add NoUpscale option 2026-06-24 23:39:03 +02:00
Milan Nikolic 92c49290ae Add cover preview in FileDlg 2026-06-24 23:14:50 +02:00
Milan Nikolic 7f025aa9c2 Fix cli progressbar 2026-06-24 23:12:43 +02:00
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
56 changed files with 6229 additions and 1571 deletions
+43
View File
@@ -0,0 +1,43 @@
on: [push, pull_request]
name: Build
jobs:
cli:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Build
run: go build -v ./...
working-directory: cmd/cbconvert
env:
CGO_ENABLED: 1
gui:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Install dependencies
if: runner.os == 'Linux'
run: sudo apt-get update -y && sudo apt-get install -y libgtk-3-dev
- name: Build
run: go build -v ./...
working-directory: cmd/cbconvert-gui
env:
CGO_ENABLED: 1
+2 -5
View File
@@ -5,19 +5,16 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go-version: [1.23.x]
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Go - name: Install Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: stable
- name: Test - name: Test
run: go test run: go test -v ./...
env: env:
CGO_ENABLED: 1 CGO_ENABLED: 1
+153 -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). 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` library
* `pkgconfig` - enable pkg-config (used with `extlib`)
### Using cbconvert in file managers to generate FreeDesktop thumbnails ### Using cbconvert in file managers to generate FreeDesktop thumbnails
Copy/install `cbconvert` cli binary to your `PATH`, create file `~/.local/share/thumbnailers/cbconvert.thumbnailer` Copy/install `cbconvert` cli binary to your `PATH`, create file `~/.local/share/thumbnailers/cbconvert.thumbnailer`
@@ -39,115 +56,149 @@ This is what it looks like in the `PCManFM` file manager:
### Using command line app ### Using command line app
``` ```
    Usage: cbconvert <command> [<flags>] [file1 dir1 ... fileOrDirN] Usage: cbconvert <command> [<flags>] [file1 dir1 ... fileOrDirN]
    Commands: Commands:
      convert convert
            Convert archive or document Convert archive or document
        --width --profile
            Image width (default "0") Load a saved GUI profile as defaults; explicit flags still override (default "")
        --height --width
            Image height (default "0") Image width (default "0")
        --fit --height
            Best fit for required width and height (default "false") Image height (default "0")
        --format --fit
            Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg") Best fit for required width and height (default "false")
        --archive --no-upscale
            Archive format, valid values are zip, tar (default "zip") Do not upscale images already smaller than the requested width/height (default "false")
        --quality --dpi
            Image quality (default "75") Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
        --filter --format
            0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2") Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg")
        --no-cover --archive
            Do not convert the cover image (default "false") Archive format, valid values are zip, tar (default "zip")
        --no-rgb --zip-level
            Do not convert images that have RGB colorspace (default "false") ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default (default "-1")
        --no-nonimage --quality
            Remove non-image files from the archive (default "false") Image quality (default "75")
        --no-convert --effort
            Do not transform or convert images (default "false") Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default (default "-1")
        --grayscale --lossless
            Convert images to grayscale (monochromatic) (default "false") Lossless compression (webp, avif, jxl), ignores quality (default "false")
        --rotate --combine
            Rotate images, valid values are 0, 90, 180, 270 (default "0") Combine all inputs into a single archive (default "false")
        --brightness --outfile
            Adjust the brightness of the images, must be in the range (-100, 100) (default "0") Output file name for --combine (default: first input + -combined) (default "")
        --contrast --filter
            Adjust the contrast of the images, must be in the range (-100, 100) (default "0") 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos (default "2")
        --suffix --no-cover
            Add suffix to file basename (default "") Do not convert the cover image (default "false")
        --outdir --no-rgb
            Output directory (default ".") Do not convert images that have RGB colorspace (default "false")
        --size --no-nonimage
            Process only files larger than size (in MB) (default "0") Remove non-image files from the archive (default "false")
        --recursive --no-convert
            Process subdirectories recursively (default "false") Do not transform or convert images (default "false")
        --quiet --grayscale
            Hide console output (default "false") 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 cover
            Extract cover Extract cover
        --width --profile
            Image width (default "0") Load a saved GUI profile as defaults; explicit flags still override (default "")
        --height --width
            Image height (default "0") Image width (default "0")
        --fit --height
            Best fit for required width and height (default "false") Image height (default "0")
        --format --fit
            Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg") Best fit for required width and height (default "false")
        --quality --no-upscale
            Image quality (default "75") Do not upscale images already smaller than the requested width/height (default "false")
        --filter --dpi
            0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2") Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
        --outdir --format
            Output directory (default ".") Image format, valid values are jpeg, png, tiff, bmp, webp, avif (default "jpeg")
        --size --quality
            Process only files larger than size (in MB) (default "0") Image quality (default "75")
        --recursive --effort
            Process subdirectories recursively (default "false") Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default (default "-1")
        --quiet --lossless
            Hide console output (default "false") Lossless compression (webp, avif, jxl), ignores quality (default "false")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=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 thumbnail
            Extract cover thumbnail (freedesktop spec.) Extract cover thumbnail (freedesktop spec.)
        --width --profile
            Image width (default "0") Load a saved GUI profile as defaults; explicit flags still override (default "")
        --height --width
            Image height (default "0") Image width (default "0")
        --fit --height
            Best fit for required width and height (default "false") Image height (default "0")
        --filter --fit
            0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2") Best fit for required width and height (default "false")
        --outdir --no-upscale
            Output directory (default ".") Do not upscale images already smaller than the requested width/height (default "false")
        --outfile --dpi
            Output file (default "") Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
        --size --filter
            Process only files larger than size (in MB) (default "0") 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos (default "2")
        --recursive --outdir
            Process subdirectories recursively (default "false") Output directory (default ".")
        --quiet --outfile
            Hide console output (default "false") 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 meta
            CBZ metadata CBZ metadata
        --cover --cover
            Print cover name (default "false") Print cover name (default "false")
        --comment --comment
            Print zip comment (default "false") Print zip comment (default "false")
        --comment-body --comment-body
            Set zip comment (default "") Set zip comment (default "")
        --file-add --file-add
            Add file to archive (default "") Add file to archive (default "")
        --file-remove --file-remove
            Remove file(s) from archive (glob pattern, i.e. *.xml) (default "") Remove file from archive (glob pattern, i.e. *.xml) (default "")
version
Print version
``` ```
### Examples ### Examples
@@ -170,6 +221,10 @@ This is what it looks like in the `PCManFM` file manager:
`cbconvert --format avif --quality 50 --width 1280 --outdir ~/comics /media/comics/Misc/` `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 ### Quality settings
This table maps quality settings for JPEG to the respective AVIF and WEBP quality settings: This table maps quality settings for JPEG to the respective AVIF and WEBP quality settings:
@@ -180,19 +235,3 @@ This table maps quality settings for JPEG to the respective AVIF and WEBP qualit
| AVIF quality | 48 | 51 | 56 | 64 | | AVIF quality | 48 | 51 | 56 | 64 |
| WEBP quality | 55 | 64 | 72 | 82 | | 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`)
+223 -32
View File
@@ -15,6 +15,7 @@ import (
pngstructure "github.com/dsoprea/go-png-image-structure" pngstructure "github.com/dsoprea/go-png-image-structure"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/gen2brain/go-fitz"
) )
// Options type. // Options type.
@@ -23,15 +24,27 @@ type Options struct {
Format string Format string
// Archive format, valid values are zip, tar // Archive format, valid values are zip, tar
Archive string Archive string
// ZIP compression level: -1 default, 0 store (no compression), 1-9 deflate (1 fastest, 9 smallest)
ZipLevel int
// JPEG image quality // JPEG image quality
Quality int 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 // Image width
Width int Width int
// Image height // Image height
Height int Height int
// Best fit for required width and height // Best fit for required width and height
Fit bool Fit bool
// 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos // Do not upscale images already smaller than the requested width/height
NoUpscale bool
// Document rendering resolution in DPI (PDF, EPUB, etc.); 0 uses the page's native resolution
DPI int
// 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos
Filter int Filter int
// Do not convert the cover image // Do not convert the cover image
NoCover bool NoCover bool
@@ -85,9 +98,15 @@ type Converter struct {
Opts Options Opts Options
// Current working directory // Current working directory
Workdir string 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
// Target size for fast previews; when set, JPEG covers are IDCT-downscaled while decoding
previewWidth, previewHeight int
// Number of files // Number of files
Nfiles int Nfiles int
// Index of current file // Index of the current file
CurrFile int CurrFile int
// Number of contents in archive/document // Number of contents in archive/document
Ncontents int Ncontents int
@@ -107,6 +126,7 @@ type Converter struct {
type File struct { type File struct {
Name string Name string
Path string Path string
Root string
Stat os.FileInfo Stat os.FileInfo
SizeHuman string SizeHuman string
} }
@@ -125,6 +145,8 @@ func NewOptions() Options {
o.Format = "jpeg" o.Format = "jpeg"
o.Archive = "zip" o.Archive = "zip"
o.Quality = 75 o.Quality = 75
o.Effort = -1
o.ZipLevel = -1
o.Filter = 2 o.Filter = 2
return o return o
@@ -138,6 +160,68 @@ func New(o Options) *Converter {
return c 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)
flag("no-upscale", o.NoUpscale)
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
}
// renderPage renders document page n at the configured DPI, or at the page's
// native resolution when DPI is unset.
func (c *Converter) renderPage(doc *fitz.Document, n int) (*image.RGBA, error) {
if c.Opts.DPI > 0 {
return doc.ImageDPI(n, float64(c.Opts.DPI))
}
return doc.Image(n)
}
// Cancel cancels the operation. // Cancel cancels the operation.
func (c *Converter) Cancel() { func (c *Converter) Cancel() {
if c.OnCancel != nil { if c.OnCancel != nil {
@@ -148,11 +232,13 @@ func (c *Converter) Cancel() {
// Files returns list of found comic files. // Files returns list of found comic files.
func (c *Converter) Files(args []string) ([]File, error) { func (c *Converter) Files(args []string) ([]File, error) {
var files []File var files []File
var root string
toFile := func(fp string, f os.FileInfo) File { toFile := func(fp string, f os.FileInfo) File {
var file File var file File
file.Name = filepath.Base(fp) file.Name = filepath.Base(fp)
file.Path = fp file.Path = fp
file.Root = root
file.Stat = f file.Stat = f
file.SizeHuman = humanize.IBytes(uint64(f.Size())) file.SizeHuman = humanize.IBytes(uint64(f.Size()))
return file return file
@@ -205,12 +291,14 @@ func (c *Converter) Files(args []string) ([]File, error) {
} }
if !stat.IsDir() { if !stat.IsDir() {
root = filepath.Dir(path)
if isArchive(path) || isDocument(path) { if isArchive(path) || isDocument(path) {
if isSize(int64(c.Opts.Size), stat.Size()) { if isSize(int64(c.Opts.Size), stat.Size()) {
files = append(files, toFile(path, stat)) files = append(files, toFile(path, stat))
} }
} }
} else { } else {
root = path
if c.Opts.Recursive { if c.Opts.Recursive {
if err := filepath.Walk(path, walkFiles); err != nil { if err := filepath.Walk(path, walkFiles); err != nil {
return files, fmt.Errorf("%s: %w", arg, err) return files, fmt.Errorf("%s: %w", arg, err)
@@ -252,9 +340,26 @@ func (c *Converter) Files(args []string) ([]File, error) {
return files, nil 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. // Cover extracts cover.
func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error { func (c *Converter) Cover(file File) error {
c.CurrFile++ c.CurrFile++
c.root = file.Root
fileName, fileInfo := file.Path, file.Stat
cover, err := c.coverImage(fileName, fileInfo) cover, err := c.coverImage(fileName, fileInfo)
if err != nil { if err != nil {
@@ -262,11 +367,7 @@ func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error {
} }
if c.Opts.Width > 0 || c.Opts.Height > 0 { if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit { cover = c.resizeFit(cover)
cover = fit(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
} else {
cover = resize(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
}
} }
ext := c.Opts.Format ext := c.Opts.Format
@@ -276,13 +377,12 @@ func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error {
var fName string var fName string
if c.Opts.Recursive { if c.Opts.Recursive {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:] outDir := c.recursiveDir(fileName)
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755) if err := os.MkdirAll(outDir, 0755); err != nil {
if err != nil {
return fmt.Errorf("%s: %w", fileName, err) 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 { } else {
fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s.%s", baseNoExt(fileName), ext)) fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s.%s", baseNoExt(fileName), ext))
} }
@@ -301,8 +401,10 @@ func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error {
} }
// Thumbnail extracts thumbnail. // Thumbnail extracts thumbnail.
func (c *Converter) Thumbnail(fileName string, fileInfo os.FileInfo) error { func (c *Converter) Thumbnail(file File) error {
c.CurrFile++ c.CurrFile++
c.root = file.Root
fileName, fileInfo := file.Path, file.Stat
cover, err := c.coverImage(fileName, fileInfo) cover, err := c.coverImage(fileName, fileInfo)
if err != nil { if err != nil {
@@ -310,13 +412,9 @@ func (c *Converter) Thumbnail(fileName string, fileInfo os.FileInfo) error {
} }
if c.Opts.Width > 0 || c.Opts.Height > 0 { if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit { cover = c.resizeFit(cover)
cover = fit(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
} else { } else {
cover = resize(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter]) cover = resize(cover, 256, 0, resampleFilter(c.Opts.Filter))
}
} else {
cover = resize(cover, 256, 0, filters[c.Opts.Filter])
} }
var buf bytes.Buffer var buf bytes.Buffer
@@ -343,13 +441,12 @@ func (c *Converter) Thumbnail(fileName string, fileInfo os.FileInfo) error {
fURI = "file://" + fileName fURI = "file://" + fileName
if c.Opts.Recursive { if c.Opts.Recursive {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:] outDir := c.recursiveDir(fileName)
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755) if err := os.MkdirAll(outDir, 0755); err != nil {
if err != nil {
return fmt.Errorf("%s: %w", fileName, err) 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 { } else {
fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%x.png", md5.Sum([]byte(fURI)))) fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%x.png", md5.Sum([]byte(fURI))))
} }
@@ -449,15 +546,51 @@ func (c *Converter) Meta(fileName string) (any, error) {
return "", nil return "", nil
} }
// Preview returns image preview. // Preview returns the cover as an image preview.
func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height int) (Image, error) { func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height int) (Image, error) {
var img Image i, err := c.coverImage(fileName, fileInfo)
if err != nil {
return Image{}, fmt.Errorf("%s: %w", fileName, err)
}
return c.previewImage(fileName, i, width, height)
}
// CoverPreview returns the cover fitted into width x height, skipping the output-codec round-trip.
func (c *Converter) CoverPreview(fileName string, fileInfo os.FileInfo, width, height int) (Image, error) {
c.previewWidth, c.previewHeight = width, height
i, err := c.coverImage(fileName, fileInfo) i, err := c.coverImage(fileName, fileInfo)
if err != nil { if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err) return Image{}, fmt.Errorf("%s: %w", fileName, err)
} }
if width != 0 && height != 0 {
i = fit(i, width, height, resampleFilter(c.Opts.Filter))
}
var img Image
img.Image = i
img.Width = i.Bounds().Dx()
img.Height = i.Bounds().Dy()
return img, nil
}
// PreviewPage returns the page-th image (0-based) as an image preview.
func (c *Converter) PreviewPage(fileName string, fileInfo os.FileInfo, page, width, height int) (Image, error) {
i, err := c.pageImage(fileName, fileInfo, page)
if err != nil {
return Image{}, fmt.Errorf("%s: %w", fileName, err)
}
return c.previewImage(fileName, i, width, height)
}
// previewImage applies the configured transforms and fits the result into width x height.
func (c *Converter) previewImage(fileName string, i image.Image, width, height int) (Image, error) {
var img Image
i = c.imageTransform(i) i = c.imageTransform(i)
var w bytes.Buffer var w bytes.Buffer
@@ -470,15 +603,13 @@ func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height
img.Height = i.Bounds().Dy() img.Height = i.Bounds().Dy()
img.SizeHuman = humanize.IBytes(uint64(len(w.Bytes()))) img.SizeHuman = humanize.IBytes(uint64(len(w.Bytes())))
r := bytes.NewReader(w.Bytes()) dec, err := c.imageDecode(bytes.NewReader(w.Bytes()))
dec, err := c.imageDecode(r)
if err != nil { if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err) return img, fmt.Errorf("%s: %w", fileName, err)
} }
if width != 0 && height != 0 { if width != 0 && height != 0 {
dec = fit(dec, width, height, filters[c.Opts.Filter]) dec = fit(dec, width, height, resampleFilter(c.Opts.Filter))
} }
img.Image = dec img.Image = dec
@@ -486,14 +617,23 @@ func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height
return img, nil return img, nil
} }
// Convert converts comic book. // Convert converts a comic book.
func (c *Converter) Convert(fileName string, fileInfo os.FileInfo) error { func (c *Converter) Convert(file File) error {
c.CurrFile++ c.CurrFile++
c.root = file.Root
fileName, fileInfo := file.Path, file.Stat
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
c.OnCancel = 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 { switch {
case fileInfo.IsDir(): case fileInfo.IsDir():
@@ -518,3 +658,54 @@ func (c *Converter) Convert(fileName string, fileInfo os.FileInfo) error {
return nil 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 ( import (
"archive/tar" "archive/tar"
"archive/zip" "archive/zip"
"compress/flate"
"context"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/gen2brain/go-unarr" "github.com/mholt/archives"
) )
// archiveSave saves workdir to CBZ archive. // archiveSave saves workdir to CBZ archive.
@@ -31,13 +34,12 @@ func (c *Converter) archiveSaveZip(fileName string) error {
var zipName string var zipName string
if c.Opts.Recursive { if c.Opts.Recursive {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:] outDir := c.recursiveDir(fileName)
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755) if err := os.MkdirAll(outDir, 0755); err != nil {
if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err) 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 { } else {
zipName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix)) 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) 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) files, err := os.ReadDir(c.Workdir)
if err != nil { if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err) return fmt.Errorf("archiveSaveZip: %w", err)
@@ -71,6 +80,9 @@ func (c *Converter) archiveSaveZip(fileName string) error {
} }
zipInfo.Method = zip.Deflate zipInfo.Method = zip.Deflate
if c.Opts.ZipLevel == 0 {
zipInfo.Method = zip.Store
}
w, err := z.CreateHeader(zipInfo) w, err := z.CreateHeader(zipInfo)
if err != nil { if err != nil {
return fmt.Errorf("archiveSaveZip: %w", err) return fmt.Errorf("archiveSaveZip: %w", err)
@@ -106,13 +118,12 @@ func (c *Converter) archiveSaveTar(fileName string) error {
var tarName string var tarName string
if c.Opts.Recursive { if c.Opts.Recursive {
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:] outDir := c.recursiveDir(fileName)
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755) if err := os.MkdirAll(outDir, 0755); err != nil {
if err != nil {
return fmt.Errorf("archiveSaveTar: %w", err) 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 { } else {
tarName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix)) 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 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. // archiveList lists contents of archive.
func (c *Converter) archiveList(fileName string) ([]string, error) { func (c *Converter) archiveList(fileName string) ([]string, error) {
var contents []string var contents []string
archive, err := unarr.NewArchive(fileName) ctx := context.Background()
file, ex, input, err := archiveOpen(ctx, fileName)
if err != nil { if err != nil {
return contents, fmt.Errorf("archiveList: %w", err) 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 { if err != nil {
return contents, fmt.Errorf("archiveList: %w", err) return contents, fmt.Errorf("archiveList: %w", err)
} }
@@ -190,6 +254,43 @@ func (c *Converter) archiveList(fileName string) ([]string, error) {
return contents, nil 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. // archiveComment returns ZIP comment.
func (c *Converter) archiveComment(fileName string) (string, error) { func (c *Converter) archiveComment(fileName string) (string, error) {
zr, err := zip.OpenReader(fileName) zr, err := zip.OpenReader(fileName)
@@ -265,7 +366,7 @@ func (c *Converter) archiveSetComment(fileName, commentBody string) error {
return nil return nil
} }
// archiveFileAdd adds file to archive. // archiveFileAdd adds a file to the archive.
func (c *Converter) archiveFileAdd(fileName, newFileName string) error { func (c *Converter) archiveFileAdd(fileName, newFileName string) error {
zr, err := zip.OpenReader(fileName) zr, err := zip.OpenReader(fileName)
if err != nil { if err != nil {
+121 -68
View File
@@ -1,9 +1,9 @@
package cbconvert package cbconvert
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"image" "image"
"image/png" "image/png"
@@ -16,24 +16,18 @@ import (
"github.com/gen2brain/avif" "github.com/gen2brain/avif"
"github.com/gen2brain/go-fitz" "github.com/gen2brain/go-fitz"
"github.com/gen2brain/go-unarr"
"github.com/gen2brain/jpegli" "github.com/gen2brain/jpegli"
"github.com/gen2brain/jpegn"
"github.com/gen2brain/jpegxl" "github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp" "github.com/gen2brain/webp"
"github.com/jsummers/gobmp" "github.com/jsummers/gobmp"
"github.com/mholt/archives"
"golang.org/x/image/tiff" "golang.org/x/image/tiff"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
// convertDocument converts PDF/EPUB document to CBZ. // convertDocument converts PDF/EPUB document to CBZ.
func (c *Converter) convertDocument(ctx context.Context, fileName string) error { 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) doc, err := fitz.New(fileName)
if err != nil { if err != nil {
return fmt.Errorf("convertDocument: %w", err) return fmt.Errorf("convertDocument: %w", err)
@@ -56,7 +50,7 @@ func (c *Converter) convertDocument(ctx context.Context, fileName string) error
return fmt.Errorf("convertDocument: %w", ctx.Err()) return fmt.Errorf("convertDocument: %w", ctx.Err())
} }
img, err := doc.Image(n) img, err := c.renderPage(doc, n)
if err != nil { if err != nil {
return fmt.Errorf("convertDocument: %w", err) return fmt.Errorf("convertDocument: %w", err)
} }
@@ -78,13 +72,6 @@ func (c *Converter) convertDocument(ctx context.Context, fileName string) error
// convertArchive converts archive to CBZ. // convertArchive converts archive to CBZ.
func (c *Converter) convertArchive(ctx context.Context, fileName string) error { 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) contents, err := c.archiveList(fileName)
if err != nil { if err != nil {
return fmt.Errorf("convertArchive: %w", err) return fmt.Errorf("convertArchive: %w", err)
@@ -101,51 +88,56 @@ func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
cover := c.coverName(images) 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, ctx := errgroup.WithContext(ctx)
eg.SetLimit(runtime.NumCPU() + 1) eg.SetLimit(runtime.NumCPU() + 1)
for { file, ex, input, err := archiveOpen(ctx, fileName)
if ctx.Err() != nil {
return fmt.Errorf("convertArchive: %w", ctx.Err())
}
err := archive.Entry()
if err != nil { if err != nil {
if errors.Is(err, io.EOF) {
break
}
return fmt.Errorf("convertArchive: %w", err) return fmt.Errorf("convertArchive: %w", err)
} }
defer file.Close()
data, err := archive.ReadAll() err = ex.Extract(ctx, input, func(ctx context.Context, f archives.FileInfo) error {
if err := ctx.Err(); err != nil {
return err
}
if f.IsDir() {
return nil
}
pathName := f.NameInArchive
rc, err := f.Open()
if err != nil { if err != nil {
return fmt.Errorf("convertArchive: %w", err) 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 isImage(pathName) {
if c.Opts.NoConvert { 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) return fmt.Errorf("convertArchive: %w", err)
} }
continue return nil
} }
if cover == pathName && c.Opts.NoCover { 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) return fmt.Errorf("convertArchive: %w", err)
} }
continue return nil
} }
var img image.Image var img image.Image
@@ -155,11 +147,11 @@ func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
} }
if c.Opts.NoRGB && !isGrayScale(img) { 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) return fmt.Errorf("convertArchive: %w", err)
} }
continue return nil
} }
if img != nil { if img != nil {
@@ -169,18 +161,23 @@ func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
} }
} else { } else {
if filepath.Ext(pathName) == ".DS_Store" || strings.Contains(pathName, "__MACOSX") { if filepath.Ext(pathName) == ".DS_Store" || strings.Contains(pathName, "__MACOSX") {
continue return nil
} }
if !c.Opts.NoNonImage { if c.prefix == "" && !c.Opts.NoNonImage {
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) 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 { if err != nil {
return fmt.Errorf("convertArchive: %w", err) return fmt.Errorf("convertArchive: %w", err)
} }
@@ -190,13 +187,6 @@ func (c *Converter) convertArchive(ctx context.Context, fileName string) error {
// convertDirectory converts directory to CBZ. // convertDirectory converts directory to CBZ.
func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error { 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) contents, err := imagesFromPath(dirPath)
if err != nil { if err != nil {
return fmt.Errorf("convertDirectory: %w", err) return fmt.Errorf("convertDirectory: %w", err)
@@ -218,13 +208,18 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
return fmt.Errorf("convertDirectory: %w", ctx.Err()) 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) file, err := os.Open(img)
if err != nil { if err != nil {
return fmt.Errorf("convertDirectory: %w", err) return fmt.Errorf("convertDirectory: %w", err)
} }
if isNonImage(img) && !c.Opts.NoNonImage { if isNonImage(img) && c.prefix == "" && !c.Opts.NoNonImage {
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) return fmt.Errorf("convertDirectory: %w", err)
} }
@@ -235,7 +230,7 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
continue continue
} else if isImage(img) { } else if isImage(img) {
if c.Opts.NoConvert { 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) return fmt.Errorf("convertDirectory: %w", err)
} }
@@ -253,7 +248,7 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
} }
if c.Opts.NoRGB && !isGrayScale(i) { 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) return fmt.Errorf("convertDirectory: %w", err)
} }
@@ -270,7 +265,7 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
if i != nil { if i != nil {
eg.Go(func() error { eg.Go(func() error {
return c.imageConvert(ctx, i, index, img) return c.imageConvert(ctx, i, index, rel)
}) })
} }
} }
@@ -284,6 +279,11 @@ func (c *Converter) convertDirectory(ctx context.Context, dirPath string) error
return nil 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. // imageConvert converts image.Image.
func (c *Converter) imageConvert(ctx context.Context, img image.Image, index int, pathName string) error { func (c *Converter) imageConvert(ctx context.Context, img image.Image, index int, pathName string) error {
err := ctx.Err() err := ctx.Err()
@@ -303,9 +303,9 @@ func (c *Converter) imageConvert(ctx context.Context, img image.Image, index int
var fileName string var fileName string
if pathName != "" { 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 { } 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) img = c.imageTransform(img)
@@ -328,11 +328,7 @@ func (c *Converter) imageTransform(img image.Image) image.Image {
var i = img var i = img
if c.Opts.Width > 0 || c.Opts.Height > 0 { if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit { i = c.resizeFit(i)
i = fit(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
} else {
i = resize(i, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
}
} }
if c.Opts.Rotate > 0 { if c.Opts.Rotate > 0 {
@@ -362,8 +358,42 @@ func (c *Converter) imageTransform(img image.Image) image.Image {
} }
// imageDecode decodes image from reader. // imageDecode decodes image from reader.
// jpegnOptions decodes straight to RGBA with high-quality chroma upsampling.
var jpegnOptions = jpegn.Options{ToRGBA: true, UpsampleMethod: jpegn.CatmullRom}
func (c *Converter) imageDecode(reader io.Reader) (image.Image, error) { func (c *Converter) imageDecode(reader io.Reader) (image.Image, error) {
img, _, err := image.Decode(reader) br := bufio.NewReader(reader)
if magic, err := br.Peek(2); err == nil && magic[0] == 0xff && magic[1] == 0xd8 {
opts := jpegnOptions
if c.previewWidth > 0 && c.previewHeight > 0 {
data, err := io.ReadAll(br)
if err != nil {
return nil, fmt.Errorf("imageDecode: %w", err)
}
if cfg, err := jpegn.DecodeConfig(bytes.NewReader(data)); err == nil {
opts.ScaleDenom = scaleDenom(cfg.Width, cfg.Height, c.previewWidth, c.previewHeight)
}
img, err := jpegn.Decode(bytes.NewReader(data), &opts)
if err != nil {
return nil, fmt.Errorf("imageDecode: %w", err)
}
return img, nil
}
img, err := jpegn.Decode(br, &opts)
if err != nil {
return nil, fmt.Errorf("imageDecode: %w", err)
}
return img, nil
}
img, _, err := image.Decode(br)
if err != nil { if err != nil {
return img, fmt.Errorf("imageDecode: %w", err) return img, fmt.Errorf("imageDecode: %w", err)
} }
@@ -371,6 +401,17 @@ func (c *Converter) imageDecode(reader io.Reader) (image.Image, error) {
return img, nil return img, nil
} }
// scaleDenom returns the largest JPEG IDCT denominator (1, 2, 4, 8) that keeps w x h at or above tw x th.
func scaleDenom(w, h, tw, th int) int {
for _, d := range []int{8, 4, 2} {
if w/d >= tw && h/d >= th {
return d
}
}
return 1
}
// imageEncode encodes image to file. // imageEncode encodes image to file.
func (c *Converter) imageEncode(img image.Image, w io.Writer) error { func (c *Converter) imageEncode(img image.Image, w io.Writer) error {
var err error var err error
@@ -389,11 +430,23 @@ func (c *Converter) imageEncode(img image.Image, w io.Writer) error {
opts.DCTMethod = jpegli.DefaultDCTMethod opts.DCTMethod = jpegli.DefaultDCTMethod
err = jpegli.Encode(w, img, opts) err = jpegli.Encode(w, img, opts)
case "webp": 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": 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": 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": case "bmp":
opts := &gobmp.EncoderOptions{} opts := &gobmp.EncoderOptions{}
opts.SupportTransparency(false) opts.SupportTransparency(false)
+128 -13
View File
@@ -11,7 +11,6 @@ import (
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/gen2brain/go-fitz" "github.com/gen2brain/go-fitz"
"github.com/gen2brain/go-unarr"
) )
// coverArchive extracts cover from archive. // coverArchive extracts cover from archive.
@@ -31,17 +30,7 @@ func (c *Converter) coverArchive(fileName string) (image.Image, error) {
cover := c.coverName(images) cover := c.coverName(images)
archive, err := unarr.NewArchive(fileName) data, err := c.archiveFile(fileName, cover)
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()
if err != nil { if err != nil {
return nil, fmt.Errorf("coverArchive: %w", err) return nil, fmt.Errorf("coverArchive: %w", err)
} }
@@ -63,7 +52,7 @@ func (c *Converter) coverDocument(fileName string) (image.Image, error) {
} }
defer doc.Close() defer doc.Close()
img, err := doc.Image(0) img, err := c.renderPage(doc, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("coverDocument: %w", err) return nil, fmt.Errorf("coverDocument: %w", err)
} }
@@ -96,6 +85,132 @@ func (c *Converter) coverDirectory(dir string) (image.Image, error) {
return img, nil return img, nil
} }
// pageArchive extracts the page-th image (natural reading order) from an archive.
func (c *Converter) pageArchive(fileName string, page int) (image.Image, error) {
contents, err := c.archiveList(fileName)
if err != nil {
return nil, fmt.Errorf("pageArchive: %w", err)
}
images := imagesFromSlice(contents)
sort.Sort(sortorder.Natural(images))
if page < 0 || page >= len(images) {
return nil, fmt.Errorf("pageArchive: page %d out of range (%d pages)", page+1, len(images))
}
data, err := c.archiveFile(fileName, images[page])
if err != nil {
return nil, fmt.Errorf("pageArchive: %w", err)
}
img, err := c.imageDecode(bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("pageArchive: %w", err)
}
return img, nil
}
// pageDocument extracts the page-th rendered page from a document.
func (c *Converter) pageDocument(fileName string, page int) (image.Image, error) {
doc, err := fitz.New(fileName)
if err != nil {
return nil, fmt.Errorf("pageDocument: %w", err)
}
defer doc.Close()
if page < 0 || page >= doc.NumPage() {
return nil, fmt.Errorf("pageDocument: page %d out of range (%d pages)", page+1, doc.NumPage())
}
img, err := c.renderPage(doc, page)
if err != nil {
return nil, fmt.Errorf("pageDocument: %w", err)
}
return img, nil
}
// pageDirectory extracts the page-th image (natural reading order) from a directory.
func (c *Converter) pageDirectory(dir string, page int) (image.Image, error) {
contents, err := imagesFromPath(dir)
if err != nil {
return nil, fmt.Errorf("pageDirectory: %w", err)
}
images := imagesFromSlice(contents)
sort.Sort(sortorder.Natural(images))
if page < 0 || page >= len(images) {
return nil, fmt.Errorf("pageDirectory: page %d out of range (%d pages)", page+1, len(images))
}
file, err := os.Open(images[page])
if err != nil {
return nil, fmt.Errorf("pageDirectory: %w", err)
}
defer file.Close()
img, err := c.imageDecode(file)
if err != nil {
return nil, fmt.Errorf("pageDirectory: %w", err)
}
return img, nil
}
// pageImage returns the page-th image of a comic file, document or directory.
func (c *Converter) pageImage(fileName string, fileInfo os.FileInfo, page int) (image.Image, error) {
var err error
var img image.Image
switch {
case fileInfo.IsDir():
img, err = c.pageDirectory(fileName, page)
case isDocument(fileName):
img, err = c.pageDocument(fileName, page)
case isArchive(fileName):
img, err = c.pageArchive(fileName, page)
}
if err != nil {
return nil, fmt.Errorf("pageImage: %w", err)
}
return img, nil
}
// PageCount returns the number of pages (images) in a comic file, document or directory.
func (c *Converter) PageCount(fileName string, fileInfo os.FileInfo) (int, error) {
switch {
case fileInfo.IsDir():
contents, err := imagesFromPath(fileName)
if err != nil {
return 0, fmt.Errorf("PageCount: %w", err)
}
return len(imagesFromSlice(contents)), nil
case isDocument(fileName):
doc, err := fitz.New(fileName)
if err != nil {
return 0, fmt.Errorf("PageCount: %w", err)
}
defer doc.Close()
return doc.NumPage(), nil
case isArchive(fileName):
contents, err := c.archiveList(fileName)
if err != nil {
return 0, fmt.Errorf("PageCount: %w", err)
}
return len(imagesFromSlice(contents)), nil
}
return 0, nil
}
// coverName returns the filename that is the most likely to be the cover. // coverName returns the filename that is the most likely to be the cover.
func (c *Converter) coverName(images []string) string { func (c *Converter) coverName(images []string) string {
if len(images) == 0 { if len(images) == 0 {
+7 -1
View File
@@ -59,7 +59,7 @@ func imagesFromSlice(files []string) []string {
return images return images
} }
// isArchive checks if file is archive. // isArchive checks if a file is archive.
func isArchive(f string) bool { func isArchive(f string) bool {
var types = []string{".rar", ".zip", ".7z", ".tar", ".cbr", ".cbz", ".cb7", ".cbt"} var types = []string{".rar", ".zip", ".7z", ".tar", ".cbr", ".cbz", ".cb7", ".cbt"}
for _, t := range types { for _, t := range types {
@@ -123,6 +123,12 @@ func baseNoExt(filename string) string {
return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename)) 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. // copyFile copies reader to file.
func copyFile(reader io.Reader, filename string) error { func copyFile(reader io.Reader, filename string) error {
err := os.MkdirAll(filepath.Dir(filename), 0755) err := os.MkdirAll(filepath.Dir(filename), 0755)
+48 -19
View File
@@ -22,7 +22,7 @@ const (
mitchellNetravali mitchellNetravali
// CatmullRom is a sharp bicubic filter. // CatmullRom is a sharp bicubic filter.
catmullRom 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 gaussian
// Lanczos is a high-quality resampling filter, it's slower than cubic filters. // Lanczos is a high-quality resampling filter, it's slower than cubic filters.
lanczos lanczos
@@ -38,6 +38,15 @@ var filters = map[int]transform.ResampleFilter{
lanczos: transform.Lanczos, lanczos: transform.Lanczos,
} }
// resampleFilter returns the resample filter for index i, falling back to Linear for an unknown index.
func resampleFilter(i int) transform.ResampleFilter {
if f, ok := filters[i]; ok {
return f
}
return filters[linear]
}
func resize(img image.Image, width, height int, filter transform.ResampleFilter) *image.RGBA { func resize(img image.Image, width, height int, filter transform.ResampleFilter) *image.RGBA {
dstW, dstH := width, height dstW, dstH := width, height
@@ -86,6 +95,26 @@ func fit(img image.Image, width, height int, filter transform.ResampleFilter) *i
return resize(img, dstW, dstH, filter) return resize(img, dstW, dstH, filter)
} }
// withinBounds reports whether img already fits within width by height; a zero dimension is unbounded.
func withinBounds(img image.Image, width, height int) bool {
b := img.Bounds()
return (width == 0 || b.Dx() <= width) && (height == 0 || b.Dy() <= height)
}
// resizeFit resizes img to the configured width/height, honoring Fit and NoUpscale.
func (c *Converter) resizeFit(img image.Image) image.Image {
if c.Opts.Fit {
return fit(img, c.Opts.Width, c.Opts.Height, resampleFilter(c.Opts.Filter))
}
if c.Opts.NoUpscale && withinBounds(img, c.Opts.Width, c.Opts.Height) {
return img
}
return resize(img, c.Opts.Width, c.Opts.Height, resampleFilter(c.Opts.Filter))
}
func rotate(img image.Image, angle float64) *image.RGBA { func rotate(img image.Image, angle float64) *image.RGBA {
return transform.Rotate(img, angle, &transform.RotationOptions{ResizeBounds: true, Pivot: &image.Point{}}) return transform.Rotate(img, angle, &transform.RotationOptions{ResizeBounds: true, Pivot: &image.Point{}})
} }
@@ -124,7 +153,7 @@ func imageToGray(src image.Image) *image.Gray {
return dst return dst
} }
// isGrayScale checks if image is grayscale. // isGrayScale checks if the image is grayscale.
func isGrayScale(img image.Image) bool { func isGrayScale(img image.Image) bool {
model := img.ColorModel() model := img.ColorModel()
if model == color.GrayModel || model == color.Gray16Model { if model == color.GrayModel || model == color.Gray16Model {
@@ -135,29 +164,29 @@ func isGrayScale(img image.Image) bool {
} }
var colors16 = []color.Color{ var colors16 = []color.Color{
color.RGBA{0, 0, 0, 255}, color.RGBA{A: 255},
color.RGBA{17, 17, 17, 255}, color.RGBA{R: 17, G: 17, B: 17, A: 255},
color.RGBA{34, 34, 34, 255}, color.RGBA{R: 34, G: 34, B: 34, A: 255},
color.RGBA{51, 51, 51, 255}, color.RGBA{R: 51, G: 51, B: 51, A: 255},
color.RGBA{68, 68, 68, 255}, color.RGBA{R: 68, G: 68, B: 68, A: 255},
color.RGBA{85, 85, 85, 255}, color.RGBA{R: 85, G: 85, B: 85, A: 255},
color.RGBA{102, 102, 102, 255}, color.RGBA{R: 102, G: 102, B: 102, A: 255},
color.RGBA{119, 119, 119, 255}, color.RGBA{R: 119, G: 119, B: 119, A: 255},
color.RGBA{136, 136, 136, 255}, color.RGBA{R: 136, G: 136, B: 136, A: 255},
color.RGBA{153, 153, 153, 255}, color.RGBA{R: 153, G: 153, B: 153, A: 255},
color.RGBA{170, 170, 170, 255}, color.RGBA{R: 170, G: 170, B: 170, A: 255},
color.RGBA{187, 187, 187, 255}, color.RGBA{R: 187, G: 187, B: 187, A: 255},
color.RGBA{204, 204, 204, 255}, color.RGBA{R: 204, G: 204, B: 204, A: 255},
color.RGBA{221, 221, 221, 255}, color.RGBA{R: 221, G: 221, B: 221, A: 255},
color.RGBA{238, 238, 238, 255}, color.RGBA{R: 238, G: 238, B: 238, A: 255},
color.RGBA{255, 255, 255, 255}, color.RGBA{R: 255, G: 255, B: 255, A: 255},
} }
// imageToPaletted converts an image.Image to *image.Paletted using 16-color palette. // imageToPaletted converts an image.Image to *image.Paletted using 16-color palette.
func imageToPaletted(src image.Image) *image.Paletted { func imageToPaletted(src image.Image) *image.Paletted {
b := src.Bounds() b := src.Bounds()
dst := image.NewPaletted(b, colors16) 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 return dst
} }
+716 -3
View File
@@ -1,9 +1,12 @@
package cbconvert package cbconvert
import ( import (
"archive/zip"
"fmt" "fmt"
"image"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
) )
@@ -29,7 +32,7 @@ func TestConvert(t *testing.T) {
for _, file := range files { for _, file := range files {
conv.Opts.Suffix = fmt.Sprintf("_%s%s", format, filepath.Ext(file.Path)) 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 { if err != nil {
t.Errorf("format %s: file %s: %v", format, file.Name, err) t.Errorf("format %s: file %s: %v", format, file.Name, err)
} }
@@ -59,7 +62,7 @@ func TestCover(t *testing.T) {
} }
for _, file := range files { for _, file := range files {
err = conv.Cover(file.Path, file.Stat) err = conv.Cover(file)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@@ -88,7 +91,7 @@ func TestThumbnail(t *testing.T) {
} }
for _, file := range files { for _, file := range files {
err = conv.Thumbnail(file.Path, file.Stat) err = conv.Thumbnail(file)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@@ -99,3 +102,713 @@ func TestThumbnail(t *testing.T) {
t.Error(err) 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.NoUpscale = true
opts.DPI = 150
opts.Grayscale = true
opts.OutDir = "/out"
got := strings.Join(opts.Args(), " ")
want := "--width 1200 --no-upscale --dpi 150 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out"
if got != want {
t.Errorf("Args() = %q, want %q", got, want)
}
}
func TestNoUpscale(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
width := func(o Options) int {
conv := New(o)
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)
}
}
return firstPage(t, conv, filepath.Join(tmpDir, "test.cbz")).Bounds().Dx()
}
base := NewOptions()
base.OutDir = tmpDir
orig := width(base)
up := NewOptions()
up.OutDir = tmpDir
up.Width = orig * 2
up.NoUpscale = true
if got := width(up); got != orig {
t.Errorf("NoUpscale should keep original width %d, got %d", orig, got)
}
no := NewOptions()
no.OutDir = tmpDir
no.Width = orig * 2
if got := width(no); got != orig*2 {
t.Errorf("without NoUpscale should upscale to %d, got %d", orig*2, got)
}
down := NewOptions()
down.OutDir = tmpDir
down.Width = orig / 2
down.NoUpscale = true
if got := width(down); got != orig/2 {
t.Errorf("NoUpscale should still downscale to %d, got %d", orig/2, got)
}
}
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 TestPreviewPage(t *testing.T) {
for _, name := range []string{"testdata/test.cbz", "testdata/test.pdf"} {
conv := New(NewOptions())
files, err := conv.Files([]string{name})
if err != nil {
t.Fatal(err)
}
if len(files) != 1 {
t.Fatalf("%s: expected 1 file, got %d", name, len(files))
}
file := files[0]
n, err := conv.PageCount(file.Path, file.Stat)
if err != nil {
t.Fatal(err)
}
if n < 2 {
t.Fatalf("%s: expected >= 2 pages, got %d", name, n)
}
for _, page := range []int{0, 1, n - 1} {
img, err := conv.PreviewPage(file.Path, file.Stat, page, 0, 0)
if err != nil || img.Image == nil {
t.Fatalf("%s: page %d: img=%v err=%v", name, page, img.Image, err)
}
}
if _, err := conv.PreviewPage(file.Path, file.Stat, n, 0, 0); err == nil {
t.Errorf("%s: page %d (out of range) should error", name, n)
}
}
}
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
}
+192
View File
@@ -0,0 +1,192 @@
package main
import (
"os"
"path/filepath"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"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,
"SHOWPREVIEW": "YES",
"PREVIEWWIDTH": "240",
"PREVIEWHEIGHT": "320",
})
dlg.SetCallback("FILE_CB", iup.FileFunc(previewCover()))
} 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
}
const dlgPreviewName = "_FILEDLGPREVIEW_"
// previewPad insets the cover from the preview pane edges, in pixels per side.
const previewPad = 8
// previewCover returns a FILE_CB handler that draws the highlighted comic's cover in the dialog preview pane.
// Extracted covers are cached by path so re-highlighting a file doesn't re-extract it.
func previewCover() iup.FileFunc {
const maxCache = 32
cache := make(map[string]iup.Ihandle)
order := make([]string, 0, maxCache)
cover := func(path string, w, h int) iup.Ihandle {
if img, ok := cache[path]; ok {
return img
}
img := loadCover(path, w, h)
cache[path] = img
order = append(order, path)
if len(order) > maxCache {
old := order[0]
order = order[1:]
if oi := cache[old]; oi != 0 {
oi.Destroy()
}
delete(cache, old)
}
return img
}
return func(ih iup.Ihandle, filename, status string) int {
switch status {
case "PAINT":
iup.DrawBegin(ih)
cw, ch := iup.DrawGetSize(ih)
iup.DrawParentBackground(ih)
if image := cover(filename, cw-2*previewPad, ch-2*previewPad); image != 0 {
iup.SetHandle(dlgPreviewName, image)
iw, iih, _ := iup.DrawGetImageInfo(dlgPreviewName)
iup.DrawImage(ih, dlgPreviewName, (cw-iw)/2, (ch-iih)/2, iw, iih)
} else {
ih.SetAttribute("DRAWCOLOR", "128 128 128")
noPreview := i18n.Str(i18n.NoPreview)
tw, th := iup.DrawGetTextSize(ih, noPreview)
iup.DrawText(ih, noPreview, (cw-tw)/2, (ch-th)/2, 0, 0)
}
iup.DrawEnd(ih)
case "FINISH":
for _, img := range cache {
if img != 0 {
img.Destroy()
}
}
cache = make(map[string]iup.Ihandle)
order = order[:0]
}
return iup.DEFAULT
}
}
// loadCover extracts the cover of a comic file and returns it as an IUP image fitted to w by h, or 0.
func loadCover(path string, w, h int) iup.Ihandle {
if w <= 0 || h <= 0 || !isComic(path) {
return 0
}
fi, err := os.Stat(path)
if err != nil || fi.IsDir() {
return 0
}
opts := cbconvert.NewOptions()
opts.DPI = 96
img, err := cbconvert.New(opts).CoverPreview(path, fi, w, h)
if err != nil || img.Image == nil {
return 0
}
return iup.ImageFromImage(img.Image)
}
func isComic(path string) bool {
switch strings.ToLower(filepath.Ext(path)) {
case ".rar", ".zip", ".7z", ".tar", ".cbr", ".cbz", ".cb7", ".cbt",
".pdf", ".xps", ".epub", ".mobi", ".docx", ".pptx", ".xlsx":
return true
}
return false
}
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")
}
@@ -38,6 +38,23 @@
<content_rating type="oars-1.1"/> <content_rating type="oars-1.1"/>
<releases> <releases>
<release version="1.2.0" date="2026-06-27" type="stable">
<description>
<ul>
<li>Support for RAR5</li>
<li>Lossless compression for WebP, AVIF, and JXL</li>
<li>Control over encoder effort and speed</li>
<li>Combine multiple comics into a single file</li>
<li>Save and switch between settings profiles</li>
<li>Multi-language interface</li>
<li>Choose the document rendering resolution (DPI)</li>
<li>Option to skip upscaling of smaller images</li>
<li>Refreshed interface with page-by-page preview and cover thumbnails in the file picker</li>
<li>Various fixes and smaller improvements</li>
</ul>
</description>
<url type="details">https://github.com/gen2brain/cbconvert/releases/tag/v1.2.0</url>
</release>
<release version="1.1.0" date="2024-11-06" type="stable"> <release version="1.1.0" date="2024-11-06" type="stable">
<description> <description>
<ul> <ul>
@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>19H2</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>iup</string>
<key>CFBundleIdentifier</key>
<string>br.puc-rio.tecgraf.iup</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>iup</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>12D4e</string>
<key>DTPlatformName</key>
<string>macosx</string>
<key>DTPlatformVersion</key>
<string>11.1</string>
<key>DTSDKBuild</key>
<string>20C63</string>
<key>DTSDKName</key>
<string>macosx11.1</string>
<key>DTXcode</key>
<string>1240</string>
<key>DTXcodeBuild</key>
<string>12D4e</string>
<key>LSMinimumSystemVersion</key>
<string>10.14</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2015 Tecgraf, PUC-Rio, Brazil. All rights reserved.</string>
</dict>
</plist>
+38 -20
View File
@@ -1,36 +1,54 @@
module github.com/gen2brain/cbconvert/cmd/cbconvert-gui module github.com/gen2brain/cbconvert/cmd/cbconvert-gui
go 1.23 go 1.26
require ( require (
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca github.com/fvbommel/sortorder v1.1.0
github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4 github.com/gen2brain/cbconvert v1.0.5-0.20260627172825-0f6e32c177ee
github.com/godbus/dbus/v5 v5.1.0 github.com/gen2brain/iup-go/iup v0.32.1-0.20260627135200-7df674d35173
) )
require ( 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-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // 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-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // 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.5.1 // indirect
github.com/gen2brain/avif v0.4.1 // indirect github.com/gen2brain/go-fitz v1.28.0 // indirect
github.com/gen2brain/go-fitz v1.24.14 // indirect github.com/gen2brain/jpegli v0.4.1 // indirect
github.com/gen2brain/go-unarr v0.2.4 // indirect github.com/gen2brain/jpegn v0.4.2 // indirect
github.com/gen2brain/jpegli v0.3.3 // indirect github.com/gen2brain/jpegxl v0.5.2 // indirect
github.com/gen2brain/jpegxl v0.4.2 // indirect github.com/gen2brain/webp v0.6.1 // indirect
github.com/gen2brain/webp v0.5.1 // indirect
github.com/go-errors/errors v1.5.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-20260625163123-7c0e84413537 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/jupiterrider/ffi v0.2.1 // indirect github.com/klauspost/compress v1.18.6 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect github.com/klauspost/pgzip v1.2.6 // indirect
golang.org/x/image v0.21.0 // indirect github.com/mholt/archives v0.1.5 // indirect
golang.org/x/net v0.30.0 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect
golang.org/x/sync v0.8.0 // 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 gopkg.in/yaml.v2 v2.4.0 // indirect
) )
+100 -36
View File
@@ -1,5 +1,21 @@
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs= 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-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-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs= github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
@@ -18,64 +34,112 @@ 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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 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 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= 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.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.4.1/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs= github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca h1:qYA+cXWUG/Dx+wG2yQ4iuw/s5ROGdXAdo5aIUHrOq4U= github.com/gen2brain/cbconvert v1.0.5-0.20260627172825-0f6e32c177ee h1:9ggHORYXL+0zSdfC0bJA049amaH5wW6jZgO5dGisKm4=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca/go.mod h1:pQ9kQXsCZQqy7LraruUkBl7CpKNHFpWayxqQ+qliE8Y= github.com/gen2brain/cbconvert v1.0.5-0.20260627172825-0f6e32c177ee/go.mod h1:KrAZdAzcMj1XQkfDz5bp0hotVFUB2k9hRT+9F4b5f2E=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo= github.com/gen2brain/go-fitz v1.28.0 h1:RovqgQPAcOuyv5HZrWsTWl8qwlwbAHSKcAZXZUw0Vlk=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4= github.com/gen2brain/go-fitz v1.28.0/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs=
github.com/gen2brain/go-unarr v0.2.4 h1:Iu2kqtGfkLBSQoTFwMkSCmp0g3GrEM/XMVWzo9TQr/Y= github.com/gen2brain/iup-go/iup v0.32.1-0.20260627135200-7df674d35173 h1:nBt0N1ixK8eg/7RXJIC4b0WDPxfENqyT+rH1/STZGj4=
github.com/gen2brain/go-unarr v0.2.4/go.mod h1:0kdy3HtjKBcEaewifXZguHCvt4qD9V8iJCx4FPEOWT8= github.com/gen2brain/iup-go/iup v0.32.1-0.20260627135200-7df674d35173/go.mod h1:V4f7tHOJAeHtjQ+ju795QKv6DGdLEb4L5cmWB1sjSzU=
github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4 h1:ElkeiufJ5qqFbhxLH0TP7FhLOm37mXezIOk5iJgV27Q= github.com/gen2brain/jpegli v0.4.1 h1:qc11IQU0jTYFltroulT4MXmbu9YRftqHV0YBZ0Bqz5o=
github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4/go.mod h1:F026AOq3tZrMCqTMa3K32xLh7fyTdc9+KMTMUX6EtX0= github.com/gen2brain/jpegli v0.4.1/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegli v0.3.3 h1:ryCOQpmGuVk6FA+QBe9st6cW48jsRdVOPiNrAJ50m+k= github.com/gen2brain/jpegn v0.4.2 h1:sxy2yolV1eNA02uYtnqBFm4EIC3ETnars98aG7Dc4LM=
github.com/gen2brain/jpegli v0.3.3/go.mod h1:6Dbgr+ni1IUBqGVOKHn8lY+6DvwSGfAfC7pPQiSK6uA= github.com/gen2brain/jpegn v0.4.2/go.mod h1:YvcVOmVPSAsefH6yn9HBW3uY0EHlZwCMoiJXoAWfgL0=
github.com/gen2brain/jpegxl v0.4.2 h1:Ff0jAWtCRdc9yjPc9jkyak6Ji/A89Jg0KI+D7qOEtRI= github.com/gen2brain/jpegxl v0.5.2 h1:1ou9YRziU8PbpkfFJIyxrNjYM+WaMl2n9LloABxkKsU=
github.com/gen2brain/jpegxl v0.4.2/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o= github.com/gen2brain/jpegxl v0.5.2/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.5.1 h1:ly9olTGveZEpq3soJuCmex9fxLJ0ipHcQRRSRit5EUE= github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.5.1/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw= 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.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.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.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 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 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-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-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-20260625163123-7c0e84413537 h1:KeIaDS/+VEy/bhDYjG3Z78dOyLAU4HXcVxmd0WYHJTE=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/geo v0.0.0-20260625163123-7c0e84413537/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/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 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= 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/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/jupiterrider/ffi v0.2.1/go.mod h1:tJ7Q8p/3blFjdWt5qJU4W5oDE0xloImvrViE+0td0Rk= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= 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/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.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= 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-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-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-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-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.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-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.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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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=
+513
View File
@@ -0,0 +1,513 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"github.com/fvbommel/sortorder"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"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.
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
}
// addFiles appends files, natural-sorts the list, and rebuilds the table.
func addFiles(fs []cbconvert.File) {
if len(fs) == 0 {
return
}
wasEmpty := len(files) == 0
var selPath string
if index >= 0 && index < len(files) {
selPath = files[index].Path
}
files = append(files, fs...)
sort.Slice(files, func(i, j int) bool {
return sortorder.NaturalLess(files[i].Name, files[j].Name)
})
t := iup.GetHandle("Table")
t.SetAttribute("NUMLIN", strconv.Itoa(len(files)))
for i, f := range files {
lin := i + 1
iup.SetAttributeId2(t, "", lin, 1, f.Name)
iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(f.Path))
iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64))
}
if wasEmpty {
selectRow(0)
setActive()
previewPost()
return
}
index = -1
for i, f := range files {
if f.Path == selPath {
selectRow(i)
break
}
}
setActive()
}
func previewPost() {
if index == -1 || len(files) == 0 {
return
}
file := files[index]
// On a new file, fetch the count first; the Page POSTMESSAGE handler clamps previewPage and renders.
if file.Path != previewPath {
previewPath = file.Path
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
go pageCountPost(file)
return
}
previewRender()
}
// previewRenderSize is the size the cover is rendered at.
const previewRenderSize = 1200
// previewRender renders the current file at previewPage off the UI thread.
func previewRender() {
if index == -1 || len(files) == 0 {
return
}
file := files[index]
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
opts := options()
page := previewPage
go func(opts cbconvert.Options) {
conv := cbconvert.New(opts)
var s string
img, err := conv.PreviewPage(file.Path, file.Stat, page, previewRenderSize, previewRenderSize)
if err != nil {
s = err.Error()
fmt.Println(err)
}
iup.PostMessage(iup.GetHandle("Preview"), s, page, img)
}(opts)
}
// pageCountPost computes the file's page count off the UI thread and posts it, tagged with the path, to the Page spin.
func pageCountPost(file cbconvert.File) {
n, err := cbconvert.New(cbconvert.NewOptions()).PageCount(file.Path, file.Stat)
if err != nil || n < 1 {
n = 1
}
iup.PostMessage(iup.GetHandle("Page"), file.Path, n, nil)
}
// onPageChanged re-renders the preview for the spin's page; dedupes so spin and typing don't both fire.
func onPageChanged() int {
page := iup.GetHandle("Page").GetInt("VALUE") - 1
if page < 0 {
page = 0
}
if page == previewPage {
return iup.DEFAULT
}
previewPage = page
previewRender()
return iup.DEFAULT
}
func onAddFiles(ih iup.Ihandle) int {
args, err := fileDlg(i18n.Lng(i18n.DlgAddFiles), 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
}
addFiles(fs)
}
return iup.DEFAULT
}
func onAddDir(ih iup.Ihandle) int {
args, err := fileDlg(i18n.Lng(i18n.DlgAddDir), 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
}
addFiles(fs)
}
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()
if len(files) == 0 {
clearPreview()
} else {
previewPost()
}
return iup.DEFAULT
}
// clearPreview resets the preview state and repaints the canvas to its empty state.
func clearPreview() {
iup.Destroy(iup.GetHandle("cover"))
hasCover = false
previewPath = ""
previewPage = 0
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
iup.Update(iup.GetHandle("Preview"))
}
func onRemoveAll(ih iup.Ihandle) int {
index = -1
files = make([]cbconvert.File, 0)
iup.GetHandle("Table").SetAttribute("NUMLIN", "0")
clearPreview()
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(i18n.Lng(i18n.DlgOutputDir), 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(i18n.Lng(i18n.DlgOutputFile), 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", i18n.Lng(i18n.FilterNearest))
case 2:
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterBox))
case 3:
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterLinear))
case 4:
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterMitchell))
case 5:
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterCatmull))
case 6:
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterGaussian))
case 7:
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterLanczos))
}
previewPost()
return iup.DEFAULT
}
+26
View File
@@ -0,0 +1,26 @@
//go:build !windows
package i18n
import (
"os"
"strings"
)
// systemLangCode returns the two-letter language code from the POSIX locale environment, or "".
func systemLangCode() string {
for _, env := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} {
v := os.Getenv(env)
if v == "" || v == "C" || v == "POSIX" {
continue
}
if i := strings.IndexAny(v, "_.@"); i >= 0 {
v = v[:i]
}
return strings.ToLower(v)
}
return ""
}
+26
View File
@@ -0,0 +1,26 @@
//go:build windows
package i18n
import "syscall"
// primaryLang maps a Windows primary-language id to a two-letter language code.
var primaryLang = map[uint16]string{
0x09: "en",
0x16: "pt",
0x0a: "es",
0x05: "cs",
0x19: "ru",
0x07: "de",
0x0c: "fr",
0x04: "zh",
0x11: "ja",
0x10: "it",
}
// systemLangCode returns the two-letter code of the user's default UI language, or "".
func systemLangCode() string {
r, _, _ := syscall.NewLazyDLL("kernel32.dll").NewProc("GetUserDefaultUILanguage").Call()
return primaryLang[uint16(r)&0x3ff]
}
+189
View File
@@ -0,0 +1,189 @@
// Package i18n holds the cbconvert GUI message keys and per-language string packs.
package i18n
import "github.com/gen2brain/iup-go/iup"
const (
ColTitle = "COL_TITLE"
ColType = "COL_TYPE"
ColSize = "COL_SIZE"
TabInput = "TAB_INPUT"
TabOutput = "TAB_OUTPUT"
TabImage = "TAB_IMAGE"
TabTransform = "TAB_TRANSFORM"
LblPage = "LBL_PAGE"
TipPage = "TIP_PAGE"
TglRecursive = "TGL_RECURSIVE"
TipRecursive = "TIP_RECURSIVE"
TglNoRGB = "TGL_NORGB"
TipNoRGB = "TIP_NORGB"
TglNoCover = "TGL_NOCOVER"
TipNoCover = "TIP_NOCOVER"
TglNoNonImage = "TGL_NONONIMAGE"
TipNoNonImage = "TIP_NONONIMAGE"
TglNoConvert = "TGL_NOCONVERT"
TipNoConvert = "TIP_NOCONVERT"
LblMinSize = "LBL_MINSIZE"
TipSize = "TIP_SIZE"
LblDPI = "LBL_DPI"
TipDPI = "TIP_DPI"
LblOutDir = "LBL_OUTDIR"
TipOutDir = "TIP_OUTDIR"
BtnBrowse = "BTN_BROWSE"
LblSuffix = "LBL_SUFFIX"
TipSuffix = "TIP_SUFFIX"
LblArchive = "LBL_ARCHIVE"
TipArchive = "TIP_ARCHIVE"
LblCompression = "LBL_COMPRESSION"
TipZipLevel = "TIP_ZIPLEVEL"
TglCombine = "TGL_COMBINE"
TipCombine = "TIP_COMBINE"
LblOutFile = "LBL_OUTFILE"
TipOutFile = "TIP_OUTFILE"
LblFormat = "LBL_FORMAT"
TipFormat = "TIP_FORMAT"
LblSize = "LBL_SIZE"
CueWidth = "CUE_WIDTH"
CueHeight = "CUE_HEIGHT"
TipWidthHeight = "TIP_WIDTHHEIGHT"
TglFit = "TGL_FIT"
TipFit = "TIP_FIT"
TglNoUpscale = "TGL_NOUPSCALE"
TipNoUpscale = "TIP_NOUPSCALE"
LblFilter = "LBL_FILTER"
LblQuality = "LBL_QUALITY"
TipQuality = "TIP_QUALITY"
LblEffort = "LBL_EFFORT"
TipEffort = "TIP_EFFORT"
TglLossless = "TGL_LOSSLESS"
TipLossless = "TIP_LOSSLESS"
TglGrayscale = "TGL_GRAYSCALE"
TipGrayscale = "TIP_GRAYSCALE"
LblBrightness = "LBL_BRIGHTNESS"
TipBrightness = "TIP_BRIGHTNESS"
LblContrast = "LBL_CONTRAST"
TipContrast = "TIP_CONTRAST"
LblRotate = "LBL_ROTATE"
TipRotate = "TIP_ROTATE"
EffortMethod = "EFFORT_METHOD"
EffortSpeed = "EFFORT_SPEED"
EffortEffort = "EFFORT_EFFORT"
TipEffortWebp = "TIP_EFFORT_WEBP"
TipEffortAvif = "TIP_EFFORT_AVIF"
TipEffortJxl = "TIP_EFFORT_JXL"
BtnAddFiles = "BTN_ADDFILES"
BtnAddDir = "BTN_ADDDIR"
BtnRemove = "BTN_REMOVE"
BtnRemoveAll = "BTN_REMOVEALL"
BtnThumbnail = "BTN_THUMBNAIL"
BtnCover = "BTN_COVER"
BtnConvert = "BTN_CONVERT"
BtnCancel = "BTN_CANCEL"
TipCancel = "TIP_CANCEL"
BtnReset = "BTN_RESET"
TipReset = "TIP_RESET"
BtnSave = "BTN_SAVE"
TipSave = "TIP_SAVE"
BtnCommand = "BTN_COMMAND"
TipCommand = "TIP_COMMAND"
LblProfile = "LBL_PROFILE"
TipProfile = "TIP_PROFILE"
TipThumbnail = "TIP_THUMBNAIL"
TipCover = "TIP_COVER"
TipConvert = "TIP_CONVERT"
StatusNeedFilesAndDir = "STATUS_NEED_FILES_AND_DIR"
StatusNeedFiles = "STATUS_NEED_FILES"
StatusNeedOutDir = "STATUS_NEED_OUTDIR"
StatusFileOf = "STATUS_FILE_OF"
FilterNearest = "FILTER_NEAREST"
FilterBox = "FILTER_BOX"
FilterLinear = "FILTER_LINEAR"
FilterMitchell = "FILTER_MITCHELL"
FilterCatmull = "FILTER_CATMULL"
FilterGaussian = "FILTER_GAUSSIAN"
FilterLanczos = "FILTER_LANCZOS"
DlgAddFiles = "DLG_ADDFILES"
DlgAddDir = "DLG_ADDDIR"
DlgOutputDir = "DLG_OUTPUTDIR"
DlgOutputFile = "DLG_OUTPUTFILE"
DlgCommandLine = "DLG_COMMANDLINE"
DlgSaveProfile = "DLG_SAVEPROFILE"
ParamName = "PARAM_NAME"
MsgInvalidNameTitle = "MSG_INVALIDNAME_TITLE"
MsgInvalidNameBody = "MSG_INVALIDNAME_BODY"
NoPreview = "NO_PREVIEW"
)
// packs maps an IUP language name to its message pack, filled by each language file's init.
var packs = map[string]map[string]string{}
// register adds a language pack; called from each i18n_<lang>.go init.
func register(lang string, pack map[string]string) {
packs[lang] = pack
}
// langByCode maps a two-letter language code to the IUP language name.
var langByCode = map[string]string{
"en": "ENGLISH",
"pt": "PORTUGUESE",
"es": "SPANISH",
"cs": "CZECH",
"ru": "RUSSIAN",
"de": "GERMAN",
"fr": "FRENCH",
"zh": "CHINESE",
"ja": "JAPANESE",
"it": "ITALIAN",
}
// Lng wraps a message key for IUP's automatic language-string lookup.
func Lng(key string) string {
return "_@" + key
}
// Str returns the translated string for a key, for use where IUP's "_@" prefix does not apply.
func Str(key string) string {
return iup.GetLanguageString(key)
}
// Init detects the system language, switches IUP's predefined strings to it, and registers the message packs.
func Init() {
lang := detect()
iup.SetLanguage(lang)
registerPack(packs["ENGLISH"])
if lang != "ENGLISH" {
if pack, ok := packs[lang]; ok {
registerPack(pack)
}
}
}
func registerPack(pack map[string]string) {
for name, value := range pack {
iup.SetLanguageString(name, value)
}
}
// detect returns the IUP language name for the system locale, or ENGLISH.
func detect() string {
if name, ok := langByCode[systemLangCode()]; ok {
return name
}
return "ENGLISH"
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("CZECH", map[string]string{
ColTitle: "Název",
ColType: "Typ",
ColSize: "Velikost (MiB)",
TabInput: "Vstup",
TabOutput: "Výstup",
TabImage: "Obrázek",
TabTransform: "Transformace",
LblPage: "Stránka:",
TipPage: "Náhled jiné stránky vybraného komiksu",
TglRecursive: " Procházet podadresáře",
TipRecursive: "Zpracovat podadresáře rekurzivně",
TglNoRGB: " Pouze obrázky ve stupních šedi",
TipNoRGB: "Nepřevádět obrázky s barevným prostorem RGB",
TglNoCover: " Vyloučit obálku",
TipNoCover: "Nepřevádět obrázek obálky",
TglNoNonImage: " Odstranit z archivu soubory, které nejsou obrázky",
TipNoNonImage: "Odstranit z archivu soubory .nfo, .xml, .txt",
TglNoConvert: " Netransformovat ani nepřevádět obrázky",
TipNoConvert: "Kopírovat obrázky z archivu nebo adresáře beze změn",
LblMinSize: "Minimální velikost (MiB):",
TipSize: "Zpracovat pouze soubory větší než minimální velikost",
LblDPI: "DPI dokumentu:",
TipDPI: "Rozlišení pro vykreslování dokumentů (PDF, EPUB atd.); výchozí používá původní rozlišení",
LblOutDir: "Výstupní adresář:",
TipOutDir: "Adresář, do kterého se zapisují převedené soubory (povinné)",
BtnBrowse: "Procházet...",
LblSuffix: "Přidat příponu k výstupnímu souboru:",
TipSuffix: "Přidat příponu k názvu souboru, např. nazevsouboru_pripona.cbz",
LblArchive: "Formát archivu:",
TipArchive: "Výstupní kontejner: ZIP (.cbz) nebo nekomprimovaný TAR (.cbt)",
LblCompression: "Komprese:",
TipZipLevel: "Komprese ZIP: Uložit ji vypne, 1 je nejrychlejší, 9 nejmenší",
TglCombine: " Sloučit do jednoho souboru",
TipCombine: "Sloučit všechny uvedené soubory do jednoho archivu",
LblOutFile: "Výstupní soubor:",
TipOutFile: "Název sloučeného souboru (výchozí: první vstup + -combined)",
LblFormat: "Formát:",
TipFormat: "Výstupní formát obrázku pro převedené stránky",
LblSize: "Velikost:",
CueWidth: "šířka",
CueHeight: "výška",
TipWidthHeight: "Pokud není nastavena šířka nebo výška, zachová se poměr stran obrázku",
TglFit: " Nejlepší přizpůsobení",
TipFit: "Nejlepší přizpůsobení požadované šířce a výšce",
TglNoUpscale: " Nezvětšovat",
TipNoUpscale: "Nezvětšovat obrázky, které jsou již menší než požadovaná velikost",
LblFilter: "Filtr změny velikosti:",
LblQuality: "Kvalita: ",
TipQuality: "Kvalita ovlivňuje JPEG, WEBP, AVIF a JXL",
LblEffort: "Úsilí:",
TipEffort: "Rychlost/úsilí kodéru (WEBP, AVIF, JXL)",
TglLossless: " Bezztrátový",
TipLossless: "Bezztrátová komprese (WEBP, AVIF, JXL), ignoruje kvalitu",
TglGrayscale: " Stupně šedi",
TipGrayscale: "Převést obrázky do stupňů šedi (monochromaticky)",
LblBrightness: "Jas: ",
TipBrightness: "Upravit jas obrázků",
LblContrast: "Kontrast: ",
TipContrast: "Upravit kontrast obrázků",
LblRotate: "Otočit:",
TipRotate: "Otočit každou stránku po směru hodinových ručiček o zadaný úhel ve stupních",
EffortMethod: "Metoda",
EffortSpeed: "Rychlost",
EffortEffort: "Úsilí",
TipEffortWebp: "Metoda WEBP, vyšší je lepší/pomalejší (0-6, výchozí 4)",
TipEffortAvif: "Rychlost AVIF, vyšší je rychlejší/horší (0-10, výchozí 10)",
TipEffortJxl: "Úsilí JXL, vyšší je lepší/pomalejší (1-10, výchozí 7)",
BtnAddFiles: "Přidat &soubory...",
BtnAddDir: "Přidat &adresář...",
BtnRemove: "Odebrat",
BtnRemoveAll: "Odebrat vše",
BtnThumbnail: "Náhled",
BtnCover: "Obálka",
BtnConvert: "&Převést",
BtnCancel: "Zrušit",
TipCancel: "Zrušit probíhající operaci (nebo stisknout Esc)",
BtnReset: "Obnovit",
TipReset: "Obnovit všechna nastavení na výchozí hodnoty",
BtnSave: "Uložit",
TipSave: "Uložit aktuální nastavení do profilu",
BtnCommand: "Příkaz",
TipCommand: "Zobrazit odpovídající příkazový řádek",
LblProfile: "Profil:",
TipProfile: "Vybrat profil nastavení",
TipThumbnail: "Extrahovat náhledy obálek",
TipCover: "Extrahovat obálky",
TipConvert: "Převést soubory do vybraného formátu",
StatusNeedFilesAndDir: "Přidejte soubory a nastavte výstupní adresář",
StatusNeedFiles: "Přidejte soubory",
StatusNeedOutDir: "Nastavte výstupní adresář",
StatusFileOf: "Soubor %d z %d",
FilterNearest: "NearestNeighbor je nejrychlejší převzorkovací filtr, bez vyhlazování",
FilterBox: "Filtr Box (průměrování pixelů)",
FilterLinear: "Linear je bilineární filtr, hladký a přiměřeně rychlý",
FilterMitchell: "MitchellNetravali je hladký bikubický filtr",
FilterCatmull: "CatmullRom je ostrý bikubický filtr",
FilterGaussian: "Gaussian je rozostřovací filtr využívající Gaussovu funkci, užitečný pro odstranění šumu",
FilterLanczos: "Lanczos je vysoce kvalitní převzorkovací filtr, pomalejší než bikubické filtry",
DlgAddFiles: "Přidat soubory",
DlgAddDir: "Přidat adresář",
DlgOutputDir: "Výstupní adresář",
DlgOutputFile: "Výstupní soubor",
DlgCommandLine: "Příkazový řádek",
DlgSaveProfile: "Uložit profil",
ParamName: "Název: %s\n",
MsgInvalidNameTitle: "Neplatný název",
MsgInvalidNameBody: "Název profilu nesmí být prázdný ani obsahovat '.' nebo ';'.",
NoPreview: "Žádný náhled",
})
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("GERMAN", map[string]string{
ColTitle: "Titel",
ColType: "Typ",
ColSize: "Größe (MiB)",
TabInput: "Eingabe",
TabOutput: "Ausgabe",
TabImage: "Bild",
TabTransform: "Transformation",
LblPage: "Seite:",
TipPage: "Eine andere Seite des ausgewählten Comics als Vorschau anzeigen",
TglRecursive: " Unterverzeichnisse rekursiv",
TipRecursive: "Unterverzeichnisse rekursiv verarbeiten",
TglNoRGB: " Nur Graustufenbilder",
TipNoRGB: "Bilder mit RGB-Farbraum nicht konvertieren",
TglNoCover: " Titelbild ausschließen",
TipNoCover: "Das Titelbild nicht konvertieren",
TglNoNonImage: " Nicht-Bilddateien aus dem Archiv entfernen",
TipNoNonImage: ".nfo-, .xml-, .txt-Dateien aus dem Archiv entfernen",
TglNoConvert: " Bilder nicht transformieren oder konvertieren",
TipNoConvert: "Bilder aus Archiv oder Verzeichnis ohne Änderungen kopieren",
LblMinSize: "Mindestgröße (MiB):",
TipSize: "Nur Dateien größer als die Mindestgröße verarbeiten",
LblDPI: "Dokument-DPI:",
TipDPI: "Auflösung zum Rendern von Dokumenten (PDF, EPUB usw.); Standard verwendet die Originalauflösung",
LblOutDir: "Ausgabeverzeichnis:",
TipOutDir: "Verzeichnis, in das konvertierte Dateien geschrieben werden (erforderlich)",
BtnBrowse: "Durchsuchen...",
LblSuffix: "Suffix an Ausgabedatei anhängen:",
TipSuffix: "Suffix an den Dateinamen anhängen, z. B. dateiname_suffix.cbz",
LblArchive: "Archivformat:",
TipArchive: "Ausgabecontainer: ZIP (.cbz) oder unkomprimiertes TAR (.cbt)",
LblCompression: "Komprimierung:",
TipZipLevel: "ZIP-Komprimierung: Speichern deaktiviert sie, 1 ist am schnellsten, 9 am kleinsten",
TglCombine: " In einer Datei zusammenfassen",
TipCombine: "Alle aufgelisteten Dateien zu einem Archiv zusammenführen",
LblOutFile: "Ausgabedatei:",
TipOutFile: "Name der zusammengefassten Datei (Standard: erste Eingabe + -combined)",
LblFormat: "Format:",
TipFormat: "Ausgabebildformat für die konvertierten Seiten",
LblSize: "Größe:",
CueWidth: "Breite",
CueHeight: "Höhe",
TipWidthHeight: "Wenn Breite oder Höhe nicht gesetzt ist, bleibt das Seitenverhältnis erhalten",
TglFit: " Optimale Anpassung",
TipFit: "Optimale Anpassung an gewünschte Breite und Höhe",
TglNoUpscale: " Nicht vergrößern",
TipNoUpscale: "Bilder, die bereits kleiner als die gewünschte Größe sind, nicht vergrößern",
LblFilter: "Skalierungsfilter:",
LblQuality: "Qualität: ",
TipQuality: "Qualität betrifft JPEG, WEBP, AVIF und JXL",
LblEffort: "Aufwand:",
TipEffort: "Encoder-Geschwindigkeit/-Aufwand (WEBP, AVIF, JXL)",
TglLossless: " Verlustfrei",
TipLossless: "Verlustfreie Komprimierung (WEBP, AVIF, JXL), ignoriert Qualität",
TglGrayscale: " Graustufen",
TipGrayscale: "Bilder in Graustufen umwandeln (monochrom)",
LblBrightness: "Helligkeit: ",
TipBrightness: "Die Helligkeit der Bilder anpassen",
LblContrast: "Kontrast: ",
TipContrast: "Den Kontrast der Bilder anpassen",
LblRotate: "Drehen:",
TipRotate: "Jede Seite um den angegebenen Winkel im Uhrzeigersinn drehen",
EffortMethod: "Methode",
EffortSpeed: "Geschwindigkeit",
EffortEffort: "Aufwand",
TipEffortWebp: "WEBP-Methode, höher ist besser/langsamer (0-6, Standard 4)",
TipEffortAvif: "AVIF-Geschwindigkeit, höher ist schneller/schlechter (0-10, Standard 10)",
TipEffortJxl: "JXL-Aufwand, höher ist besser/langsamer (1-10, Standard 7)",
BtnAddFiles: "&Dateien hinzufügen...",
BtnAddDir: "&Verzeichnis hinzufügen...",
BtnRemove: "Entfernen",
BtnRemoveAll: "Alle entfernen",
BtnThumbnail: "Miniaturbild",
BtnCover: "Titelbild",
BtnConvert: "&Konvertieren",
BtnCancel: "Abbrechen",
TipCancel: "Laufenden Vorgang abbrechen (oder Esc drücken)",
BtnReset: "Zurücksetzen",
TipReset: "Alle Einstellungen auf die Standardwerte zurücksetzen",
BtnSave: "Speichern",
TipSave: "Aktuelle Einstellungen in einem Profil speichern",
BtnCommand: "Befehl",
TipCommand: "Die entsprechende Befehlszeile anzeigen",
LblProfile: "Profil:",
TipProfile: "Ein Einstellungsprofil auswählen",
TipThumbnail: "Titelbild-Miniaturen extrahieren",
TipCover: "Titelbilder extrahieren",
TipConvert: "Dateien in das ausgewählte Format konvertieren",
StatusNeedFilesAndDir: "Dateien hinzufügen und Ausgabeverzeichnis festlegen",
StatusNeedFiles: "Dateien hinzufügen",
StatusNeedOutDir: "Ausgabeverzeichnis festlegen",
StatusFileOf: "Datei %d von %d",
FilterNearest: "NearestNeighbor ist der schnellste Skalierungsfilter, ohne Antialiasing",
FilterBox: "Box-Filter (Mittelung der Pixel)",
FilterLinear: "Linear ist der bilineare Filter, glatt und einigermaßen schnell",
FilterMitchell: "MitchellNetravali ist ein glatter bikubischer Filter",
FilterCatmull: "CatmullRom ist ein scharfer bikubischer Filter",
FilterGaussian: "Gaussian ist ein Weichzeichnungsfilter mit Gauß-Funktion, nützlich zur Rauschentfernung",
FilterLanczos: "Lanczos ist ein hochwertiger Skalierungsfilter, langsamer als bikubische Filter",
DlgAddFiles: "Dateien hinzufügen",
DlgAddDir: "Verzeichnis hinzufügen",
DlgOutputDir: "Ausgabeverzeichnis",
DlgOutputFile: "Ausgabedatei",
DlgCommandLine: "Befehlszeile",
DlgSaveProfile: "Profil speichern",
ParamName: "Name: %s\n",
MsgInvalidNameTitle: "Ungültiger Name",
MsgInvalidNameBody: "Der Profilname darf nicht leer sein oder '.' oder ';' enthalten.",
NoPreview: "Keine Vorschau",
})
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("ENGLISH", map[string]string{
ColTitle: "Title",
ColType: "Type",
ColSize: "Size (MiB)",
TabInput: "Input",
TabOutput: "Output",
TabImage: "Image",
TabTransform: "Transform",
LblPage: "Page:",
TipPage: "Preview a different page of the selected comic",
TglRecursive: " Recurse SubDirectories",
TipRecursive: "Process subdirectories recursively",
TglNoRGB: " Only Grayscale Images",
TipNoRGB: "Do not convert images that have RGB colorspace",
TglNoCover: " Exclude Cover",
TipNoCover: "Do not convert the cover image",
TglNoNonImage: " Remove Non-Image Files from the Archive",
TipNoNonImage: "Remove .nfo, .xml, .txt files from the archive",
TglNoConvert: " Do not Transform or Convert Images",
TipNoConvert: "Copy images from archive or directory without modifications",
LblMinSize: "Minimum Size (MiB):",
TipSize: "Process only files larger than minimum size",
LblDPI: "Document DPI:",
TipDPI: "Resolution for rendering documents (PDF, EPUB, etc.); Default uses the original resolution",
LblOutDir: "Output Directory:",
TipOutDir: "Directory where converted files are written (required)",
BtnBrowse: "Browse...",
LblSuffix: "Add Suffix to Output File:",
TipSuffix: "Add suffix to filename, i.e. filename_suffix.cbz",
LblArchive: "Archive Format:",
TipArchive: "Output container: ZIP (.cbz) or uncompressed TAR (.cbt)",
LblCompression: "Compression:",
TipZipLevel: "ZIP compression: Store disables it, 1 is fastest, 9 is smallest",
TglCombine: " Combine into single file",
TipCombine: "Merge all listed files into one archive",
LblOutFile: "Output File:",
TipOutFile: "Combined file name (default: first input + -combined)",
LblFormat: "Format:",
TipFormat: "Output image format for the converted pages",
LblSize: "Size:",
CueWidth: "width",
CueHeight: "height",
TipWidthHeight: "If one of, width or height is not set, the image aspect ratio is preserved",
TglFit: " Best Fit",
TipFit: "Best fit for required width and height",
TglNoUpscale: " No Upscale",
TipNoUpscale: "Do not enlarge images already smaller than the requested size",
LblFilter: "Resize Filter:",
LblQuality: "Quality: ",
TipQuality: "Quality affects JPEG, WEBP, AVIF and JXL",
LblEffort: "Effort:",
TipEffort: "Encoder speed/effort (WEBP, AVIF, JXL)",
TglLossless: " Lossless",
TipLossless: "Lossless compression (WEBP, AVIF, JXL), ignores quality",
TglGrayscale: " Grayscale",
TipGrayscale: "Convert images to grayscale (monochromatic)",
LblBrightness: "Brightness: ",
TipBrightness: "Adjust the brightness of the images",
LblContrast: "Contrast: ",
TipContrast: "Adjust the contrast of the images",
LblRotate: "Rotate:",
TipRotate: "Rotate every page clockwise by the given angle in degrees",
EffortMethod: "Method",
EffortSpeed: "Speed",
EffortEffort: "Effort",
TipEffortWebp: "WEBP method, higher is better/slower (0-6, default 4)",
TipEffortAvif: "AVIF speed, higher is faster/worse (0-10, default 10)",
TipEffortJxl: "JXL effort, higher is better/slower (1-10, default 7)",
BtnAddFiles: "Add &Files...",
BtnAddDir: "Add &Dir...",
BtnRemove: "Remove",
BtnRemoveAll: "Remove All",
BtnThumbnail: "Thumbnail",
BtnCover: "Cover",
BtnConvert: "&Convert",
BtnCancel: "Cancel",
TipCancel: "Cancel the running operation (or press Esc)",
BtnReset: "Reset",
TipReset: "Restore all settings to their defaults",
BtnSave: "Save",
TipSave: "Save current settings to a profile",
BtnCommand: "Command",
TipCommand: "Show the equivalent command line",
LblProfile: "Profile:",
TipProfile: "Select a settings profile",
TipThumbnail: "Extract cover thumbnails",
TipCover: "Extract covers",
TipConvert: "Convert files to the selected format",
StatusNeedFilesAndDir: "Add files and set output directory",
StatusNeedFiles: "Add files",
StatusNeedOutDir: "Set output directory",
StatusFileOf: "File %d of %d",
FilterNearest: "NearestNeighbor is the fastest resampling filter, no antialiasing",
FilterBox: "Box filter (averaging pixels)",
FilterLinear: "Linear is the bilinear filter, smooth and reasonably fast",
FilterMitchell: "MitchellNetravali is a smooth bicubic filter",
FilterCatmull: "CatmullRom is a sharp bicubic filter",
FilterGaussian: "Gaussian is a blurring filter that uses gaussian function, useful for noise removal",
FilterLanczos: "Lanczos is a high-quality resampling filter, it's slower than cubic filters",
DlgAddFiles: "Add Files",
DlgAddDir: "Add Directory",
DlgOutputDir: "Output Directory",
DlgOutputFile: "Output File",
DlgCommandLine: "Command Line",
DlgSaveProfile: "Save Profile",
ParamName: "Name: %s\n",
MsgInvalidNameTitle: "Invalid Name",
MsgInvalidNameBody: "Profile name must not be empty or contain '.' or ';'.",
NoPreview: "No preview",
})
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("SPANISH", map[string]string{
ColTitle: "Título",
ColType: "Tipo",
ColSize: "Tamaño (MiB)",
TabInput: "Entrada",
TabOutput: "Salida",
TabImage: "Imagen",
TabTransform: "Transformar",
LblPage: "Página:",
TipPage: "Previsualizar otra página del cómic seleccionado",
TglRecursive: " Recorrer subdirectorios",
TipRecursive: "Procesar subdirectorios de forma recursiva",
TglNoRGB: " Solo imágenes en escala de grises",
TipNoRGB: "No convertir imágenes que tengan espacio de color RGB",
TglNoCover: " Excluir la portada",
TipNoCover: "No convertir la imagen de portada",
TglNoNonImage: " Eliminar archivos que no sean imágenes del archivo",
TipNoNonImage: "Eliminar archivos .nfo, .xml, .txt del archivo",
TglNoConvert: " No transformar ni convertir imágenes",
TipNoConvert: "Copiar imágenes del archivo o directorio sin modificaciones",
LblMinSize: "Tamaño mínimo (MiB):",
TipSize: "Procesar solo archivos mayores que el tamaño mínimo",
LblDPI: "DPI del documento:",
TipDPI: "Resolución para renderizar documentos (PDF, EPUB, etc.); el valor predeterminado usa la resolución original",
LblOutDir: "Directorio de salida:",
TipOutDir: "Directorio donde se escriben los archivos convertidos (obligatorio)",
BtnBrowse: "Examinar...",
LblSuffix: "Añadir sufijo al archivo de salida:",
TipSuffix: "Añadir sufijo al nombre de archivo, p. ej. nombrearchivo_sufijo.cbz",
LblArchive: "Formato de archivo:",
TipArchive: "Contenedor de salida: ZIP (.cbz) o TAR sin comprimir (.cbt)",
LblCompression: "Compresión:",
TipZipLevel: "Compresión ZIP: Almacenar la desactiva, 1 es la más rápida, 9 la más pequeña",
TglCombine: " Combinar en un solo archivo",
TipCombine: "Fusionar todos los archivos listados en un solo archivo",
LblOutFile: "Archivo de salida:",
TipOutFile: "Nombre del archivo combinado (predeterminado: primera entrada + -combined)",
LblFormat: "Formato:",
TipFormat: "Formato de imagen de salida para las páginas convertidas",
LblSize: "Tamaño:",
CueWidth: "ancho",
CueHeight: "alto",
TipWidthHeight: "Si no se define el ancho o el alto, se conserva la relación de aspecto de la imagen",
TglFit: " Ajuste óptimo",
TipFit: "Ajuste óptimo al ancho y alto requeridos",
TglNoUpscale: " No ampliar",
TipNoUpscale: "No ampliar imágenes que ya son más pequeñas que el tamaño solicitado",
LblFilter: "Filtro de redimensionado:",
LblQuality: "Calidad: ",
TipQuality: "La calidad afecta a JPEG, WEBP, AVIF y JXL",
LblEffort: "Esfuerzo:",
TipEffort: "Velocidad/esfuerzo del codificador (WEBP, AVIF, JXL)",
TglLossless: " Sin pérdidas",
TipLossless: "Compresión sin pérdidas (WEBP, AVIF, JXL), ignora la calidad",
TglGrayscale: " Escala de grises",
TipGrayscale: "Convertir imágenes a escala de grises (monocromático)",
LblBrightness: "Brillo: ",
TipBrightness: "Ajustar el brillo de las imágenes",
LblContrast: "Contraste: ",
TipContrast: "Ajustar el contraste de las imágenes",
LblRotate: "Rotar:",
TipRotate: "Rotar cada página en sentido horario el ángulo dado en grados",
EffortMethod: "Método",
EffortSpeed: "Velocidad",
EffortEffort: "Esfuerzo",
TipEffortWebp: "Método WEBP, más alto es mejor/más lento (0-6, predeterminado 4)",
TipEffortAvif: "Velocidad AVIF, más alto es más rápido/peor (0-10, predeterminado 10)",
TipEffortJxl: "Esfuerzo JXL, más alto es mejor/más lento (1-10, predeterminado 7)",
BtnAddFiles: "Añadir &archivos...",
BtnAddDir: "Añadir &directorio...",
BtnRemove: "Eliminar",
BtnRemoveAll: "Eliminar todo",
BtnThumbnail: "Miniatura",
BtnCover: "Portada",
BtnConvert: "&Convertir",
BtnCancel: "Cancelar",
TipCancel: "Cancelar la operación en curso (o pulsar Esc)",
BtnReset: "Restablecer",
TipReset: "Restaurar todos los ajustes a sus valores predeterminados",
BtnSave: "Guardar",
TipSave: "Guardar los ajustes actuales en un perfil",
BtnCommand: "Comando",
TipCommand: "Mostrar la línea de comandos equivalente",
LblProfile: "Perfil:",
TipProfile: "Seleccionar un perfil de ajustes",
TipThumbnail: "Extraer miniaturas de portada",
TipCover: "Extraer portadas",
TipConvert: "Convertir archivos al formato seleccionado",
StatusNeedFilesAndDir: "Añadir archivos y establecer el directorio de salida",
StatusNeedFiles: "Añadir archivos",
StatusNeedOutDir: "Establecer el directorio de salida",
StatusFileOf: "Archivo %d de %d",
FilterNearest: "NearestNeighbor es el filtro de remuestreo más rápido, sin antialiasing",
FilterBox: "Filtro Box (promedio de píxeles)",
FilterLinear: "Linear es el filtro bilineal, suave y razonablemente rápido",
FilterMitchell: "MitchellNetravali es un filtro bicúbico suave",
FilterCatmull: "CatmullRom es un filtro bicúbico nítido",
FilterGaussian: "Gaussian es un filtro de desenfoque que usa la función gaussiana, útil para eliminar ruido",
FilterLanczos: "Lanczos es un filtro de remuestreo de alta calidad, más lento que los filtros cúbicos",
DlgAddFiles: "Añadir archivos",
DlgAddDir: "Añadir directorio",
DlgOutputDir: "Directorio de salida",
DlgOutputFile: "Archivo de salida",
DlgCommandLine: "Línea de comandos",
DlgSaveProfile: "Guardar perfil",
ParamName: "Nombre: %s\n",
MsgInvalidNameTitle: "Nombre no válido",
MsgInvalidNameBody: "El nombre del perfil no debe estar vacío ni contener '.' o ';'.",
NoPreview: "Sin vista previa",
})
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("FRENCH", map[string]string{
ColTitle: "Titre",
ColType: "Type",
ColSize: "Taille (Mio)",
TabInput: "Entrée",
TabOutput: "Sortie",
TabImage: "Image",
TabTransform: "Transformation",
LblPage: "Page :",
TipPage: "Prévisualiser une autre page de la bande dessinée sélectionnée",
TglRecursive: " Parcourir les sous-dossiers",
TipRecursive: "Traiter les sous-dossiers de manière récursive",
TglNoRGB: " Images en niveaux de gris uniquement",
TipNoRGB: "Ne pas convertir les images ayant un espace colorimétrique RVB",
TglNoCover: " Exclure la couverture",
TipNoCover: "Ne pas convertir l'image de couverture",
TglNoNonImage: " Supprimer les fichiers non-image de l'archive",
TipNoNonImage: "Supprimer les fichiers .nfo, .xml, .txt de l'archive",
TglNoConvert: " Ne pas transformer ni convertir les images",
TipNoConvert: "Copier les images de l'archive ou du dossier sans modifications",
LblMinSize: "Taille minimale (Mio) :",
TipSize: "Ne traiter que les fichiers plus grands que la taille minimale",
LblDPI: "DPI du document :",
TipDPI: "Résolution pour le rendu des documents (PDF, EPUB, etc.) ; la valeur par défaut utilise la résolution d'origine",
LblOutDir: "Dossier de sortie :",
TipOutDir: "Dossier où les fichiers convertis sont écrits (obligatoire)",
BtnBrowse: "Parcourir...",
LblSuffix: "Ajouter un suffixe au fichier de sortie :",
TipSuffix: "Ajouter un suffixe au nom de fichier, par ex. nomfichier_suffixe.cbz",
LblArchive: "Format d'archive :",
TipArchive: "Conteneur de sortie : ZIP (.cbz) ou TAR non compressé (.cbt)",
LblCompression: "Compression :",
TipZipLevel: "Compression ZIP : Stocker la désactive, 1 est le plus rapide, 9 le plus petit",
TglCombine: " Combiner en un seul fichier",
TipCombine: "Fusionner tous les fichiers listés en une seule archive",
LblOutFile: "Fichier de sortie :",
TipOutFile: "Nom du fichier combiné (par défaut : première entrée + -combined)",
LblFormat: "Format :",
TipFormat: "Format d'image de sortie pour les pages converties",
LblSize: "Taille :",
CueWidth: "largeur",
CueHeight: "hauteur",
TipWidthHeight: "Si la largeur ou la hauteur n'est pas définie, le rapport d'aspect de l'image est conservé",
TglFit: " Ajustement optimal",
TipFit: "Ajustement optimal à la largeur et la hauteur requises",
TglNoUpscale: " Ne pas agrandir",
TipNoUpscale: "Ne pas agrandir les images déjà plus petites que la taille demandée",
LblFilter: "Filtre de redimensionnement :",
LblQuality: "Qualité : ",
TipQuality: "La qualité concerne JPEG, WEBP, AVIF et JXL",
LblEffort: "Effort :",
TipEffort: "Vitesse/effort de l'encodeur (WEBP, AVIF, JXL)",
TglLossless: " Sans perte",
TipLossless: "Compression sans perte (WEBP, AVIF, JXL), ignore la qualité",
TglGrayscale: " Niveaux de gris",
TipGrayscale: "Convertir les images en niveaux de gris (monochrome)",
LblBrightness: "Luminosité : ",
TipBrightness: "Ajuster la luminosité des images",
LblContrast: "Contraste : ",
TipContrast: "Ajuster le contraste des images",
LblRotate: "Rotation :",
TipRotate: "Faire pivoter chaque page dans le sens horaire de l'angle donné en degrés",
EffortMethod: "Méthode",
EffortSpeed: "Vitesse",
EffortEffort: "Effort",
TipEffortWebp: "Méthode WEBP, plus élevé est meilleur/plus lent (0-6, défaut 4)",
TipEffortAvif: "Vitesse AVIF, plus élevé est plus rapide/moins bon (0-10, défaut 10)",
TipEffortJxl: "Effort JXL, plus élevé est meilleur/plus lent (1-10, défaut 7)",
BtnAddFiles: "Ajouter des &fichiers...",
BtnAddDir: "Ajouter un &dossier...",
BtnRemove: "Supprimer",
BtnRemoveAll: "Tout supprimer",
BtnThumbnail: "Miniature",
BtnCover: "Couverture",
BtnConvert: "&Convertir",
BtnCancel: "Annuler",
TipCancel: "Annuler l'opération en cours (ou appuyer sur Échap)",
BtnReset: "Réinitialiser",
TipReset: "Restaurer tous les paramètres à leurs valeurs par défaut",
BtnSave: "Enregistrer",
TipSave: "Enregistrer les paramètres actuels dans un profil",
BtnCommand: "Commande",
TipCommand: "Afficher la ligne de commande équivalente",
LblProfile: "Profil :",
TipProfile: "Sélectionner un profil de paramètres",
TipThumbnail: "Extraire les miniatures de couverture",
TipCover: "Extraire les couvertures",
TipConvert: "Convertir les fichiers vers le format sélectionné",
StatusNeedFilesAndDir: "Ajouter des fichiers et définir le dossier de sortie",
StatusNeedFiles: "Ajouter des fichiers",
StatusNeedOutDir: "Définir le dossier de sortie",
StatusFileOf: "Fichier %d sur %d",
FilterNearest: "NearestNeighbor est le filtre de rééchantillonnage le plus rapide, sans anticrénelage",
FilterBox: "Filtre Box (moyenne des pixels)",
FilterLinear: "Linear est le filtre bilinéaire, lisse et raisonnablement rapide",
FilterMitchell: "MitchellNetravali est un filtre bicubique lisse",
FilterCatmull: "CatmullRom est un filtre bicubique net",
FilterGaussian: "Gaussian est un filtre de flou utilisant la fonction gaussienne, utile pour la réduction du bruit",
FilterLanczos: "Lanczos est un filtre de rééchantillonnage de haute qualité, plus lent que les filtres cubiques",
DlgAddFiles: "Ajouter des fichiers",
DlgAddDir: "Ajouter un dossier",
DlgOutputDir: "Dossier de sortie",
DlgOutputFile: "Fichier de sortie",
DlgCommandLine: "Ligne de commande",
DlgSaveProfile: "Enregistrer le profil",
ParamName: "Nom : %s\n",
MsgInvalidNameTitle: "Nom invalide",
MsgInvalidNameBody: "Le nom du profil ne doit pas être vide ni contenir '.' ou ';'.",
NoPreview: "Aucun aperçu",
})
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("ITALIAN", map[string]string{
ColTitle: "Titolo",
ColType: "Tipo",
ColSize: "Dimensione (MiB)",
TabInput: "Ingresso",
TabOutput: "Uscita",
TabImage: "Immagine",
TabTransform: "Trasforma",
LblPage: "Pagina:",
TipPage: "Anteprima di un'altra pagina del fumetto selezionato",
TglRecursive: " Esplora sottocartelle",
TipRecursive: "Elabora le sottocartelle in modo ricorsivo",
TglNoRGB: " Solo immagini in scala di grigi",
TipNoRGB: "Non convertire le immagini con spazio colore RGB",
TglNoCover: " Escludi la copertina",
TipNoCover: "Non convertire l'immagine di copertina",
TglNoNonImage: " Rimuovi i file non immagine dall'archivio",
TipNoNonImage: "Rimuovi i file .nfo, .xml, .txt dall'archivio",
TglNoConvert: " Non trasformare né convertire le immagini",
TipNoConvert: "Copia le immagini dall'archivio o dalla cartella senza modifiche",
LblMinSize: "Dimensione minima (MiB):",
TipSize: "Elabora solo i file più grandi della dimensione minima",
LblDPI: "DPI del documento:",
TipDPI: "Risoluzione per il rendering dei documenti (PDF, EPUB, ecc.); il valore predefinito usa la risoluzione originale",
LblOutDir: "Cartella di uscita:",
TipOutDir: "Cartella in cui vengono scritti i file convertiti (obbligatoria)",
BtnBrowse: "Sfoglia...",
LblSuffix: "Aggiungi suffisso al file di uscita:",
TipSuffix: "Aggiungi un suffisso al nome del file, ad es. nomefile_suffisso.cbz",
LblArchive: "Formato archivio:",
TipArchive: "Contenitore di uscita: ZIP (.cbz) o TAR non compresso (.cbt)",
LblCompression: "Compressione:",
TipZipLevel: "Compressione ZIP: Memorizza la disattiva, 1 è la più veloce, 9 la più piccola",
TglCombine: " Combina in un unico file",
TipCombine: "Unisci tutti i file elencati in un unico archivio",
LblOutFile: "File di uscita:",
TipOutFile: "Nome del file combinato (predefinito: primo ingresso + -combined)",
LblFormat: "Formato:",
TipFormat: "Formato immagine di uscita per le pagine convertite",
LblSize: "Dimensione:",
CueWidth: "larghezza",
CueHeight: "altezza",
TipWidthHeight: "Se la larghezza o l'altezza non è impostata, viene mantenuto il rapporto d'aspetto dell'immagine",
TglFit: " Adattamento ottimale",
TipFit: "Adattamento ottimale alla larghezza e all'altezza richieste",
TglNoUpscale: " Non ingrandire",
TipNoUpscale: "Non ingrandire le immagini già più piccole della dimensione richiesta",
LblFilter: "Filtro di ridimensionamento:",
LblQuality: "Qualità: ",
TipQuality: "La qualità riguarda JPEG, WEBP, AVIF e JXL",
LblEffort: "Sforzo:",
TipEffort: "Velocità/sforzo del codificatore (WEBP, AVIF, JXL)",
TglLossless: " Senza perdita",
TipLossless: "Compressione senza perdita (WEBP, AVIF, JXL), ignora la qualità",
TglGrayscale: " Scala di grigi",
TipGrayscale: "Converti le immagini in scala di grigi (monocromatico)",
LblBrightness: "Luminosità: ",
TipBrightness: "Regola la luminosità delle immagini",
LblContrast: "Contrasto: ",
TipContrast: "Regola il contrasto delle immagini",
LblRotate: "Ruota:",
TipRotate: "Ruota ogni pagina in senso orario dell'angolo indicato in gradi",
EffortMethod: "Metodo",
EffortSpeed: "Velocità",
EffortEffort: "Sforzo",
TipEffortWebp: "Metodo WEBP, più alto è migliore/più lento (0-6, predefinito 4)",
TipEffortAvif: "Velocità AVIF, più alto è più veloce/peggiore (0-10, predefinito 10)",
TipEffortJxl: "Sforzo JXL, più alto è migliore/più lento (1-10, predefinito 7)",
BtnAddFiles: "Aggiungi &file...",
BtnAddDir: "Aggiungi &cartella...",
BtnRemove: "Rimuovi",
BtnRemoveAll: "Rimuovi tutto",
BtnThumbnail: "Miniatura",
BtnCover: "Copertina",
BtnConvert: "&Converti",
BtnCancel: "Annulla",
TipCancel: "Annulla l'operazione in corso (o premi Esc)",
BtnReset: "Ripristina",
TipReset: "Ripristina tutte le impostazioni ai valori predefiniti",
BtnSave: "Salva",
TipSave: "Salva le impostazioni correnti in un profilo",
BtnCommand: "Comando",
TipCommand: "Mostra la riga di comando equivalente",
LblProfile: "Profilo:",
TipProfile: "Seleziona un profilo di impostazioni",
TipThumbnail: "Estrai le miniature delle copertine",
TipCover: "Estrai le copertine",
TipConvert: "Converti i file nel formato selezionato",
StatusNeedFilesAndDir: "Aggiungi file e imposta la cartella di uscita",
StatusNeedFiles: "Aggiungi file",
StatusNeedOutDir: "Imposta la cartella di uscita",
StatusFileOf: "File %d di %d",
FilterNearest: "NearestNeighbor è il filtro di ricampionamento più veloce, senza antialiasing",
FilterBox: "Filtro Box (media dei pixel)",
FilterLinear: "Linear è il filtro bilineare, morbido e ragionevolmente veloce",
FilterMitchell: "MitchellNetravali è un filtro bicubico morbido",
FilterCatmull: "CatmullRom è un filtro bicubico nitido",
FilterGaussian: "Gaussian è un filtro di sfocatura che usa la funzione gaussiana, utile per la rimozione del rumore",
FilterLanczos: "Lanczos è un filtro di ricampionamento di alta qualità, più lento dei filtri cubici",
DlgAddFiles: "Aggiungi file",
DlgAddDir: "Aggiungi cartella",
DlgOutputDir: "Cartella di uscita",
DlgOutputFile: "File di uscita",
DlgCommandLine: "Riga di comando",
DlgSaveProfile: "Salva profilo",
ParamName: "Nome: %s\n",
MsgInvalidNameTitle: "Nome non valido",
MsgInvalidNameBody: "Il nome del profilo non deve essere vuoto né contenere '.' o ';'.",
NoPreview: "Nessuna anteprima",
})
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("JAPANESE", map[string]string{
ColTitle: "タイトル",
ColType: "種類",
ColSize: "サイズ (MiB)",
TabInput: "入力",
TabOutput: "出力",
TabImage: "画像",
TabTransform: "変換",
LblPage: "ページ:",
TipPage: "選択したコミックの別のページをプレビュー",
TglRecursive: " サブディレクトリを再帰的に処理",
TipRecursive: "サブディレクトリを再帰的に処理する",
TglNoRGB: " グレースケール画像のみ",
TipNoRGB: "RGB 色空間の画像を変換しない",
TglNoCover: " 表紙を除外",
TipNoCover: "表紙画像を変換しない",
TglNoNonImage: " アーカイブから画像以外のファイルを削除",
TipNoNonImage: "アーカイブから .nfo、.xml、.txt ファイルを削除する",
TglNoConvert: " 画像を変換・変形しない",
TipNoConvert: "アーカイブまたはディレクトリから画像を変更せずにコピーする",
LblMinSize: "最小サイズ (MiB)",
TipSize: "最小サイズより大きいファイルのみ処理する",
LblDPI: "ドキュメント DPI",
TipDPI: "ドキュメント(PDF、EPUB など)をレンダリングする解像度。既定値は元の解像度を使用します",
LblOutDir: "出力ディレクトリ:",
TipOutDir: "変換されたファイルが書き込まれるディレクトリ(必須)",
BtnBrowse: "参照...",
LblSuffix: "出力ファイルにサフィックスを追加:",
TipSuffix: "ファイル名にサフィックスを追加する。例: filename_suffix.cbz",
LblArchive: "アーカイブ形式:",
TipArchive: "出力コンテナ: ZIP (.cbz) または非圧縮の TAR (.cbt)",
LblCompression: "圧縮:",
TipZipLevel: "ZIP 圧縮: 「保存」で無効化、1 が最速、9 が最小",
TglCombine: " 単一ファイルに結合",
TipCombine: "リストされたすべてのファイルを 1 つのアーカイブに結合する",
LblOutFile: "出力ファイル:",
TipOutFile: "結合ファイル名(既定: 最初の入力 + -combined",
LblFormat: "形式:",
TipFormat: "変換されるページの出力画像形式",
LblSize: "サイズ:",
CueWidth: "幅",
CueHeight: "高さ",
TipWidthHeight: "幅または高さが設定されていない場合、画像の縦横比が保持されます",
TglFit: " 最適化",
TipFit: "必要な幅と高さに最適化",
TglNoUpscale: " 拡大しない",
TipNoUpscale: "要求されたサイズより既に小さい画像を拡大しない",
LblFilter: "リサイズフィルター:",
LblQuality: "品質:",
TipQuality: "品質は JPEG、WEBP、AVIF、JXL に影響します",
LblEffort: "労力:",
TipEffort: "エンコーダーの速度/労力(WEBP、AVIF、JXL",
TglLossless: " ロスレス",
TipLossless: "ロスレス圧縮(WEBP、AVIF、JXL)、品質を無視します",
TglGrayscale: " グレースケール",
TipGrayscale: "画像をグレースケール(モノクロ)に変換する",
LblBrightness: "明るさ:",
TipBrightness: "画像の明るさを調整する",
LblContrast: "コントラスト:",
TipContrast: "画像のコントラストを調整する",
LblRotate: "回転:",
TipRotate: "各ページを指定した角度(度)だけ時計回りに回転する",
EffortMethod: "方式",
EffortSpeed: "速度",
EffortEffort: "労力",
TipEffortWebp: "WEBP 方式、高いほど高品質/低速(0-6、既定 4)",
TipEffortAvif: "AVIF 速度、高いほど高速/低品質(0-10、既定 10)",
TipEffortJxl: "JXL 労力、高いほど高品質/低速(1-10、既定 7)",
BtnAddFiles: "ファイルを追加(&F)...",
BtnAddDir: "ディレクトリを追加(&D)...",
BtnRemove: "削除",
BtnRemoveAll: "すべて削除",
BtnThumbnail: "サムネイル",
BtnCover: "表紙",
BtnConvert: "変換(&C)",
BtnCancel: "キャンセル",
TipCancel: "実行中の操作をキャンセル(または Esc キーを押す)",
BtnReset: "リセット",
TipReset: "すべての設定を既定値に戻す",
BtnSave: "保存",
TipSave: "現在の設定をプロファイルに保存する",
BtnCommand: "コマンド",
TipCommand: "同等のコマンドラインを表示する",
LblProfile: "プロファイル:",
TipProfile: "設定プロファイルを選択する",
TipThumbnail: "表紙のサムネイルを抽出する",
TipCover: "表紙を抽出する",
TipConvert: "ファイルを選択した形式に変換する",
StatusNeedFilesAndDir: "ファイルを追加し、出力ディレクトリを設定してください",
StatusNeedFiles: "ファイルを追加してください",
StatusNeedOutDir: "出力ディレクトリを設定してください",
StatusFileOf: "ファイル %d / %d",
FilterNearest: "NearestNeighbor は最速のリサンプリングフィルターで、アンチエイリアスなし",
FilterBox: "Box フィルター(ピクセルの平均化)",
FilterLinear: "Linear はバイリニアフィルターで、滑らかでそれなりに高速",
FilterMitchell: "MitchellNetravali は滑らかなバイキュービックフィルター",
FilterCatmull: "CatmullRom はシャープなバイキュービックフィルター",
FilterGaussian: "Gaussian はガウス関数を使用するぼかしフィルターで、ノイズ除去に便利",
FilterLanczos: "Lanczos は高品質なリサンプリングフィルターで、キュービックフィルターより低速",
DlgAddFiles: "ファイルを追加",
DlgAddDir: "ディレクトリを追加",
DlgOutputDir: "出力ディレクトリ",
DlgOutputFile: "出力ファイル",
DlgCommandLine: "コマンドライン",
DlgSaveProfile: "プロファイルを保存",
ParamName: "名前:%s\n",
MsgInvalidNameTitle: "無効な名前",
MsgInvalidNameBody: "プロファイル名は空にすることはできず、'.' または ';' を含めることはできません。",
NoPreview: "プレビューなし",
})
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("PORTUGUESE", map[string]string{
ColTitle: "Título",
ColType: "Tipo",
ColSize: "Tamanho (MiB)",
TabInput: "Entrada",
TabOutput: "Saída",
TabImage: "Imagem",
TabTransform: "Transformar",
LblPage: "Página:",
TipPage: "Pré-visualizar outra página da revista em quadrinhos selecionada",
TglRecursive: " Percorrer subdiretórios",
TipRecursive: "Processar subdiretórios recursivamente",
TglNoRGB: " Apenas imagens em escala de cinza",
TipNoRGB: "Não converter imagens que tenham espaço de cor RGB",
TglNoCover: " Excluir a capa",
TipNoCover: "Não converter a imagem de capa",
TglNoNonImage: " Remover arquivos não-imagem do arquivo",
TipNoNonImage: "Remover arquivos .nfo, .xml, .txt do arquivo",
TglNoConvert: " Não transformar nem converter imagens",
TipNoConvert: "Copiar imagens do arquivo ou diretório sem modificações",
LblMinSize: "Tamanho mínimo (MiB):",
TipSize: "Processar apenas arquivos maiores que o tamanho mínimo",
LblDPI: "DPI do documento:",
TipDPI: "Resolução para renderizar documentos (PDF, EPUB, etc.); o padrão usa a resolução original",
LblOutDir: "Diretório de saída:",
TipOutDir: "Diretório onde os arquivos convertidos são gravados (obrigatório)",
BtnBrowse: "Procurar...",
LblSuffix: "Adicionar sufixo ao arquivo de saída:",
TipSuffix: "Adicionar sufixo ao nome do arquivo, p. ex. nomearquivo_sufixo.cbz",
LblArchive: "Formato de arquivo:",
TipArchive: "Contêiner de saída: ZIP (.cbz) ou TAR não comprimido (.cbt)",
LblCompression: "Compressão:",
TipZipLevel: "Compressão ZIP: Armazenar a desativa, 1 é a mais rápida, 9 a menor",
TglCombine: " Combinar em um único arquivo",
TipCombine: "Mesclar todos os arquivos listados em um único arquivo",
LblOutFile: "Arquivo de saída:",
TipOutFile: "Nome do arquivo combinado (padrão: primeira entrada + -combined)",
LblFormat: "Formato:",
TipFormat: "Formato de imagem de saída para as páginas convertidas",
LblSize: "Tamanho:",
CueWidth: "largura",
CueHeight: "altura",
TipWidthHeight: "Se a largura ou a altura não for definida, a proporção da imagem é preservada",
TglFit: " Melhor ajuste",
TipFit: "Melhor ajuste à largura e altura desejadas",
TglNoUpscale: " Não ampliar",
TipNoUpscale: "Não ampliar imagens que já são menores que o tamanho solicitado",
LblFilter: "Filtro de redimensionamento:",
LblQuality: "Qualidade: ",
TipQuality: "A qualidade afeta JPEG, WEBP, AVIF e JXL",
LblEffort: "Esforço:",
TipEffort: "Velocidade/esforço do codificador (WEBP, AVIF, JXL)",
TglLossless: " Sem perdas",
TipLossless: "Compressão sem perdas (WEBP, AVIF, JXL), ignora a qualidade",
TglGrayscale: " Escala de cinza",
TipGrayscale: "Converter imagens para escala de cinza (monocromático)",
LblBrightness: "Brilho: ",
TipBrightness: "Ajustar o brilho das imagens",
LblContrast: "Contraste: ",
TipContrast: "Ajustar o contraste das imagens",
LblRotate: "Girar:",
TipRotate: "Girar cada página no sentido horário pelo ângulo dado em graus",
EffortMethod: "Método",
EffortSpeed: "Velocidade",
EffortEffort: "Esforço",
TipEffortWebp: "Método WEBP, mais alto é melhor/mais lento (0-6, padrão 4)",
TipEffortAvif: "Velocidade AVIF, mais alto é mais rápido/pior (0-10, padrão 10)",
TipEffortJxl: "Esforço JXL, mais alto é melhor/mais lento (1-10, padrão 7)",
BtnAddFiles: "Adicionar &arquivos...",
BtnAddDir: "Adicionar &diretório...",
BtnRemove: "Remover",
BtnRemoveAll: "Remover tudo",
BtnThumbnail: "Miniatura",
BtnCover: "Capa",
BtnConvert: "&Converter",
BtnCancel: "Cancelar",
TipCancel: "Cancelar a operação em andamento (ou pressionar Esc)",
BtnReset: "Redefinir",
TipReset: "Restaurar todas as configurações para os valores padrão",
BtnSave: "Salvar",
TipSave: "Salvar as configurações atuais em um perfil",
BtnCommand: "Comando",
TipCommand: "Mostrar a linha de comando equivalente",
LblProfile: "Perfil:",
TipProfile: "Selecionar um perfil de configurações",
TipThumbnail: "Extrair miniaturas de capa",
TipCover: "Extrair capas",
TipConvert: "Converter arquivos para o formato selecionado",
StatusNeedFilesAndDir: "Adicionar arquivos e definir o diretório de saída",
StatusNeedFiles: "Adicionar arquivos",
StatusNeedOutDir: "Definir o diretório de saída",
StatusFileOf: "Arquivo %d de %d",
FilterNearest: "NearestNeighbor é o filtro de reamostragem mais rápido, sem antisserrilhamento",
FilterBox: "Filtro Box (média dos pixels)",
FilterLinear: "Linear é o filtro bilinear, suave e razoavelmente rápido",
FilterMitchell: "MitchellNetravali é um filtro bicúbico suave",
FilterCatmull: "CatmullRom é um filtro bicúbico nítido",
FilterGaussian: "Gaussian é um filtro de desfoque que usa a função gaussiana, útil para remoção de ruído",
FilterLanczos: "Lanczos é um filtro de reamostragem de alta qualidade, mais lento que os filtros cúbicos",
DlgAddFiles: "Adicionar arquivos",
DlgAddDir: "Adicionar diretório",
DlgOutputDir: "Diretório de saída",
DlgOutputFile: "Arquivo de saída",
DlgCommandLine: "Linha de comando",
DlgSaveProfile: "Salvar perfil",
ParamName: "Nome: %s\n",
MsgInvalidNameTitle: "Nome inválido",
MsgInvalidNameBody: "O nome do perfil não deve estar vazio nem conter '.' ou ';'.",
NoPreview: "Sem pré-visualização",
})
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("RUSSIAN", map[string]string{
ColTitle: "Название",
ColType: "Тип",
ColSize: "Размер (МиБ)",
TabInput: "Ввод",
TabOutput: "Вывод",
TabImage: "Изображение",
TabTransform: "Преобразование",
LblPage: "Страница:",
TipPage: "Предпросмотр другой страницы выбранного комикса",
TglRecursive: " Обрабатывать подкаталоги",
TipRecursive: "Рекурсивно обрабатывать подкаталоги",
TglNoRGB: " Только изображения в оттенках серого",
TipNoRGB: "Не преобразовывать изображения в цветовом пространстве RGB",
TglNoCover: " Исключить обложку",
TipNoCover: "Не преобразовывать изображение обложки",
TglNoNonImage: " Удалить из архива файлы, не являющиеся изображениями",
TipNoNonImage: "Удалить из архива файлы .nfo, .xml, .txt",
TglNoConvert: " Не преобразовывать изображения",
TipNoConvert: "Копировать изображения из архива или каталога без изменений",
LblMinSize: "Минимальный размер (МиБ):",
TipSize: "Обрабатывать только файлы больше минимального размера",
LblDPI: "DPI документа:",
TipDPI: "Разрешение для рендеринга документов (PDF, EPUB и т. д.); по умолчанию используется исходное разрешение",
LblOutDir: "Каталог вывода:",
TipOutDir: "Каталог, в который записываются преобразованные файлы (обязательно)",
BtnBrowse: "Обзор...",
LblSuffix: "Добавить суффикс к выходному файлу:",
TipSuffix: "Добавить суффикс к имени файла, например имяфайла_суффикс.cbz",
LblArchive: "Формат архива:",
TipArchive: "Выходной контейнер: ZIP (.cbz) или несжатый TAR (.cbt)",
LblCompression: "Сжатие:",
TipZipLevel: "Сжатие ZIP: «Хранить» отключает его, 1 — самое быстрое, 9 — наименьшее",
TglCombine: " Объединить в один файл",
TipCombine: "Объединить все перечисленные файлы в один архив",
LblOutFile: "Выходной файл:",
TipOutFile: "Имя объединённого файла (по умолчанию: первый ввод + -combined)",
LblFormat: "Формат:",
TipFormat: "Выходной формат изображения для преобразованных страниц",
LblSize: "Размер:",
CueWidth: "ширина",
CueHeight: "высота",
TipWidthHeight: "Если ширина или высота не задана, соотношение сторон изображения сохраняется",
TglFit: " Наилучшее соответствие",
TipFit: "Наилучшее соответствие требуемой ширине и высоте",
TglNoUpscale: " Не увеличивать",
TipNoUpscale: "Не увеличивать изображения, которые уже меньше запрошенного размера",
LblFilter: "Фильтр масштабирования:",
LblQuality: "Качество: ",
TipQuality: "Качество влияет на JPEG, WEBP, AVIF и JXL",
LblEffort: "Усилие:",
TipEffort: "Скорость/усилие кодировщика (WEBP, AVIF, JXL)",
TglLossless: " Без потерь",
TipLossless: "Сжатие без потерь (WEBP, AVIF, JXL), игнорирует качество",
TglGrayscale: " Оттенки серого",
TipGrayscale: "Преобразовать изображения в оттенки серого (монохромные)",
LblBrightness: "Яркость: ",
TipBrightness: "Настроить яркость изображений",
LblContrast: "Контраст: ",
TipContrast: "Настроить контраст изображений",
LblRotate: "Поворот:",
TipRotate: "Поворачивать каждую страницу по часовой стрелке на заданный угол в градусах",
EffortMethod: "Метод",
EffortSpeed: "Скорость",
EffortEffort: "Усилие",
TipEffortWebp: "Метод WEBP, выше — лучше/медленнее (0-6, по умолчанию 4)",
TipEffortAvif: "Скорость AVIF, выше — быстрее/хуже (0-10, по умолчанию 10)",
TipEffortJxl: "Усилие JXL, выше — лучше/медленнее (1-10, по умолчанию 7)",
BtnAddFiles: "Добавить &файлы...",
BtnAddDir: "Добавить &каталог...",
BtnRemove: "Удалить",
BtnRemoveAll: "Удалить все",
BtnThumbnail: "Эскиз",
BtnCover: "Обложка",
BtnConvert: "&Преобразовать",
BtnCancel: "Отмена",
TipCancel: "Отменить выполняемую операцию (или нажать Esc)",
BtnReset: "Сбросить",
TipReset: "Восстановить все настройки до значений по умолчанию",
BtnSave: "Сохранить",
TipSave: "Сохранить текущие настройки в профиль",
BtnCommand: "Команда",
TipCommand: "Показать эквивалентную командную строку",
LblProfile: "Профиль:",
TipProfile: "Выбрать профиль настроек",
TipThumbnail: "Извлечь эскизы обложек",
TipCover: "Извлечь обложки",
TipConvert: "Преобразовать файлы в выбранный формат",
StatusNeedFilesAndDir: "Добавьте файлы и задайте каталог вывода",
StatusNeedFiles: "Добавьте файлы",
StatusNeedOutDir: "Задайте каталог вывода",
StatusFileOf: "Файл %d из %d",
FilterNearest: "NearestNeighbor — самый быстрый фильтр передискретизации, без сглаживания",
FilterBox: "Фильтр Box (усреднение пикселей)",
FilterLinear: "Linear — билинейный фильтр, гладкий и достаточно быстрый",
FilterMitchell: "MitchellNetravali — гладкий бикубический фильтр",
FilterCatmull: "CatmullRom — резкий бикубический фильтр",
FilterGaussian: "Gaussian — размывающий фильтр на основе функции Гаусса, полезен для удаления шума",
FilterLanczos: "Lanczos — высококачественный фильтр передискретизации, медленнее кубических фильтров",
DlgAddFiles: "Добавить файлы",
DlgAddDir: "Добавить каталог",
DlgOutputDir: "Каталог вывода",
DlgOutputFile: "Выходной файл",
DlgCommandLine: "Командная строка",
DlgSaveProfile: "Сохранить профиль",
ParamName: "Имя: %s\n",
MsgInvalidNameTitle: "Недопустимое имя",
MsgInvalidNameBody: "Имя профиля не должно быть пустым или содержать «.» или «;».",
NoPreview: "Нет предпросмотра",
})
}
+128
View File
@@ -0,0 +1,128 @@
package i18n
func init() {
register("CHINESE", map[string]string{
ColTitle: "标题",
ColType: "类型",
ColSize: "大小 (MiB)",
TabInput: "输入",
TabOutput: "输出",
TabImage: "图像",
TabTransform: "变换",
LblPage: "页面:",
TipPage: "预览所选漫画的其他页面",
TglRecursive: " 递归子目录",
TipRecursive: "递归处理子目录",
TglNoRGB: " 仅灰度图像",
TipNoRGB: "不转换具有 RGB 色彩空间的图像",
TglNoCover: " 排除封面",
TipNoCover: "不转换封面图像",
TglNoNonImage: " 从存档中移除非图像文件",
TipNoNonImage: "从存档中移除 .nfo、.xml、.txt 文件",
TglNoConvert: " 不变换或转换图像",
TipNoConvert: "从存档或目录复制图像而不做修改",
LblMinSize: "最小大小 (MiB)",
TipSize: "仅处理大于最小大小的文件",
LblDPI: "文档 DPI",
TipDPI: "渲染文档的分辨率(PDF、EPUB 等);默认使用原始分辨率",
LblOutDir: "输出目录:",
TipOutDir: "写入转换后文件的目录(必填)",
BtnBrowse: "浏览...",
LblSuffix: "为输出文件添加后缀:",
TipSuffix: "为文件名添加后缀,例如 filename_suffix.cbz",
LblArchive: "存档格式:",
TipArchive: "输出容器:ZIP (.cbz) 或未压缩的 TAR (.cbt)",
LblCompression: "压缩:",
TipZipLevel: "ZIP 压缩:存储将其禁用,1 最快,9 最小",
TglCombine: " 合并为单个文件",
TipCombine: "将所有列出的文件合并为一个存档",
LblOutFile: "输出文件:",
TipOutFile: "合并文件名(默认:第一个输入 + -combined",
LblFormat: "格式:",
TipFormat: "转换页面的输出图像格式",
LblSize: "大小:",
CueWidth: "宽度",
CueHeight: "高度",
TipWidthHeight: "如果未设置宽度或高度,则保留图像的纵横比",
TglFit: " 最佳适应",
TipFit: "最佳适应所需的宽度和高度",
TglNoUpscale: " 不放大",
TipNoUpscale: "不放大已经小于请求大小的图像",
LblFilter: "缩放滤镜:",
LblQuality: "质量:",
TipQuality: "质量影响 JPEG、WEBP、AVIF 和 JXL",
LblEffort: "强度:",
TipEffort: "编码器速度/强度(WEBP、AVIF、JXL",
TglLossless: " 无损",
TipLossless: "无损压缩(WEBP、AVIF、JXL),忽略质量",
TglGrayscale: " 灰度",
TipGrayscale: "将图像转换为灰度(单色)",
LblBrightness: "亮度:",
TipBrightness: "调整图像的亮度",
LblContrast: "对比度:",
TipContrast: "调整图像的对比度",
LblRotate: "旋转:",
TipRotate: "按给定角度(度)顺时针旋转每一页",
EffortMethod: "方法",
EffortSpeed: "速度",
EffortEffort: "强度",
TipEffortWebp: "WEBP 方法,越高越好/越慢(0-6,默认 4)",
TipEffortAvif: "AVIF 速度,越高越快/越差(0-10,默认 10)",
TipEffortJxl: "JXL 强度,越高越好/越慢(1-10,默认 7)",
BtnAddFiles: "添加文件(&F)...",
BtnAddDir: "添加目录(&D)...",
BtnRemove: "移除",
BtnRemoveAll: "全部移除",
BtnThumbnail: "缩略图",
BtnCover: "封面",
BtnConvert: "转换(&C)",
BtnCancel: "取消",
TipCancel: "取消正在运行的操作(或按 Esc)",
BtnReset: "重置",
TipReset: "将所有设置恢复为默认值",
BtnSave: "保存",
TipSave: "将当前设置保存到配置文件",
BtnCommand: "命令",
TipCommand: "显示等效的命令行",
LblProfile: "配置文件:",
TipProfile: "选择设置配置文件",
TipThumbnail: "提取封面缩略图",
TipCover: "提取封面",
TipConvert: "将文件转换为所选格式",
StatusNeedFilesAndDir: "添加文件并设置输出目录",
StatusNeedFiles: "添加文件",
StatusNeedOutDir: "设置输出目录",
StatusFileOf: "文件 %d / %d",
FilterNearest: "NearestNeighbor 是最快的重采样滤镜,无抗锯齿",
FilterBox: "Box 滤镜(像素平均)",
FilterLinear: "Linear 是双线性滤镜,平滑且相当快",
FilterMitchell: "MitchellNetravali 是平滑的双三次滤镜",
FilterCatmull: "CatmullRom 是锐利的双三次滤镜",
FilterGaussian: "Gaussian 是使用高斯函数的模糊滤镜,适用于降噪",
FilterLanczos: "Lanczos 是高质量的重采样滤镜,比三次滤镜慢",
DlgAddFiles: "添加文件",
DlgAddDir: "添加目录",
DlgOutputDir: "输出目录",
DlgOutputFile: "输出文件",
DlgCommandLine: "命令行",
DlgSaveProfile: "保存配置文件",
ParamName: "名称:%s\n",
MsgInvalidNameTitle: "无效名称",
MsgInvalidNameBody: "配置文件名称不能为空,也不能包含 '.' 或 ';'。",
NoPreview: "无预览",
})
}
+27 -831
View File
@@ -2,26 +2,22 @@ package main
import ( import (
"bytes" "bytes"
"context"
_ "embed" _ "embed"
"errors"
"flag" "flag"
"fmt" "fmt"
"image/gif"
"image/png" "image/png"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"runtime/debug" "runtime/debug"
"slices"
"strconv"
"strings" "strings"
"github.com/gen2brain/cbconvert" "github.com/gen2brain/cbconvert"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"github.com/gen2brain/iup-go/iup" "github.com/gen2brain/iup-go/iup"
) )
//go:generate rsrc --ico dist/windows/icon.ico --arch amd64 -o main_windows_amd64.syso //go:generate rsrc --ico dist/windows/icon.ico --arch amd64 -o main_windows_amd64.syso
//go:generate rsrc --ico dist/windows/icon.ico --arch arm64 -o main_windows_arm64.syso
//go:embed assets/logo.png //go:embed assets/logo.png
var appLogo []byte var appLogo []byte
@@ -34,6 +30,15 @@ var appVersion string
var ( var (
index = -1 index = -1
files []cbconvert.File files []cbconvert.File
config iup.Ihandle
activeConv *cbconvert.Converter
busy bool
previewPage int // 0-based page shown in the preview
previewPath string // path of the file whose page range is loaded
hasCover bool // whether a cover image is loaded for the preview canvas
) )
func init() { func init() {
@@ -69,8 +74,16 @@ func main() {
iup.Open() iup.Open()
defer iup.Close() defer iup.Close()
iup.SetGlobal("UTF8MODE", "YES") iup.SetGlobal("APPNAME", "cbconvert")
iup.SetGlobal("UTF8MODE_FILE", "YES") iup.SetGlobal("APPID", "io.github.gen2brain.cbconvert")
iup.SetGlobal("AUTODARKMODE", "YES")
iup.SetGlobal("GSKRENDERER", "cairo")
i18n.Init()
config = iup.Config()
iup.ConfigLoad(config)
img, _ := png.Decode(bytes.NewReader(appLogo)) img, _ := png.Decode(bytes.NewReader(appLogo))
iup.ImageFromImage(img).SetHandle("logo") iup.ImageFromImage(img).SetHandle("logo")
@@ -86,17 +99,17 @@ func main() {
return iup.DEFAULT return iup.DEFAULT
})) }))
dlg.SetCallback("RESIZE_CB", iup.ResizeFunc(func(ih iup.Ihandle, width, height int) int { dlg.SetCallback("THEMECHANGED_CB", iup.ThemeChangedFunc(func(ih iup.Ihandle, darkMode int) int {
iup.GetHandle("Preview").SetAttribute("IMAGE", "logo") t := iup.GetHandle("Table")
iup.Refresh(ih) tableRowColors(t, darkMode == 1)
t.SetAttribute("REDRAW", "YES")
previewPost() iup.Update(iup.GetHandle("Preview"))
return iup.DEFAULT return iup.DEFAULT
})) }))
iup.Map(dlg) iup.Map(dlg)
setActive() profilesInit()
iup.ShowXY(dlg, iup.CENTER, iup.CENTER) iup.ShowXY(dlg, iup.CENTER, iup.CENTER)
iup.MainLoop() iup.MainLoop()
@@ -123,90 +136,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 { func layout() iup.Ihandle {
return iup.Vbox( return iup.Vbox(
iup.Hbox( iup.Hbox(
@@ -224,736 +153,3 @@ func layout() iup.Ihandle {
status(), 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
}
Binary file not shown.
+103
View File
@@ -0,0 +1,103 @@
package main
import (
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"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.NoUpscale = iup.GetHandle("NoUpscale").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(i18n.Str(i18n.DlgCommandLine), 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
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 185 KiB

+252
View File
@@ -0,0 +1,252 @@
package main
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"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"},
{"NoUpscale", 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)
}
}
userLossless = iup.GetHandle("Lossless").GetAttribute("VALUE") == "ON"
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(i18n.Str(i18n.DlgSaveProfile), nil, i18n.Str(i18n.ParamName), &name) != 1 {
return iup.DEFAULT
}
name = strings.TrimSpace(name)
if name == "" || strings.ContainsAny(name, ".;") {
iup.Message(i18n.Str(i18n.MsgInvalidNameTitle), i18n.Str(i18n.MsgInvalidNameBody))
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
}
+176
View File
@@ -0,0 +1,176 @@
package main
import (
"runtime/debug"
"strings"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"github.com/gen2brain/iup-go/iup"
)
// jxlLosslessBuild reports whether wasm2go and nodynamic leave zune-jpegxl (lossless-only) as the only jxl encoder.
var jxlLosslessBuild = func() bool {
info, ok := debug.ReadBuildInfo()
if !ok {
return false
}
var wasm2go, nodynamic bool
for _, kv := range info.Settings {
if kv.Key != "-tags" {
continue
}
for _, t := range strings.Split(kv.Value, ",") {
switch t {
case "wasm2go":
wasm2go = true
case "nodynamic":
nodynamic = true
}
}
}
return wasm2go && nodynamic
}()
// userLossless is the user's Lossless preference, tracked separately because a jxl
// wasm2go build force-sets the widget on.
var userLossless bool
func setActive() {
if busy {
return
}
opts := options()
count := iup.GetHandle("Table").GetInt("NUMLIN")
if count > 0 && index != -1 {
iup.GetHandle("PageBox").SetAttribute("VISIBLE", "YES")
} else {
iup.GetHandle("PageBox").SetAttribute("VISIBLE", "NO")
}
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", i18n.Lng(i18n.StatusNeedFilesAndDir)
case count == 0:
active, tip = "NO", i18n.Lng(i18n.StatusNeedFiles)
case opts.OutDir == "":
active, tip = "NO", i18n.Lng(i18n.StatusNeedOutDir)
}
enabledTip := map[string]string{
"Thumbnail": i18n.Lng(i18n.TipThumbnail),
"Cover": i18n.Lng(i18n.TipCover),
"Convert": i18n.Lng(i18n.TipConvert),
}
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"
jxlLossless := jxlLosslessBuild && opts.Format == "jxl"
// jxl wasm2go forces lossless on; otherwise show the user's preference so it doesn't stay stuck on.
losslessVal := "OFF"
if jxlLossless || userLossless {
losslessVal = "ON"
}
iup.GetHandle("Lossless").SetAttribute("VALUE", losslessVal)
losslessOn := jxlLossless || (canLossless && userLossless)
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 jxlLossless {
iup.GetHandle("Lossless").SetAttribute("ACTIVE", "NO")
iup.GetHandle("VboxEffort").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", i18n.Lng(i18n.BtnCancel))
convert.SetAttribute("TIP", i18n.Lng(i18n.TipCancel))
} else {
activeConv = nil
convert.SetAttribute("TITLE", i18n.Lng(i18n.BtnConvert))
convert.SetAttribute("TIP", i18n.Lng(i18n.TipConvert))
setActive() // restores the conditional buttons and option boxes
}
}
+798
View File
@@ -0,0 +1,798 @@
package main
import (
"bytes"
"fmt"
"image/gif"
"math"
"net/url"
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"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", i18n.Lng(i18n.TipEffortWebp))
name = i18n.Str(i18n.EffortMethod)
case "avif":
val.SetAttributes("MIN=0, MAX=10, SHOWTICKS=11, VALUE=10")
val.SetAttribute("TIP", i18n.Lng(i18n.TipEffortAvif))
name = i18n.Str(i18n.EffortSpeed)
case "jxl":
val.SetAttributes("MIN=1, MAX=10, SHOWTICKS=10, VALUE=7")
val.SetAttribute("TIP", i18n.Lng(i18n.TipEffortJxl))
name = i18n.Str(i18n.EffortEffort)
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": i18n.Lng(i18n.ColTitle),
"TITLE2": i18n.Lng(i18n.ColType),
"TITLE3": i18n.Lng(i18n.ColSize),
"WIDTH1": "250",
"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
}
addFiles(fs)
return iup.DEFAULT
}))
return iup.Vbox(t)
}
func preview() iup.Ihandle {
return iup.Frame(
iup.Vbox(
iup.Canvas().SetAttributes("EXPAND=YES, MINSIZE=400x, BORDER=NO").SetHandle("Preview").
SetCallback("ACTION", iup.ActionFunc(drawPreview)).
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(previewMessage)),
iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"),
),
)
}
// previewTheme holds the preview canvas colors for light or dark mode.
type previewTheme struct {
gradTop, gradBottom string
card, cardBorder string
frame, shadow string
}
func previewThemeFor(dark bool) previewTheme {
if dark {
return previewTheme{
gradTop: "#3A3F45", gradBottom: "#202327",
card: "#FFFFFF", cardBorder: "#16181B",
frame: "#101214", shadow: "#141619",
}
}
return previewTheme{
gradTop: "#F7F9FC", gradBottom: "#DBE2EA",
card: "#FFFFFF", cardBorder: "#CDD5DE",
frame: "#BFC8D2", shadow: "#B7C0CB",
}
}
// drawPreview paints a gradient background, then the cover framed with a drop shadow, or an empty page card with the logo.
func drawPreview(ih iup.Ihandle) int {
iup.DrawBegin(ih)
defer iup.DrawEnd(ih)
cw, ch := iup.DrawGetSize(ih)
th := previewThemeFor(iup.GetGlobal("DARKMODE") == "YES" && iup.GetGlobal("AUTODARKMODE") == "YES")
iup.DrawLinearGradient(ih, 0, 0, cw, ch, 90, th.gradTop, th.gradBottom)
margin := cw / 24
if m := ch / 24; m < margin {
margin = m
}
if margin < 12 {
margin = 12
}
if hasCover {
drawCover(ih, th, cw, ch, margin)
} else {
drawPlaceholder(ih, th, cw, ch, margin)
}
return iup.DEFAULT
}
// drawCover draws the loaded cover scaled to fit, with a drop shadow and a thin frame.
func drawCover(ih iup.Ihandle, th previewTheme, cw, ch, margin int) {
iw, ihh, _ := iup.DrawGetImageInfo("cover")
if iw <= 0 || ihh <= 0 {
return
}
s := math.Min(float64(cw-2*margin)/float64(iw), float64(ch-2*margin)/float64(ihh))
dw, dh := int(float64(iw)*s), int(float64(ihh)*s)
x, y := (cw-dw)/2, (ch-dh)/2
const off = 6
ih.SetAttribute("DRAWSTYLE", "FILL")
ih.SetAttribute("DRAWCOLOR", th.shadow)
iup.DrawRectangle(ih, x+off, y+off, x+dw+off, y+dh+off)
iup.DrawImage(ih, "cover", x, y, dw, dh)
ih.SetAttribute("DRAWSTYLE", "STROKE")
ih.SetAttribute("DRAWLINEWIDTH", "1")
ih.SetAttribute("DRAWCOLOR", th.frame)
iup.DrawRectangle(ih, x, y, x+dw, y+dh)
}
// drawPlaceholder draws an empty comic-page card with the logo centered, shown until a cover is loaded.
func drawPlaceholder(ih iup.Ihandle, th previewTheme, cw, ch, margin int) {
cardH := ch - 2*margin
cardW := cardH * 2 / 3
if cardW > cw-2*margin {
cardW = cw - 2*margin
cardH = cardW * 3 / 2
}
x, y := (cw-cardW)/2, (ch-cardH)/2
r := cardW / 14
if r < 8 {
r = 8
}
const off = 7
ih.SetAttribute("DRAWSTYLE", "FILL")
ih.SetAttribute("DRAWCOLOR", th.shadow)
iup.DrawRoundedRectangle(ih, x+off, y+off, x+cardW+off, y+cardH+off, r)
ih.SetAttribute("DRAWCOLOR", th.card)
iup.DrawRoundedRectangle(ih, x, y, x+cardW, y+cardH, r)
ih.SetAttribute("DRAWSTYLE", "STROKE")
ih.SetAttribute("DRAWLINEWIDTH", "1")
ih.SetAttribute("DRAWCOLOR", th.cardBorder)
iup.DrawRoundedRectangle(ih, x, y, x+cardW, y+cardH, r)
lw, lh, _ := iup.DrawGetImageInfo("logo")
if lw <= 0 || lh <= 0 {
return
}
s := float64(cardW*2/5) / float64(lw)
dw, dh := int(float64(lw)*s), int(float64(lh)*s)
iup.DrawImage(ih, "logo", x+(cardW-dw)/2, y+(cardH-dh)/2, dw, dh)
}
// previewMessage receives a rendered cover from previewRender and triggers a canvas redraw.
func previewMessage(ih iup.Ihandle, s string, i int, p any) int {
if i != previewPage {
return iup.DEFAULT
}
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")
hasCover = true
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
} else {
iup.Destroy(iup.GetHandle("cover"))
hasCover = false
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:], ": ")))
}
}
iup.Update(ih)
return iup.DEFAULT
}
// pageBox is the page-navigation spin shown in the status bar; hidden until a comic is selected.
func pageBox() iup.Ihandle {
return iup.Hbox(
iup.Space().SetAttribute("SIZE", "5"),
iup.Label(i18n.Lng(i18n.LblPage)),
iup.Space().SetAttribute("SIZE", "3"),
iup.Text().SetAttributes(`SPIN=YES, SPINMIN=1, SPINMAX=1, VALUE=1, VISIBLECOLUMNS=3, MASK="/d*"`).SetHandle("Page").
SetAttribute("TIP", i18n.Lng(i18n.TipPage)).
SetCallback("SPIN_CB", iup.SpinFunc(func(ih iup.Ihandle, pos int) int {
return onPageChanged()
})).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
return onPageChanged()
})).
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
if s != previewPath {
return iup.DEFAULT
}
ih.SetAttribute("SPINMAX", strconv.Itoa(i))
iup.GetHandle("PageCount").SetAttribute("TITLE", fmt.Sprintf("/ %d", i))
if previewPage > i-1 {
previewPage = i - 1
}
if previewPage < 0 {
previewPage = 0
}
ih.SetAttribute("VALUE", strconv.Itoa(previewPage+1))
iup.Refresh(iup.GetHandle("PageBox"))
previewRender()
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "3"),
iup.Label("").SetHandle("PageCount"),
).SetAttributes("ALIGNMENT=ACENTER, VISIBLE=NO").SetHandle("PageBox")
}
func tabInput() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Toggle(i18n.Lng(i18n.TglRecursive)).SetHandle("Recursive").
SetAttribute("TIP", i18n.Lng(i18n.TipRecursive)),
iup.Toggle(i18n.Lng(i18n.TglNoRGB)).SetHandle("NoRGB").
SetAttribute("TIP", i18n.Lng(i18n.TipNoRGB)),
iup.Toggle(i18n.Lng(i18n.TglNoCover)).SetHandle("NoCover").
SetAttribute("TIP", i18n.Lng(i18n.TipNoCover)),
iup.Toggle(i18n.Lng(i18n.TglNoNonImage)).SetHandle("NoNonImage").
SetAttribute("TIP", i18n.Lng(i18n.TipNoNonImage)),
iup.Toggle(i18n.Lng(i18n.TglNoConvert)).SetHandle("NoConvert").
SetAttribute("TIP", i18n.Lng(i18n.TipNoConvert)).
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(i18n.Lng(i18n.LblMinSize)),
iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size").
SetAttribute("TIP", i18n.Lng(i18n.TipSize)),
),
iup.Vbox(
iup.Label(i18n.Lng(i18n.LblDPI)),
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", i18n.Lng(i18n.TipDPI)),
),
).SetAttributes("NGAP=10"),
).SetHandle("VboxInput")
}
func tabOutput() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Vbox(
iup.Label(i18n.Lng(i18n.LblOutDir)),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutDir").
SetAttribute("TIP", i18n.Lng(i18n.TipOutDir)).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button(i18n.Lng(i18n.BtnBrowse)).SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputDirectory)),
),
iup.Vbox(
iup.Label(i18n.Lng(i18n.LblSuffix)),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("Suffix").
SetAttribute("TIP", i18n.Lng(i18n.TipSuffix)),
),
iup.Vbox(
iup.Label(i18n.Lng(i18n.LblArchive)),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "ZIP",
"2": "TAR",
}).SetHandle("Archive").
SetAttribute("TIP", i18n.Lng(i18n.TipArchive)).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label(i18n.Lng(i18n.LblCompression)),
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", i18n.Lng(i18n.TipZipLevel)),
).SetHandle("VboxZipLevel"),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Toggle(i18n.Lng(i18n.TglCombine)).SetHandle("Combine").
SetAttribute("TIP", i18n.Lng(i18n.TipCombine)).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label(i18n.Lng(i18n.LblOutFile)),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutFile").
SetAttribute("TIP", i18n.Lng(i18n.TipOutFile)),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button(i18n.Lng(i18n.BtnBrowse)).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(i18n.Lng(i18n.LblFormat)),
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", i18n.Lng(i18n.TipFormat)).
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(i18n.Lng(i18n.LblSize)),
iup.Hbox(
iup.Text().SetAttributes(`VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Width").
SetAttribute("CUEBANNER", i18n.Lng(i18n.CueWidth)).
SetAttribute("TIP", i18n.Lng(i18n.TipWidthHeight)).
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(`VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Height").
SetAttribute("CUEBANNER", i18n.Lng(i18n.CueHeight)).
SetAttribute("TIP", i18n.Lng(i18n.TipWidthHeight)).
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(i18n.Lng(i18n.TglFit)).SetHandle("Fit").
SetAttribute("TIP", i18n.Lng(i18n.TipFit)),
iup.Toggle(i18n.Lng(i18n.TglNoUpscale)).SetHandle("NoUpscale").
SetAttribute("TIP", i18n.Lng(i18n.TipNoUpscale)),
),
iup.Vbox(
iup.Label(i18n.Lng(i18n.LblFilter)),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "3",
"TIP": i18n.Lng(i18n.FilterLinear),
"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(i18n.Lng(i18n.LblQuality)),
iup.Label("75").SetHandle("LabelQuality"),
).SetAttributes("NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=0, MAX=100, VALUE=75, SHOWTICKS=10`).SetHandle("Quality").
SetAttribute("TIP", i18n.Lng(i18n.TipQuality)).
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(i18n.Lng(i18n.LblEffort)).SetHandle("LabelEffort"),
iup.Val("").SetAttributes(`MIN=0, MAX=10, VALUE=0, SHOWTICKS=11`).SetHandle("Effort").
SetAttribute("TIP", i18n.Lng(i18n.TipEffort)).
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(i18n.Lng(i18n.TglLossless)).SetHandle("Lossless").
SetAttribute("TIP", i18n.Lng(i18n.TipLossless)).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
userLossless = ih.GetAttribute("VALUE") == "ON"
setActive()
previewPost()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Toggle(i18n.Lng(i18n.TglGrayscale)).SetHandle("Grayscale").
SetAttribute("TIP", i18n.Lng(i18n.TipGrayscale)).
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(i18n.Lng(i18n.LblBrightness)),
iup.Label("0").SetHandle("LabelBrightness"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Brightness").
SetAttribute("TIP", i18n.Lng(i18n.TipBrightness)).
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(i18n.Lng(i18n.LblContrast)),
iup.Label("0").SetHandle("LabelContrast"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Contrast").
SetAttribute("TIP", i18n.Lng(i18n.TipContrast)).
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(i18n.Lng(i18n.LblRotate)),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "0",
"2": "90",
"3": "180",
"4": "270",
}).SetHandle("Rotate").
SetAttribute("TIP", i18n.Lng(i18n.TipRotate)).
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="+i18n.Lng(i18n.TabInput)+", NMARGIN=10x10"),
tabOutput().SetAttributes("TABTITLE="+i18n.Lng(i18n.TabOutput)+", NMARGIN=10x10"),
tabImage().SetAttributes("TABTITLE="+i18n.Lng(i18n.TabImage)+", NMARGIN=10x10"),
tabTransform().SetAttributes("TABTITLE="+i18n.Lng(i18n.TabTransform)+", NMARGIN=10x10"),
).SetHandle("Tabs")
}
func buttons() iup.Ihandle {
addFiles := iup.Button(i18n.Lng(i18n.BtnAddFiles)).SetHandle("AddFiles").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddFiles))
addDir := iup.Button(i18n.Lng(i18n.BtnAddDir)).SetHandle("AddDir").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddDir))
remove := iup.Button(i18n.Lng(i18n.BtnRemove)).SetHandle("Remove").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemove))
removeAll := iup.Button(i18n.Lng(i18n.BtnRemoveAll)).SetHandle("RemoveAll").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemoveAll))
thumbnail := iup.Button(i18n.Lng(i18n.BtnThumbnail)).SetHandle("Thumbnail").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onThumbnail))
cover := iup.Button(i18n.Lng(i18n.BtnCover)).SetHandle("Cover").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onCover))
convert := iup.Button(i18n.Lng(i18n.BtnConvert)).SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onConvert))
reset := iup.Button(i18n.Lng(i18n.BtnReset)).SetHandle("Reset").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", i18n.Lng(i18n.TipReset)).
SetCallback("ACTION", iup.ActionFunc(onReset))
save := iup.Button(i18n.Lng(i18n.BtnSave)).SetHandle("Save").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", i18n.Lng(i18n.TipSave)).
SetCallback("ACTION", iup.ActionFunc(onSave))
command := iup.Button(i18n.Lng(i18n.BtnCommand)).SetHandle("Command").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", i18n.Lng(i18n.TipCommand)).
SetCallback("ACTION", iup.ActionFunc(onCommand))
profile := iup.List().SetAttributes("DROPDOWN=YES").SetHandle("Profile").
SetAttribute("TIP", i18n.Lng(i18n.TipProfile)).
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(i18n.Lng(i18n.LblProfile)),
profile,
reset,
save,
command,
).SetAttribute("NGAP", "2"),
).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER")
}
func status() iup.Ihandle {
return iup.Hbox(
loading(),
pageBox(),
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(i18n.Str(i18n.StatusFileOf), 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")
}
+40 -21
View File
@@ -1,39 +1,58 @@
module github.com/gen2brain/cbconvert/cmd/cbconvert module github.com/gen2brain/cbconvert/cmd/cbconvert
go 1.23 go 1.26
require ( require (
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca github.com/gen2brain/cbconvert v1.0.5-0.20260627172825-0f6e32c177ee
github.com/schollz/progressbar/v3 v3.13.1 github.com/schollz/progressbar/v3 v3.19.0
golang.org/x/term v0.44.0
) )
require ( 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-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // 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-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // 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/fvbommel/sortorder v1.1.0 // indirect
github.com/gen2brain/avif v0.4.1 // indirect github.com/gen2brain/avif v0.5.1 // indirect
github.com/gen2brain/go-fitz v1.24.14 // indirect github.com/gen2brain/go-fitz v1.28.0 // indirect
github.com/gen2brain/go-unarr v0.2.4 // indirect github.com/gen2brain/jpegli v0.4.1 // indirect
github.com/gen2brain/jpegli v0.3.3 // indirect github.com/gen2brain/jpegn v0.4.2 // indirect
github.com/gen2brain/jpegxl v0.4.2 // indirect github.com/gen2brain/jpegxl v0.5.2 // indirect
github.com/gen2brain/webp v0.5.1 // indirect github.com/gen2brain/webp v0.6.1 // indirect
github.com/go-errors/errors v1.5.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-20260625163123-7c0e84413537 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/jupiterrider/ffi v0.2.1 // indirect github.com/klauspost/compress v1.18.6 // indirect
github.com/mattn/go-runewidth v0.0.15 // 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/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.3.4 // indirect github.com/nwaples/rardecode/v2 v2.2.5 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect github.com/pierrec/lz4/v4 v4.1.27 // indirect
golang.org/x/image v0.21.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/net v0.30.0 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect
golang.org/x/sync v0.8.0 // indirect github.com/spf13/afero v1.15.0 // indirect
golang.org/x/sys v0.26.0 // indirect github.com/stangelandcl/ppmd v0.1.1 // indirect
golang.org/x/term v0.25.0 // 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 gopkg.in/yaml.v2 v2.4.0 // indirect
) )
+104 -49
View File
@@ -1,8 +1,25 @@
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs= 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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-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-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs= github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
@@ -21,24 +38,24 @@ 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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 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 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= 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.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.4.1/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs= github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca h1:qYA+cXWUG/Dx+wG2yQ4iuw/s5ROGdXAdo5aIUHrOq4U= github.com/gen2brain/cbconvert v1.0.5-0.20260627172825-0f6e32c177ee h1:9ggHORYXL+0zSdfC0bJA049amaH5wW6jZgO5dGisKm4=
github.com/gen2brain/cbconvert v1.0.5-0.20241106192421-4d845afa43ca/go.mod h1:pQ9kQXsCZQqy7LraruUkBl7CpKNHFpWayxqQ+qliE8Y= github.com/gen2brain/cbconvert v1.0.5-0.20260627172825-0f6e32c177ee/go.mod h1:KrAZdAzcMj1XQkfDz5bp0hotVFUB2k9hRT+9F4b5f2E=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo= github.com/gen2brain/go-fitz v1.28.0 h1:RovqgQPAcOuyv5HZrWsTWl8qwlwbAHSKcAZXZUw0Vlk=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4= github.com/gen2brain/go-fitz v1.28.0/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs=
github.com/gen2brain/go-unarr v0.2.4 h1:Iu2kqtGfkLBSQoTFwMkSCmp0g3GrEM/XMVWzo9TQr/Y= github.com/gen2brain/jpegli v0.4.1 h1:qc11IQU0jTYFltroulT4MXmbu9YRftqHV0YBZ0Bqz5o=
github.com/gen2brain/go-unarr v0.2.4/go.mod h1:0kdy3HtjKBcEaewifXZguHCvt4qD9V8iJCx4FPEOWT8= github.com/gen2brain/jpegli v0.4.1/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegli v0.3.3 h1:ryCOQpmGuVk6FA+QBe9st6cW48jsRdVOPiNrAJ50m+k= github.com/gen2brain/jpegn v0.4.2 h1:sxy2yolV1eNA02uYtnqBFm4EIC3ETnars98aG7Dc4LM=
github.com/gen2brain/jpegli v0.3.3/go.mod h1:6Dbgr+ni1IUBqGVOKHn8lY+6DvwSGfAfC7pPQiSK6uA= github.com/gen2brain/jpegn v0.4.2/go.mod h1:YvcVOmVPSAsefH6yn9HBW3uY0EHlZwCMoiJXoAWfgL0=
github.com/gen2brain/jpegxl v0.4.2 h1:Ff0jAWtCRdc9yjPc9jkyak6Ji/A89Jg0KI+D7qOEtRI= github.com/gen2brain/jpegxl v0.5.2 h1:1ou9YRziU8PbpkfFJIyxrNjYM+WaMl2n9LloABxkKsU=
github.com/gen2brain/jpegxl v0.4.2/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o= github.com/gen2brain/jpegxl v0.5.2/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.5.1 h1:ly9olTGveZEpq3soJuCmex9fxLJ0ipHcQRRSRit5EUE= github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.5.1/go.mod h1:Nb3xO5sy6MeUAHhru9H3GT7nlOQO5dKRNNlE92CZrJw= 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.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.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.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -46,57 +63,95 @@ 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/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-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-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-20260625163123-7c0e84413537 h1:KeIaDS/+VEy/bhDYjG3Z78dOyLAU4HXcVxmd0WYHJTE=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/geo v0.0.0-20260625163123-7c0e84413537/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/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 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= 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/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/jupiterrider/ffi v0.2.1/go.mod h1:tJ7Q8p/3blFjdWt5qJU4W5oDE0xloImvrViE+0td0Rk= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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 h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE= github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ= 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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= 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/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.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= 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-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-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-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-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.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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=
+376 -71
View File
@@ -2,17 +2,22 @@ package main
import ( import (
"bufio" "bufio"
"errors"
"flag" "flag"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime"
"runtime/debug" "runtime/debug"
"strconv"
"strings"
"syscall" "syscall"
"github.com/gen2brain/cbconvert" "github.com/gen2brain/cbconvert"
pb "github.com/schollz/progressbar/v3" pb "github.com/schollz/progressbar/v3"
"golang.org/x/term"
) )
var appVersion string var appVersion string
@@ -68,9 +73,9 @@ func main() {
if _, err := os.Stat(opts.OutDir); err != nil { if _, err := os.Stat(opts.OutDir); err != nil {
if err := os.MkdirAll(opts.OutDir, 0775); err != nil { if err := os.MkdirAll(opts.OutDir, 0775); err != nil {
fmt.Println(err) fmt.Println(err)
}
os.Exit(1) os.Exit(1)
} }
}
files, err := conv.Files(args) files, err := conv.Files(args)
if err != nil { if err != nil {
@@ -78,40 +83,67 @@ func main() {
os.Exit(1) os.Exit(1)
} }
interactive := !opts.Quiet && term.IsTerminal(int(os.Stderr.Fd()))
var bar *pb.ProgressBar var bar *pb.ProgressBar
if opts.Cover || opts.Thumbnail || opts.Meta { newBar := func(max int, description string) {
if !opts.Quiet { bar = pb.NewOptions(max,
bar = pb.NewOptions(conv.Nfiles,
pb.OptionShowCount(), pb.OptionShowCount(),
pb.OptionClearOnFinish(), pb.OptionClearOnFinish(),
pb.OptionUseANSICodes(true), pb.OptionUseANSICodes(true),
pb.OptionSetPredictTime(false), pb.OptionSetPredictTime(false),
pb.OptionSetDescription(description),
) )
} }
clearLine := func() {
fmt.Fprint(os.Stderr, "\033[2K\r")
}
cleanup := func() {
if interactive {
if bar != nil {
_ = bar.Finish()
}
clearLine()
}
}
if interactive && (opts.Cover || opts.Thumbnail || opts.Meta) {
newBar(conv.Nfiles, "")
} }
conv.OnStart = func() { conv.OnStart = func() {
if !opts.Quiet { if interactive {
bar = pb.NewOptions(conv.Ncontents, clearLine()
pb.OptionShowCount(), newBar(conv.Ncontents, fmt.Sprintf("Converting %d of %d:", conv.CurrFile, conv.Nfiles))
pb.OptionClearOnFinish(),
pb.OptionUseANSICodes(true),
pb.OptionSetDescription(fmt.Sprintf("Converting %d of %d:", conv.CurrFile, conv.Nfiles)),
pb.OptionSetPredictTime(false),
)
} }
} }
conv.OnProgress = func() { conv.OnProgress = func() {
if !opts.Quiet { if bar != nil {
_ = bar.Add(1) _ = bar.Add(1)
} }
} }
conv.OnCompress = func() { conv.OnCompress = func() {
if !opts.Quiet { if interactive {
fmt.Fprintf(os.Stderr, "Compressing %d of %d...\r", conv.CurrFile, conv.Nfiles) if bar != nil {
_ = bar.Finish()
} }
fmt.Fprintf(os.Stderr, "Compressing %d of %d...", conv.CurrFile, conv.Nfiles)
}
}
if opts.Combine {
if err := conv.Combine(files); err != nil {
fmt.Println(err)
os.Exit(1)
}
cleanup()
return
} }
for _, file := range files { for _, file := range files {
@@ -131,14 +163,14 @@ func main() {
continue continue
case opts.Cover: case opts.Cover:
if err := conv.Cover(file.Path, file.Stat); err != nil { if err := conv.Cover(file); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
continue continue
case opts.Thumbnail: case opts.Thumbnail:
if err = conv.Thumbnail(file.Path, file.Stat); err != nil { if err = conv.Thumbnail(file); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
@@ -146,13 +178,13 @@ func main() {
continue continue
} }
if err := conv.Convert(file.Path, file.Stat); err != nil { if err := conv.Convert(file); err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
} }
fmt.Fprintf(os.Stderr, "\r") cleanup()
} }
// parseFlags parses command line flags. // parseFlags parses command line flags.
@@ -160,50 +192,84 @@ func parseFlags() (cbconvert.Options, []string) {
opts := cbconvert.Options{} opts := cbconvert.Options{}
var args []string var args []string
base := defaultOptions()
if len(os.Args) >= 2 {
switch os.Args[1] {
case "convert", "cover", "thumbnail":
if name := profileArg(os.Args[2:]); name != "" {
o, err := loadProfile(name)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
base = o
}
}
}
var profile string
const profileUsage = "Load a saved GUI profile as defaults; explicit flags still override"
convert := flag.NewFlagSet("convert", flag.ExitOnError) convert := flag.NewFlagSet("convert", flag.ExitOnError)
convert.IntVar(&opts.Width, "width", 0, "Image width") convert.StringVar(&profile, "profile", "", profileUsage)
convert.IntVar(&opts.Height, "height", 0, "Image height") convert.IntVar(&opts.Width, "width", base.Width, "Image width")
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") convert.IntVar(&opts.Height, "height", base.Height, "Image height")
convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl") convert.BoolVar(&opts.Fit, "fit", base.Fit, "Best fit for required width and height")
convert.StringVar(&opts.Archive, "archive", "zip", "Archive format, valid values are zip, tar") convert.BoolVar(&opts.NoUpscale, "no-upscale", base.NoUpscale, "Do not upscale images already smaller than the requested width/height")
convert.IntVar(&opts.Quality, "quality", 75, "Image quality") convert.IntVar(&opts.DPI, "dpi", base.DPI, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the original resolution")
convert.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos") convert.StringVar(&opts.Format, "format", base.Format, "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl")
convert.BoolVar(&opts.NoCover, "no-cover", false, "Do not convert the cover image") convert.StringVar(&opts.Archive, "archive", base.Archive, "Archive format, valid values are zip, tar")
convert.BoolVar(&opts.NoRGB, "no-rgb", false, "Do not convert images that have RGB colorspace") convert.IntVar(&opts.ZipLevel, "zip-level", base.ZipLevel, "ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default")
convert.BoolVar(&opts.NoNonImage, "no-nonimage", false, "Remove non-image files from the archive") convert.IntVar(&opts.Quality, "quality", base.Quality, "Image quality")
convert.BoolVar(&opts.NoConvert, "no-convert", false, "Do not transform or convert images") convert.IntVar(&opts.Effort, "effort", base.Effort, "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.Grayscale, "grayscale", false, "Convert images to grayscale (monochromatic)") convert.BoolVar(&opts.Lossless, "lossless", base.Lossless, "Lossless compression (webp, avif, jxl), ignores quality")
convert.IntVar(&opts.Rotate, "rotate", 0, "Rotate images, valid values are 0, 90, 180, 270") convert.BoolVar(&opts.Combine, "combine", base.Combine, "Combine all inputs into a single archive")
convert.IntVar(&opts.Brightness, "brightness", 0, "Adjust the brightness of the images, must be in the range (-100, 100)") convert.StringVar(&opts.OutFile, "outfile", base.OutFile, "Output file name for --combine (default: first input + -combined)")
convert.IntVar(&opts.Contrast, "contrast", 0, "Adjust the contrast of the images, must be in the range (-100, 100)") convert.IntVar(&opts.Filter, "filter", base.Filter, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos")
convert.StringVar(&opts.Suffix, "suffix", "", "Add suffix to file basename") convert.BoolVar(&opts.NoCover, "no-cover", base.NoCover, "Do not convert the cover image")
convert.StringVar(&opts.OutDir, "outdir", ".", "Output directory") convert.BoolVar(&opts.NoRGB, "no-rgb", base.NoRGB, "Do not convert images that have RGB colorspace")
convert.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)") convert.BoolVar(&opts.NoNonImage, "no-nonimage", base.NoNonImage, "Remove non-image files from the archive")
convert.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively") convert.BoolVar(&opts.NoConvert, "no-convert", base.NoConvert, "Do not transform or convert images")
convert.BoolVar(&opts.Quiet, "quiet", false, "Hide console output") convert.BoolVar(&opts.Grayscale, "grayscale", base.Grayscale, "Convert images to grayscale (monochromatic)")
convert.IntVar(&opts.Rotate, "rotate", base.Rotate, "Rotate images, valid values are 0, 90, 180, 270")
convert.IntVar(&opts.Brightness, "brightness", base.Brightness, "Adjust the brightness of the images, must be in the range (-100, 100)")
convert.IntVar(&opts.Contrast, "contrast", base.Contrast, "Adjust the contrast of the images, must be in the range (-100, 100)")
convert.StringVar(&opts.Suffix, "suffix", base.Suffix, "Add suffix to file basename")
convert.StringVar(&opts.OutDir, "outdir", base.OutDir, "Output directory")
convert.IntVar(&opts.Size, "size", base.Size, "Process only files larger than size (in MB)")
convert.BoolVar(&opts.Recursive, "recursive", base.Recursive, "Process subdirectories recursively")
convert.BoolVar(&opts.Quiet, "quiet", base.Quiet, "Hide console output")
cover := flag.NewFlagSet("cover", flag.ExitOnError) cover := flag.NewFlagSet("cover", flag.ExitOnError)
cover.IntVar(&opts.Width, "width", 0, "Image width") cover.StringVar(&profile, "profile", "", profileUsage)
cover.IntVar(&opts.Height, "height", 0, "Image height") cover.IntVar(&opts.Width, "width", base.Width, "Image width")
cover.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") cover.IntVar(&opts.Height, "height", base.Height, "Image height")
cover.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif") cover.BoolVar(&opts.Fit, "fit", base.Fit, "Best fit for required width and height")
cover.IntVar(&opts.Quality, "quality", 75, "Image quality") cover.BoolVar(&opts.NoUpscale, "no-upscale", base.NoUpscale, "Do not upscale images already smaller than the requested width/height")
cover.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos") cover.IntVar(&opts.DPI, "dpi", base.DPI, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the original resolution")
cover.StringVar(&opts.OutDir, "outdir", ".", "Output directory") cover.StringVar(&opts.Format, "format", base.Format, "Image format, valid values are jpeg, png, tiff, bmp, webp, avif")
cover.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)") cover.IntVar(&opts.Quality, "quality", base.Quality, "Image quality")
cover.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively") cover.IntVar(&opts.Effort, "effort", base.Effort, "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.Quiet, "quiet", false, "Hide console output") cover.BoolVar(&opts.Lossless, "lossless", base.Lossless, "Lossless compression (webp, avif, jxl), ignores quality")
cover.IntVar(&opts.Filter, "filter", base.Filter, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos")
cover.StringVar(&opts.OutDir, "outdir", base.OutDir, "Output directory")
cover.IntVar(&opts.Size, "size", base.Size, "Process only files larger than size (in MB)")
cover.BoolVar(&opts.Recursive, "recursive", base.Recursive, "Process subdirectories recursively")
cover.BoolVar(&opts.Quiet, "quiet", base.Quiet, "Hide console output")
thumbnail := flag.NewFlagSet("thumbnail", flag.ExitOnError) thumbnail := flag.NewFlagSet("thumbnail", flag.ExitOnError)
thumbnail.IntVar(&opts.Width, "width", 0, "Image width") thumbnail.StringVar(&profile, "profile", "", profileUsage)
thumbnail.IntVar(&opts.Height, "height", 0, "Image height") thumbnail.IntVar(&opts.Width, "width", base.Width, "Image width")
thumbnail.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") thumbnail.IntVar(&opts.Height, "height", base.Height, "Image height")
thumbnail.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos") thumbnail.BoolVar(&opts.Fit, "fit", base.Fit, "Best fit for required width and height")
thumbnail.StringVar(&opts.OutDir, "outdir", ".", "Output directory") thumbnail.BoolVar(&opts.NoUpscale, "no-upscale", base.NoUpscale, "Do not upscale images already smaller than the requested width/height")
thumbnail.StringVar(&opts.OutFile, "outfile", "", "Output file") thumbnail.IntVar(&opts.DPI, "dpi", base.DPI, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the original resolution")
thumbnail.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)") thumbnail.IntVar(&opts.Filter, "filter", base.Filter, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos")
thumbnail.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively") thumbnail.StringVar(&opts.OutDir, "outdir", base.OutDir, "Output directory")
thumbnail.BoolVar(&opts.Quiet, "quiet", false, "Hide console output") thumbnail.StringVar(&opts.OutFile, "outfile", base.OutFile, "Output file")
thumbnail.IntVar(&opts.Size, "size", base.Size, "Process only files larger than size (in MB)")
thumbnail.BoolVar(&opts.Recursive, "recursive", base.Recursive, "Process subdirectories recursively")
thumbnail.BoolVar(&opts.Quiet, "quiet", base.Quiet, "Hide console output")
meta := flag.NewFlagSet("meta", flag.ExitOnError) meta := flag.NewFlagSet("meta", flag.ExitOnError)
meta.BoolVar(&opts.Cover, "cover", false, "Print cover name") meta.BoolVar(&opts.Cover, "cover", false, "Print cover name")
@@ -218,7 +284,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, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
fmt.Fprintf(os.Stderr, "\nCommands:\n") fmt.Fprintf(os.Stderr, "\nCommands:\n")
fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\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{"profile", "width", "height", "fit", "no-upscale", "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"} "no-nonimage", "no-convert", "grayscale", "rotate", "brightness", "contrast", "suffix", "outdir", "size", "recursive", "quiet"}
for _, name := range order { for _, name := range order {
f := convert.Lookup(name) f := convert.Lookup(name)
@@ -226,14 +292,14 @@ func parseFlags() (cbconvert.Options, []string) {
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
} }
fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n") 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{"profile", "width", "height", "fit", "no-upscale", "dpi", "format", "quality", "effort", "lossless", "filter", "outdir", "size", "recursive", "quiet"}
for _, name := range order { for _, name := range order {
f := cover.Lookup(name) f := cover.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.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, "%v (default %q)\n", f.Usage, f.DefValue)
} }
fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n") 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{"profile", "width", "height", "fit", "no-upscale", "dpi", "filter", "outdir", "outfile", "size", "recursive", "quiet"}
for _, name := range order { for _, name := range order {
f := thumbnail.Lookup(name) f := thumbnail.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name) fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
@@ -262,27 +328,27 @@ func parseFlags() (cbconvert.Options, []string) {
switch os.Args[1] { switch os.Args[1] {
case "convert": case "convert":
_ = convert.Parse(os.Args[2:]) operands := parseArgs(convert, os.Args[2:])
if !pipe { if !pipe {
args = convert.Args() args = operands
} }
case "cover": case "cover":
opts.Cover = true opts.Cover = true
_ = cover.Parse(os.Args[2:]) operands := parseArgs(cover, os.Args[2:])
if !pipe { if !pipe {
args = cover.Args() args = operands
} }
case "thumbnail": case "thumbnail":
opts.Thumbnail = true opts.Thumbnail = true
_ = thumbnail.Parse(os.Args[2:]) operands := parseArgs(thumbnail, os.Args[2:])
if !pipe { if !pipe {
args = thumbnail.Args() args = operands
} }
case "meta": case "meta":
opts.Meta = true opts.Meta = true
_ = meta.Parse(os.Args[2:]) operands := parseArgs(meta, os.Args[2:])
if !pipe { if !pipe {
args = meta.Args() args = operands
} }
case "version": case "version":
opts.Version = true opts.Version = true
@@ -297,7 +363,20 @@ func parseFlags() (cbconvert.Options, []string) {
return opts, args 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 { func piped() bool {
f, err := os.Stdin.Stat() f, err := os.Stdin.Stat()
if err != nil { if err != nil {
@@ -311,7 +390,7 @@ func piped() bool {
return true return true
} }
// lines returns slice of lines from reader. // lines returns slice of lines from the reader.
func lines(r io.Reader) []string { func lines(r io.Reader) []string {
data := make([]string, 0) data := make([]string, 0)
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
@@ -323,3 +402,229 @@ func lines(r io.Reader) []string {
return data return data
} }
// configPath returns the IupConfig file the GUI writes, matching IUP's per-platform location for APPNAME "cbconvert".
func configPath() (string, error) {
if runtime.GOOS == "windows" {
dir := os.Getenv("LocalAppData")
if dir == "" {
return "", errors.New("configPath: LocalAppData is not set")
}
return filepath.Join(dir, "cbconvert", "config.cfg"), nil
}
dir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("configPath: %w", err)
}
return filepath.Join(dir, "cbconvert", "config"), nil
}
// parseINI reads a simple INI file into section -> key -> value.
func parseINI(path string) (map[string]map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
sections := make(map[string]map[string]string)
var cur map[string]string
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
cur = make(map[string]string)
sections[line[1:len(line)-1]] = cur
continue
}
if cur == nil {
continue
}
if k, v, ok := strings.Cut(line, "="); ok {
cur[strings.TrimSpace(k)] = strings.TrimSpace(v)
}
}
if err := sc.Err(); err != nil {
return nil, err
}
return sections, nil
}
// defaultOptions returns the convert defaults used when no profile is loaded.
func defaultOptions() cbconvert.Options {
o := cbconvert.NewOptions()
o.OutDir = "."
return o
}
// loadProfile reads the named GUI profile and translates its control values into Options.
func loadProfile(name string) (cbconvert.Options, error) {
o := defaultOptions()
path, err := configPath()
if err != nil {
return o, fmt.Errorf("loadProfile: %w", err)
}
ini, err := parseINI(path)
if err != nil {
return o, fmt.Errorf("loadProfile: %w", err)
}
sec, ok := ini["Profile:"+name]
if !ok {
return o, fmt.Errorf("loadProfile: profile %q not found in %s%s", name, path, knownProfiles(ini))
}
str := func(key string, set func(string)) {
if v, ok := sec[key]; ok {
set(v)
}
}
boolean := func(key string, set func(bool)) {
if v, ok := sec[key]; ok {
set(v == "1")
}
}
integer := func(key string, set func(int)) {
if v, ok := sec[key]; ok {
if n, err := strconv.Atoi(v); err == nil {
set(n)
}
}
}
integer("Width", func(n int) { o.Width = n })
integer("Height", func(n int) { o.Height = n })
boolean("Fit", func(b bool) { o.Fit = b })
boolean("NoUpscale", func(b bool) { o.NoUpscale = b })
str("DPI", func(v string) { o.DPI = dpiFromString(v) })
str("Format", func(v string) { o.Format = formatFromIndex(v) })
str("Archive", func(v string) { o.Archive = archiveFromIndex(v) })
str("ZipLevel", func(v string) { o.ZipLevel = zipLevelFromIndex(v) })
integer("Quality", func(n int) { o.Quality = n })
integer("Effort", func(n int) { o.Effort = n })
boolean("Lossless", func(b bool) { o.Lossless = b })
boolean("Combine", func(b bool) { o.Combine = b })
integer("Filter", func(n int) { o.Filter = n - 1 })
boolean("NoCover", func(b bool) { o.NoCover = b })
boolean("NoRGB", func(b bool) { o.NoRGB = b })
boolean("NoNonImage", func(b bool) { o.NoNonImage = b })
boolean("NoConvert", func(b bool) { o.NoConvert = b })
boolean("Grayscale", func(b bool) { o.Grayscale = b })
str("Rotate", func(v string) { o.Rotate = rotateFromIndex(v) })
integer("Brightness", func(n int) { o.Brightness = n })
integer("Contrast", func(n int) { o.Contrast = n })
str("Suffix", func(v string) { o.Suffix = v })
str("OutDir", func(v string) { o.OutDir = v })
integer("Size", func(n int) { o.Size = n })
boolean("Recursive", func(b bool) { o.Recursive = b })
// Effort is format-specific in the GUI: only webp/avif/jxl use the slider, others fall back to the format default.
switch o.Format {
case "webp", "avif", "jxl":
default:
o.Effort = -1
}
return o, nil
}
// knownProfiles lists the profile names from the config, for a helpful "not found" message.
func knownProfiles(ini map[string]map[string]string) string {
if p, ok := ini["Profiles"]; ok {
if names := p["Names"]; names != "" {
return "\navailable profiles: " + strings.ReplaceAll(names, ";", ", ")
}
}
return ""
}
// profileArg extracts the --profile value from args, since it must be known before flag defaults are built.
func profileArg(args []string) string {
for i := 0; i < len(args); i++ {
if args[i] == "--profile" || args[i] == "-profile" {
if i+1 < len(args) {
return args[i+1]
}
return ""
}
for _, pfx := range []string{"--profile=", "-profile="} {
if v, ok := strings.CutPrefix(args[i], pfx); ok {
return v
}
}
}
return ""
}
// The index translations below mirror the GUI dropdown encodings stored in the profile.
func dpiFromString(s string) int {
n, err := strconv.Atoi(strings.TrimSpace(s))
if err != nil {
return 0
}
return n
}
var profileFormats = []string{"jpeg", "png", "tiff", "bmp", "webp", "avif", "jxl"}
func formatFromIndex(s string) string {
if i, _ := strconv.Atoi(s); i >= 1 && i <= len(profileFormats) {
return profileFormats[i-1]
}
return "jpeg"
}
func archiveFromIndex(s string) string {
if s == "2" {
return "tar"
}
return "zip"
}
func zipLevelFromIndex(s string) int {
switch i, _ := strconv.Atoi(s); i {
case 1:
return -1
case 2:
return 0
default:
return i - 2
}
}
func rotateFromIndex(s string) int {
switch s {
case "2":
return 90
case "3":
return 180
case "4":
return 270
default:
return 0
}
}
+160
View File
@@ -0,0 +1,160 @@
package main
import (
"os"
"path/filepath"
"runtime"
"testing"
)
func TestProfileArg(t *testing.T) {
cases := []struct {
args []string
want string
}{
{[]string{"--profile", "webp", "a.cbz"}, "webp"},
{[]string{"--profile=webp", "a.cbz"}, "webp"},
{[]string{"-profile", "webp"}, "webp"},
{[]string{"-profile=webp"}, "webp"},
{[]string{"--width", "800", "--profile", "x", "a.cbz"}, "x"},
{[]string{"--width", "800", "a.cbz"}, ""},
{[]string{"--profile"}, ""},
}
for _, c := range cases {
if got := profileArg(c.args); got != c.want {
t.Errorf("profileArg(%v) = %q, want %q", c.args, got, c.want)
}
}
}
func TestIndexTranslations(t *testing.T) {
if got := formatFromIndex("5"); got != "webp" {
t.Errorf("formatFromIndex(5) = %q, want webp", got)
}
if got := formatFromIndex("1"); got != "jpeg" {
t.Errorf("formatFromIndex(1) = %q, want jpeg", got)
}
if got := formatFromIndex("99"); got != "jpeg" {
t.Errorf("formatFromIndex(99) = %q, want jpeg fallback", got)
}
if got := archiveFromIndex("2"); got != "tar" {
t.Errorf("archiveFromIndex(2) = %q, want tar", got)
}
if got := archiveFromIndex("1"); got != "zip" {
t.Errorf("archiveFromIndex(1) = %q, want zip", got)
}
zip := map[string]int{"1": -1, "2": 0, "3": 1, "11": 9}
for in, want := range zip {
if got := zipLevelFromIndex(in); got != want {
t.Errorf("zipLevelFromIndex(%s) = %d, want %d", in, got, want)
}
}
rot := map[string]int{"1": 0, "2": 90, "3": 180, "4": 270}
for in, want := range rot {
if got := rotateFromIndex(in); got != want {
t.Errorf("rotateFromIndex(%s) = %d, want %d", in, got, want)
}
}
if got := dpiFromString("Default"); got != 0 {
t.Errorf("dpiFromString(Default) = %d, want 0", got)
}
if got := dpiFromString("150"); got != 150 {
t.Errorf("dpiFromString(150) = %d, want 150", got)
}
}
func TestParseINI(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config")
data := "[Profiles]\nNames=Default;webp\n\n[Profile:webp]\nFormat=5\nQuality=90\n"
if err := os.WriteFile(path, []byte(data), 0644); err != nil {
t.Fatal(err)
}
ini, err := parseINI(path)
if err != nil {
t.Fatal(err)
}
if ini["Profile:webp"]["Format"] != "5" {
t.Errorf("Format = %q, want 5", ini["Profile:webp"]["Format"])
}
if ini["Profiles"]["Names"] != "Default;webp" {
t.Errorf("Names = %q", ini["Profiles"]["Names"])
}
}
func TestLoadProfile(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("config path override via XDG_CONFIG_HOME is Linux-specific")
}
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "cbconvert"), 0755); err != nil {
t.Fatal(err)
}
data := "[Profile:webp]\nFormat=5\nQuality=90\nEffort=4\nArchive=2\nWidth=800\nFit=1\nFilter=7\nRotate=2\n"
if err := os.WriteFile(filepath.Join(dir, "cbconvert", "config"), []byte(data), 0644); err != nil {
t.Fatal(err)
}
t.Setenv("XDG_CONFIG_HOME", dir)
o, err := loadProfile("webp")
if err != nil {
t.Fatal(err)
}
if o.Format != "webp" {
t.Errorf("Format = %q, want webp", o.Format)
}
if o.Quality != 90 {
t.Errorf("Quality = %d, want 90", o.Quality)
}
if o.Effort != 4 {
t.Errorf("Effort = %d, want 4 (webp keeps the slider value)", o.Effort)
}
if o.Archive != "tar" {
t.Errorf("Archive = %q, want tar", o.Archive)
}
if o.Width != 800 || !o.Fit {
t.Errorf("Width/Fit = %d/%v, want 800/true", o.Width, o.Fit)
}
if o.Filter != 6 {
t.Errorf("Filter = %d, want 6 (GUI index 7 - 1)", o.Filter)
}
if o.Rotate != 90 {
t.Errorf("Rotate = %d, want 90", o.Rotate)
}
if _, err := loadProfile("missing"); err == nil {
t.Error("loadProfile(missing) should error")
}
}
func TestLoadProfileEffortGate(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("config path override via XDG_CONFIG_HOME is Linux-specific")
}
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "cbconvert"), 0755); err != nil {
t.Fatal(err)
}
// Format=1 (jpeg) with a stored Effort must collapse to -1, mirroring the GUI.
data := "[Profile:jpeg]\nFormat=1\nEffort=4\n"
if err := os.WriteFile(filepath.Join(dir, "cbconvert", "config"), []byte(data), 0644); err != nil {
t.Fatal(err)
}
t.Setenv("XDG_CONFIG_HOME", dir)
o, err := loadProfile("jpeg")
if err != nil {
t.Fatal(err)
}
if o.Effort != -1 {
t.Errorf("Effort = %d, want -1 for non-effort format", o.Effort)
}
}
+35 -15
View File
@@ -1,32 +1,52 @@
module github.com/gen2brain/cbconvert module github.com/gen2brain/cbconvert
go 1.23 go 1.26
require ( 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/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/fvbommel/sortorder v1.1.0 github.com/fvbommel/sortorder v1.1.0
github.com/gen2brain/avif v0.4.1 github.com/gen2brain/avif v0.5.1
github.com/gen2brain/go-fitz v1.24.14 github.com/gen2brain/go-fitz v1.28.1
github.com/gen2brain/go-unarr v0.2.3 github.com/gen2brain/jpegli v0.4.1
github.com/gen2brain/jpegli v0.3.3 github.com/gen2brain/jpegxl v0.5.2
github.com/gen2brain/jpegxl v0.4.2 github.com/gen2brain/webp v0.6.1
github.com/gen2brain/webp v0.5.1
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25
golang.org/x/image v0.21.0 github.com/mholt/archives v0.1.5
golang.org/x/sync v0.8.0 golang.org/x/image v0.43.0
golang.org/x/sync v0.21.0
) )
require ( 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-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // 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/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/gen2brain/jpegn v0.4.2
github.com/go-errors/errors v1.5.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-20260625163123-7c0e84413537 // indirect
github.com/jupiterrider/ffi v0.2.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/tetratelabs/wazero v1.8.1 // indirect github.com/klauspost/compress v1.18.6 // indirect
golang.org/x/net v0.30.0 // 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 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/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs= 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-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-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs= github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs=
@@ -18,28 +34,22 @@ 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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 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 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= 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.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.4.0/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs= github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/avif v0.4.1 h1:fjwv5SDNYHdI1gbW6MJn3Yaxs1ldUEfAIAH8Ahee538= github.com/gen2brain/go-fitz v1.28.1 h1:ToEYb2vN4ByaL2VmRNGk92Sa1UAkCn8bsObpA3WkQ48=
github.com/gen2brain/avif v0.4.1/go.mod h1:oePci7KPleKZ8X/2rjZ3FlVm2JFYjPwXiQpNgq9wrzs= github.com/gen2brain/go-fitz v1.28.1/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs=
github.com/gen2brain/go-fitz v1.24.14 h1:09weRkjVtLYNGo7l0J7DyOwBExbwi8SJ9h8YPhw9WEo= github.com/gen2brain/jpegli v0.4.1 h1:qc11IQU0jTYFltroulT4MXmbu9YRftqHV0YBZ0Bqz5o=
github.com/gen2brain/go-fitz v1.24.14/go.mod h1:0KaZeQgASc20Yp5R/pFzyy7SmP01XcoHKNF842U2/S4= github.com/gen2brain/jpegli v0.4.1/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/go-unarr v0.2.3 h1:VwZg0P6Dc/8Uh51McjVhzUMg4wHwwbiyqjEFsFELc0c= github.com/gen2brain/jpegn v0.4.2 h1:sxy2yolV1eNA02uYtnqBFm4EIC3ETnars98aG7Dc4LM=
github.com/gen2brain/go-unarr v0.2.3/go.mod h1:hoHheVuf0KT8/hfvkEL7GMwj2h7fq0lF72NdyySdr3c= github.com/gen2brain/jpegn v0.4.2/go.mod h1:YvcVOmVPSAsefH6yn9HBW3uY0EHlZwCMoiJXoAWfgL0=
github.com/gen2brain/jpegli v0.3.3 h1:ryCOQpmGuVk6FA+QBe9st6cW48jsRdVOPiNrAJ50m+k= github.com/gen2brain/jpegxl v0.5.2 h1:1ou9YRziU8PbpkfFJIyxrNjYM+WaMl2n9LloABxkKsU=
github.com/gen2brain/jpegli v0.3.3/go.mod h1:6Dbgr+ni1IUBqGVOKHn8lY+6DvwSGfAfC7pPQiSK6uA= github.com/gen2brain/jpegxl v0.5.2/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/jpegxl v0.4.1 h1:jWaVp5GkXLJXjRvC+g0R9+uo+xoWiS7DYKxJ3n5gHL4= github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/jpegxl v0.4.1/go.mod h1:zIIDnzh7WqG+z66zyzLWQ0M4AS5xi//pyJLgu32GB1o= github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
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/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 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.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.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -47,33 +57,85 @@ 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/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-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-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-20260625163123-7c0e84413537 h1:KeIaDS/+VEy/bhDYjG3Z78dOyLAU4HXcVxmd0WYHJTE=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/geo v0.0.0-20260625163123-7c0e84413537/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/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 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= 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/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/jupiterrider/ffi v0.2.1/go.mod h1:tJ7Q8p/3blFjdWt5qJU4W5oDE0xloImvrViE+0td0Rk= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= 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/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.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= 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-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-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-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-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.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-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.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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.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.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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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=
+7
View File
@@ -0,0 +1,7 @@
go 1.26
use (
.
./cmd/cbconvert
./cmd/cbconvert-gui
)
+114
View File
@@ -0,0 +1,114 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.56.0 h1:iixmq2Fse2tqxMbWhLWC9HfBj1qdxqAmiK8/eqtsLxI=
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780 h1:tFh1tRc4CA31yP6qDcu+Trax5wW5GuMxvkIba07qVLY=
github.com/dsoprea/go-exif/v3 v3.0.0-20210512043655-120bcdb2a55e h1:E4XTSQZF/JtOQWcSaJBJho7t+RNWfdO92W/5skg10Jk=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E=
github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo=
github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmdtest v0.4.0 h1:ToXh6W5spLp3npJV92tk6d5hIpUPYEzHLkD+rncbyhI=
github.com/google/go-cmdtest v0.4.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
github.com/google/go-units v0.0.0-20250612230646-eddd77f68220 h1:hM8xVjUr4Iv/iQIx4Jq1xckZkKlXu51Gqku5HlEpQAE=
github.com/google/go-units v0.0.0-20250612230646-eddd77f68220/go.mod h1:wBcRMlRM/bVzYk9xtR2hOp3+iWOhEh1FiK8sAzeR9eA=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
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/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs=
go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=