Author SHA1 Message Date
Milan Nikolic d45a14ecc5 Update modules.txt 2026-06-27 21:49:18 +02:00
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
53 changed files with 3399 additions and 488 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:
fail-fast: false
matrix:
go-version: [1.23.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
go-version: stable
- name: Test
run: go test
run: go test -v ./...
env:
CGO_ENABLED: 1
+16 -4
View File
@@ -40,7 +40,7 @@ For GUI app, check [IUP](https://github.com/gen2brain/iup-go) requirements, and
### Build tags
* `extlib` - use external `libmupdf` and `libunarr` libraries
* `extlib` - use external `libmupdf` library
* `pkgconfig` - enable pkg-config (used with `extlib`)
### Using cbconvert in file managers to generate FreeDesktop thumbnails
@@ -64,12 +64,16 @@ Commands:
convert
Convert archive or document
--profile
Load a saved GUI profile as defaults; explicit flags still override (default "")
--width
Image width (default "0")
--height
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--no-upscale
Do not upscale images already smaller than the requested width/height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--format
@@ -89,7 +93,7 @@ Commands:
--outfile
Output file name for --combine (default: first input + -combined) (default "")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos (default "2")
--no-cover
Do not convert the cover image (default "false")
--no-rgb
@@ -120,12 +124,16 @@ Commands:
cover
Extract cover
--profile
Load a saved GUI profile as defaults; explicit flags still override (default "")
--width
Image width (default "0")
--height
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--no-upscale
Do not upscale images already smaller than the requested width/height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--format
@@ -137,7 +145,7 @@ Commands:
--lossless
Lossless compression (webp, avif, jxl), ignores quality (default "false")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos (default "2")
--outdir
Output directory (default ".")
--size
@@ -150,16 +158,20 @@ Commands:
thumbnail
Extract cover thumbnail (freedesktop spec.)
--profile
Load a saved GUI profile as defaults; explicit flags still override (default "")
--width
Image width (default "0")
--height
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--no-upscale
Do not upscale images already smaller than the requested width/height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos (default "2")
--outdir
Output directory (default ".")
--outfile
+57 -24
View File
@@ -15,6 +15,7 @@ import (
pngstructure "github.com/dsoprea/go-png-image-structure"
"github.com/dustin/go-humanize"
"github.com/gen2brain/go-fitz"
)
// Options type.
@@ -39,9 +40,11 @@ type Options struct {
Height int
// Best fit for required width and height
Fit bool
// Document rendering resolution in DPI (PDF, EPUB, etc.); 0 uses the default
// 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, 6=Gaussian, 7=Lanczos
// 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos
Filter int
// Do not convert the cover image
NoCover bool
@@ -99,6 +102,8 @@ type Converter struct {
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
Nfiles int
// Index of the current file
@@ -180,6 +185,7 @@ func (o Options) Args() []string {
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)
@@ -206,13 +212,14 @@ func (o Options) Args() []string {
return args
}
// renderDPI returns the document rendering resolution, falling back to 300 when unset.
func (c *Converter) renderDPI() float64 {
// 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 float64(c.Opts.DPI)
return doc.ImageDPI(n, float64(c.Opts.DPI))
}
return 300
return doc.Image(n)
}
// Cancel cancels the operation.
@@ -360,11 +367,7 @@ func (c *Converter) Cover(file File) error {
}
if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit {
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])
}
cover = c.resizeFit(cover)
}
ext := c.Opts.Format
@@ -409,13 +412,9 @@ func (c *Converter) Thumbnail(file File) error {
}
if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit {
cover = fit(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
cover = c.resizeFit(cover)
} else {
cover = resize(cover, c.Opts.Width, c.Opts.Height, filters[c.Opts.Filter])
}
} else {
cover = resize(cover, 256, 0, filters[c.Opts.Filter])
cover = resize(cover, 256, 0, resampleFilter(c.Opts.Filter))
}
var buf bytes.Buffer
@@ -547,15 +546,51 @@ func (c *Converter) Meta(fileName string) (any, error) {
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) {
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)
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)
var w bytes.Buffer
@@ -568,15 +603,13 @@ func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height
img.Height = i.Bounds().Dy()
img.SizeHuman = humanize.IBytes(uint64(len(w.Bytes())))
r := bytes.NewReader(w.Bytes())
dec, err := c.imageDecode(r)
dec, err := c.imageDecode(bytes.NewReader(w.Bytes()))
if err != nil {
return img, fmt.Errorf("%s: %w", fileName, err)
}
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
+50 -7
View File
@@ -1,6 +1,7 @@
package cbconvert
import (
"bufio"
"bytes"
"context"
"fmt"
@@ -16,6 +17,7 @@ import (
"github.com/gen2brain/avif"
"github.com/gen2brain/go-fitz"
"github.com/gen2brain/jpegli"
"github.com/gen2brain/jpegn"
"github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp"
"github.com/jsummers/gobmp"
@@ -48,7 +50,7 @@ func (c *Converter) convertDocument(ctx context.Context, fileName string) error
return fmt.Errorf("convertDocument: %w", ctx.Err())
}
img, err := doc.ImageDPI(n, c.renderDPI())
img, err := c.renderPage(doc, n)
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
@@ -326,11 +328,7 @@ func (c *Converter) imageTransform(img image.Image) image.Image {
var i = img
if c.Opts.Width > 0 || c.Opts.Height > 0 {
if c.Opts.Fit {
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])
}
i = c.resizeFit(i)
}
if c.Opts.Rotate > 0 {
@@ -360,8 +358,42 @@ func (c *Converter) imageTransform(img image.Image) image.Image {
}
// 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) {
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 {
return img, fmt.Errorf("imageDecode: %w", err)
}
@@ -369,6 +401,17 @@ func (c *Converter) imageDecode(reader io.Reader) (image.Image, error) {
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.
func (c *Converter) imageEncode(img image.Image, w io.Writer) error {
var err error
+127 -1
View File
@@ -52,7 +52,7 @@ func (c *Converter) coverDocument(fileName string) (image.Image, error) {
}
defer doc.Close()
img, err := doc.ImageDPI(0, c.renderDPI())
img, err := c.renderPage(doc, 0)
if err != nil {
return nil, fmt.Errorf("coverDocument: %w", err)
}
@@ -85,6 +85,132 @@ func (c *Converter) coverDirectory(dir string) (image.Image, error) {
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.
func (c *Converter) coverName(images []string) string {
if len(images) == 0 {
+29
View File
@@ -38,6 +38,15 @@ var filters = map[int]transform.ResampleFilter{
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 {
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)
}
// 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 {
return transform.Rotate(img, angle, &transform.RotationOptions{ResizeBounds: true, Pivot: &image.Point{}})
}
+86 -1
View File
@@ -114,17 +114,68 @@ func TestArgs(t *testing.T) {
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 --dpi 150 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out"
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 {
@@ -160,6 +211,40 @@ func TestConvertDPI(t *testing.T) {
}
}
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 {
+113 -1
View File
@@ -1,6 +1,14 @@
package main
import "github.com/gen2brain/iup-go/iup"
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)
@@ -21,7 +29,12 @@ func fileDlg(title string, multiple, directory bool, dirKey string) ([]string, e
"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",
@@ -55,6 +68,105 @@ func fileDlg(title string, multiple, directory bool, dirKey string) ([]string, e
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()
@@ -38,6 +38,23 @@
<content_rating type="oars-1.1"/>
<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">
<description>
<ul>
+164 -49
View File
@@ -1,11 +1,50 @@
# github.com/anthonynsimon/bild v0.14.0
## explicit; go 1.21
# github.com/STARRY-S/zip v0.2.3
## explicit; go 1.23
github.com/STARRY-S/zip
# github.com/andybalholm/brotli v1.2.1
## explicit; go 1.22
github.com/andybalholm/brotli
github.com/andybalholm/brotli/flate
github.com/andybalholm/brotli/matchfinder
# github.com/anthonynsimon/bild v0.15.0
## explicit; go 1.26
github.com/anthonynsimon/bild/adjust
github.com/anthonynsimon/bild/clone
github.com/anthonynsimon/bild/math/f64
github.com/anthonynsimon/bild/parallel
github.com/anthonynsimon/bild/transform
github.com/anthonynsimon/bild/util
# github.com/bodgit/plumbing v1.3.0
## explicit; go 1.13
github.com/bodgit/plumbing
# github.com/bodgit/sevenzip v1.6.4
## explicit; go 1.25.0
github.com/bodgit/sevenzip
github.com/bodgit/sevenzip/internal/aes7z
github.com/bodgit/sevenzip/internal/bcj2
github.com/bodgit/sevenzip/internal/bra
github.com/bodgit/sevenzip/internal/brotli
github.com/bodgit/sevenzip/internal/bzip2
github.com/bodgit/sevenzip/internal/deflate
github.com/bodgit/sevenzip/internal/delta
github.com/bodgit/sevenzip/internal/lz4
github.com/bodgit/sevenzip/internal/lzma
github.com/bodgit/sevenzip/internal/lzma2
github.com/bodgit/sevenzip/internal/pool
github.com/bodgit/sevenzip/internal/ppmd
github.com/bodgit/sevenzip/internal/util
github.com/bodgit/sevenzip/internal/zstd
# github.com/bodgit/windows v1.0.1
## explicit; go 1.13
github.com/bodgit/windows
# github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
## explicit; go 1.9
github.com/dsnet/compress
github.com/dsnet/compress/bzip2
github.com/dsnet/compress/bzip2/internal/sais
github.com/dsnet/compress/internal
github.com/dsnet/compress/internal/errors
github.com/dsnet/compress/internal/prefix
# github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d
## explicit; go 1.13
github.com/dsoprea/go-exif/v2
@@ -23,88 +62,140 @@ github.com/dsoprea/go-utility/image
# github.com/dustin/go-humanize v1.0.1
## explicit; go 1.16
github.com/dustin/go-humanize
# github.com/ebitengine/purego v0.8.1
# github.com/ebitengine/purego v0.10.1
## explicit; go 1.18
github.com/ebitengine/purego
github.com/ebitengine/purego/internal/cgo
github.com/ebitengine/purego/internal/fakecgo
github.com/ebitengine/purego/internal/strings
github.com/ebitengine/purego/internal/xreflect
# github.com/fvbommel/sortorder v1.1.0
## explicit; go 1.13
github.com/fvbommel/sortorder
# github.com/gen2brain/avif v0.4.1
## explicit; go 1.21
# github.com/gen2brain/avif v0.5.1
## explicit; go 1.25.0
github.com/gen2brain/avif
# github.com/gen2brain/cbconvert v1.0.5-0.20241106181414-0dee611bf1de
## explicit; go 1.23
# github.com/gen2brain/cbconvert v1.2.0
## explicit; go 1.26
github.com/gen2brain/cbconvert
# github.com/gen2brain/go-fitz v1.24.14
## explicit; go 1.22
# github.com/gen2brain/go-fitz v1.28.1
## explicit; go 1.24.0
github.com/gen2brain/go-fitz
github.com/gen2brain/go-fitz/include/mupdf
github.com/gen2brain/go-fitz/include/mupdf/fitz
github.com/gen2brain/go-fitz/libs
# github.com/gen2brain/go-unarr v0.2.4
# github.com/gen2brain/iup-go/iup v0.32.1-0.20260627135200-7df674d35173
## explicit; go 1.21
github.com/gen2brain/go-unarr
github.com/gen2brain/go-unarr/unarrc
github.com/gen2brain/go-unarr/unarrc/external
github.com/gen2brain/go-unarr/unarrc/external/bzip2
github.com/gen2brain/go-unarr/unarrc/external/unarr
github.com/gen2brain/go-unarr/unarrc/external/unarr/_7z
github.com/gen2brain/go-unarr/unarrc/external/unarr/common
github.com/gen2brain/go-unarr/unarrc/external/unarr/lzmasdk
github.com/gen2brain/go-unarr/unarrc/external/unarr/rar
github.com/gen2brain/go-unarr/unarrc/external/unarr/tar
github.com/gen2brain/go-unarr/unarrc/external/unarr/zip
github.com/gen2brain/go-unarr/unarrc/external/zlib
# github.com/gen2brain/iup-go/iup v0.0.0-20241106050025-0f971ac33ed4
## explicit; go 1.22
github.com/gen2brain/iup-go/iup
github.com/gen2brain/iup-go/iup/external
github.com/gen2brain/iup-go/iup/external/include
github.com/gen2brain/iup-go/iup/external/src
github.com/gen2brain/iup-go/iup/external/src/android
github.com/gen2brain/iup-go/iup/external/src/cocoa
github.com/gen2brain/iup-go/iup/external/src/cocoatouch
github.com/gen2brain/iup-go/iup/external/src/efl
github.com/gen2brain/iup-go/iup/external/src/gtk
github.com/gen2brain/iup-go/iup/external/src/gtk4
github.com/gen2brain/iup-go/iup/external/src/haiku
github.com/gen2brain/iup-go/iup/external/src/mot
github.com/gen2brain/iup-go/iup/external/src/qt
github.com/gen2brain/iup-go/iup/external/src/win
github.com/gen2brain/iup-go/iup/external/src/win/wdl
github.com/gen2brain/iup-go/iup/external/src/win/wdl/dummy
github.com/gen2brain/iup-go/iup/external/src/winui
github.com/gen2brain/iup-go/iup/external/src/winui/winrt
github.com/gen2brain/iup-go/iup/external/src/winui/winrt/impl
github.com/gen2brain/iup-go/iup/external/srcctrl
github.com/gen2brain/iup-go/iup/external/srcctrl/matrix
github.com/gen2brain/iup-go/iup/external/srcctrl/matrixex
github.com/gen2brain/iup-go/iup/external/srcgl
github.com/gen2brain/iup-go/iup/external/srcplot
github.com/gen2brain/iup-go/iup/external/srcweb
github.com/gen2brain/iup-go/iup/manifest
# github.com/gen2brain/jpegli v0.3.3
## explicit; go 1.22
# github.com/gen2brain/jpegli v0.4.1
## explicit; go 1.25.0
github.com/gen2brain/jpegli
# github.com/gen2brain/jpegxl v0.4.2
## explicit; go 1.22
# github.com/gen2brain/jpegn v0.4.2
## explicit; go 1.23.0
github.com/gen2brain/jpegn
# github.com/gen2brain/jpegxl v0.5.2
## explicit; go 1.25.0
github.com/gen2brain/jpegxl
# github.com/gen2brain/webp v0.5.1
## explicit; go 1.22
# github.com/gen2brain/webp v0.6.1
## explicit; go 1.23
github.com/gen2brain/webp
# github.com/go-errors/errors v1.5.1
## explicit; go 1.14
github.com/go-errors/errors
# github.com/godbus/dbus/v5 v5.1.0
## explicit; go 1.12
github.com/godbus/dbus/v5
# github.com/golang/geo v0.0.0-20230421003525-6adc56603217
## explicit; go 1.18
# github.com/golang/geo v0.0.0-20260625163123-7c0e84413537
## explicit; go 1.23.0
github.com/golang/geo/r1
github.com/golang/geo/r2
github.com/golang/geo/r3
github.com/golang/geo/s1
github.com/golang/geo/s2
# github.com/google/uuid v1.6.0
## explicit
github.com/google/uuid
# github.com/hashicorp/golang-lru/v2 v2.0.7
## explicit; go 1.18
github.com/hashicorp/golang-lru/v2
github.com/hashicorp/golang-lru/v2/internal
github.com/hashicorp/golang-lru/v2/simplelru
# github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25
## explicit
github.com/jsummers/gobmp
# github.com/jupiterrider/ffi v0.2.1
## explicit; go 1.18
github.com/jupiterrider/ffi
# github.com/tetratelabs/wazero v1.8.1
# github.com/klauspost/compress v1.18.6
## explicit; go 1.24
github.com/klauspost/compress
github.com/klauspost/compress/flate
github.com/klauspost/compress/fse
github.com/klauspost/compress/gzip
github.com/klauspost/compress/huff0
github.com/klauspost/compress/internal/cpuinfo
github.com/klauspost/compress/internal/godebug
github.com/klauspost/compress/internal/le
github.com/klauspost/compress/internal/race
github.com/klauspost/compress/internal/snapref
github.com/klauspost/compress/s2
github.com/klauspost/compress/zip
github.com/klauspost/compress/zlib
github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd/internal/xxhash
# github.com/klauspost/pgzip v1.2.6
## explicit
github.com/klauspost/pgzip
# github.com/mholt/archives v0.1.5
## explicit; go 1.24.0
github.com/mholt/archives
# github.com/mikelolasagasti/xz v1.0.1
## explicit; go 1.15
github.com/mikelolasagasti/xz
# github.com/minio/minlz v1.1.1
## explicit; go 1.21
github.com/minio/minlz
github.com/minio/minlz/internal/race
# github.com/nwaples/rardecode/v2 v2.2.5
## explicit; go 1.21
github.com/nwaples/rardecode/v2
# github.com/pierrec/lz4/v4 v4.1.27
## explicit; go 1.17
github.com/pierrec/lz4/v4
github.com/pierrec/lz4/v4/internal/lz4block
github.com/pierrec/lz4/v4/internal/lz4errors
github.com/pierrec/lz4/v4/internal/lz4stream
github.com/pierrec/lz4/v4/internal/xxh32
# github.com/sorairolake/lzip-go v0.3.8
## explicit; go 1.22
github.com/sorairolake/lzip-go
# github.com/spf13/afero v1.15.0
## explicit; go 1.23.0
github.com/spf13/afero
github.com/spf13/afero/internal/common
github.com/spf13/afero/mem
# github.com/stangelandcl/ppmd v0.1.1
## explicit; go 1.17
github.com/stangelandcl/ppmd
github.com/stangelandcl/ppmd/internal/h7z
# github.com/tetratelabs/wazero v1.12.0
## explicit; go 1.25.0
github.com/tetratelabs/wazero
github.com/tetratelabs/wazero/api
github.com/tetratelabs/wazero/experimental
@@ -122,7 +213,6 @@ github.com/tetratelabs/wazero/internal/engine/wazevo/ssa
github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi
github.com/tetratelabs/wazero/internal/expctxkeys
github.com/tetratelabs/wazero/internal/filecache
github.com/tetratelabs/wazero/internal/fsapi
github.com/tetratelabs/wazero/internal/ieee754
github.com/tetratelabs/wazero/internal/internalapi
github.com/tetratelabs/wazero/internal/leb128
@@ -140,17 +230,42 @@ github.com/tetratelabs/wazero/internal/wasm/binary
github.com/tetratelabs/wazero/internal/wasmdebug
github.com/tetratelabs/wazero/internal/wasmruntime
github.com/tetratelabs/wazero/sys
# golang.org/x/image v0.21.0
## explicit; go 1.18
# github.com/ulikunitz/xz v0.5.15
## explicit; go 1.12
github.com/ulikunitz/xz
github.com/ulikunitz/xz/internal/hash
github.com/ulikunitz/xz/internal/xlog
github.com/ulikunitz/xz/lzma
# go4.org v0.0.0-20260112195520-a5071408f32f
## explicit; go 1.24.0
go4.org/readerutil
# golang.org/x/image v0.43.0
## explicit; go 1.25.0
golang.org/x/image/ccitt
golang.org/x/image/internal/safemath
golang.org/x/image/tiff
golang.org/x/image/tiff/lzw
# golang.org/x/net v0.30.0
## explicit; go 1.18
# golang.org/x/net v0.56.0
## explicit; go 1.25.0
golang.org/x/net/context
# golang.org/x/sync v0.8.0
## explicit; go 1.18
# golang.org/x/sync v0.21.0
## explicit; go 1.25.0
golang.org/x/sync/errgroup
# golang.org/x/sys v0.46.0
## explicit; go 1.25.0
golang.org/x/sys/cpu
golang.org/x/sys/unix
golang.org/x/sys/windows
# golang.org/x/text v0.38.0
## explicit; go 1.25.0
golang.org/x/text/encoding
golang.org/x/text/encoding/internal
golang.org/x/text/encoding/internal/identifier
golang.org/x/text/encoding/unicode
golang.org/x/text/internal/utf8internal
golang.org/x/text/runes
golang.org/x/text/transform
golang.org/x/text/unicode/norm
# gopkg.in/yaml.v2 v2.4.0
## explicit; go 1.15
gopkg.in/yaml.v2
@@ -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>
+8 -10
View File
@@ -3,8 +3,9 @@ module github.com/gen2brain/cbconvert/cmd/cbconvert-gui
go 1.26
require (
github.com/gen2brain/cbconvert v1.0.5-0.20260623161611-a5817c3ba5de
github.com/gen2brain/iup-go/iup v0.32.1-0.20260604112206-a19da6654449
github.com/fvbommel/sortorder v1.1.0
github.com/gen2brain/cbconvert v1.2.0
github.com/gen2brain/iup-go/iup v0.32.1-0.20260627135200-7df674d35173
)
require (
@@ -21,17 +22,16 @@ require (
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/gen2brain/avif v0.5.1 // indirect
github.com/gen2brain/go-fitz v1.24.15 // indirect
github.com/gen2brain/jpegli v0.4.0 // indirect
github.com/gen2brain/jpegxl v0.5.1 // indirect
github.com/gen2brain/go-fitz v1.28.1 // indirect
github.com/gen2brain/jpegli v0.4.1 // indirect
github.com/gen2brain/jpegn v0.4.2 // indirect
github.com/gen2brain/jpegxl v0.5.2 // indirect
github.com/gen2brain/webp v0.6.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 // 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/jupiterrider/ffi v0.7.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mholt/archives v0.1.5 // indirect
@@ -52,5 +52,3 @@ require (
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/gen2brain/cbconvert => ../..
+14 -12
View File
@@ -40,14 +40,18 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/gen2brain/iup-go/iup v0.32.1-0.20260604112206-a19da6654449 h1:HPqLUcspf4aPnyQIgeFKlLmphFVpFP34fzr1ZdnNyfc=
github.com/gen2brain/iup-go/iup v0.32.1-0.20260604112206-a19da6654449/go.mod h1:V4f7tHOJAeHtjQ+ju795QKv6DGdLEb4L5cmWB1sjSzU=
github.com/gen2brain/jpegli v0.4.0 h1:TnmAO8EcekS/XCboyLPjP6cqz025tkhdaFkiYNd3wH8=
github.com/gen2brain/jpegli v0.4.0/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE=
github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/cbconvert v1.2.0 h1:eUs5MPzft+Lo9ADMYYQ5AzydO54Pv0YYUTm2lPo3XDo=
github.com/gen2brain/cbconvert v1.2.0/go.mod h1:/6QtSgf47bDBmtkidNoYwNMHaebsDhPTKnDMmyNUd9E=
github.com/gen2brain/go-fitz v1.28.1 h1:ToEYb2vN4ByaL2VmRNGk92Sa1UAkCn8bsObpA3WkQ48=
github.com/gen2brain/go-fitz v1.28.1/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs=
github.com/gen2brain/iup-go/iup v0.32.1-0.20260627135200-7df674d35173 h1:nBt0N1ixK8eg/7RXJIC4b0WDPxfENqyT+rH1/STZGj4=
github.com/gen2brain/iup-go/iup v0.32.1-0.20260627135200-7df674d35173/go.mod h1:V4f7tHOJAeHtjQ+ju795QKv6DGdLEb4L5cmWB1sjSzU=
github.com/gen2brain/jpegli v0.4.1 h1:qc11IQU0jTYFltroulT4MXmbu9YRftqHV0YBZ0Bqz5o=
github.com/gen2brain/jpegli v0.4.1/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegn v0.4.2 h1:sxy2yolV1eNA02uYtnqBFm4EIC3ETnars98aG7Dc4LM=
github.com/gen2brain/jpegn v0.4.2/go.mod h1:YvcVOmVPSAsefH6yn9HBW3uY0EHlZwCMoiJXoAWfgL0=
github.com/gen2brain/jpegxl v0.5.2 h1:1ou9YRziU8PbpkfFJIyxrNjYM+WaMl2n9LloABxkKsU=
github.com/gen2brain/jpegxl v0.5.2/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@@ -57,8 +61,8 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 h1:q100v18oYIaE4p5myLFqDyfXTOxivzwGyLjGCEfv8x4=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/golang/geo v0.0.0-20260625163123-7c0e84413537 h1:KeIaDS/+VEy/bhDYjG3Z78dOyLAU4HXcVxmd0WYHJTE=
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=
@@ -67,8 +71,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+128 -57
View File
@@ -7,10 +7,13 @@ import (
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"github.com/fvbommel/sortorder"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"github.com/gen2brain/iup-go/iup"
)
@@ -24,7 +27,7 @@ func selectRow(i int) {
iup.GetHandle("Table").SetAttribute("FOCUSCELL", fmt.Sprintf("%d:1", i+1))
}
// onSort re-syncs the files slice to the table's displayed order after a sort, so rows keep mapping to the right file.
// 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 {
@@ -73,16 +76,49 @@ func onSort(ih iup.Ihandle, col int) int {
return iup.DEFAULT
}
// appendFile adds a file as a new row to the table and the files slice.
func appendFile(file cbconvert.File) {
files = append(files, file)
// 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")
lin := len(files)
t.SetAttribute("NUMLIN", strconv.Itoa(lin))
iup.SetAttributeId2(t, "", lin, 1, file.Name)
iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(file.Path))
iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(file.Stat.Size())/(1024*1024), 'f', 2, 64))
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() {
@@ -90,32 +126,79 @@ func previewPost() {
return
}
width, height := previewSize()
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")
if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" {
iup.GetHandle("Preview").SetAttribute("IMAGE", "")
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
file := files[index]
img, err := conv.Preview(file.Path, file.Stat, width, height)
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, 0, img)
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("Add Files", true, false, inputDirKey)
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)
@@ -134,28 +217,14 @@ func onAddFiles(ih iup.Ihandle) int {
return iup.DEFAULT
}
wasEmpty := len(files) == 0
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
addFiles(fs)
}
return iup.DEFAULT
}
func onAddDir(ih iup.Ihandle) int {
args, err := fileDlg("Add Directory", false, true, inputDirKey)
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)
@@ -174,21 +243,7 @@ func onAddDir(ih iup.Ihandle) int {
return iup.DEFAULT
}
wasEmpty := len(files) == 0
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
addFiles(fs)
}
return iup.DEFAULT
@@ -207,16 +262,32 @@ func onRemove(ih iup.Ihandle) int {
}
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
@@ -390,7 +461,7 @@ func onConvert(ih iup.Ihandle) int {
}
func onOutputDirectory(ih iup.Ihandle) int {
args, err := fileDlg("Output Directory", false, true, outputDirKey)
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)
@@ -408,7 +479,7 @@ func onOutputDirectory(ih iup.Ihandle) int {
}
func onOutputFile(ih iup.Ihandle) int {
name := saveDlg("Output File", outputDirKey)
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))
@@ -421,19 +492,19 @@ func onOutputFile(ih iup.Ihandle) int {
func onFilterChanged(ih iup.Ihandle) int {
switch ih.GetInt("VALUE") {
case 1:
ih.SetAttribute("TIP", "NearestNeighbor is the fastest resampling filter, no antialiasing")
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterNearest))
case 2:
ih.SetAttribute("TIP", "Box filter (averaging pixels)")
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterBox))
case 3:
ih.SetAttribute("TIP", "Linear is the bilinear filter, smooth and reasonably fast")
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterLinear))
case 4:
ih.SetAttribute("TIP", "MitchellNetravali is a smooth bicubic filter")
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterMitchell))
case 5:
ih.SetAttribute("TIP", "CatmullRom is a sharp bicubic filter")
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterCatmull))
case 6:
ih.SetAttribute("TIP", "Gaussian is a blurring filter that uses gaussian function, useful for noise removal")
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterGaussian))
case 7:
ih.SetAttribute("TIP", "Lanczos is a high-quality resampling filter, it's slower than cubic filters")
ih.SetAttribute("TIP", i18n.Lng(i18n.FilterLanczos))
}
previewPost()
+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: "无预览",
})
}
+12 -10
View File
@@ -12,10 +12,12 @@ import (
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"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 arm64 -o main_windows_arm64.syso
//go:embed assets/logo.png
var appLogo []byte
@@ -33,6 +35,10 @@ var (
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() {
@@ -70,7 +76,11 @@ func main() {
iup.SetGlobal("APPNAME", "cbconvert")
iup.SetGlobal("APPID", "io.github.gen2brain.cbconvert")
iup.SetGlobal("AUTODARKMODE", "YES")
iup.SetGlobal("GSKRENDERER", "cairo")
i18n.Init()
config = iup.Config()
iup.ConfigLoad(config)
@@ -78,7 +88,7 @@ func main() {
img, _ := png.Decode(bytes.NewReader(appLogo))
iup.ImageFromImage(img).SetHandle("logo")
dlg := iup.Dialog(layout()).SetAttributes(fmt.Sprintf(`TITLE="CBconvert %s", ICON=logo, SHRINK=YES`, appVersion)).SetHandle("dlg")
dlg := iup.Dialog(layout()).SetAttributes(fmt.Sprintf(`TITLE="CBconvert %s", ICON=logo`, appVersion)).SetHandle("dlg")
dlg.SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
sp := strings.Split(s, ": ")
@@ -89,19 +99,11 @@ func main() {
return iup.DEFAULT
}))
dlg.SetCallback("RESIZE_CB", iup.ResizeFunc(func(ih iup.Ihandle, width, height int) int {
iup.GetHandle("Preview").SetAttribute("IMAGE", "logo")
iup.Refresh(ih)
previewPost()
return iup.DEFAULT
}))
dlg.SetCallback("THEMECHANGED_CB", iup.ThemeChangedFunc(func(ih iup.Ihandle, darkMode int) int {
t := iup.GetHandle("Table")
tableRowColors(t, darkMode == 1)
t.SetAttribute("REDRAW", "YES")
iup.Update(iup.GetHandle("Preview"))
return iup.DEFAULT
}))
Binary file not shown.
+3 -1
View File
@@ -5,6 +5,7 @@ import (
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"github.com/gen2brain/iup-go/iup"
)
@@ -25,6 +26,7 @@ func options() cbconvert.Options {
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 {
@@ -69,7 +71,7 @@ func commandLine() string {
}
func onCommand(iup.Ihandle) int {
iup.GetText("Command Line", commandLine(), -1)
iup.GetText(i18n.Str(i18n.DlgCommandLine), commandLine(), -1)
return iup.DEFAULT
}
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

+6 -2
View File
@@ -6,6 +6,7 @@ import (
"strconv"
"strings"
"github.com/gen2brain/cbconvert/cmd/cbconvert-gui/i18n"
"github.com/gen2brain/iup-go/iup"
)
@@ -39,6 +40,7 @@ var settings = []setting{
{"NoNonImage", kindBool, "OFF"},
{"Combine", kindBool, "OFF"},
{"Fit", kindBool, "OFF"},
{"NoUpscale", kindBool, "OFF"},
{"Lossless", kindBool, "OFF"},
{"Grayscale", kindBool, "OFF"},
{"OutDir", kindStr, ""},
@@ -146,6 +148,8 @@ func settingsApply(group string) {
}
}
userLossless = iup.GetHandle("Lossless").GetAttribute("VALUE") == "ON"
syncLabels()
setActive()
previewPost()
@@ -214,13 +218,13 @@ func onProfileSelect(ih iup.Ihandle) int {
func onSave(iup.Ihandle) int {
name := currentProfile()
if iup.GetParam("Save Profile", nil, "Name: %s\n", &name) != 1 {
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("Invalid Name", "Profile name must not be empty or contain '.' or ';'.")
iup.Message(i18n.Str(i18n.MsgInvalidNameTitle), i18n.Str(i18n.MsgInvalidNameBody))
return iup.DEFAULT
}
+64 -12
View File
@@ -1,6 +1,39 @@
package main
import "github.com/gen2brain/iup-go/iup"
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 {
@@ -10,6 +43,12 @@ func setActive() {
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")
@@ -27,17 +66,17 @@ func setActive() {
var tip string
switch {
case count == 0 && opts.OutDir == "":
active, tip = "NO", "Add files and set output directory"
active, tip = "NO", i18n.Lng(i18n.StatusNeedFilesAndDir)
case count == 0:
active, tip = "NO", "Add files"
active, tip = "NO", i18n.Lng(i18n.StatusNeedFiles)
case opts.OutDir == "":
active, tip = "NO", "Set output directory"
active, tip = "NO", i18n.Lng(i18n.StatusNeedOutDir)
}
enabledTip := map[string]string{
"Thumbnail": "Extract cover thumbnails",
"Cover": "Extract covers",
"Convert": "Convert files to the selected format",
"Thumbnail": i18n.Lng(i18n.TipThumbnail),
"Cover": i18n.Lng(i18n.TipCover),
"Convert": i18n.Lng(i18n.TipConvert),
}
for _, h := range []string{"Thumbnail", "Cover", "Convert"} {
@@ -59,7 +98,15 @@ func setActive() {
}
canLossless := opts.Format == "webp" || opts.Format == "avif" || opts.Format == "jxl"
losslessOn := canLossless && opts.Lossless
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")
@@ -75,6 +122,11 @@ func setActive() {
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 {
@@ -113,12 +165,12 @@ func setBusy(on bool) {
iup.GetHandle(h).SetAttribute("ACTIVE", "NO")
}
convert.SetAttribute("ACTIVE", "YES")
convert.SetAttribute("TITLE", "Cancel")
convert.SetAttribute("TIP", "Cancel the running operation (or press Esc)")
convert.SetAttribute("TITLE", i18n.Lng(i18n.BtnCancel))
convert.SetAttribute("TIP", i18n.Lng(i18n.TipCancel))
} else {
activeConv = nil
convert.SetAttribute("TITLE", "&Convert")
convert.SetAttribute("TIP", "Convert files to the selected format")
convert.SetAttribute("TITLE", i18n.Lng(i18n.BtnConvert))
convert.SetAttribute("TIP", i18n.Lng(i18n.TipConvert))
setActive() // restores the conditional buttons and option boxes
}
}
+263 -118
View File
@@ -4,11 +4,13 @@ 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"
)
@@ -20,16 +22,16 @@ func setEffort(format string) {
switch format {
case "webp":
val.SetAttributes("MIN=0, MAX=6, SHOWTICKS=7, VALUE=4")
val.SetAttribute("TIP", "WEBP method, higher is better/slower (0-6, default 4)")
name = "Method"
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", "AVIF speed, higher is faster/worse (0-10, default 10)")
name = "Speed"
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", "JXL effort, higher is better/slower (1-10, default 7)")
name = "Effort"
val.SetAttribute("TIP", i18n.Lng(i18n.TipEffortJxl))
name = i18n.Str(i18n.EffortEffort)
default:
return
}
@@ -55,10 +57,10 @@ func list() iup.Ihandle {
"EXPAND": "YES",
"NUMCOL": "3",
"NUMLIN": "0",
"TITLE1": "Title",
"TITLE2": "Type",
"TITLE3": "Size (MiB)",
"WIDTH1": "150",
"TITLE1": i18n.Lng(i18n.ColTitle),
"TITLE2": i18n.Lng(i18n.ColType),
"TITLE3": i18n.Lng(i18n.ColSize),
"WIDTH1": "250",
"WIDTH2": "50",
"WIDTH3": "100",
"ALIGNMENT2": "ACENTER",
@@ -102,21 +104,7 @@ func list() iup.Ihandle {
return iup.DEFAULT
}
wasEmpty := len(files) == 0
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
addFiles(fs)
return iup.DEFAULT
}))
@@ -124,34 +112,146 @@ func list() iup.Ihandle {
return iup.Vbox(t)
}
func previewSize() (int, int) {
var width, height int
sp := strings.Split(iup.GetHandle("Preview").GetAttribute("RASTERSIZE"), "x")
if len(sp) == 2 {
width, _ = strconv.Atoi(sp[0])
height, _ = strconv.Atoi(sp[1])
}
return width, height
}
func preview() iup.Ihandle {
return iup.Frame(
iup.Vbox(
iup.Label("").SetAttributes("EXPAND=YES, ALIGNMENT=ACENTER, MINSIZE=400x, IMAGE=cover").SetHandle("Preview").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
img := p.(cbconvert.Image)
iup.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")
ih.SetAttribute("IMAGE", "cover")
hasCover = true
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
} else {
ih.SetAttribute("IMAGE", "logo")
iup.Destroy(iup.GetHandle("cover"))
hasCover = false
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
sp := strings.Split(s, ": ")
@@ -160,26 +260,65 @@ func preview() iup.Ihandle {
}
}
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.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"),
),
)
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(" 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"`).
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()
@@ -189,12 +328,12 @@ func tabInput() iup.Ihandle {
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Label("Minimum Size (MiB):"),
iup.Label(i18n.Lng(i18n.LblMinSize)),
iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size").
SetAttributes(`TIP="Process only files larger than minimum size"`),
SetAttribute("TIP", i18n.Lng(i18n.TipSize)),
),
iup.Vbox(
iup.Label("Document DPI:"),
iup.Label(i18n.Lng(i18n.LblDPI)),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"EDITBOX": "YES",
@@ -206,7 +345,7 @@ func tabInput() iup.Ihandle {
"4": "600",
"5": "1200",
}).SetHandle("DPI").
SetAttribute("TIP", "Resolution for rendering documents (PDF, EPUB, etc.); Default is 300"),
SetAttribute("TIP", i18n.Lng(i18n.TipDPI)),
),
).SetAttributes("NGAP=10"),
).SetHandle("VboxInput")
@@ -216,32 +355,32 @@ func tabOutput() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Vbox(
iup.Label("Output Directory:"),
iup.Label(i18n.Lng(i18n.LblOutDir)),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutDir").
SetAttribute("TIP", "Directory where converted files are written (required)").
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("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
iup.Button(i18n.Lng(i18n.BtnBrowse)).SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputDirectory)),
),
iup.Vbox(
iup.Label("Add Suffix to Output File:"),
iup.Label(i18n.Lng(i18n.LblSuffix)),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("Suffix").
SetAttribute("TIP", "Add suffix to filename, i.e. filename_suffix.cbz"),
SetAttribute("TIP", i18n.Lng(i18n.TipSuffix)),
),
iup.Vbox(
iup.Label("Archive Format:"),
iup.Label(i18n.Lng(i18n.LblArchive)),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "ZIP",
"2": "TAR",
}).SetHandle("Archive").
SetAttribute("TIP", "Output container: ZIP (.cbz) or uncompressed TAR (.cbt)").
SetAttribute("TIP", i18n.Lng(i18n.TipArchive)).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
@@ -249,7 +388,7 @@ func tabOutput() iup.Ihandle {
})),
),
iup.Vbox(
iup.Label("Compression:"),
iup.Label(i18n.Lng(i18n.LblCompression)),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
@@ -265,14 +404,14 @@ func tabOutput() iup.Ihandle {
"10": "8",
"11": "9",
}).SetHandle("ZipLevel").
SetAttribute("TIP", "ZIP compression: Store disables it, 1 is fastest, 9 is smallest"),
SetAttribute("TIP", i18n.Lng(i18n.TipZipLevel)),
).SetHandle("VboxZipLevel"),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Toggle(" Combine into single file").SetHandle("Combine").
SetAttributes(`TIP="Merge all listed files into one archive"`).
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()
@@ -280,11 +419,11 @@ func tabOutput() iup.Ihandle {
})),
),
iup.Vbox(
iup.Label("Output File:"),
iup.Label(i18n.Lng(i18n.LblOutFile)),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutFile").
SetAttribute("TIP", "Combined file name (default: first input + -combined)"),
SetAttribute("TIP", i18n.Lng(i18n.TipOutFile)),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
iup.Button(i18n.Lng(i18n.BtnBrowse)).SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputFile)),
).SetHandle("VboxOutFile"),
).SetAttributes("NGAP=10"),
@@ -295,7 +434,7 @@ func tabImage() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Vbox(
iup.Label("Format:"),
iup.Label(i18n.Lng(i18n.LblFormat)),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
@@ -307,7 +446,7 @@ func tabImage() iup.Ihandle {
"6": "AVIF",
"7": "JXL",
}).SetHandle("Format").
SetAttribute("TIP", "Output image format for the converted pages").
SetAttribute("TIP", i18n.Lng(i18n.TipFormat)).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setEffort(strings.ToLower(ih.GetAttribute("VALUESTRING")))
setActive()
@@ -317,10 +456,11 @@ func tabImage() iup.Ihandle {
})),
),
iup.Vbox(
iup.Label("Size:"),
iup.Label(i18n.Lng(i18n.LblSize)),
iup.Hbox(
iup.Text().SetAttributes(`CUEBANNER="width", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Width").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
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"))
@@ -338,8 +478,9 @@ func tabImage() iup.Ihandle {
iup.Space().SetAttribute("SIZE", "2"),
iup.Label("x"),
iup.Space().SetAttribute("SIZE", "2"),
iup.Text().SetAttributes(`CUEBANNER="height", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Height").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
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"))
@@ -357,15 +498,17 @@ func tabImage() iup.Ihandle {
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
),
iup.Vbox(
iup.Toggle(" Best Fit").SetHandle("Fit").
SetAttributes(`TIP="Best fit for required width and height"`),
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("Resize Filter:"),
iup.Label(i18n.Lng(i18n.LblFilter)),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "3",
"TIP": "Linear is the bilinear filter, smooth and reasonably fast",
"TIP": i18n.Lng(i18n.FilterLinear),
"1": "NearestNeighbor",
"2": "Box",
"3": "Linear",
@@ -380,11 +523,11 @@ func tabImage() iup.Ihandle {
iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Quality: "),
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", "Quality affects JPEG, WEBP, AVIF and JXL").
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"))
@@ -402,9 +545,9 @@ func tabImage() iup.Ihandle {
})),
).SetHandle("VboxQuality"),
iup.Vbox(
iup.Label("Effort:").SetHandle("LabelEffort"),
iup.Label(i18n.Lng(i18n.LblEffort)).SetHandle("LabelEffort"),
iup.Val("").SetAttributes(`MIN=0, MAX=10, VALUE=0, SHOWTICKS=11`).SetHandle("Effort").
SetAttribute("TIP", "Encoder speed/effort (WEBP, AVIF, JXL)").
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"))
@@ -422,9 +565,10 @@ func tabImage() iup.Ihandle {
})),
).SetHandle("VboxEffort"),
iup.Vbox(
iup.Toggle(" Lossless").SetHandle("Lossless").
SetAttributes(`TIP="Lossless compression (WEBP, AVIF, JXL), ignores quality"`).
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()
@@ -432,8 +576,8 @@ func tabImage() iup.Ihandle {
})),
),
iup.Vbox(
iup.Toggle(" Grayscale").SetHandle("Grayscale").
SetAttributes(`TIP="Convert images to grayscale (monochromatic)"`).
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()
@@ -448,11 +592,11 @@ func tabTransform() iup.Ihandle {
return iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Brightness: "),
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").
SetAttributes(`TIP="Adjust the brightness of the images"`).
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"))
@@ -471,11 +615,11 @@ func tabTransform() iup.Ihandle {
),
iup.Vbox(
iup.Hbox(
iup.Label("Contrast: "),
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").
SetAttributes(`TIP="Adjust the contrast of the images"`).
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"))
@@ -493,7 +637,7 @@ func tabTransform() iup.Ihandle {
})),
),
iup.Vbox(
iup.Label("Rotate:"),
iup.Label(i18n.Lng(i18n.LblRotate)),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
@@ -502,7 +646,7 @@ func tabTransform() iup.Ihandle {
"3": "180",
"4": "270",
}).SetHandle("Rotate").
SetAttribute("TIP", "Rotate every page clockwise by the given angle in degrees").
SetAttribute("TIP", i18n.Lng(i18n.TipRotate)).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
@@ -514,41 +658,41 @@ func tabTransform() iup.Ihandle {
func tabs() iup.Ihandle {
return iup.Tabs(
tabInput().SetAttributes("TABTITLE=Input, NMARGIN=10x10"),
tabOutput().SetAttributes("TABTITLE=Output, NMARGIN=10x10"),
tabImage().SetAttributes("TABTITLE=Image, NMARGIN=10x10"),
tabTransform().SetAttributes("TABTITLE=Transform, NMARGIN=10x10"),
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("Add &Files...").SetHandle("AddFiles").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
addFiles := iup.Button(i18n.Lng(i18n.BtnAddFiles)).SetHandle("AddFiles").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddFiles))
addDir := iup.Button("Add &Dir...").SetHandle("AddDir").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
addDir := iup.Button(i18n.Lng(i18n.BtnAddDir)).SetHandle("AddDir").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddDir))
remove := iup.Button("Remove").SetHandle("Remove").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
remove := iup.Button(i18n.Lng(i18n.BtnRemove)).SetHandle("Remove").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemove))
removeAll := iup.Button("Remove All").SetHandle("RemoveAll").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
removeAll := iup.Button(i18n.Lng(i18n.BtnRemoveAll)).SetHandle("RemoveAll").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemoveAll))
thumbnail := iup.Button("Thumbnail").SetHandle("Thumbnail").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
thumbnail := iup.Button(i18n.Lng(i18n.BtnThumbnail)).SetHandle("Thumbnail").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onThumbnail))
cover := iup.Button("Cover").SetHandle("Cover").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
cover := iup.Button(i18n.Lng(i18n.BtnCover)).SetHandle("Cover").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onCover))
convert := iup.Button("&Convert").SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
convert := iup.Button(i18n.Lng(i18n.BtnConvert)).SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onConvert))
reset := iup.Button("Reset").SetHandle("Reset").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Restore all settings to their defaults").
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("Save").SetHandle("Save").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Save current settings to a profile").
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("Command").SetHandle("Command").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Show the equivalent command line").
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", "Select a settings 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")
@@ -572,7 +716,7 @@ func buttons() iup.Ihandle {
),
iup.Fill(),
iup.Vbox(
iup.Label("Profile:"),
iup.Label(i18n.Lng(i18n.LblProfile)),
profile,
reset,
save,
@@ -584,6 +728,7 @@ func buttons() iup.Ihandle {
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"),
@@ -597,7 +742,7 @@ func status() iup.Ihandle {
ih.SetAttributes("VALUE=0, VISIBLE=YES")
ih.SetAttribute("MAX", conv.Ncontents)
iup.GetHandle("LabelStatus1").SetAttribute("TITLE", fmt.Sprintf("File %d of %d", conv.CurrFile, conv.Nfiles))
iup.GetHandle("LabelStatus1").SetAttribute("TITLE", fmt.Sprintf(i18n.Str(i18n.StatusFileOf), conv.CurrFile, conv.Nfiles))
iup.GetHandle("LabelStatus1").SetAttributes("VISIBLE=YES")
iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES")
+7 -9
View File
@@ -3,8 +3,9 @@ module github.com/gen2brain/cbconvert/cmd/cbconvert
go 1.26
require (
github.com/gen2brain/cbconvert v1.0.5-0.20260623161611-a5817c3ba5de
github.com/gen2brain/cbconvert v1.2.0
github.com/schollz/progressbar/v3 v3.19.0
golang.org/x/term v0.44.0
)
require (
@@ -23,15 +24,15 @@ require (
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/go-fitz v1.24.15 // indirect
github.com/gen2brain/jpegli v0.4.0 // indirect
github.com/gen2brain/jpegxl v0.5.1 // indirect
github.com/gen2brain/go-fitz v1.28.1 // indirect
github.com/gen2brain/jpegli v0.4.1 // indirect
github.com/gen2brain/jpegn v0.4.2 // indirect
github.com/gen2brain/jpegxl v0.5.2 // indirect
github.com/gen2brain/webp v0.6.1 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 // 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/jupiterrider/ffi v0.7.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
@@ -52,9 +53,6 @@ require (
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/gen2brain/cbconvert => ../..
+12 -10
View File
@@ -44,12 +44,16 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/gen2brain/jpegli v0.4.0 h1:TnmAO8EcekS/XCboyLPjP6cqz025tkhdaFkiYNd3wH8=
github.com/gen2brain/jpegli v0.4.0/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE=
github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/cbconvert v1.2.0 h1:eUs5MPzft+Lo9ADMYYQ5AzydO54Pv0YYUTm2lPo3XDo=
github.com/gen2brain/cbconvert v1.2.0/go.mod h1:/6QtSgf47bDBmtkidNoYwNMHaebsDhPTKnDMmyNUd9E=
github.com/gen2brain/go-fitz v1.28.1 h1:ToEYb2vN4ByaL2VmRNGk92Sa1UAkCn8bsObpA3WkQ48=
github.com/gen2brain/go-fitz v1.28.1/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs=
github.com/gen2brain/jpegli v0.4.1 h1:qc11IQU0jTYFltroulT4MXmbu9YRftqHV0YBZ0Bqz5o=
github.com/gen2brain/jpegli v0.4.1/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegn v0.4.2 h1:sxy2yolV1eNA02uYtnqBFm4EIC3ETnars98aG7Dc4LM=
github.com/gen2brain/jpegn v0.4.2/go.mod h1:YvcVOmVPSAsefH6yn9HBW3uY0EHlZwCMoiJXoAWfgL0=
github.com/gen2brain/jpegxl v0.5.2 h1:1ou9YRziU8PbpkfFJIyxrNjYM+WaMl2n9LloABxkKsU=
github.com/gen2brain/jpegxl v0.5.2/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@@ -59,8 +63,8 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 h1:q100v18oYIaE4p5myLFqDyfXTOxivzwGyLjGCEfv8x4=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/golang/geo v0.0.0-20260625163123-7c0e84413537 h1:KeIaDS/+VEy/bhDYjG3Z78dOyLAU4HXcVxmd0WYHJTE=
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=
@@ -69,8 +73,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+339 -68
View File
@@ -2,17 +2,22 @@ package main
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"syscall"
"github.com/gen2brain/cbconvert"
pb "github.com/schollz/progressbar/v3"
"golang.org/x/term"
)
var appVersion string
@@ -78,39 +83,55 @@ func main() {
os.Exit(1)
}
interactive := !opts.Quiet && term.IsTerminal(int(os.Stderr.Fd()))
var bar *pb.ProgressBar
if opts.Cover || opts.Thumbnail || opts.Meta {
if !opts.Quiet {
bar = pb.NewOptions(conv.Nfiles,
newBar := func(max int, description string) {
bar = pb.NewOptions(max,
pb.OptionShowCount(),
pb.OptionClearOnFinish(),
pb.OptionUseANSICodes(true),
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() {
if !opts.Quiet {
bar = pb.NewOptions(conv.Ncontents,
pb.OptionShowCount(),
pb.OptionClearOnFinish(),
pb.OptionUseANSICodes(true),
pb.OptionSetDescription(fmt.Sprintf("Converting %d of %d:", conv.CurrFile, conv.Nfiles)),
pb.OptionSetPredictTime(false),
)
if interactive {
clearLine()
newBar(conv.Ncontents, fmt.Sprintf("Converting %d of %d:", conv.CurrFile, conv.Nfiles))
}
}
conv.OnProgress = func() {
if !opts.Quiet {
if bar != nil {
_ = bar.Add(1)
}
}
conv.OnCompress = func() {
if !opts.Quiet {
fmt.Fprintf(os.Stderr, "Compressing %d of %d...\r", conv.CurrFile, conv.Nfiles)
if interactive {
if bar != nil {
_ = bar.Finish()
}
fmt.Fprintf(os.Stderr, "Compressing %d of %d...", conv.CurrFile, conv.Nfiles)
}
}
@@ -120,7 +141,7 @@ func main() {
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "\r")
cleanup()
return
}
@@ -163,7 +184,7 @@ func main() {
}
}
fmt.Fprintf(os.Stderr, "\r")
cleanup()
}
// parseFlags parses command line flags.
@@ -171,60 +192,84 @@ func parseFlags() (cbconvert.Options, []string) {
opts := cbconvert.Options{}
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.IntVar(&opts.Width, "width", 0, "Image width")
convert.IntVar(&opts.Height, "height", 0, "Image height")
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
convert.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl")
convert.StringVar(&opts.Archive, "archive", "zip", "Archive format, valid values are zip, tar")
convert.IntVar(&opts.ZipLevel, "zip-level", -1, "ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default")
convert.IntVar(&opts.Quality, "quality", 75, "Image quality")
convert.IntVar(&opts.Effort, "effort", -1, "Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default")
convert.BoolVar(&opts.Lossless, "lossless", false, "Lossless compression (webp, avif, jxl), ignores quality")
convert.BoolVar(&opts.Combine, "combine", false, "Combine all inputs into a single archive")
convert.StringVar(&opts.OutFile, "outfile", "", "Output file name for --combine (default: first input + -combined)")
convert.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
convert.BoolVar(&opts.NoCover, "no-cover", false, "Do not convert the cover image")
convert.BoolVar(&opts.NoRGB, "no-rgb", false, "Do not convert images that have RGB colorspace")
convert.BoolVar(&opts.NoNonImage, "no-nonimage", false, "Remove non-image files from the archive")
convert.BoolVar(&opts.NoConvert, "no-convert", false, "Do not transform or convert images")
convert.BoolVar(&opts.Grayscale, "grayscale", false, "Convert images to grayscale (monochromatic)")
convert.IntVar(&opts.Rotate, "rotate", 0, "Rotate images, valid values are 0, 90, 180, 270")
convert.IntVar(&opts.Brightness, "brightness", 0, "Adjust the brightness of the images, must be in the range (-100, 100)")
convert.IntVar(&opts.Contrast, "contrast", 0, "Adjust the contrast of the images, must be in the range (-100, 100)")
convert.StringVar(&opts.Suffix, "suffix", "", "Add suffix to file basename")
convert.StringVar(&opts.OutDir, "outdir", ".", "Output directory")
convert.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)")
convert.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively")
convert.BoolVar(&opts.Quiet, "quiet", false, "Hide console output")
convert.StringVar(&profile, "profile", "", profileUsage)
convert.IntVar(&opts.Width, "width", base.Width, "Image width")
convert.IntVar(&opts.Height, "height", base.Height, "Image height")
convert.BoolVar(&opts.Fit, "fit", base.Fit, "Best fit for required width and height")
convert.BoolVar(&opts.NoUpscale, "no-upscale", base.NoUpscale, "Do not upscale images already smaller than the requested width/height")
convert.IntVar(&opts.DPI, "dpi", base.DPI, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the original resolution")
convert.StringVar(&opts.Format, "format", base.Format, "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl")
convert.StringVar(&opts.Archive, "archive", base.Archive, "Archive format, valid values are zip, tar")
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.IntVar(&opts.Quality, "quality", base.Quality, "Image quality")
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.Lossless, "lossless", base.Lossless, "Lossless compression (webp, avif, jxl), ignores quality")
convert.BoolVar(&opts.Combine, "combine", base.Combine, "Combine all inputs into a single archive")
convert.StringVar(&opts.OutFile, "outfile", base.OutFile, "Output file name for --combine (default: first input + -combined)")
convert.IntVar(&opts.Filter, "filter", base.Filter, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos")
convert.BoolVar(&opts.NoCover, "no-cover", base.NoCover, "Do not convert the cover image")
convert.BoolVar(&opts.NoRGB, "no-rgb", base.NoRGB, "Do not convert images that have RGB colorspace")
convert.BoolVar(&opts.NoNonImage, "no-nonimage", base.NoNonImage, "Remove non-image files from the archive")
convert.BoolVar(&opts.NoConvert, "no-convert", base.NoConvert, "Do not transform or convert images")
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.IntVar(&opts.Width, "width", 0, "Image width")
cover.IntVar(&opts.Height, "height", 0, "Image height")
cover.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
cover.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
cover.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif")
cover.IntVar(&opts.Quality, "quality", 75, "Image quality")
cover.IntVar(&opts.Effort, "effort", -1, "Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default")
cover.BoolVar(&opts.Lossless, "lossless", false, "Lossless compression (webp, avif, jxl), ignores quality")
cover.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
cover.StringVar(&opts.OutDir, "outdir", ".", "Output directory")
cover.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)")
cover.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively")
cover.BoolVar(&opts.Quiet, "quiet", false, "Hide console output")
cover.StringVar(&profile, "profile", "", profileUsage)
cover.IntVar(&opts.Width, "width", base.Width, "Image width")
cover.IntVar(&opts.Height, "height", base.Height, "Image height")
cover.BoolVar(&opts.Fit, "fit", base.Fit, "Best fit for required width and height")
cover.BoolVar(&opts.NoUpscale, "no-upscale", base.NoUpscale, "Do not upscale images already smaller than the requested width/height")
cover.IntVar(&opts.DPI, "dpi", base.DPI, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the original resolution")
cover.StringVar(&opts.Format, "format", base.Format, "Image format, valid values are jpeg, png, tiff, bmp, webp, avif")
cover.IntVar(&opts.Quality, "quality", base.Quality, "Image quality")
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.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.IntVar(&opts.Width, "width", 0, "Image width")
thumbnail.IntVar(&opts.Height, "height", 0, "Image height")
thumbnail.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
thumbnail.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
thumbnail.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
thumbnail.StringVar(&opts.OutDir, "outdir", ".", "Output directory")
thumbnail.StringVar(&opts.OutFile, "outfile", "", "Output file")
thumbnail.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)")
thumbnail.BoolVar(&opts.Recursive, "recursive", false, "Process subdirectories recursively")
thumbnail.BoolVar(&opts.Quiet, "quiet", false, "Hide console output")
thumbnail.StringVar(&profile, "profile", "", profileUsage)
thumbnail.IntVar(&opts.Width, "width", base.Width, "Image width")
thumbnail.IntVar(&opts.Height, "height", base.Height, "Image height")
thumbnail.BoolVar(&opts.Fit, "fit", base.Fit, "Best fit for required width and height")
thumbnail.BoolVar(&opts.NoUpscale, "no-upscale", base.NoUpscale, "Do not upscale images already smaller than the requested width/height")
thumbnail.IntVar(&opts.DPI, "dpi", base.DPI, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the original resolution")
thumbnail.IntVar(&opts.Filter, "filter", base.Filter, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 5=Gaussian, 6=Lanczos")
thumbnail.StringVar(&opts.OutDir, "outdir", base.OutDir, "Output directory")
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.BoolVar(&opts.Cover, "cover", false, "Print cover name")
@@ -239,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, "\nCommands:\n")
fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n")
order := []string{"width", "height", "fit", "dpi", "format", "archive", "zip-level", "quality", "effort", "lossless", "combine", "outfile", "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"}
for _, name := range order {
f := convert.Lookup(name)
@@ -247,14 +292,14 @@ func parseFlags() (cbconvert.Options, []string) {
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n")
order = []string{"width", "height", "fit", "dpi", "format", "quality", "effort", "lossless", "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 {
f := cover.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n")
order = []string{"width", "height", "fit", "dpi", "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 {
f := thumbnail.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
@@ -357,3 +402,229 @@ func lines(r io.Reader) []string {
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)
}
}
+5 -5
View File
@@ -8,9 +8,9 @@ require (
github.com/dustin/go-humanize v1.0.1
github.com/fvbommel/sortorder v1.1.0
github.com/gen2brain/avif v0.5.1
github.com/gen2brain/go-fitz v1.24.15
github.com/gen2brain/jpegli v0.4.0
github.com/gen2brain/jpegxl v0.5.1
github.com/gen2brain/go-fitz v1.28.1
github.com/gen2brain/jpegli v0.4.1
github.com/gen2brain/jpegxl v0.5.2
github.com/gen2brain/webp v0.6.1
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25
github.com/mholt/archives v0.1.5
@@ -29,10 +29,10 @@ require (
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/gen2brain/jpegn v0.4.2
github.com/go-errors/errors v1.5.1 // indirect
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 // indirect
github.com/golang/geo v0.0.0-20260625163123-7c0e84413537 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jupiterrider/ffi v0.7.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
+10 -10
View File
@@ -40,12 +40,14 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/gen2brain/avif v0.5.1 h1:LQzLsJpWyGlsa4wuZ3D57qEbCiICIK7Yidz5ZPEwzTk=
github.com/gen2brain/avif v0.5.1/go.mod h1:QgrYqdVE9y40PCfArK9VakcMIpYeDYpZmCSLkW6C1n8=
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
github.com/gen2brain/jpegli v0.4.0 h1:TnmAO8EcekS/XCboyLPjP6cqz025tkhdaFkiYNd3wH8=
github.com/gen2brain/jpegli v0.4.0/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegxl v0.5.1 h1:UuBUIkZ35DErImU3bTA6rltfV5zSgVNOA/K5a6JibfE=
github.com/gen2brain/jpegxl v0.5.1/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/go-fitz v1.28.1 h1:ToEYb2vN4ByaL2VmRNGk92Sa1UAkCn8bsObpA3WkQ48=
github.com/gen2brain/go-fitz v1.28.1/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs=
github.com/gen2brain/jpegli v0.4.1 h1:qc11IQU0jTYFltroulT4MXmbu9YRftqHV0YBZ0Bqz5o=
github.com/gen2brain/jpegli v0.4.1/go.mod h1:zJ++s4symmKCN1CLkrY0dGXTY3s0NWbd94Rz9KLdCzk=
github.com/gen2brain/jpegn v0.4.2 h1:sxy2yolV1eNA02uYtnqBFm4EIC3ETnars98aG7Dc4LM=
github.com/gen2brain/jpegn v0.4.2/go.mod h1:YvcVOmVPSAsefH6yn9HBW3uY0EHlZwCMoiJXoAWfgL0=
github.com/gen2brain/jpegxl v0.5.2 h1:1ou9YRziU8PbpkfFJIyxrNjYM+WaMl2n9LloABxkKsU=
github.com/gen2brain/jpegxl v0.5.2/go.mod h1:Wlc6lqx03RJfhiQRyHa2e+8VQwT4/qv7zSRsNv9T+yE=
github.com/gen2brain/webp v0.6.1 h1:ei7Y1SWpQcdqz3YNDNyn4y2nQanxs9WLzwW5/2DKS64=
github.com/gen2brain/webp v0.6.1/go.mod h1:iGWMaCSw7t3I/Cv9llzEKmpnR36S8lS8VL/ZVjxU0JE=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@@ -55,8 +57,8 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82 h1:q100v18oYIaE4p5myLFqDyfXTOxivzwGyLjGCEfv8x4=
github.com/golang/geo v0.0.0-20260622181633-f80708c60e82/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/golang/geo v0.0.0-20260625163123-7c0e84413537 h1:KeIaDS/+VEy/bhDYjG3Z78dOyLAU4HXcVxmd0WYHJTE=
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=
@@ -65,8 +67,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/jupiterrider/ffi v0.7.0 h1:RKsl6Ascal+3kyAqR5Qcbp83LceQMLc1VZbPfHWoNzs=
github.com/jupiterrider/ffi v0.7.0/go.mod h1:9dauhpOfNqrqk28fxuu0kkdeFtT9Qr4vbfigiuIXN7c=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+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=