mirror of
https://github.com/gen2brain/cbconvert
synced 2026-07-24 12:25:38 +02:00
Compare commits
5
Commits
b8d82e920c
...
c4f236bdf9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4f236bdf9 | ||
|
|
23ac71ee0c | ||
|
|
3f0ae41456 | ||
|
|
04a047aa2e | ||
|
|
1284b9ded7 |
+92
-11
@@ -23,6 +23,8 @@ type Options struct {
|
|||||||
Format string
|
Format string
|
||||||
// Archive format, valid values are zip, tar
|
// Archive format, valid values are zip, tar
|
||||||
Archive string
|
Archive string
|
||||||
|
// ZIP compression level: -1 default, 0 store (no compression), 1-9 deflate (1 fastest, 9 smallest)
|
||||||
|
ZipLevel int
|
||||||
// JPEG image quality
|
// JPEG image quality
|
||||||
Quality int
|
Quality int
|
||||||
// Encoder speed/effort, format-specific: webp method 0-6, avif speed 0-10, jxl effort 1-10; -1 uses the format default
|
// Encoder speed/effort, format-specific: webp method 0-6, avif speed 0-10, jxl effort 1-10; -1 uses the format default
|
||||||
@@ -93,6 +95,8 @@ type Converter struct {
|
|||||||
Workdir string
|
Workdir string
|
||||||
// Page name prefix, set per input when combining
|
// Page name prefix, set per input when combining
|
||||||
prefix string
|
prefix string
|
||||||
|
// Input root for the current file, used to build recursive output paths
|
||||||
|
root string
|
||||||
// Number of files
|
// Number of files
|
||||||
Nfiles int
|
Nfiles int
|
||||||
// Index of the current file
|
// Index of the current file
|
||||||
@@ -115,6 +119,7 @@ type Converter struct {
|
|||||||
type File struct {
|
type File struct {
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
|
Root string
|
||||||
Stat os.FileInfo
|
Stat os.FileInfo
|
||||||
SizeHuman string
|
SizeHuman string
|
||||||
}
|
}
|
||||||
@@ -134,6 +139,7 @@ func NewOptions() Options {
|
|||||||
o.Archive = "zip"
|
o.Archive = "zip"
|
||||||
o.Quality = 75
|
o.Quality = 75
|
||||||
o.Effort = -1
|
o.Effort = -1
|
||||||
|
o.ZipLevel = -1
|
||||||
o.Filter = 2
|
o.Filter = 2
|
||||||
|
|
||||||
return o
|
return o
|
||||||
@@ -147,6 +153,56 @@ func New(o Options) *Converter {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Args returns the non-default options as cbconvert convert command-line flags.
|
||||||
|
func (o Options) Args() []string {
|
||||||
|
def := NewOptions()
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
|
||||||
|
str := func(name, val, dflt string) {
|
||||||
|
if val != dflt {
|
||||||
|
args = append(args, "--"+name, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
num := func(name string, val, dflt int) {
|
||||||
|
if val != dflt {
|
||||||
|
args = append(args, "--"+name, strconv.Itoa(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flag := func(name string, val bool) {
|
||||||
|
if val {
|
||||||
|
args = append(args, "--"+name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
num("width", o.Width, def.Width)
|
||||||
|
num("height", o.Height, def.Height)
|
||||||
|
flag("fit", o.Fit)
|
||||||
|
str("format", o.Format, def.Format)
|
||||||
|
str("archive", o.Archive, def.Archive)
|
||||||
|
num("zip-level", o.ZipLevel, def.ZipLevel)
|
||||||
|
num("quality", o.Quality, def.Quality)
|
||||||
|
num("effort", o.Effort, def.Effort)
|
||||||
|
flag("lossless", o.Lossless)
|
||||||
|
flag("combine", o.Combine)
|
||||||
|
str("outfile", o.OutFile, def.OutFile)
|
||||||
|
num("filter", o.Filter, def.Filter)
|
||||||
|
flag("no-cover", o.NoCover)
|
||||||
|
flag("no-rgb", o.NoRGB)
|
||||||
|
flag("no-nonimage", o.NoNonImage)
|
||||||
|
flag("no-convert", o.NoConvert)
|
||||||
|
str("suffix", o.Suffix, def.Suffix)
|
||||||
|
flag("grayscale", o.Grayscale)
|
||||||
|
num("rotate", o.Rotate, def.Rotate)
|
||||||
|
num("brightness", o.Brightness, def.Brightness)
|
||||||
|
num("contrast", o.Contrast, def.Contrast)
|
||||||
|
flag("recursive", o.Recursive)
|
||||||
|
str("outdir", o.OutDir, def.OutDir)
|
||||||
|
num("size", o.Size, def.Size)
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel cancels the operation.
|
// Cancel cancels the operation.
|
||||||
func (c *Converter) Cancel() {
|
func (c *Converter) Cancel() {
|
||||||
if c.OnCancel != nil {
|
if c.OnCancel != nil {
|
||||||
@@ -157,11 +213,13 @@ func (c *Converter) Cancel() {
|
|||||||
// Files returns list of found comic files.
|
// Files returns list of found comic files.
|
||||||
func (c *Converter) Files(args []string) ([]File, error) {
|
func (c *Converter) Files(args []string) ([]File, error) {
|
||||||
var files []File
|
var files []File
|
||||||
|
var root string
|
||||||
|
|
||||||
toFile := func(fp string, f os.FileInfo) File {
|
toFile := func(fp string, f os.FileInfo) File {
|
||||||
var file File
|
var file File
|
||||||
file.Name = filepath.Base(fp)
|
file.Name = filepath.Base(fp)
|
||||||
file.Path = fp
|
file.Path = fp
|
||||||
|
file.Root = root
|
||||||
file.Stat = f
|
file.Stat = f
|
||||||
file.SizeHuman = humanize.IBytes(uint64(f.Size()))
|
file.SizeHuman = humanize.IBytes(uint64(f.Size()))
|
||||||
return file
|
return file
|
||||||
@@ -214,12 +272,14 @@ func (c *Converter) Files(args []string) ([]File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !stat.IsDir() {
|
if !stat.IsDir() {
|
||||||
|
root = filepath.Dir(path)
|
||||||
if isArchive(path) || isDocument(path) {
|
if isArchive(path) || isDocument(path) {
|
||||||
if isSize(int64(c.Opts.Size), stat.Size()) {
|
if isSize(int64(c.Opts.Size), stat.Size()) {
|
||||||
files = append(files, toFile(path, stat))
|
files = append(files, toFile(path, stat))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
root = path
|
||||||
if c.Opts.Recursive {
|
if c.Opts.Recursive {
|
||||||
if err := filepath.Walk(path, walkFiles); err != nil {
|
if err := filepath.Walk(path, walkFiles); err != nil {
|
||||||
return files, fmt.Errorf("%s: %w", arg, err)
|
return files, fmt.Errorf("%s: %w", arg, err)
|
||||||
@@ -261,9 +321,26 @@ func (c *Converter) Files(args []string) ([]File, error) {
|
|||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recursiveDir mirrors the source path under OutDir, relative to the input root.
|
||||||
|
func (c *Converter) recursiveDir(fileName string) string {
|
||||||
|
dir := filepath.Dir(fileName)
|
||||||
|
|
||||||
|
if c.root != "" {
|
||||||
|
if rel, err := filepath.Rel(c.root, dir); err == nil {
|
||||||
|
return filepath.Join(c.Opts.OutDir, rel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = strings.TrimPrefix(dir[len(filepath.VolumeName(dir)):], string(os.PathSeparator))
|
||||||
|
|
||||||
|
return filepath.Join(c.Opts.OutDir, dir)
|
||||||
|
}
|
||||||
|
|
||||||
// Cover extracts cover.
|
// Cover extracts cover.
|
||||||
func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error {
|
func (c *Converter) Cover(file File) error {
|
||||||
c.CurrFile++
|
c.CurrFile++
|
||||||
|
c.root = file.Root
|
||||||
|
fileName, fileInfo := file.Path, file.Stat
|
||||||
|
|
||||||
cover, err := c.coverImage(fileName, fileInfo)
|
cover, err := c.coverImage(fileName, fileInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -285,13 +362,12 @@ func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error {
|
|||||||
|
|
||||||
var fName string
|
var fName string
|
||||||
if c.Opts.Recursive {
|
if c.Opts.Recursive {
|
||||||
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
|
outDir := c.recursiveDir(fileName)
|
||||||
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
|
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", fileName, err)
|
return fmt.Errorf("%s: %w", fileName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%s.%s", baseNoExt(fileName), ext))
|
fName = filepath.Join(outDir, fmt.Sprintf("%s.%s", baseNoExt(fileName), ext))
|
||||||
} else {
|
} else {
|
||||||
fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s.%s", baseNoExt(fileName), ext))
|
fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s.%s", baseNoExt(fileName), ext))
|
||||||
}
|
}
|
||||||
@@ -310,8 +386,10 @@ func (c *Converter) Cover(fileName string, fileInfo os.FileInfo) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Thumbnail extracts thumbnail.
|
// Thumbnail extracts thumbnail.
|
||||||
func (c *Converter) Thumbnail(fileName string, fileInfo os.FileInfo) error {
|
func (c *Converter) Thumbnail(file File) error {
|
||||||
c.CurrFile++
|
c.CurrFile++
|
||||||
|
c.root = file.Root
|
||||||
|
fileName, fileInfo := file.Path, file.Stat
|
||||||
|
|
||||||
cover, err := c.coverImage(fileName, fileInfo)
|
cover, err := c.coverImage(fileName, fileInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -352,13 +430,12 @@ func (c *Converter) Thumbnail(fileName string, fileInfo os.FileInfo) error {
|
|||||||
fURI = "file://" + fileName
|
fURI = "file://" + fileName
|
||||||
|
|
||||||
if c.Opts.Recursive {
|
if c.Opts.Recursive {
|
||||||
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
|
outDir := c.recursiveDir(fileName)
|
||||||
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
|
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", fileName, err)
|
return fmt.Errorf("%s: %w", fileName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%x.png", md5.Sum([]byte(fURI))))
|
fName = filepath.Join(outDir, fmt.Sprintf("%x.png", md5.Sum([]byte(fURI))))
|
||||||
} else {
|
} else {
|
||||||
fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%x.png", md5.Sum([]byte(fURI))))
|
fName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%x.png", md5.Sum([]byte(fURI))))
|
||||||
}
|
}
|
||||||
@@ -496,8 +573,10 @@ func (c *Converter) Preview(fileName string, fileInfo os.FileInfo, width, height
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert converts a comic book.
|
// Convert converts a comic book.
|
||||||
func (c *Converter) Convert(fileName string, fileInfo os.FileInfo) error {
|
func (c *Converter) Convert(file File) error {
|
||||||
c.CurrFile++
|
c.CurrFile++
|
||||||
|
c.root = file.Root
|
||||||
|
fileName, fileInfo := file.Path, file.Stat
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -541,6 +620,8 @@ func (c *Converter) Combine(files []File) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.root = ""
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|||||||
+17
-8
@@ -3,6 +3,7 @@ package cbconvert
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"compress/flate"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -33,13 +34,12 @@ func (c *Converter) archiveSaveZip(fileName string) error {
|
|||||||
|
|
||||||
var zipName string
|
var zipName string
|
||||||
if c.Opts.Recursive {
|
if c.Opts.Recursive {
|
||||||
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
|
outDir := c.recursiveDir(fileName)
|
||||||
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
|
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("archiveSaveZip: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
zipName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix))
|
zipName = filepath.Join(outDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix))
|
||||||
} else {
|
} else {
|
||||||
zipName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix))
|
zipName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbz", baseNoExt(fileName), c.Opts.Suffix))
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,13 @@ func (c *Converter) archiveSaveZip(fileName string) error {
|
|||||||
|
|
||||||
z := zip.NewWriter(zipFile)
|
z := zip.NewWriter(zipFile)
|
||||||
|
|
||||||
|
if c.Opts.ZipLevel >= 1 {
|
||||||
|
level := c.Opts.ZipLevel
|
||||||
|
z.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
|
||||||
|
return flate.NewWriter(out, level)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
files, err := os.ReadDir(c.Workdir)
|
files, err := os.ReadDir(c.Workdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archiveSaveZip: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
@@ -73,6 +80,9 @@ func (c *Converter) archiveSaveZip(fileName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
zipInfo.Method = zip.Deflate
|
zipInfo.Method = zip.Deflate
|
||||||
|
if c.Opts.ZipLevel == 0 {
|
||||||
|
zipInfo.Method = zip.Store
|
||||||
|
}
|
||||||
w, err := z.CreateHeader(zipInfo)
|
w, err := z.CreateHeader(zipInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("archiveSaveZip: %w", err)
|
return fmt.Errorf("archiveSaveZip: %w", err)
|
||||||
@@ -108,13 +118,12 @@ func (c *Converter) archiveSaveTar(fileName string) error {
|
|||||||
|
|
||||||
var tarName string
|
var tarName string
|
||||||
if c.Opts.Recursive {
|
if c.Opts.Recursive {
|
||||||
fDir := strings.Split(filepath.Dir(fileName), string(os.PathSeparator))[1:]
|
outDir := c.recursiveDir(fileName)
|
||||||
err := os.MkdirAll(filepath.Join(c.Opts.OutDir, filepath.Join(fDir...)), 0755)
|
if err := os.MkdirAll(outDir, 0755); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("archiveSaveTar: %w", err)
|
return fmt.Errorf("archiveSaveTar: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tarName = filepath.Join(c.Opts.OutDir, filepath.Join(fDir...), fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix))
|
tarName = filepath.Join(outDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix))
|
||||||
} else {
|
} else {
|
||||||
tarName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix))
|
tarName = filepath.Join(c.Opts.OutDir, fmt.Sprintf("%s%s.cbt", baseNoExt(fileName), c.Opts.Suffix))
|
||||||
}
|
}
|
||||||
|
|||||||
+595
-3
@@ -1,9 +1,12 @@
|
|||||||
package cbconvert
|
package cbconvert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +32,7 @@ func TestConvert(t *testing.T) {
|
|||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
conv.Opts.Suffix = fmt.Sprintf("_%s%s", format, filepath.Ext(file.Path))
|
conv.Opts.Suffix = fmt.Sprintf("_%s%s", format, filepath.Ext(file.Path))
|
||||||
|
|
||||||
err = conv.Convert(file.Path, file.Stat)
|
err = conv.Convert(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("format %s: file %s: %v", format, file.Name, err)
|
t.Errorf("format %s: file %s: %v", format, file.Name, err)
|
||||||
}
|
}
|
||||||
@@ -59,7 +62,7 @@ func TestCover(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
err = conv.Cover(file.Path, file.Stat)
|
err = conv.Cover(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
@@ -88,7 +91,7 @@ func TestThumbnail(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
err = conv.Thumbnail(file.Path, file.Stat)
|
err = conv.Thumbnail(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
@@ -99,3 +102,592 @@ func TestThumbnail(t *testing.T) {
|
|||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestArgs(t *testing.T) {
|
||||||
|
opts := NewOptions()
|
||||||
|
if got := opts.Args(); len(got) != 0 {
|
||||||
|
t.Errorf("defaults should emit no flags, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Format = "webp"
|
||||||
|
opts.Quality = 90
|
||||||
|
opts.Effort = 4
|
||||||
|
opts.Lossless = true
|
||||||
|
opts.Width = 1200
|
||||||
|
opts.Grayscale = true
|
||||||
|
opts.OutDir = "/out"
|
||||||
|
|
||||||
|
got := strings.Join(opts.Args(), " ")
|
||||||
|
want := "--width 1200 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("Args() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertResize(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
opts := NewOptions()
|
||||||
|
opts.OutDir = tmpDir
|
||||||
|
opts.Width = 100
|
||||||
|
|
||||||
|
conv := New(opts)
|
||||||
|
|
||||||
|
files, err := conv.Files([]string{"testdata/test.cbz"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if err := conv.Convert(file); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img := firstPage(t, conv, filepath.Join(tmpDir, "test.cbz"))
|
||||||
|
if got := img.Bounds().Dx(); got != 100 {
|
||||||
|
t.Errorf("resized width: got %d, want 100", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertFit(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
opts := NewOptions()
|
||||||
|
opts.OutDir = tmpDir
|
||||||
|
opts.Width = 120
|
||||||
|
opts.Height = 120
|
||||||
|
opts.Fit = true
|
||||||
|
|
||||||
|
conv := New(opts)
|
||||||
|
|
||||||
|
files, err := conv.Files([]string{"testdata/test.cbz"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if err := conv.Convert(file); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img := firstPage(t, conv, filepath.Join(tmpDir, "test.cbz"))
|
||||||
|
w, h := img.Bounds().Dx(), img.Bounds().Dy()
|
||||||
|
if w > 120 || h > 120 {
|
||||||
|
t.Errorf("fit exceeded bounds: got %dx%d, want <= 120x120", w, h)
|
||||||
|
}
|
||||||
|
if w != 120 && h != 120 {
|
||||||
|
t.Errorf("fit did not touch a bound: got %dx%d, want one side == 120", w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertTar(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
opts := NewOptions()
|
||||||
|
opts.OutDir = tmpDir
|
||||||
|
opts.Archive = "tar"
|
||||||
|
|
||||||
|
conv := New(opts)
|
||||||
|
|
||||||
|
files, err := conv.Files([]string{"testdata/test.cbz"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if err := conv.Convert(file); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := filepath.Join(tmpDir, "test.cbt")
|
||||||
|
list, err := conv.archiveList(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read tar output: %v", err)
|
||||||
|
}
|
||||||
|
if len(list) != 2 {
|
||||||
|
t.Errorf("expected 2 pages in tar output, got %d: %v", len(list), list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZipLevel(t *testing.T) {
|
||||||
|
convertWith := func(level int) *zip.ReadCloser {
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { os.RemoveAll(tmpDir) })
|
||||||
|
|
||||||
|
opts := NewOptions()
|
||||||
|
opts.OutDir = tmpDir
|
||||||
|
opts.ZipLevel = level
|
||||||
|
opts.NoConvert = true
|
||||||
|
|
||||||
|
conv := New(opts)
|
||||||
|
|
||||||
|
files, err := conv.Files([]string{"testdata/test.cbz"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
if err := conv.Convert(file); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zr, err := zip.OpenReader(filepath.Join(tmpDir, "test.cbz"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { zr.Close() })
|
||||||
|
|
||||||
|
return zr
|
||||||
|
}
|
||||||
|
|
||||||
|
store := convertWith(0)
|
||||||
|
for _, f := range store.File {
|
||||||
|
if f.Method != zip.Store {
|
||||||
|
t.Errorf("level 0: %s stored with method %d, want Store", f.Name, f.Method)
|
||||||
|
}
|
||||||
|
if f.CompressedSize64 != f.UncompressedSize64 {
|
||||||
|
t.Errorf("level 0: %s is compressed (%d < %d)", f.Name, f.CompressedSize64, f.UncompressedSize64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deflate := convertWith(9)
|
||||||
|
for _, f := range deflate.File {
|
||||||
|
if f.Method != zip.Deflate {
|
||||||
|
t.Errorf("level 9: %s method %d, want Deflate", f.Name, f.Method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageTransforms(t *testing.T) {
|
||||||
|
conv := New(NewOptions())
|
||||||
|
|
||||||
|
f, err := os.Open("testdata/test/00.jpg")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
src, err := conv.imageDecode(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
srcW, srcH := src.Bounds().Dx(), src.Bounds().Dy()
|
||||||
|
|
||||||
|
conv.Opts.Rotate = 90
|
||||||
|
rotated := conv.imageTransform(src)
|
||||||
|
if rotated.Bounds().Dx() != srcH || rotated.Bounds().Dy() != srcW {
|
||||||
|
t.Errorf("rotate 90: got %dx%d, want %dx%d", rotated.Bounds().Dx(), rotated.Bounds().Dy(), srcH, srcW)
|
||||||
|
}
|
||||||
|
|
||||||
|
conv.Opts = NewOptions()
|
||||||
|
conv.Opts.Grayscale = true
|
||||||
|
gray := conv.imageTransform(src)
|
||||||
|
if !isGrayScale(gray) {
|
||||||
|
t.Errorf("grayscale: result is not grayscale")
|
||||||
|
}
|
||||||
|
|
||||||
|
conv.Opts = NewOptions()
|
||||||
|
conv.Opts.Brightness = 20
|
||||||
|
conv.Opts.Contrast = 20
|
||||||
|
adjusted := conv.imageTransform(src)
|
||||||
|
if adjusted.Bounds().Dx() != srcW || adjusted.Bounds().Dy() != srcH {
|
||||||
|
t.Errorf("brightness/contrast changed dimensions: got %dx%d, want %dx%d",
|
||||||
|
adjusted.Bounds().Dx(), adjusted.Bounds().Dy(), srcW, srcH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoverName(t *testing.T) {
|
||||||
|
conv := New(NewOptions())
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
images []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"empty", nil, ""},
|
||||||
|
{"natural sort", []string{"10.jpg", "2.jpg", "1.jpg"}, "1.jpg"},
|
||||||
|
{"cover prefix wins", []string{"01.jpg", "cover.jpg", "02.jpg"}, "cover.jpg"},
|
||||||
|
{"front prefix wins", []string{"01.jpg", "front.png", "00.jpg"}, "front.png"},
|
||||||
|
{"cover suffix wins", []string{"01.jpg", "page_cover.jpg"}, "page_cover.jpg"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := conv.coverName(tt.images); got != tt.want {
|
||||||
|
t.Errorf("coverName(%v) = %q, want %q", tt.images, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoverDirectory(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
opts := NewOptions()
|
||||||
|
opts.OutDir = tmpDir
|
||||||
|
|
||||||
|
conv := New(opts)
|
||||||
|
|
||||||
|
files, err := conv.Files([]string{"testdata/test"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(files) != 1 {
|
||||||
|
t.Fatalf("expected 1 directory file, got %d", len(files))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if err := conv.Cover(file); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(tmpDir, "test.jpg")); err != nil {
|
||||||
|
t.Errorf("directory cover not written: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"testdata/test.cbz", "ZIP"},
|
||||||
|
{"testdata/test.cbr", "RAR"},
|
||||||
|
{"testdata/test.cb7", "7Z"},
|
||||||
|
{"testdata/test.cbt", "TAR"},
|
||||||
|
{"testdata/test.pdf", "PDF"},
|
||||||
|
{"testdata/test", "DIR"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.path, func(t *testing.T) {
|
||||||
|
if got := FileType(tt.path); got != tt.want {
|
||||||
|
t.Errorf("FileType(%q) = %q, want %q", tt.path, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCombine(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
opts := NewOptions()
|
||||||
|
opts.OutDir = tmpDir
|
||||||
|
opts.OutFile = "merged"
|
||||||
|
|
||||||
|
conv := New(opts)
|
||||||
|
|
||||||
|
files, err := conv.Files([]string{"testdata/test.cbz", "testdata/test.cbt"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(files) != 2 {
|
||||||
|
t.Fatalf("expected 2 input files, got %d", len(files))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conv.Combine(files); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zr, err := zip.OpenReader(filepath.Join(tmpDir, "merged.cbz"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open combined archive: %v", err)
|
||||||
|
}
|
||||||
|
defer zr.Close()
|
||||||
|
|
||||||
|
var names []string
|
||||||
|
for _, f := range zr.File {
|
||||||
|
names = append(names, f.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(names) != 4 {
|
||||||
|
t.Fatalf("expected 4 pages in combined archive, got %d: %v", len(names), names)
|
||||||
|
}
|
||||||
|
|
||||||
|
// each input is prefixed so identically named pages do not collide
|
||||||
|
var first, second int
|
||||||
|
for _, n := range names {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(n, "0001_"):
|
||||||
|
first++
|
||||||
|
case strings.HasPrefix(n, "0002_"):
|
||||||
|
second++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if first != 2 || second != 2 {
|
||||||
|
t.Errorf("expected 2 pages from each input, got 0001_=%d 0002_=%d: %v", first, second, names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubfolders(t *testing.T) {
|
||||||
|
page0, err := os.ReadFile("testdata/test/00.jpg")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
page1, err := os.ReadFile("testdata/test/01.jpg")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inDir, err := os.MkdirTemp(os.TempDir(), "cbc-in")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(inDir)
|
||||||
|
|
||||||
|
src := filepath.Join(inDir, "chapters.cbz")
|
||||||
|
buildZip(t, src, []zipEntry{
|
||||||
|
{"chapter1/00.jpg", page0},
|
||||||
|
{"chapter1/01.jpg", page1},
|
||||||
|
{"chapter2/00.jpg", page0},
|
||||||
|
{"chapter2/01.jpg", page1},
|
||||||
|
})
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc-out")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
opts := NewOptions()
|
||||||
|
opts.OutDir = tmpDir
|
||||||
|
|
||||||
|
conv := New(opts)
|
||||||
|
|
||||||
|
files, err := conv.Files([]string{src})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if err := conv.Convert(file); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zr, err := zip.OpenReader(filepath.Join(tmpDir, "chapters.cbz"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open output archive: %v", err)
|
||||||
|
}
|
||||||
|
defer zr.Close()
|
||||||
|
|
||||||
|
// without subfolder preservation chapter2/00 overwrites chapter1/00 and only 2 pages survive
|
||||||
|
if len(zr.File) != 4 {
|
||||||
|
var names []string
|
||||||
|
for _, f := range zr.File {
|
||||||
|
names = append(names, f.Name)
|
||||||
|
}
|
||||||
|
t.Fatalf("expected 4 pages from numbered subfolders, got %d: %v", len(zr.File), names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMeta(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// operate on a copy so the fixture stays intact
|
||||||
|
data, err := os.ReadFile("testdata/test.cbz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
archive := filepath.Join(tmpDir, "meta.cbz")
|
||||||
|
if err := os.WriteFile(archive, data, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conv := New(NewOptions())
|
||||||
|
|
||||||
|
conv.Opts = NewOptions()
|
||||||
|
conv.Opts.CommentBody = "hello world"
|
||||||
|
if _, err := conv.Meta(archive); err != nil {
|
||||||
|
t.Fatalf("set comment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conv.Opts = NewOptions()
|
||||||
|
conv.Opts.Comment = true
|
||||||
|
got, err := conv.Meta(archive)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get comment: %v", err)
|
||||||
|
}
|
||||||
|
if got != "hello world" {
|
||||||
|
t.Errorf("comment roundtrip: got %q, want %q", got, "hello world")
|
||||||
|
}
|
||||||
|
|
||||||
|
extra := filepath.Join(tmpDir, "ComicInfo.xml")
|
||||||
|
if err := os.WriteFile(extra, []byte("<ComicInfo/>"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conv.Opts = NewOptions()
|
||||||
|
conv.Opts.FileAdd = extra
|
||||||
|
if _, err := conv.Meta(archive); err != nil {
|
||||||
|
t.Fatalf("add file: %v", err)
|
||||||
|
}
|
||||||
|
if !archiveHas(t, conv, archive, "ComicInfo.xml") {
|
||||||
|
t.Errorf("added file not found in archive")
|
||||||
|
}
|
||||||
|
|
||||||
|
conv.Opts = NewOptions()
|
||||||
|
conv.Opts.FileRemove = "ComicInfo.xml"
|
||||||
|
if _, err := conv.Meta(archive); err != nil {
|
||||||
|
t.Fatalf("remove file: %v", err)
|
||||||
|
}
|
||||||
|
if archiveHas(t, conv, archive, "ComicInfo.xml") {
|
||||||
|
t.Errorf("removed file still present in archive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecursive(t *testing.T) {
|
||||||
|
inDir, err := os.MkdirTemp(os.TempDir(), "cbc-in")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(inDir)
|
||||||
|
|
||||||
|
sub := filepath.Join(inDir, "chapter1")
|
||||||
|
if err := os.MkdirAll(sub, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := os.ReadFile("testdata/test.cbz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(sub, "test.cbz"), src, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outDir, err := os.MkdirTemp(os.TempDir(), "cbc-out")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(outDir)
|
||||||
|
|
||||||
|
opts := NewOptions()
|
||||||
|
opts.OutDir = outDir
|
||||||
|
opts.Recursive = true
|
||||||
|
|
||||||
|
conv := New(opts)
|
||||||
|
|
||||||
|
files, err := conv.Files([]string{inDir})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(files) != 1 {
|
||||||
|
t.Fatalf("expected 1 file, got %d", len(files))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if err := conv.Convert(file); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// output must mirror the input subtree relative to the input root, not the absolute path
|
||||||
|
want := filepath.Join(outDir, "chapter1", "test.cbz")
|
||||||
|
if _, err := os.Stat(want); err != nil {
|
||||||
|
t.Errorf("expected output relative to input root at %s: %v", want, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type zipEntry struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildZip(t *testing.T, path string, entries []zipEntry) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
zw := zip.NewWriter(f)
|
||||||
|
for _, e := range entries {
|
||||||
|
w, err := zw.Create(e.name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := w.Write(e.data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := zw.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstPage(t *testing.T, conv *Converter, archive string) image.Image {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
zr, err := zip.OpenReader(archive)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer zr.Close()
|
||||||
|
|
||||||
|
if len(zr.File) == 0 {
|
||||||
|
t.Fatalf("archive %s has no entries", archive)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc, err := zr.File[0].Open()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
img, err := conv.imageDecode(rc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
func archiveHas(t *testing.T, conv *Converter, archive, name string) bool {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
list, err := conv.archiveList(archive)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, n := range list {
|
||||||
|
if n == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
+381
-30
@@ -34,8 +34,58 @@ var appVersion string
|
|||||||
var (
|
var (
|
||||||
index = -1
|
index = -1
|
||||||
files []cbconvert.File
|
files []cbconvert.File
|
||||||
|
|
||||||
|
config iup.Ihandle
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pathsGroup = "Paths"
|
||||||
|
profilesGroup = "Profiles"
|
||||||
|
|
||||||
|
inputDirKey = "InputDir"
|
||||||
|
outputDirKey = "OutputDir"
|
||||||
|
)
|
||||||
|
|
||||||
|
type settingKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
kindBool settingKind = iota
|
||||||
|
kindInt
|
||||||
|
kindStr
|
||||||
|
)
|
||||||
|
|
||||||
|
type setting struct {
|
||||||
|
handle string
|
||||||
|
kind settingKind
|
||||||
|
def string
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings = []setting{
|
||||||
|
{"Recursive", kindBool, "OFF"},
|
||||||
|
{"NoRGB", kindBool, "OFF"},
|
||||||
|
{"NoCover", kindBool, "OFF"},
|
||||||
|
{"NoConvert", kindBool, "OFF"},
|
||||||
|
{"NoNonImage", kindBool, "OFF"},
|
||||||
|
{"Combine", kindBool, "OFF"},
|
||||||
|
{"Fit", kindBool, "OFF"},
|
||||||
|
{"Lossless", kindBool, "OFF"},
|
||||||
|
{"Grayscale", kindBool, "OFF"},
|
||||||
|
{"OutDir", kindStr, ""},
|
||||||
|
{"Suffix", kindStr, ""},
|
||||||
|
{"Width", kindStr, ""},
|
||||||
|
{"Height", kindStr, ""},
|
||||||
|
{"Size", kindInt, "0"},
|
||||||
|
{"Quality", kindInt, "75"},
|
||||||
|
{"Effort", kindInt, "0"},
|
||||||
|
{"Brightness", kindInt, "0"},
|
||||||
|
{"Contrast", kindInt, "0"},
|
||||||
|
{"Format", kindInt, "1"},
|
||||||
|
{"Archive", kindInt, "1"},
|
||||||
|
{"ZipLevel", kindInt, "1"},
|
||||||
|
{"Filter", kindInt, "3"},
|
||||||
|
{"Rotate", kindInt, "1"},
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if appVersion != "" {
|
if appVersion != "" {
|
||||||
return
|
return
|
||||||
@@ -69,8 +119,13 @@ func main() {
|
|||||||
iup.Open()
|
iup.Open()
|
||||||
defer iup.Close()
|
defer iup.Close()
|
||||||
|
|
||||||
|
iup.SetGlobal("APPNAME", "cbconvert")
|
||||||
|
iup.SetGlobal("APPID", "io.github.gen2brain.cbconvert")
|
||||||
iup.SetGlobal("AUTODARKMODE", "YES")
|
iup.SetGlobal("AUTODARKMODE", "YES")
|
||||||
|
|
||||||
|
config = iup.Config()
|
||||||
|
iup.ConfigLoad(config)
|
||||||
|
|
||||||
img, _ := png.Decode(bytes.NewReader(appLogo))
|
img, _ := png.Decode(bytes.NewReader(appLogo))
|
||||||
iup.ImageFromImage(img).SetHandle("logo")
|
iup.ImageFromImage(img).SetHandle("logo")
|
||||||
|
|
||||||
@@ -103,7 +158,7 @@ func main() {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
iup.Map(dlg)
|
iup.Map(dlg)
|
||||||
setActive()
|
profilesInit()
|
||||||
|
|
||||||
iup.ShowXY(dlg, iup.CENTER, iup.CENTER)
|
iup.ShowXY(dlg, iup.CENTER, iup.CENTER)
|
||||||
iup.MainLoop()
|
iup.MainLoop()
|
||||||
@@ -141,6 +196,7 @@ func options() cbconvert.Options {
|
|||||||
opts.NoConvert = iup.GetHandle("NoConvert").GetAttribute("VALUE") == "ON"
|
opts.NoConvert = iup.GetHandle("NoConvert").GetAttribute("VALUE") == "ON"
|
||||||
opts.NoNonImage = iup.GetHandle("NoNonImage").GetAttribute("VALUE") == "ON"
|
opts.NoNonImage = iup.GetHandle("NoNonImage").GetAttribute("VALUE") == "ON"
|
||||||
opts.Archive = strings.ToLower(iup.GetHandle("Archive").GetAttribute("VALUESTRING"))
|
opts.Archive = strings.ToLower(iup.GetHandle("Archive").GetAttribute("VALUESTRING"))
|
||||||
|
opts.ZipLevel = zipLevel(iup.GetHandle("ZipLevel").GetAttribute("VALUESTRING"))
|
||||||
opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
|
opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
|
||||||
opts.Width = iup.GetHandle("Width").GetInt("VALUE")
|
opts.Width = iup.GetHandle("Width").GetInt("VALUE")
|
||||||
opts.Height = iup.GetHandle("Height").GetInt("VALUE")
|
opts.Height = iup.GetHandle("Height").GetInt("VALUE")
|
||||||
@@ -246,6 +302,246 @@ func setActive() {
|
|||||||
} else {
|
} else {
|
||||||
iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "NO")
|
iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "NO")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.Archive == "zip" {
|
||||||
|
iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "YES")
|
||||||
|
} else {
|
||||||
|
iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "NO")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shellArg quotes a command-line argument that contains whitespace.
|
||||||
|
func shellArg(s string) string {
|
||||||
|
if strings.ContainsAny(s, " \t") {
|
||||||
|
return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandLine() string {
|
||||||
|
parts := append([]string{"cbconvert", "convert"}, options().Args()...)
|
||||||
|
for _, file := range files {
|
||||||
|
parts = append(parts, file.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range parts {
|
||||||
|
parts[i] = shellArg(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func onCommand(iup.Ihandle) int {
|
||||||
|
iup.GetText("Command Line", commandLine(), -1)
|
||||||
|
|
||||||
|
return iup.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
// zipLevel maps the compression dropdown selection to Options.ZipLevel.
|
||||||
|
func zipLevel(value string) int {
|
||||||
|
switch value {
|
||||||
|
case "Default":
|
||||||
|
return -1
|
||||||
|
case "Store (none)":
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
level, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileGroup(name string) string {
|
||||||
|
return "Profile:" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
func profileNames() []string {
|
||||||
|
s := iup.ConfigGetVariableStr(config, profilesGroup, "Names")
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Split(s, ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentProfile() string {
|
||||||
|
return iup.ConfigGetVariableStrDef(config, profilesGroup, "Current", "Default")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setStartDir(dlg iup.Ihandle, key string) {
|
||||||
|
if dir := iup.ConfigGetVariableStr(config, pathsGroup, key); dir != "" {
|
||||||
|
dlg.SetAttribute("DIRECTORY", dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rememberDir(dlg iup.Ihandle, key string) {
|
||||||
|
dir := dlg.GetAttribute("DIRECTORY")
|
||||||
|
if dir == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iup.ConfigSetVariableStr(config, pathsGroup, key, dir)
|
||||||
|
iup.ConfigSave(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func settingsSave(group string) {
|
||||||
|
for _, s := range settings {
|
||||||
|
h := iup.GetHandle(s.handle)
|
||||||
|
switch s.kind {
|
||||||
|
case kindBool:
|
||||||
|
v := 0
|
||||||
|
if h.GetAttribute("VALUE") == "ON" {
|
||||||
|
v = 1
|
||||||
|
}
|
||||||
|
iup.ConfigSetVariableInt(config, group, s.handle, v)
|
||||||
|
case kindInt:
|
||||||
|
iup.ConfigSetVariableInt(config, group, s.handle, h.GetInt("VALUE"))
|
||||||
|
case kindStr:
|
||||||
|
iup.ConfigSetVariableStr(config, group, s.handle, h.GetAttribute("VALUE"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iup.ConfigSave(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// settingsApply sets every control from the given profile group, or from defaults when group is empty.
|
||||||
|
func settingsApply(group string) {
|
||||||
|
for _, s := range settings {
|
||||||
|
h := iup.GetHandle(s.handle)
|
||||||
|
switch s.kind {
|
||||||
|
case kindBool:
|
||||||
|
def := 0
|
||||||
|
if s.def == "ON" {
|
||||||
|
def = 1
|
||||||
|
}
|
||||||
|
v := def
|
||||||
|
if group != "" {
|
||||||
|
v = iup.ConfigGetVariableIntDef(config, group, s.handle, def)
|
||||||
|
}
|
||||||
|
if v != 0 {
|
||||||
|
h.SetAttribute("VALUE", "ON")
|
||||||
|
} else {
|
||||||
|
h.SetAttribute("VALUE", "OFF")
|
||||||
|
}
|
||||||
|
case kindInt:
|
||||||
|
def, _ := strconv.Atoi(s.def)
|
||||||
|
v := def
|
||||||
|
if group != "" {
|
||||||
|
v = iup.ConfigGetVariableIntDef(config, group, s.handle, def)
|
||||||
|
}
|
||||||
|
h.SetAttribute("VALUE", strconv.Itoa(v))
|
||||||
|
case kindStr:
|
||||||
|
v := s.def
|
||||||
|
if group != "" {
|
||||||
|
v = iup.ConfigGetVariableStrDef(config, group, s.handle, s.def)
|
||||||
|
}
|
||||||
|
h.SetAttribute("VALUE", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncLabels()
|
||||||
|
setActive()
|
||||||
|
previewPost()
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncLabels mirrors slider values into their value labels and retunes the effort slider for the current format.
|
||||||
|
func syncLabels() {
|
||||||
|
iup.GetHandle("LabelQuality").SetAttribute("TITLE", iup.GetHandle("Quality").GetInt("VALUE"))
|
||||||
|
iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE"))
|
||||||
|
iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE"))
|
||||||
|
|
||||||
|
format := strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
|
||||||
|
eff := iup.GetHandle("Effort").GetInt("VALUE")
|
||||||
|
setEffort(format)
|
||||||
|
switch format {
|
||||||
|
case "webp", "avif", "jxl":
|
||||||
|
val := iup.GetHandle("Effort")
|
||||||
|
val.SetAttribute("VALUE", strconv.Itoa(eff))
|
||||||
|
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", val.GetAttribute("EFFORTNAME"), eff))
|
||||||
|
}
|
||||||
|
|
||||||
|
iup.Refresh(iup.GetHandle("Tabs"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillProfileList() {
|
||||||
|
list := iup.GetHandle("Profile")
|
||||||
|
list.SetAttribute("REMOVEITEM", "ALL")
|
||||||
|
|
||||||
|
cur := currentProfile()
|
||||||
|
sel := 1
|
||||||
|
for i, n := range profileNames() {
|
||||||
|
list.SetAttribute(strconv.Itoa(i+1), n)
|
||||||
|
if n == cur {
|
||||||
|
sel = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.SetAttribute("VALUE", strconv.Itoa(sel))
|
||||||
|
}
|
||||||
|
|
||||||
|
// profilesInit loads the current profile on startup, creating a default one on first run.
|
||||||
|
func profilesInit() {
|
||||||
|
if len(profileNames()) == 0 {
|
||||||
|
iup.ConfigSetVariableStr(config, profilesGroup, "Names", "Default")
|
||||||
|
iup.ConfigSetVariableStr(config, profilesGroup, "Current", "Default")
|
||||||
|
settingsSave(profileGroup("Default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fillProfileList()
|
||||||
|
settingsApply(profileGroup(currentProfile()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func onProfileSelect(ih iup.Ihandle) int {
|
||||||
|
name := ih.GetAttribute("VALUESTRING")
|
||||||
|
if name == "" {
|
||||||
|
return iup.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
iup.ConfigSetVariableStr(config, profilesGroup, "Current", name)
|
||||||
|
iup.ConfigSave(config)
|
||||||
|
|
||||||
|
settingsApply(profileGroup(name))
|
||||||
|
|
||||||
|
return iup.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
func onSave(iup.Ihandle) int {
|
||||||
|
name := currentProfile()
|
||||||
|
if iup.GetParam("Save Profile", nil, "Name: %s\n", &name) != 1 {
|
||||||
|
return iup.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" || strings.ContainsAny(name, ".;") {
|
||||||
|
iup.Message("Invalid Name", "Profile name must not be empty or contain '.' or ';'.")
|
||||||
|
|
||||||
|
return iup.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsSave(profileGroup(name))
|
||||||
|
|
||||||
|
names := profileNames()
|
||||||
|
if !slices.Contains(names, name) {
|
||||||
|
names = append(names, name)
|
||||||
|
iup.ConfigSetVariableStr(config, profilesGroup, "Names", strings.Join(names, ";"))
|
||||||
|
}
|
||||||
|
|
||||||
|
iup.ConfigSetVariableStr(config, profilesGroup, "Current", name)
|
||||||
|
iup.ConfigSave(config)
|
||||||
|
|
||||||
|
fillProfileList()
|
||||||
|
|
||||||
|
return iup.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
func onReset(iup.Ihandle) int {
|
||||||
|
settingsApply("")
|
||||||
|
|
||||||
|
return iup.DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
func setEffort(format string) {
|
func setEffort(format string) {
|
||||||
@@ -521,8 +817,32 @@ func tabs() iup.Ihandle {
|
|||||||
"VALUE": "1",
|
"VALUE": "1",
|
||||||
"1": "ZIP",
|
"1": "ZIP",
|
||||||
"2": "TAR",
|
"2": "TAR",
|
||||||
}).SetHandle("Archive"),
|
}).SetHandle("Archive").
|
||||||
|
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
|
||||||
|
setActive()
|
||||||
|
|
||||||
|
return iup.DEFAULT
|
||||||
|
})),
|
||||||
),
|
),
|
||||||
|
iup.Vbox(
|
||||||
|
iup.Label("Compression:"),
|
||||||
|
iup.List().SetAttributes(map[string]string{
|
||||||
|
"DROPDOWN": "YES",
|
||||||
|
"VALUE": "1",
|
||||||
|
"1": "Default",
|
||||||
|
"2": "Store (none)",
|
||||||
|
"3": "1",
|
||||||
|
"4": "2",
|
||||||
|
"5": "3",
|
||||||
|
"6": "4",
|
||||||
|
"7": "5",
|
||||||
|
"8": "6",
|
||||||
|
"9": "7",
|
||||||
|
"10": "8",
|
||||||
|
"11": "9",
|
||||||
|
}).SetHandle("ZipLevel").
|
||||||
|
SetAttribute("TIP", "ZIP compression: Store disables it, 1 is fastest, 9 is smallest"),
|
||||||
|
).SetHandle("VboxZipLevel"),
|
||||||
).SetAttributes("NGAP=10"),
|
).SetAttributes("NGAP=10"),
|
||||||
iup.Space().SetAttribute("SIZE", "15"),
|
iup.Space().SetAttribute("SIZE", "15"),
|
||||||
iup.Vbox(
|
iup.Vbox(
|
||||||
@@ -784,8 +1104,23 @@ func buttons() iup.Ihandle {
|
|||||||
SetCallback("ACTION", iup.ActionFunc(onCover))
|
SetCallback("ACTION", iup.ActionFunc(onCover))
|
||||||
convert := iup.Button("&Convert").SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
|
convert := iup.Button("&Convert").SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
|
||||||
SetCallback("ACTION", iup.ActionFunc(onConvert))
|
SetCallback("ACTION", iup.ActionFunc(onConvert))
|
||||||
|
reset := iup.Button("Reset").SetHandle("Reset").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
|
||||||
|
SetAttribute("TIP", "Restore all settings to their defaults").
|
||||||
|
SetCallback("ACTION", iup.ActionFunc(onReset))
|
||||||
|
save := iup.Button("Save").SetHandle("Save").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
|
||||||
|
SetAttribute("TIP", "Save current settings to a profile").
|
||||||
|
SetCallback("ACTION", iup.ActionFunc(onSave))
|
||||||
|
|
||||||
iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert).SetAttribute("NORMALIZE", "BOTH")
|
command := iup.Button("Command").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
|
||||||
|
SetAttribute("TIP", "Show the equivalent command line").
|
||||||
|
SetCallback("ACTION", iup.ActionFunc(onCommand))
|
||||||
|
|
||||||
|
profile := iup.List().SetAttributes("DROPDOWN=YES").SetHandle("Profile").
|
||||||
|
SetAttribute("TIP", "Select a settings profile").
|
||||||
|
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onProfileSelect))
|
||||||
|
|
||||||
|
iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command).SetAttribute("NORMALIZE", "BOTH")
|
||||||
|
iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command, profile).SetAttribute("NORMALIZE", "HORIZONTAL")
|
||||||
|
|
||||||
return iup.Vbox(
|
return iup.Vbox(
|
||||||
iup.Vbox(
|
iup.Vbox(
|
||||||
@@ -794,15 +1129,23 @@ func buttons() iup.Ihandle {
|
|||||||
remove,
|
remove,
|
||||||
removeAll,
|
removeAll,
|
||||||
).SetAttribute("NGAP", "2"),
|
).SetAttribute("NGAP", "2"),
|
||||||
iup.Space().SetAttribute("SIZE", "x5"),
|
iup.Space().SetAttribute("SIZE", "x8"),
|
||||||
iup.Vbox(
|
iup.Vbox(
|
||||||
thumbnail,
|
thumbnail,
|
||||||
cover,
|
cover,
|
||||||
).SetAttribute("NGAP", "2"),
|
).SetAttribute("NGAP", "2"),
|
||||||
iup.Space().SetAttribute("SIZE", "x5"),
|
iup.Space().SetAttribute("SIZE", "x8"),
|
||||||
iup.Vbox(
|
iup.Vbox(
|
||||||
convert,
|
convert,
|
||||||
),
|
),
|
||||||
|
iup.Fill(),
|
||||||
|
iup.Vbox(
|
||||||
|
iup.Label("Profile:"),
|
||||||
|
profile,
|
||||||
|
reset,
|
||||||
|
save,
|
||||||
|
command,
|
||||||
|
).SetAttribute("NGAP", "2"),
|
||||||
).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER")
|
).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,7 +1231,7 @@ func loading() iup.Ihandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func onAddFiles(ih iup.Ihandle) int {
|
func onAddFiles(ih iup.Ihandle) int {
|
||||||
args, err := fileDlg("Add Files", true, false)
|
args, err := fileDlg("Add Files", true, false, inputDirKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@@ -928,7 +1271,7 @@ func onAddFiles(ih iup.Ihandle) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func onAddDir(ih iup.Ihandle) int {
|
func onAddDir(ih iup.Ihandle) int {
|
||||||
args, err := fileDlg("Add Directory", false, true)
|
args, err := fileDlg("Add Directory", false, true, inputDirKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@@ -1024,7 +1367,7 @@ func onThumbnail(ih iup.Ihandle) int {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Thumbnail(file.Path, file.Stat); err != nil {
|
if err := c.Thumbnail(file); err != nil {
|
||||||
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
||||||
@@ -1067,7 +1410,7 @@ func onCover(ih iup.Ihandle) int {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Cover(file.Path, file.Stat); err != nil {
|
if err := c.Cover(file); err != nil {
|
||||||
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
||||||
@@ -1131,7 +1474,7 @@ func onConvert(ih iup.Ihandle) int {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if err := c.Convert(file.Path, file.Stat); err != nil {
|
if err := c.Convert(file); err != nil {
|
||||||
convertErr(err)
|
convertErr(err)
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
break
|
break
|
||||||
@@ -1149,7 +1492,7 @@ func onConvert(ih iup.Ihandle) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func onOutputDirectory(ih iup.Ihandle) int {
|
func onOutputDirectory(ih iup.Ihandle) int {
|
||||||
args, err := fileDlg("Output Directory", false, true)
|
args, err := fileDlg("Output Directory", false, true, outputDirKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@@ -1167,7 +1510,7 @@ func onOutputDirectory(ih iup.Ihandle) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func onOutputFile(ih iup.Ihandle) int {
|
func onOutputFile(ih iup.Ihandle) int {
|
||||||
name := saveDlg("Output File")
|
name := saveDlg("Output File", outputDirKey)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
iup.GetHandle("OutFile").SetAttribute("VALUE", filepath.Base(name))
|
iup.GetHandle("OutFile").SetAttribute("VALUE", filepath.Base(name))
|
||||||
iup.GetHandle("OutDir").SetAttribute("VALUE", filepath.Dir(name))
|
iup.GetHandle("OutDir").SetAttribute("VALUE", filepath.Dir(name))
|
||||||
@@ -1200,7 +1543,7 @@ func onFilterChanged(ih iup.Ihandle) int {
|
|||||||
return iup.DEFAULT
|
return iup.DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileDlg(title string, multiple, directory bool) ([]string, error) {
|
func fileDlg(title string, multiple, directory bool, dirKey string) ([]string, error) {
|
||||||
ret := make([]string, 0)
|
ret := make([]string, 0)
|
||||||
|
|
||||||
dlg := iup.FileDlg()
|
dlg := iup.FileDlg()
|
||||||
@@ -1213,11 +1556,12 @@ func fileDlg(title string, multiple, directory bool) ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dlg.SetAttributes(map[string]string{
|
dlg.SetAttributes(map[string]string{
|
||||||
"DIALOGTYPE": "OPEN",
|
"DIALOGTYPE": "OPEN",
|
||||||
"MULTIPLEFILES": mf,
|
"MULTIPLEFILES": mf,
|
||||||
"EXTFILTER": "Comic Files|*.rar;*.zip;*.7z;*.tar;*.cbr;*.cbz;*.cb7;*.cbt;*.pdf;*.epub;*.mobi;*.docx;*.pptx|",
|
"MULTIVALUEPATH": "YES",
|
||||||
"FILTER": "*.cb*", // for Motif
|
"EXTFILTER": "Comic Files|*.rar;*.zip;*.7z;*.tar;*.cbr;*.cbz;*.cb7;*.cbt;*.pdf;*.epub;*.mobi;*.docx;*.pptx|",
|
||||||
"TITLE": title,
|
"FILTER": "*.cb*", // for Motif
|
||||||
|
"TITLE": title,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
dlg.SetAttributes(map[string]string{
|
dlg.SetAttributes(map[string]string{
|
||||||
@@ -1226,30 +1570,33 @@ func fileDlg(title string, multiple, directory bool) ([]string, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStartDir(dlg, dirKey)
|
||||||
|
|
||||||
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
|
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
|
||||||
|
|
||||||
if dlg.GetInt("STATUS") == 0 {
|
if dlg.GetInt("STATUS") == 0 {
|
||||||
if !directory {
|
switch {
|
||||||
value := dlg.GetAttribute("VALUE")
|
case multiple:
|
||||||
sp := strings.Split(value, "|")
|
// MULTIVALUEPATH makes each MULTIVALUE a full path (id 0 is the path), so a folder-spanning selection works.
|
||||||
|
count := dlg.GetInt("MULTIVALUECOUNT")
|
||||||
if len(sp) > 1 {
|
if count > 1 {
|
||||||
for _, file := range sp[1 : len(sp)-1] {
|
for i := 1; i < count; i++ {
|
||||||
ret = append(ret, filepath.Join(sp[0], file))
|
ret = append(ret, iup.GetAttributeId(dlg, "MULTIVALUE", i))
|
||||||
}
|
}
|
||||||
} else {
|
} else if value := dlg.GetAttribute("VALUE"); value != "" {
|
||||||
ret = append(ret, value)
|
ret = append(ret, value)
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
value := dlg.GetAttribute("VALUE")
|
ret = append(ret, dlg.GetAttribute("VALUE"))
|
||||||
ret = append(ret, value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rememberDir(dlg, dirKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveDlg(title string) string {
|
func saveDlg(title, dirKey string) string {
|
||||||
dlg := iup.FileDlg()
|
dlg := iup.FileDlg()
|
||||||
defer dlg.Destroy()
|
defer dlg.Destroy()
|
||||||
|
|
||||||
@@ -1260,11 +1607,15 @@ func saveDlg(title string) string {
|
|||||||
"TITLE": title,
|
"TITLE": title,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setStartDir(dlg, dirKey)
|
||||||
|
|
||||||
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
|
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
|
||||||
|
|
||||||
if dlg.GetInt("STATUS") == -1 {
|
if dlg.GetInt("STATUS") == -1 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rememberDir(dlg, dirKey)
|
||||||
|
|
||||||
return dlg.GetAttribute("VALUE")
|
return dlg.GetAttribute("VALUE")
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-13
@@ -68,8 +68,8 @@ func main() {
|
|||||||
if _, err := os.Stat(opts.OutDir); err != nil {
|
if _, err := os.Stat(opts.OutDir); err != nil {
|
||||||
if err := os.MkdirAll(opts.OutDir, 0775); err != nil {
|
if err := os.MkdirAll(opts.OutDir, 0775); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := conv.Files(args)
|
files, err := conv.Files(args)
|
||||||
@@ -142,14 +142,14 @@ func main() {
|
|||||||
|
|
||||||
continue
|
continue
|
||||||
case opts.Cover:
|
case opts.Cover:
|
||||||
if err := conv.Cover(file.Path, file.Stat); err != nil {
|
if err := conv.Cover(file); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
case opts.Thumbnail:
|
case opts.Thumbnail:
|
||||||
if err = conv.Thumbnail(file.Path, file.Stat); err != nil {
|
if err = conv.Thumbnail(file); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := conv.Convert(file.Path, file.Stat); err != nil {
|
if err := conv.Convert(file); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -177,6 +177,7 @@ func parseFlags() (cbconvert.Options, []string) {
|
|||||||
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
|
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
|
||||||
convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl")
|
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.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.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.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.Lossless, "lossless", false, "Lossless compression (webp, avif, jxl), ignores quality")
|
||||||
@@ -235,7 +236,7 @@ func parseFlags() (cbconvert.Options, []string) {
|
|||||||
fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
|
fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
|
||||||
fmt.Fprintf(os.Stderr, "\nCommands:\n")
|
fmt.Fprintf(os.Stderr, "\nCommands:\n")
|
||||||
fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n")
|
fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n")
|
||||||
order := []string{"width", "height", "fit", "format", "archive", "quality", "effort", "lossless", "combine", "outfile", "filter", "no-cover", "no-rgb",
|
order := []string{"width", "height", "fit", "format", "archive", "zip-level", "quality", "effort", "lossless", "combine", "outfile", "filter", "no-cover", "no-rgb",
|
||||||
"no-nonimage", "no-convert", "grayscale", "rotate", "brightness", "contrast", "suffix", "outdir", "size", "recursive", "quiet"}
|
"no-nonimage", "no-convert", "grayscale", "rotate", "brightness", "contrast", "suffix", "outdir", "size", "recursive", "quiet"}
|
||||||
for _, name := range order {
|
for _, name := range order {
|
||||||
f := convert.Lookup(name)
|
f := convert.Lookup(name)
|
||||||
@@ -279,27 +280,27 @@ func parseFlags() (cbconvert.Options, []string) {
|
|||||||
|
|
||||||
switch os.Args[1] {
|
switch os.Args[1] {
|
||||||
case "convert":
|
case "convert":
|
||||||
_ = convert.Parse(os.Args[2:])
|
operands := parseArgs(convert, os.Args[2:])
|
||||||
if !pipe {
|
if !pipe {
|
||||||
args = convert.Args()
|
args = operands
|
||||||
}
|
}
|
||||||
case "cover":
|
case "cover":
|
||||||
opts.Cover = true
|
opts.Cover = true
|
||||||
_ = cover.Parse(os.Args[2:])
|
operands := parseArgs(cover, os.Args[2:])
|
||||||
if !pipe {
|
if !pipe {
|
||||||
args = cover.Args()
|
args = operands
|
||||||
}
|
}
|
||||||
case "thumbnail":
|
case "thumbnail":
|
||||||
opts.Thumbnail = true
|
opts.Thumbnail = true
|
||||||
_ = thumbnail.Parse(os.Args[2:])
|
operands := parseArgs(thumbnail, os.Args[2:])
|
||||||
if !pipe {
|
if !pipe {
|
||||||
args = thumbnail.Args()
|
args = operands
|
||||||
}
|
}
|
||||||
case "meta":
|
case "meta":
|
||||||
opts.Meta = true
|
opts.Meta = true
|
||||||
_ = meta.Parse(os.Args[2:])
|
operands := parseArgs(meta, os.Args[2:])
|
||||||
if !pipe {
|
if !pipe {
|
||||||
args = meta.Args()
|
args = operands
|
||||||
}
|
}
|
||||||
case "version":
|
case "version":
|
||||||
opts.Version = true
|
opts.Version = true
|
||||||
@@ -314,6 +315,19 @@ func parseFlags() (cbconvert.Options, []string) {
|
|||||||
return opts, args
|
return opts, args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseArgs parses flags interspersed with file/dir operands.
|
||||||
|
func parseArgs(fs *flag.FlagSet, args []string) []string {
|
||||||
|
var operands []string
|
||||||
|
|
||||||
|
_ = fs.Parse(args)
|
||||||
|
for fs.NArg() > 0 {
|
||||||
|
operands = append(operands, fs.Arg(0))
|
||||||
|
_ = fs.Parse(fs.Args()[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return operands
|
||||||
|
}
|
||||||
|
|
||||||
// piped checks if we have piped stdin.
|
// piped checks if we have piped stdin.
|
||||||
func piped() bool {
|
func piped() bool {
|
||||||
f, err := os.Stdin.Stat()
|
f, err := os.Stdin.Stat()
|
||||||
|
|||||||
Reference in New Issue
Block a user