diff --git a/cbconvert.go b/cbconvert.go index 4523d03..5b8ac42 100644 --- a/cbconvert.go +++ b/cbconvert.go @@ -25,6 +25,8 @@ type Options struct { Archive string // JPEG image quality Quality int + // Encoder speed/effort, format-specific: webp method 0-6, avif speed 0-10, jxl effort 1-10; -1 uses the format default + Effort int // Image width Width int // Image height @@ -125,6 +127,7 @@ func NewOptions() Options { o.Format = "jpeg" o.Archive = "zip" o.Quality = 75 + o.Effort = -1 o.Filter = 2 return o diff --git a/cbconvert_convert.go b/cbconvert_convert.go index 1f5fd29..b1f097e 100644 --- a/cbconvert_convert.go +++ b/cbconvert_convert.go @@ -398,11 +398,23 @@ func (c *Converter) imageEncode(img image.Image, w io.Writer) error { opts.DCTMethod = jpegli.DefaultDCTMethod err = jpegli.Encode(w, img, opts) case "webp": - err = webp.Encode(w, img, webp.Options{Quality: c.Opts.Quality, Method: webp.DefaultMethod}) + method := webp.DefaultMethod + if c.Opts.Effort >= 0 { + method = min(max(c.Opts.Effort, 0), 6) + } + err = webp.Encode(w, img, webp.Options{Quality: c.Opts.Quality, Method: method}) case "avif": - err = avif.Encode(w, img, avif.Options{Quality: c.Opts.Quality, Speed: avif.DefaultSpeed}) + speed := avif.DefaultSpeed + if c.Opts.Effort >= 0 { + speed = min(max(c.Opts.Effort, 0), 10) + } + err = avif.Encode(w, img, avif.Options{Quality: c.Opts.Quality, Speed: speed}) case "jxl": - err = jpegxl.Encode(w, img, jpegxl.Options{Quality: c.Opts.Quality, Effort: jpegxl.DefaultEffort}) + effort := jpegxl.DefaultEffort + if c.Opts.Effort >= 0 { + effort = min(max(c.Opts.Effort, 1), 10) + } + err = jpegxl.Encode(w, img, jpegxl.Options{Quality: c.Opts.Quality, Effort: effort}) case "bmp": opts := &gobmp.EncoderOptions{} opts.SupportTransparency(false) diff --git a/cmd/cbconvert-gui/main.go b/cmd/cbconvert-gui/main.go index e11af0b..e3b2927 100644 --- a/cmd/cbconvert-gui/main.go +++ b/cmd/cbconvert-gui/main.go @@ -140,6 +140,12 @@ func options() cbconvert.Options { 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.Grayscale = iup.GetHandle("Grayscale").GetAttribute("VALUE") == "ON" opts.Brightness = iup.GetHandle("Brightness").GetInt("VALUE") opts.Contrast = iup.GetHandle("Contrast").GetInt("VALUE") @@ -200,6 +206,12 @@ func setActive() { iup.GetHandle("VboxQuality").SetAttribute("ACTIVE", "NO") } + if (opts.Format == "webp" || opts.Format == "avif" || opts.Format == "jxl") && !opts.NoConvert { + iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "YES") + } else { + iup.GetHandle("VboxEffort").SetAttribute("ACTIVE", "NO") + } + if opts.Width != 0 && opts.Height != 0 && !opts.NoConvert { iup.GetHandle("Fit").SetAttribute("ACTIVE", "YES") } else { @@ -207,6 +219,32 @@ func setActive() { } } +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"))) +} + func layout() iup.Ihandle { return iup.Vbox( iup.Hbox( @@ -409,6 +447,7 @@ func tabs() iup.Ihandle { "7": "JXL", }).SetHandle("Format"). SetCallback("VALUECHANGED_CB", iup.ValueChangedFunc(func(ih iup.Ihandle) int { + setEffort(strings.ToLower(ih.GetAttribute("VALUESTRING"))) setActive() previewPost() @@ -494,6 +533,25 @@ func tabs() iup.Ihandle { return iup.DEFAULT })), ).SetHandle("VboxQuality"), + iup.Vbox( + iup.Label("").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"))) + 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(" Grayscale").SetHandle("Grayscale"). SetAttributes(`TIP="Convert images to grayscale (monochromatic)"`). diff --git a/cmd/cbconvert/main.go b/cmd/cbconvert/main.go index a6cde7c..d438471 100644 --- a/cmd/cbconvert/main.go +++ b/cmd/cbconvert/main.go @@ -167,6 +167,7 @@ func parseFlags() (cbconvert.Options, []string) { 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.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.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos") convert.BoolVar(&opts.NoCover, "no-cover", false, "Do not convert the cover image") convert.BoolVar(&opts.NoRGB, "no-rgb", false, "Do not convert images that have RGB colorspace") @@ -188,6 +189,7 @@ func parseFlags() (cbconvert.Options, []string) { cover.BoolVar(&opts.Fit, "fit", false, "Best fit for required width and height") cover.StringVar(&opts.Format, "format", "jpeg", "Image format, valid values are jpeg, png, tiff, bmp, webp, avif") cover.IntVar(&opts.Quality, "quality", 75, "Image quality") + cover.IntVar(&opts.Effort, "effort", -1, "Encoder speed/effort, format-specific (webp method 0-6, avif speed 0-10, jxl effort 1-10), -1 uses the format default") cover.IntVar(&opts.Filter, "filter", 2, "0=NearestNeighbor, 1=Box, 2=Linear, 3=MitchellNetravali, 4=CatmullRom, 6=Gaussian, 7=Lanczos") cover.StringVar(&opts.OutDir, "outdir", ".", "Output directory") cover.IntVar(&opts.Size, "size", 0, "Process only files larger than size (in MB)") @@ -218,7 +220,7 @@ func parseFlags() (cbconvert.Options, []string) { fmt.Fprintf(os.Stderr, "Usage: %s [] [file1 dir1 ... fileOrDirN]\n\n", filepath.Base(os.Args[0])) fmt.Fprintf(os.Stderr, "\nCommands:\n") fmt.Fprintf(os.Stderr, "\n convert\n \tConvert archive or document\n\n") - order := []string{"width", "height", "fit", "format", "archive", "quality", "filter", "no-cover", "no-rgb", + order := []string{"width", "height", "fit", "format", "archive", "quality", "effort", "filter", "no-cover", "no-rgb", "no-nonimage", "no-convert", "grayscale", "rotate", "brightness", "contrast", "suffix", "outdir", "size", "recursive", "quiet"} for _, name := range order { f := convert.Lookup(name) @@ -226,7 +228,7 @@ func parseFlags() (cbconvert.Options, []string) { fmt.Fprintf(os.Stderr, "%v (default %q)\n", f.Usage, f.DefValue) } fmt.Fprintf(os.Stderr, "\n cover\n \tExtract cover\n\n") - order = []string{"width", "height", "fit", "format", "quality", "filter", "outdir", "size", "recursive", "quiet"} + order = []string{"width", "height", "fit", "format", "quality", "effort", "filter", "outdir", "size", "recursive", "quiet"} for _, name := range order { f := cover.Lookup(name) fmt.Fprintf(os.Stderr, " --%s\n \t", f.Name) @@ -297,7 +299,7 @@ func parseFlags() (cbconvert.Options, []string) { return opts, args } -// piped checks if we have a piped stdin. +// piped checks if we have piped stdin. func piped() bool { f, err := os.Stdin.Stat() if err != nil { @@ -311,7 +313,7 @@ func piped() bool { return true } -// lines returns slice of lines from reader. +// lines returns slice of lines from the reader. func lines(r io.Reader) []string { data := make([]string, 0) scanner := bufio.NewScanner(r)