6 Commits
Author SHA1 Message Date
Milan Nikolic a8b2f65ca5 Update README.md 2026-06-24 21:40:44 +02:00
Milan Nikolic 8f75ffc43b Split GUI files 2026-06-24 21:36:32 +02:00
Milan Nikolic aca062a3f2 Organize Input tab, add more tooltips 2026-06-24 21:18:19 +02:00
Milan Nikolic 9de98599a9 Add DPI option, issue #52 2026-06-24 20:44:29 +02:00
Milan Nikolic f65fc4bafa Turn Convert into Cancel button 2026-06-24 19:53:27 +02:00
Milan Nikolic 26a320ef9d Handle SORT_CB 2026-06-24 18:39:16 +02:00
13 changed files with 1732 additions and 1477 deletions
+24
View File
@@ -70,12 +70,24 @@ Commands:
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--format
Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl (default "jpeg")
--archive
Archive format, valid values are zip, tar (default "zip")
--zip-level
ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default (default "-1")
--quality
Image quality (default "75")
--effort
Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default (default "-1")
--lossless
Lossless compression (webp, avif, jxl), ignores quality (default "false")
--combine
Combine all inputs into a single archive (default "false")
--outfile
Output file name for --combine (default: first input + -combined) (default "")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
--no-cover
@@ -114,10 +126,16 @@ Commands:
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--format
Image format, valid values are jpeg, png, tiff, bmp, webp, avif (default "jpeg")
--quality
Image quality (default "75")
--effort
Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default (default "-1")
--lossless
Lossless compression (webp, avif, jxl), ignores quality (default "false")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
--outdir
@@ -138,6 +156,8 @@ Commands:
Image height (default "0")
--fit
Best fit for required width and height (default "false")
--dpi
Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300) (default "0")
--filter
0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos (default "2")
--outdir
@@ -189,6 +209,10 @@ Commands:
`cbconvert --format avif --quality 50 --width 1280 --outdir ~/comics /media/comics/Misc/`
* Combine several issues into a single archive:
`cbconvert convert --combine --outfile Series.cbz --outdir ~/comics issue1.cbz issue2.cbr issue3.cb7`
### Quality settings
This table maps quality settings for JPEG to the respective AVIF and WEBP quality settings:
+12
View File
@@ -39,6 +39,8 @@ 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
DPI int
// 0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos
Filter int
// Do not convert the cover image
@@ -178,6 +180,7 @@ func (o Options) Args() []string {
num("width", o.Width, def.Width)
num("height", o.Height, def.Height)
flag("fit", o.Fit)
num("dpi", o.DPI, def.DPI)
str("format", o.Format, def.Format)
str("archive", o.Archive, def.Archive)
num("zip-level", o.ZipLevel, def.ZipLevel)
@@ -203,6 +206,15 @@ func (o Options) Args() []string {
return args
}
// renderDPI returns the document rendering resolution, falling back to 300 when unset.
func (c *Converter) renderDPI() float64 {
if c.Opts.DPI > 0 {
return float64(c.Opts.DPI)
}
return 300
}
// Cancel cancels the operation.
func (c *Converter) Cancel() {
if c.OnCancel != nil {
+1 -1
View File
@@ -48,7 +48,7 @@ func (c *Converter) convertDocument(ctx context.Context, fileName string) error
return fmt.Errorf("convertDocument: %w", ctx.Err())
}
img, err := doc.Image(n)
img, err := doc.ImageDPI(n, c.renderDPI())
if err != nil {
return fmt.Errorf("convertDocument: %w", err)
}
+1 -1
View File
@@ -52,7 +52,7 @@ func (c *Converter) coverDocument(fileName string) (image.Image, error) {
}
defer doc.Close()
img, err := doc.Image(0)
img, err := doc.ImageDPI(0, c.renderDPI())
if err != nil {
return nil, fmt.Errorf("coverDocument: %w", err)
}
+37 -1
View File
@@ -114,16 +114,52 @@ func TestArgs(t *testing.T) {
opts.Effort = 4
opts.Lossless = true
opts.Width = 1200
opts.DPI = 150
opts.Grayscale = true
opts.OutDir = "/out"
got := strings.Join(opts.Args(), " ")
want := "--width 1200 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out"
want := "--width 1200 --dpi 150 --format webp --quality 90 --effort 4 --lossless --grayscale --outdir /out"
if got != want {
t.Errorf("Args() = %q, want %q", got, want)
}
}
func TestConvertDPI(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
dims := func(dpi int) int {
opts := NewOptions()
opts.OutDir = tmpDir
opts.DPI = dpi
conv := New(opts)
files, err := conv.Files([]string{"testdata/test.pdf"})
if err != nil {
t.Fatal(err)
}
for _, file := range files {
if err := conv.Convert(file); err != nil {
t.Fatal(err)
}
}
return firstPage(t, conv, filepath.Join(tmpDir, "test.cbz")).Bounds().Dx()
}
low := dims(150)
high := dims(600)
if low >= high {
t.Errorf("higher DPI should render larger pages: 150dpi=%d, 600dpi=%d", low, high)
}
}
func TestConvertResize(t *testing.T) {
tmpDir, err := os.MkdirTemp(os.TempDir(), "cbc")
if err != nil {
+80
View File
@@ -0,0 +1,80 @@
package main
import "github.com/gen2brain/iup-go/iup"
func fileDlg(title string, multiple, directory bool, dirKey string) ([]string, error) {
ret := make([]string, 0)
dlg := iup.FileDlg()
defer dlg.Destroy()
if !directory {
mf := "YES"
if !multiple {
mf = "NO"
}
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "OPEN",
"MULTIPLEFILES": mf,
"MULTIVALUEPATH": "YES",
"EXTFILTER": "Comic Files|*.rar;*.zip;*.7z;*.tar;*.cbr;*.cbz;*.cb7;*.cbt;*.pdf;*.epub;*.mobi;*.docx;*.pptx|",
"FILTER": "*.cb*", // for Motif
"TITLE": title,
})
} else {
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "DIR",
"TITLE": title,
})
}
setStartDir(dlg, dirKey)
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
if dlg.GetInt("STATUS") == 0 {
switch {
case multiple:
// MULTIVALUEPATH makes each MULTIVALUE a full path (id 0 is the path), so a folder-spanning selection works.
count := dlg.GetInt("MULTIVALUECOUNT")
if count > 1 {
for i := 1; i < count; i++ {
ret = append(ret, iup.GetAttributeId(dlg, "MULTIVALUE", i))
}
} else if value := dlg.GetAttribute("VALUE"); value != "" {
ret = append(ret, value)
}
default:
ret = append(ret, dlg.GetAttribute("VALUE"))
}
rememberDir(dlg, dirKey)
}
return ret, nil
}
func saveDlg(title, dirKey string) string {
dlg := iup.FileDlg()
defer dlg.Destroy()
dlg.SetAttributes(map[string]string{
"DIALOGTYPE": "SAVE",
"EXTFILTER": "Comic Files|*.cbz;*.cbt|",
"FILTER": "*.cb*", // for Motif
"TITLE": title,
})
setStartDir(dlg, dirKey)
iup.Popup(dlg, iup.CENTERPARENT, iup.CENTERPARENT)
if dlg.GetInt("STATUS") == -1 {
return ""
}
rememberDir(dlg, dirKey)
return dlg.GetAttribute("VALUE")
}
+442
View File
@@ -0,0 +1,442 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
// selectRow focuses and selects the given 0-based row in the table.
func selectRow(i int) {
if i < 0 || i >= len(files) {
return
}
index = i
iup.GetHandle("Table").SetAttribute("FOCUSCELL", fmt.Sprintf("%d:1", i+1))
}
// onSort re-syncs the files slice to the table's displayed order after a sort, so rows keep mapping to the right file.
func onSort(ih iup.Ihandle, col int) int {
n := len(files)
if n < 2 {
return iup.DEFAULT
}
rowKey := func(name, size string) string {
return name + "\x00" + size
}
buckets := make(map[string][]int, n)
for i, f := range files {
size := strconv.FormatFloat(float64(f.Stat.Size())/(1024*1024), 'f', 2, 64)
k := rowKey(f.Name, size)
buckets[k] = append(buckets[k], i)
}
var selPath string
if index >= 0 && index < len(files) {
selPath = files[index].Path
}
reordered := make([]cbconvert.File, 0, n)
for lin := 1; lin <= n; lin++ {
k := rowKey(iup.GetAttributeId2(ih, "", lin, 1), iup.GetAttributeId2(ih, "", lin, 3))
idxs := buckets[k]
if len(idxs) == 0 {
return iup.DEFAULT
}
reordered = append(reordered, files[idxs[0]])
buckets[k] = idxs[1:]
}
files = reordered
index = -1
if selPath != "" {
for i, f := range files {
if f.Path == selPath {
selectRow(i)
break
}
}
}
return iup.DEFAULT
}
// appendFile adds a file as a new row to the table and the files slice.
func appendFile(file cbconvert.File) {
files = append(files, file)
t := iup.GetHandle("Table")
lin := len(files)
t.SetAttribute("NUMLIN", strconv.Itoa(lin))
iup.SetAttributeId2(t, "", lin, 1, file.Name)
iup.SetAttributeId2(t, "", lin, 2, cbconvert.FileType(file.Path))
iup.SetAttributeId2(t, "", lin, 3, strconv.FormatFloat(float64(file.Stat.Size())/(1024*1024), 'f', 2, 64))
}
func previewPost() {
if index == -1 || len(files) == 0 {
return
}
width, height := previewSize()
iup.GetHandle("Loading").SetAttributes("VISIBLE=YES, START=YES")
if strings.ToLower(iup.GetGlobal("DRIVER")) == "motif" {
iup.GetHandle("Preview").SetAttribute("IMAGE", "")
}
opts := options()
go func(opts cbconvert.Options) {
conv := cbconvert.New(opts)
var s string
file := files[index]
img, err := conv.Preview(file.Path, file.Stat, width, height)
if err != nil {
s = err.Error()
fmt.Println(err)
}
iup.PostMessage(iup.GetHandle("Preview"), s, 0, img)
}(opts)
}
func onAddFiles(ih iup.Ihandle) int {
args, err := fileDlg("Add Files", true, false, inputDirKey)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) > 0 {
conv := cbconvert.New(options())
fs, err := conv.Files(args)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
wasEmpty := len(files) == 0
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
}
return iup.DEFAULT
}
func onAddDir(ih iup.Ihandle) int {
args, err := fileDlg("Add Directory", false, true, inputDirKey)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) > 0 {
conv := cbconvert.New(options())
fs, err := conv.Files(args)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
wasEmpty := len(files) == 0
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
}
return iup.DEFAULT
}
func onRemove(ih iup.Ihandle) int {
if index < 0 || index >= len(files) {
return iup.IGNORE
}
iup.GetHandle("Table").SetAttribute("DELLIN", strconv.Itoa(index+1))
files = slices.Delete(files, index, index+1)
if index >= len(files) {
index = len(files) - 1
}
setActive()
previewPost()
return iup.DEFAULT
}
func onRemoveAll(ih iup.Ihandle) int {
index = -1
files = make([]cbconvert.File, 0)
iup.GetHandle("Table").SetAttribute("NUMLIN", "0")
setActive()
return iup.DEFAULT
}
func onThumbnail(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
activeConv = conv
setBusy(true)
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
}
var canceled = false
conv.OnCancel = func() {
canceled = true
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
}))
iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)
go func(c *cbconvert.Converter) {
for _, file := range files {
if canceled {
break
}
if err := c.Thumbnail(file); err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
continue
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onCover(ih iup.Ihandle) int {
conv := cbconvert.New(options())
conv.Nfiles = len(files)
activeConv = conv
setBusy(true)
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress2", 0, conv)
}
var canceled = false
conv.OnCancel = func() {
canceled = true
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
}))
iup.PostMessage(iup.GetHandle("ProgressBar"), "start", 0, conv)
go func(c *cbconvert.Converter) {
for _, file := range files {
if canceled {
break
}
if err := c.Cover(file); err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
continue
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onConvert(ih iup.Ihandle) int {
if busy {
if activeConv != nil {
activeConv.Cancel()
}
return iup.DEFAULT
}
conv := cbconvert.New(options())
conv.Nfiles = len(files)
activeConv = conv
setBusy(true)
conv.OnStart = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "convert", 0, conv)
}
conv.OnProgress = func() {
iup.PostMessage(iup.GetHandle("ProgressBar"), "progress", 0, conv)
}
iup.GetHandle("dlg").SetCallback("K_ANY", iup.KAnyFunc(func(ih iup.Ihandle, c int) int {
if c == iup.K_ESC {
conv.Cancel()
}
return iup.DEFAULT
})).SetCallback("CLOSE_CB", iup.CloseFunc(func(ih iup.Ihandle) int {
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
return iup.DEFAULT
}))
convertErr := func(err error) {
if errors.Is(err, context.Canceled) {
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
return
}
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
if err := os.RemoveAll(conv.Workdir); err != nil {
fmt.Println(err)
}
}
go func(c *cbconvert.Converter) {
if c.Opts.Combine {
if err := c.Combine(files); err != nil {
convertErr(err)
}
} else {
for _, file := range files {
if err := c.Convert(file); err != nil {
convertErr(err)
if errors.Is(err, context.Canceled) {
break
}
continue
}
}
}
iup.PostMessage(iup.GetHandle("ProgressBar"), "finish", 0, 0)
}(conv)
return iup.DEFAULT
}
func onOutputDirectory(ih iup.Ihandle) int {
args, err := fileDlg("Output Directory", false, true, outputDirKey)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
if len(args) == 1 {
iup.GetHandle("OutDir").SetAttribute("VALUE", args[0])
}
setActive()
return iup.DEFAULT
}
func onOutputFile(ih iup.Ihandle) int {
name := saveDlg("Output File", outputDirKey)
if name != "" {
iup.GetHandle("OutFile").SetAttribute("VALUE", filepath.Base(name))
iup.GetHandle("OutDir").SetAttribute("VALUE", filepath.Dir(name))
setActive()
}
return iup.DEFAULT
}
func onFilterChanged(ih iup.Ihandle) int {
switch ih.GetInt("VALUE") {
case 1:
ih.SetAttribute("TIP", "NearestNeighbor is the fastest resampling filter, no antialiasing")
case 2:
ih.SetAttribute("TIP", "Box filter (averaging pixels)")
case 3:
ih.SetAttribute("TIP", "Linear is the bilinear filter, smooth and reasonably fast")
case 4:
ih.SetAttribute("TIP", "MitchellNetravali is a smooth bicubic filter")
case 5:
ih.SetAttribute("TIP", "CatmullRom is a sharp bicubic filter")
case 6:
ih.SetAttribute("TIP", "Gaussian is a blurring filter that uses gaussian function, useful for noise removal")
case 7:
ih.SetAttribute("TIP", "Lanczos is a high-quality resampling filter, it's slower than cubic filters")
}
previewPost()
return iup.DEFAULT
}
+3 -1471
View File
File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
package main
import (
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
func options() cbconvert.Options {
var opts cbconvert.Options
opts.Recursive = iup.GetHandle("Recursive").GetAttribute("VALUE") == "ON"
opts.NoRGB = iup.GetHandle("NoRGB").GetAttribute("VALUE") == "ON"
opts.NoCover = iup.GetHandle("NoCover").GetAttribute("VALUE") == "ON"
opts.Size = iup.GetHandle("Size").GetInt("VALUE")
opts.OutDir = iup.GetHandle("OutDir").GetAttribute("VALUE")
opts.Suffix = iup.GetHandle("Suffix").GetAttribute("VALUE")
opts.NoConvert = iup.GetHandle("NoConvert").GetAttribute("VALUE") == "ON"
opts.NoNonImage = iup.GetHandle("NoNonImage").GetAttribute("VALUE") == "ON"
opts.Archive = strings.ToLower(iup.GetHandle("Archive").GetAttribute("VALUESTRING"))
opts.ZipLevel = zipLevel(iup.GetHandle("ZipLevel").GetAttribute("VALUESTRING"))
opts.Format = strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
opts.Width = iup.GetHandle("Width").GetInt("VALUE")
opts.Height = iup.GetHandle("Height").GetInt("VALUE")
opts.DPI = dpiValue(iup.GetHandle("DPI").GetAttribute("VALUE"))
opts.Fit = iup.GetHandle("Fit").GetAttribute("VALUE") == "ON"
opts.Filter = iup.GetHandle("Filter").GetInt("VALUE") - 1
opts.Quality = iup.GetHandle("Quality").GetInt("VALUE")
switch opts.Format {
case "webp", "avif", "jxl":
opts.Effort = iup.GetHandle("Effort").GetInt("VALUE")
default:
opts.Effort = -1
}
opts.Lossless = iup.GetHandle("Lossless").GetAttribute("VALUE") == "ON"
opts.Combine = iup.GetHandle("Combine").GetAttribute("VALUE") == "ON"
if opts.Combine {
opts.OutFile = iup.GetHandle("OutFile").GetAttribute("VALUE")
}
opts.Grayscale = iup.GetHandle("Grayscale").GetAttribute("VALUE") == "ON"
opts.Brightness = iup.GetHandle("Brightness").GetInt("VALUE")
opts.Contrast = iup.GetHandle("Contrast").GetInt("VALUE")
opts.Rotate = iup.GetHandle("Rotate").GetInt("VALUESTRING")
return opts
}
// shellArg quotes a command-line argument that contains whitespace.
func shellArg(s string) string {
if strings.ContainsAny(s, " \t") {
return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"`
}
return s
}
func commandLine() string {
parts := append([]string{"cbconvert", "convert"}, options().Args()...)
for _, file := range files {
parts = append(parts, file.Path)
}
for i, p := range parts {
parts[i] = shellArg(p)
}
return strings.Join(parts, " ")
}
func onCommand(iup.Ihandle) int {
iup.GetText("Command Line", commandLine(), -1)
return iup.DEFAULT
}
// zipLevel maps the compression dropdown selection to Options.ZipLevel.
func zipLevel(value string) int {
switch value {
case "Default":
return -1
case "Store (none)":
return 0
default:
level, err := strconv.Atoi(value)
if err != nil {
return -1
}
return level
}
}
func dpiValue(value string) int {
dpi, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil {
return 0
}
return dpi
}
+248
View File
@@ -0,0 +1,248 @@
package main
import (
"fmt"
"slices"
"strconv"
"strings"
"github.com/gen2brain/iup-go/iup"
)
const (
pathsGroup = "Paths"
profilesGroup = "Profiles"
inputDirKey = "InputDir"
outputDirKey = "OutputDir"
)
type settingKind int
const (
kindBool settingKind = iota
kindInt
kindStr
)
type setting struct {
handle string
kind settingKind
def string
}
var settings = []setting{
{"Recursive", kindBool, "OFF"},
{"NoRGB", kindBool, "OFF"},
{"NoCover", kindBool, "OFF"},
{"NoConvert", kindBool, "OFF"},
{"NoNonImage", kindBool, "OFF"},
{"Combine", kindBool, "OFF"},
{"Fit", kindBool, "OFF"},
{"Lossless", kindBool, "OFF"},
{"Grayscale", kindBool, "OFF"},
{"OutDir", kindStr, ""},
{"Suffix", kindStr, ""},
{"Width", kindStr, ""},
{"Height", kindStr, ""},
{"DPI", kindStr, "Default"},
{"Size", kindInt, "0"},
{"Quality", kindInt, "75"},
{"Effort", kindInt, "0"},
{"Brightness", kindInt, "0"},
{"Contrast", kindInt, "0"},
{"Format", kindInt, "1"},
{"Archive", kindInt, "1"},
{"ZipLevel", kindInt, "1"},
{"Filter", kindInt, "3"},
{"Rotate", kindInt, "1"},
}
func profileGroup(name string) string {
return "Profile:" + name
}
func profileNames() []string {
s := iup.ConfigGetVariableStr(config, profilesGroup, "Names")
if s == "" {
return nil
}
return strings.Split(s, ";")
}
func currentProfile() string {
return iup.ConfigGetVariableStrDef(config, profilesGroup, "Current", "Default")
}
func setStartDir(dlg iup.Ihandle, key string) {
if dir := iup.ConfigGetVariableStr(config, pathsGroup, key); dir != "" {
dlg.SetAttribute("DIRECTORY", dir)
}
}
func rememberDir(dlg iup.Ihandle, key string) {
dir := dlg.GetAttribute("DIRECTORY")
if dir == "" {
return
}
iup.ConfigSetVariableStr(config, pathsGroup, key, dir)
iup.ConfigSave(config)
}
func settingsSave(group string) {
for _, s := range settings {
h := iup.GetHandle(s.handle)
switch s.kind {
case kindBool:
v := 0
if h.GetAttribute("VALUE") == "ON" {
v = 1
}
iup.ConfigSetVariableInt(config, group, s.handle, v)
case kindInt:
iup.ConfigSetVariableInt(config, group, s.handle, h.GetInt("VALUE"))
case kindStr:
iup.ConfigSetVariableStr(config, group, s.handle, h.GetAttribute("VALUE"))
}
}
iup.ConfigSave(config)
}
// settingsApply sets every control from the given profile group, or from defaults when a group is empty.
func settingsApply(group string) {
for _, s := range settings {
h := iup.GetHandle(s.handle)
switch s.kind {
case kindBool:
def := 0
if s.def == "ON" {
def = 1
}
v := def
if group != "" {
v = iup.ConfigGetVariableIntDef(config, group, s.handle, def)
}
if v != 0 {
h.SetAttribute("VALUE", "ON")
} else {
h.SetAttribute("VALUE", "OFF")
}
case kindInt:
def, _ := strconv.Atoi(s.def)
v := def
if group != "" {
v = iup.ConfigGetVariableIntDef(config, group, s.handle, def)
}
h.SetAttribute("VALUE", strconv.Itoa(v))
case kindStr:
v := s.def
if group != "" {
v = iup.ConfigGetVariableStrDef(config, group, s.handle, s.def)
}
h.SetAttribute("VALUE", v)
}
}
syncLabels()
setActive()
previewPost()
}
// syncLabels mirrors slider values into their value labels and retunes the effort slider for the current format.
func syncLabels() {
iup.GetHandle("LabelQuality").SetAttribute("TITLE", iup.GetHandle("Quality").GetInt("VALUE"))
iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE"))
iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE"))
format := strings.ToLower(iup.GetHandle("Format").GetAttribute("VALUESTRING"))
eff := iup.GetHandle("Effort").GetInt("VALUE")
setEffort(format)
switch format {
case "webp", "avif", "jxl":
val := iup.GetHandle("Effort")
val.SetAttribute("VALUE", strconv.Itoa(eff))
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", val.GetAttribute("EFFORTNAME"), eff))
}
iup.Refresh(iup.GetHandle("Tabs"))
}
func fillProfileList() {
list := iup.GetHandle("Profile")
list.SetAttribute("REMOVEITEM", "ALL")
cur := currentProfile()
sel := 1
for i, n := range profileNames() {
list.SetAttribute(strconv.Itoa(i+1), n)
if n == cur {
sel = i + 1
}
}
list.SetAttribute("VALUE", strconv.Itoa(sel))
}
// profilesInit loads the current profile on startup, creating a default one on the first run.
func profilesInit() {
if len(profileNames()) == 0 {
iup.ConfigSetVariableStr(config, profilesGroup, "Names", "Default")
iup.ConfigSetVariableStr(config, profilesGroup, "Current", "Default")
settingsSave(profileGroup("Default"))
}
fillProfileList()
settingsApply(profileGroup(currentProfile()))
}
func onProfileSelect(ih iup.Ihandle) int {
name := ih.GetAttribute("VALUESTRING")
if name == "" {
return iup.DEFAULT
}
iup.ConfigSetVariableStr(config, profilesGroup, "Current", name)
iup.ConfigSave(config)
settingsApply(profileGroup(name))
return iup.DEFAULT
}
func onSave(iup.Ihandle) int {
name := currentProfile()
if iup.GetParam("Save Profile", nil, "Name: %s\n", &name) != 1 {
return iup.DEFAULT
}
name = strings.TrimSpace(name)
if name == "" || strings.ContainsAny(name, ".;") {
iup.Message("Invalid Name", "Profile name must not be empty or contain '.' or ';'.")
return iup.DEFAULT
}
settingsSave(profileGroup(name))
names := profileNames()
if !slices.Contains(names, name) {
names = append(names, name)
iup.ConfigSetVariableStr(config, profilesGroup, "Names", strings.Join(names, ";"))
}
iup.ConfigSetVariableStr(config, profilesGroup, "Current", name)
iup.ConfigSave(config)
fillProfileList()
return iup.DEFAULT
}
func onReset(iup.Ihandle) int {
settingsApply("")
return iup.DEFAULT
}
+124
View File
@@ -0,0 +1,124 @@
package main
import "github.com/gen2brain/iup-go/iup"
func setActive() {
if busy {
return
}
opts := options()
count := iup.GetHandle("Table").GetInt("NUMLIN")
if count == 0 {
iup.GetHandle("Remove").SetAttribute("ACTIVE", "NO")
iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "NO")
iup.GetHandle("Preview").SetAttribute("IMAGE", "logo")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
} else {
if index != -1 {
iup.GetHandle("Remove").SetAttribute("ACTIVE", "YES")
}
iup.GetHandle("RemoveAll").SetAttribute("ACTIVE", "YES")
}
active := "YES"
var tip string
switch {
case count == 0 && opts.OutDir == "":
active, tip = "NO", "Add files and set output directory"
case count == 0:
active, tip = "NO", "Add files"
case opts.OutDir == "":
active, tip = "NO", "Set output directory"
}
enabledTip := map[string]string{
"Thumbnail": "Extract cover thumbnails",
"Cover": "Extract covers",
"Convert": "Convert files to the selected format",
}
for _, h := range []string{"Thumbnail", "Cover", "Convert"} {
b := iup.GetHandle(h)
b.SetAttribute("ACTIVE", active)
if active == "YES" {
b.SetAttribute("TIP", enabledTip[h])
} else {
b.SetAttribute("TIP", tip)
}
}
if opts.NoConvert {
iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "NO")
iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "NO")
} else {
iup.GetHandle("VboxImage").SetAttribute("ACTIVE", "YES")
iup.GetHandle("VboxTransform").SetAttribute("ACTIVE", "YES")
}
canLossless := opts.Format == "webp" || opts.Format == "avif" || opts.Format == "jxl"
losslessOn := canLossless && opts.Lossless
if (opts.Format == "jpeg" || canLossless) && !opts.NoConvert && !losslessOn {
iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "NO")
}
if canLossless && !opts.NoConvert {
iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "YES")
iup.GetHandle("Lossless").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "NO")
iup.GetHandle("Lossless").SetAttribute("ACTIVE", "NO")
}
if opts.Width != 0 && opts.Height != 0 && !opts.NoConvert {
iup.GetHandle("Fit").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("Fit").SetAttribute("ACTIVE", "NO")
}
if opts.Combine {
iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxOutFile").SetAttribute("ACTIVE", "NO")
}
if opts.Archive == "zip" {
iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "YES")
} else {
iup.GetHandle("VboxZipLevel").SetAttribute("ACTIVE", "NO")
}
}
// setBusy locks the UI while an operation runs and turns Convert into a Cancel button.
func setBusy(on bool) {
busy = on
// Controls not governed by setActive; setActive owns the rest.
always := "YES"
if on {
always = "NO"
}
for _, h := range []string{"AddFiles", "AddDir", "Profile", "Reset", "Save", "Command", "Tabs", "Table"} {
iup.GetHandle(h).SetAttribute("ACTIVE", always)
}
convert := iup.GetHandle("Convert")
if on {
for _, h := range []string{"Remove", "RemoveAll", "Thumbnail", "Cover"} {
iup.GetHandle(h).SetAttribute("ACTIVE", "NO")
}
convert.SetAttribute("ACTIVE", "YES")
convert.SetAttribute("TITLE", "Cancel")
convert.SetAttribute("TIP", "Cancel the running operation (or press Esc)")
} else {
activeConv = nil
convert.SetAttribute("TITLE", "&Convert")
convert.SetAttribute("TIP", "Convert files to the selected format")
setActive() // restores the conditional buttons and option boxes
}
}
+653
View File
@@ -0,0 +1,653 @@
package main
import (
"bytes"
"fmt"
"image/gif"
"net/url"
"strconv"
"strings"
"github.com/gen2brain/cbconvert"
"github.com/gen2brain/iup-go/iup"
)
func setEffort(format string) {
val := iup.GetHandle("Effort")
var name string
switch format {
case "webp":
val.SetAttributes("MIN=0, MAX=6, SHOWTICKS=7, VALUE=4")
val.SetAttribute("TIP", "WEBP method, higher is better/slower (0-6, default 4)")
name = "Method"
case "avif":
val.SetAttributes("MIN=0, MAX=10, SHOWTICKS=11, VALUE=10")
val.SetAttribute("TIP", "AVIF speed, higher is faster/worse (0-10, default 10)")
name = "Speed"
case "jxl":
val.SetAttributes("MIN=1, MAX=10, SHOWTICKS=10, VALUE=7")
val.SetAttribute("TIP", "JXL effort, higher is better/slower (1-10, default 7)")
name = "Effort"
default:
return
}
val.SetAttribute("EFFORTNAME", name)
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", name, val.GetInt("VALUE")))
iup.Refresh(iup.GetHandle("LabelEffort"))
}
// tableRowColors sets the alternating row colors for dark or light mode.
func tableRowColors(t iup.Ihandle, dark bool) {
even, odd := "#F0F0F0", "#FFFFFF"
if dark {
even, odd = "#3A3A3A", "#2D2D2D"
}
t.SetAttribute("EVENROWCOLOR", even)
t.SetAttribute("ODDROWCOLOR", odd)
}
func list() iup.Ihandle {
t := iup.Table().SetHandle("Table")
t.SetAttributes(map[string]string{
"EXPAND": "YES",
"NUMCOL": "3",
"NUMLIN": "0",
"TITLE1": "Title",
"TITLE2": "Type",
"TITLE3": "Size (MiB)",
"WIDTH1": "150",
"WIDTH2": "50",
"WIDTH3": "100",
"ALIGNMENT2": "ACENTER",
"ALIGNMENT3": "ARIGHT",
"SELECTIONMODE": "SINGLE",
"USERRESIZE": "YES",
"STRETCHLAST": "NO",
"FOCUSRECT": "NO",
"SORTABLE": "YES",
"ALTERNATECOLOR": "YES",
})
tableRowColors(t, iup.GetGlobal("DARKMODE") == "YES" && iup.GetGlobal("AUTODARKMODE") == "YES")
t.SetCallback("ENTERITEM_CB", iup.EnterItemFunc(func(ih iup.Ihandle, lin, col int) int {
index = lin - 1
setActive()
previewPost()
return iup.DEFAULT
}))
t.SetCallback("SORT_CB", iup.TableSortFunc(onSort))
t.SetCallback("DROPFILES_CB", iup.DropFilesFunc(func(ih iup.Ihandle, fileName string, num, x, y int) int {
dec, err := url.QueryUnescape(fileName)
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
conv := cbconvert.New(options())
fs, err := conv.Files([]string{dec})
if err != nil {
iup.PostMessage(iup.GetHandle("dlg"), err.Error(), 0, 0)
fmt.Println(err)
return iup.DEFAULT
}
wasEmpty := len(files) == 0
for _, file := range fs {
appendFile(file)
}
if wasEmpty && len(files) > 0 {
selectRow(0)
}
setActive()
if wasEmpty {
previewPost()
}
return iup.DEFAULT
}))
return iup.Vbox(t)
}
func previewSize() (int, int) {
var width, height int
sp := strings.Split(iup.GetHandle("Preview").GetAttribute("RASTERSIZE"), "x")
if len(sp) == 2 {
width, _ = strconv.Atoi(sp[0])
height, _ = strconv.Atoi(sp[1])
}
return width, height
}
func preview() iup.Ihandle {
return iup.Frame(
iup.Vbox(
iup.Label("").SetAttributes("EXPAND=YES, ALIGNMENT=ACENTER, MINSIZE=400x, IMAGE=cover").SetHandle("Preview").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
img := p.(cbconvert.Image)
iup.GetHandle("Loading").SetAttributes("VISIBLE=NO, STOP=YES")
if img.Image != nil && len(s) == 0 {
iup.Destroy(iup.GetHandle("cover"))
iup.ImageFromImage(img.Image).SetHandle("cover")
ih.SetAttribute("IMAGE", "cover")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", fmt.Sprintf("%s (%dx%d)", img.SizeHuman, img.Width, img.Height))
} else {
ih.SetAttribute("IMAGE", "logo")
iup.GetHandle("PreviewInfo").SetAttribute("TITLE", "")
sp := strings.Split(s, ": ")
if len(sp) > 1 {
iup.MessageError(ih, fmt.Sprintf("%s\n\n%s", sp[0], strings.Join(sp[1:], ": ")))
}
}
return iup.DEFAULT
})),
iup.Label("").SetAttributes("EXPAND=HORIZONTAL, ALIGNMENT=ACENTER").SetHandle("PreviewInfo"),
),
)
}
func tabInput() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Toggle(" Recurse SubDirectories").SetHandle("Recursive").
SetAttributes(`TIP="Process subdirectories recursively"`),
iup.Toggle(" Only Grayscale Images").SetHandle("NoRGB").
SetAttributes(`TIP="Do not convert images that have RGB colorspace"`),
iup.Toggle(" Exclude Cover").SetHandle("NoCover").
SetAttributes(`TIP="Do not convert the cover image"`),
iup.Toggle(" Remove Non-Image Files from the Archive").SetHandle("NoNonImage").
SetAttribute("TIP", "Remove .nfo, .xml, .txt files from the archive"),
iup.Toggle(" Do not Transform or Convert Images").SetHandle("NoConvert").
SetAttributes(`TIP="Copy images from archive or directory without modifications"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Label("Minimum Size (MiB):"),
iup.Text().SetAttributes(`SPIN=YES, SPINMAX=2048, VISIBLECOLUMNS=4, MASK="/d*"`).SetHandle("Size").
SetAttributes(`TIP="Process only files larger than minimum size"`),
),
iup.Vbox(
iup.Label("Document DPI:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"EDITBOX": "YES",
"VISIBLECOLUMNS": "6",
"VALUE": "Default",
"1": "Default",
"2": "150",
"3": "300",
"4": "600",
"5": "1200",
}).SetHandle("DPI").
SetAttribute("TIP", "Resolution for rendering documents (PDF, EPUB, etc.); Default is 300"),
),
).SetAttributes("NGAP=10"),
).SetHandle("VboxInput")
}
func tabOutput() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Vbox(
iup.Label("Output Directory:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutDir").
SetAttribute("TIP", "Directory where converted files are written (required)").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputDirectory)),
),
iup.Vbox(
iup.Label("Add Suffix to Output File:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("Suffix").
SetAttribute("TIP", "Add suffix to filename, i.e. filename_suffix.cbz"),
),
iup.Vbox(
iup.Label("Archive Format:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "ZIP",
"2": "TAR",
}).SetHandle("Archive").
SetAttribute("TIP", "Output container: ZIP (.cbz) or uncompressed TAR (.cbt)").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Compression:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "Default",
"2": "Store (none)",
"3": "1",
"4": "2",
"5": "3",
"6": "4",
"7": "5",
"8": "6",
"9": "7",
"10": "8",
"11": "9",
}).SetHandle("ZipLevel").
SetAttribute("TIP", "ZIP compression: Store disables it, 1 is fastest, 9 is smallest"),
).SetHandle("VboxZipLevel"),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Toggle(" Combine into single file").SetHandle("Combine").
SetAttributes(`TIP="Merge all listed files into one archive"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Output File:"),
iup.Text().SetAttributes("VISIBLECOLUMNS=16, MINSIZE=100x").SetHandle("OutFile").
SetAttribute("TIP", "Combined file name (default: first input + -combined)"),
iup.Space().SetAttribute("SIZE", "5x0"),
iup.Button("Browse...").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onOutputFile)),
).SetHandle("VboxOutFile"),
).SetAttributes("NGAP=10"),
).SetHandle("VboxOutput")
}
func tabImage() iup.Ihandle {
return iup.Hbox(
iup.Vbox(
iup.Vbox(
iup.Label("Format:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "JPEG",
"2": "PNG",
"3": "TIFF",
"4": "BMP",
"5": "WEBP",
"6": "AVIF",
"7": "JXL",
}).SetHandle("Format").
SetAttribute("TIP", "Output image format for the converted pages").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setEffort(strings.ToLower(ih.GetAttribute("VALUESTRING")))
setActive()
previewPost()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Size:"),
iup.Hbox(
iup.Text().SetAttributes(`CUEBANNER="width", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Width").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "2"),
iup.Label("x"),
iup.Space().SetAttribute("SIZE", "2"),
iup.Text().SetAttributes(`CUEBANNER="height", VISIBLECOLUMNS=6, MASK="/d*"`).SetHandle("Height").
SetAttribute("TIP", "If one of, width or height is not set, the image aspect ratio is preserved").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
),
iup.Vbox(
iup.Toggle(" Best Fit").SetHandle("Fit").
SetAttributes(`TIP="Best fit for required width and height"`),
),
iup.Vbox(
iup.Label("Resize Filter:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "3",
"TIP": "Linear is the bilinear filter, smooth and reasonably fast",
"1": "NearestNeighbor",
"2": "Box",
"3": "Linear",
"4": "MitchellNetravali",
"5": "CatmullRom",
"6": "Gaussian",
"7": "Lanczos",
}).SetHandle("Filter").SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onFilterChanged)),
),
).SetAttributes("NGAP=10"),
iup.Space().SetAttribute("SIZE", "15"),
iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Quality: "),
iup.Label("75").SetHandle("LabelQuality"),
).SetAttributes("NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=0, MAX=100, VALUE=75, SHOWTICKS=10`).SetHandle("Quality").
SetAttribute("TIP", "Quality affects JPEG, WEBP, AVIF and JXL").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelQuality").SetAttribute("TITLE", ih.GetInt("VALUE"))
iup.Refresh(iup.GetHandle("LabelQuality"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetHandle("VboxQuality"),
iup.Vbox(
iup.Label("Effort:").SetHandle("LabelEffort"),
iup.Val("").SetAttributes(`MIN=0, MAX=10, VALUE=0, SHOWTICKS=11`).SetHandle("Effort").
SetAttribute("TIP", "Encoder speed/effort (WEBP, AVIF, JXL)").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelEffort").SetAttribute("TITLE", fmt.Sprintf("%s: %d", ih.GetAttribute("EFFORTNAME"), ih.GetInt("VALUE")))
iup.Refresh(iup.GetHandle("LabelEffort"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
).SetHandle("VboxEffort"),
iup.Vbox(
iup.Toggle(" Lossless").SetHandle("Lossless").
SetAttributes(`TIP="Lossless compression (WEBP, AVIF, JXL), ignores quality"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
setActive()
previewPost()
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Toggle(" Grayscale").SetHandle("Grayscale").
SetAttributes(`TIP="Convert images to grayscale (monochromatic)"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
return iup.DEFAULT
})),
),
).SetAttributes("NGAP=10"),
).SetHandle("VboxImage")
}
func tabTransform() iup.Ihandle {
return iup.Vbox(
iup.Vbox(
iup.Hbox(
iup.Label("Brightness: "),
iup.Label("0").SetHandle("LabelBrightness"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Brightness").
SetAttributes(`TIP="Adjust the brightness of the images"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelBrightness").SetAttribute("TITLE", iup.GetHandle("Brightness").GetInt("VALUE"))
iup.Refresh(iup.GetHandle("LabelBrightness"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Hbox(
iup.Label("Contrast: "),
iup.Label("0").SetHandle("LabelContrast"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=0"),
iup.Val("").SetAttributes(`MIN=-100, MAX=100, VALUE=0, SHOWTICKS=10`).SetHandle("Contrast").
SetAttributes(`TIP="Adjust the contrast of the images"`).
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
iup.GetHandle("LabelContrast").SetAttribute("TITLE", iup.GetHandle("Contrast").GetInt("VALUE"))
iup.Refresh(iup.GetHandle("LabelContrast"))
ih.SetAttribute("MYVALUE", ih.GetInt("VALUE"))
return iup.DEFAULT
})).
SetCallback("KILLFOCUS_CB", iup.KillFocusFunc(func(ih iup.Ihandle) int {
if ih.GetAttribute("MYVALUE") != "" {
previewPost()
}
ih.SetAttribute("MYVALUE", "")
return iup.DEFAULT
})),
),
iup.Vbox(
iup.Label("Rotate:"),
iup.List().SetAttributes(map[string]string{
"DROPDOWN": "YES",
"VALUE": "1",
"1": "0",
"2": "90",
"3": "180",
"4": "270",
}).SetHandle("Rotate").
SetAttribute("TIP", "Rotate every page clockwise by the given angle in degrees").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int {
previewPost()
return iup.DEFAULT
})),
),
).SetHandle("VboxTransform").SetAttributes("NGAP=10")
}
func tabs() iup.Ihandle {
return iup.Tabs(
tabInput().SetAttributes("TABTITLE=Input, NMARGIN=10x10"),
tabOutput().SetAttributes("TABTITLE=Output, NMARGIN=10x10"),
tabImage().SetAttributes("TABTITLE=Image, NMARGIN=10x10"),
tabTransform().SetAttributes("TABTITLE=Transform, NMARGIN=10x10"),
).SetHandle("Tabs")
}
func buttons() iup.Ihandle {
addFiles := iup.Button("Add &Files...").SetHandle("AddFiles").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddFiles))
addDir := iup.Button("Add &Dir...").SetHandle("AddDir").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onAddDir))
remove := iup.Button("Remove").SetHandle("Remove").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemove))
removeAll := iup.Button("Remove All").SetHandle("RemoveAll").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onRemoveAll))
thumbnail := iup.Button("Thumbnail").SetHandle("Thumbnail").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onThumbnail))
cover := iup.Button("Cover").SetHandle("Cover").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onCover))
convert := iup.Button("&Convert").SetHandle("Convert").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetCallback("ACTION", iup.ActionFunc(onConvert))
reset := iup.Button("Reset").SetHandle("Reset").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Restore all settings to their defaults").
SetCallback("ACTION", iup.ActionFunc(onReset))
save := iup.Button("Save").SetHandle("Save").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Save current settings to a profile").
SetCallback("ACTION", iup.ActionFunc(onSave))
command := iup.Button("Command").SetHandle("Command").SetAttributes("PADDING=DEFAULTBUTTONPADDING").
SetAttribute("TIP", "Show the equivalent command line").
SetCallback("ACTION", iup.ActionFunc(onCommand))
profile := iup.List().SetAttributes("DROPDOWN=YES").SetHandle("Profile").
SetAttribute("TIP", "Select a settings profile").
SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(onProfileSelect))
iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command).SetAttribute("NORMALIZE", "BOTH")
iup.Normalizer(addFiles, addDir, remove, removeAll, thumbnail, cover, convert, reset, save, command, profile).SetAttribute("NORMALIZE", "HORIZONTAL")
return iup.Vbox(
iup.Vbox(
addFiles,
addDir,
remove,
removeAll,
).SetAttribute("NGAP", "2"),
iup.Space().SetAttribute("SIZE", "x5"),
iup.Vbox(
thumbnail,
cover,
).SetAttribute("NGAP", "2"),
iup.Space().SetAttribute("SIZE", "x5"),
iup.Vbox(
convert,
),
iup.Fill(),
iup.Vbox(
iup.Label("Profile:"),
profile,
reset,
save,
command,
).SetAttribute("NGAP", "2"),
).SetHandle("Buttons").SetAttributes("ALIGNMENT=ACENTER")
}
func status() iup.Ihandle {
return iup.Hbox(
loading(),
iup.Fill(),
iup.Label("File 1 of 1").SetHandle("LabelStatus1").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5"),
iup.Label("(000/000)").SetHandle("LabelStatus2").SetAttributes("VISIBLE=NO"),
iup.Space().SetAttribute("SIZE", "5"),
iup.ProgressBar().SetAttributes("VISIBLE=NO").SetHandle("ProgressBar").
SetCallback("POSTMESSAGE_CB", iup.PostMessageFunc(func(ih iup.Ihandle, s string, i int, p any) int {
switch s {
case "convert":
conv := p.(*cbconvert.Converter)
ih.SetAttributes("VALUE=0, VISIBLE=YES")
ih.SetAttribute("MAX", conv.Ncontents)
iup.GetHandle("LabelStatus1").SetAttribute("TITLE", fmt.Sprintf("File %d of %d", conv.CurrFile, conv.Nfiles))
iup.GetHandle("LabelStatus1").SetAttributes("VISIBLE=YES")
iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES")
iup.Refresh(iup.GetHandle("StatusBar"))
case "start":
conv := p.(*cbconvert.Converter)
ih.SetAttributes("VALUE=0, VISIBLE=YES")
ih.SetAttribute("MAX", conv.Nfiles)
iup.GetHandle("LabelStatus2").SetAttributes("VISIBLE=YES")
case "progress":
conv := p.(*cbconvert.Converter)
ih.SetAttribute("VALUE", conv.CurrContent)
iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrContent, conv.Ncontents))
iup.Refresh(iup.GetHandle("StatusBar"))
case "progress2":
conv := p.(*cbconvert.Converter)
ih.SetAttribute("VALUE", conv.CurrFile)
iup.GetHandle("LabelStatus2").SetAttribute("TITLE", fmt.Sprintf("(%03d/%03d)", conv.CurrFile, conv.Nfiles))
iup.Refresh(iup.GetHandle("StatusBar"))
case "finish":
setBusy(false)
iup.GetHandle("LabelStatus1").SetAttributes(`TITLE="", VISIBLE=NO`)
iup.GetHandle("LabelStatus2").SetAttributes(`TITLE="", VISIBLE=NO`)
ih.SetAttributes("VALUE=0, VISIBLE=NO")
iup.Refresh(iup.GetHandle("StatusBar"))
iup.GetHandle("dlg").SetCallback("K_ANY", nil)
iup.GetHandle("dlg").SetCallback("CLOSE_CB", nil)
}
return iup.DEFAULT
})),
iup.Space().SetAttribute("SIZE", "5x0"),
).SetAttributes("ALIGNMENT=ACENTER, NMARGIN=5x5").SetHandle("StatusBar")
}
func loading() iup.Ihandle {
img, _ := gif.DecodeAll(bytes.NewReader(appLoading))
animation := iup.User()
for idx, i := range img.Image {
name := fmt.Sprintf("Loading%d", idx)
iup.ImageFromImage(i).SetHandle(name)
iup.Append(animation, iup.GetHandle(name))
}
return iup.AnimatedLabel(animation).SetAttributes("VISIBLE=NO").SetHandle("Loading")
}
+6 -3
View File
@@ -175,6 +175,7 @@ func parseFlags() (cbconvert.Options, []string) {
convert.IntVar(&opts.Width, "width", 0, "Image width")
convert.IntVar(&opts.Height, "height", 0, "Image height")
convert.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
convert.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
convert.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif, jxl")
convert.StringVar(&opts.Archive, "archive", "zip", "Archive format, valid values are zip, tar")
convert.IntVar(&opts.ZipLevel, "zip-level", -1, "ZIP compression level, 0 disables compression, 1-9 sets deflate level (1 fastest, 9 smallest), -1 uses the default")
@@ -202,6 +203,7 @@ func parseFlags() (cbconvert.Options, []string) {
cover.IntVar(&opts.Width, "width", 0, "Image width")
cover.IntVar(&opts.Height, "height", 0, "Image height")
cover.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
cover.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
cover.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif")
cover.IntVar(&opts.Quality, "quality", 75, "Image quality")
cover.IntVar(&opts.Effort, "effort", -1, "Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default")
@@ -216,6 +218,7 @@ func parseFlags() (cbconvert.Options, []string) {
thumbnail.IntVar(&opts.Width, "width", 0, "Image width")
thumbnail.IntVar(&opts.Height, "height", 0, "Image height")
thumbnail.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height")
thumbnail.IntVar(&opts.DPI, "dpi", 0, "Document rendering resolution in DPI (PDF, EPUB, etc.), 0 uses the default (300)")
thumbnail.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos")
thumbnail.StringVar(&opts.OutDir, "outdir", ".", "Output directory")
thumbnail.StringVar(&opts.OutFile, "outfile", "", "Output file")
@@ -236,7 +239,7 @@ func parseFlags() (cbconvert.Options, []string) {
fmt.Fprintf(os.Stderr, "Usage: %s <command> [<flags>] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0]))
fmt.Fprintf(os.Stderr, "\nCommands:\n")
fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n")
order := []string{"width", "height", "fit", "format", "archive", "zip-level", "quality", "effort", "lossless", "combine", "outfile", "filter", "no-cover", "no-rgb",
order := []string{"width", "height", "fit", "dpi", "format", "archive", "zip-level", "quality", "effort", "lossless", "combine", "outfile", "filter", "no-cover", "no-rgb",
"no-nonimage", "no-convert", "grayscale", "rotate", "brightness", "contrast", "suffix", "outdir", "size", "recursive", "quiet"}
for _, name := range order {
f := convert.Lookup(name)
@@ -244,14 +247,14 @@ func parseFlags() (cbconvert.Options, []string) {
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n")
order = []string{"width", "height", "fit", "format", "quality", "effort", "lossless", "combine", "outfile", "filter", "outdir", "size", "recursive", "quiet"}
order = []string{"width", "height", "fit", "dpi", "format", "quality", "effort", "lossless", "filter", "outdir", "size", "recursive", "quiet"}
for _, name := range order {
f := cover.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)
fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue)
}
fmt.Fprintf(os.Stderr, "\n thumbnail\n \tExtract cover thumbnail (freedesktop spec.)\n\n")
order = []string{"width", "height", "fit", "filter", "outdir", "outfile", "size", "recursive", "quiet"}
order = []string{"width", "height", "fit", "dpi", "filter", "outdir", "outfile", "size", "recursive", "quiet"}
for _, name := range order {
f := thumbnail.Lookup(name)
fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name)