1
0
mirror of https://github.com/coalaura/ffwebp.git synced 2025-07-18 06:14:34 +00:00

various improvements

This commit is contained in:
Laura
2025-01-22 18:19:20 +01:00
parent 8c09ef1178
commit 04e61b791f
10 changed files with 266 additions and 109 deletions

View File

@ -27,6 +27,9 @@ var (
func TestFFWebP(t *testing.T) { func TestFFWebP(t *testing.T) {
opts.Silent = true opts.Silent = true
opts.Format = "png"
encoder, _ := ResolveImageEncoder()
for _, file := range TestFiles { for _, file := range TestFiles {
log.Printf("Testing file: %s\n", file) log.Printf("Testing file: %s\n", file)
@ -47,7 +50,7 @@ func TestFFWebP(t *testing.T) {
var result bytes.Buffer var result bytes.Buffer
err = WriteImage(&result, img, "png") err = encoder(&result, img)
if err != nil { if err != nil {
log.Fatalf("Failed to encode png image: %v", err) log.Fatalf("Failed to encode png image: %v", err)
} }

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.23.4
require ( require (
github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670 github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670
github.com/coalaura/arguments v1.5.2 github.com/coalaura/arguments v1.5.2
github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4
github.com/gen2brain/avif v0.3.2 github.com/gen2brain/avif v0.3.2
github.com/gen2brain/heic v0.3.1 github.com/gen2brain/heic v0.3.1
github.com/gen2brain/jpegxl v0.3.1 github.com/gen2brain/jpegxl v0.3.1

6
go.sum
View File

@ -1,13 +1,11 @@
github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670 h1:FQPKKjDhzG0T4ew6dm6MGrXb4PRAi8ZmTuYuxcF62BM= github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670 h1:FQPKKjDhzG0T4ew6dm6MGrXb4PRAi8ZmTuYuxcF62BM=
github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670/go.mod h1:iRWAFbKXMMkVQyxZ1PfGlkBr1TjATx1zy2MRprV7A3Q= github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670/go.mod h1:iRWAFbKXMMkVQyxZ1PfGlkBr1TjATx1zy2MRprV7A3Q=
github.com/coalaura/arguments v1.5.0 h1:apdZXxINepy8qCHYKJYEnLHvV6VbYgsIMPbZCWZAPQY=
github.com/coalaura/arguments v1.5.0/go.mod h1:F5cdI+Gn1qi5K6qqvAdxdTD2TXkny+gTKU0o6NN1MlU=
github.com/coalaura/arguments v1.5.1 h1:Gc7uODI3WlcVxmQpxoUQF7Y2j3EMDpSlVqhmGMtLC8Q=
github.com/coalaura/arguments v1.5.1/go.mod h1:F5cdI+Gn1qi5K6qqvAdxdTD2TXkny+gTKU0o6NN1MlU=
github.com/coalaura/arguments v1.5.2 h1:hRLKo6XmAzCDOS/unCUVAIYl3WU/i6QX59nBh0T31cw= github.com/coalaura/arguments v1.5.2 h1:hRLKo6XmAzCDOS/unCUVAIYl3WU/i6QX59nBh0T31cw=
github.com/coalaura/arguments v1.5.2/go.mod h1:F5cdI+Gn1qi5K6qqvAdxdTD2TXkny+gTKU0o6NN1MlU= github.com/coalaura/arguments v1.5.2/go.mod h1:F5cdI+Gn1qi5K6qqvAdxdTD2TXkny+gTKU0o6NN1MlU=
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4 h1:BBade+JlV/f7JstZ4pitd4tHhpN+w+6I+LyOS7B4fyU=
github.com/ericpauley/go-quantize v0.0.0-20200331213906-ae555eb2afa4/go.mod h1:H7chHJglrhPPzetLdzBleF8d22WYOv7UM/lEKYiwlKM=
github.com/gen2brain/avif v0.3.2 h1:XUR0CBl5n4ISFJE8/pc1RMEKt5KUVoW8InctN+M7+DQ= github.com/gen2brain/avif v0.3.2 h1:XUR0CBl5n4ISFJE8/pc1RMEKt5KUVoW8InctN+M7+DQ=
github.com/gen2brain/avif v0.3.2/go.mod h1:tdL2sV6oOJXBZZvT5iP55VEM1X2c3/yJmYKMJTl8fXg= github.com/gen2brain/avif v0.3.2/go.mod h1:tdL2sV6oOJXBZZvT5iP55VEM1X2c3/yJmYKMJTl8fXg=
github.com/gen2brain/heic v0.3.1 h1:ClY5YTdXdIanw7pe9ZVUM9XcsqH6CCCa5CZBlm58qOs= github.com/gen2brain/heic v0.3.1 h1:ClY5YTdXdIanw7pe9ZVUM9XcsqH6CCCa5CZBlm58qOs=

24
help.go
View File

@ -1,27 +1,29 @@
package main package main
import ( import (
"fmt"
"os" "os"
"github.com/coalaura/arguments" "github.com/coalaura/arguments"
) )
func header() {
println(" __ __ _")
println(" / _|/ _|_ _____| |__ _ __")
println("| |_| |_\\ \\ /\\ / / _ \\ '_ \\| '_ \\")
println("| _| _|\\ V V / __/ |_) | |_) |")
println("|_| |_| \\_/\\_/ \\___|_.__/| .__/")
print(" |_| ")
println(Version)
println()
}
func help() { func help() {
if !opts.Help { if !opts.Help {
return return
} }
println(" __ __ _") println("ffwebp [options] <input> [output]\n")
println(" / _|/ _| | |")
println("| |_| |___ _____| |__ _ __")
println("| _| _\\ \\ /\\ / / _ \\ '_ \\| '_ \\")
println("| | | | \\ V V / __/ |_) | |_) |")
println("|_| |_| \\_/\\_/ \\___|_.__/| .__/")
println(" | |")
fmt.Printf(" %s |_|\n", Version)
println("\nffwebp [options] <input> [output]\n")
arguments.ShowHelp(true) arguments.ShowHelp(true)

View File

@ -15,6 +15,8 @@ import (
"golang.org/x/image/tiff" "golang.org/x/image/tiff"
) )
type Encoder func(io.Writer, image.Image) error
func ReadImage(input io.ReadSeeker) (image.Image, error) { func ReadImage(input io.ReadSeeker) (image.Image, error) {
decoder, err := GetDecoderFromContent(input) decoder, err := GetDecoderFromContent(input)
if err != nil { if err != nil {
@ -24,55 +26,77 @@ func ReadImage(input io.ReadSeeker) (image.Image, error) {
return decoder(input) return decoder(input)
} }
func WriteImage(output io.Writer, img image.Image, format string) error { func ResolveImageEncoder() (Encoder, error) {
switch format { table := NewOptionsTable()
switch opts.Format {
case "webp": case "webp":
options := GetWebPOptions() options := GetWebPOptions()
LogWebPOptions(options) table.AddWebPOptions(options)
return webp.Encode(output, img, options) return func(output io.Writer, img image.Image) error {
return webp.Encode(output, img, options)
}, nil
case "jpeg": case "jpeg":
options := GetJpegOptions() options := GetJpegOptions()
LogJpegOptions(options) table.AddJpegOptions(options)
return jpeg.Encode(output, img, options) return func(output io.Writer, img image.Image) error {
return jpeg.Encode(output, img, options)
}, nil
case "png": case "png":
encoder := GetPNGOptions() encoder := GetPNGOptions()
LogPNGOptions(encoder) table.AddPNGOptions(encoder)
return encoder.Encode(output, img) return func(output io.Writer, img image.Image) error {
return encoder.Encode(output, img)
}, nil
case "gif": case "gif":
options := GetGifOptions() options := GetGifOptions()
LogGifOptions(options) table.AddGifOptions(options)
return gif.Encode(output, img, options) return func(output io.Writer, img image.Image) error {
return gif.Encode(output, img, options)
}, nil
case "bmp": case "bmp":
return bmp.Encode(output, img) return func(output io.Writer, img image.Image) error {
return bmp.Encode(output, img)
}, nil
case "tiff": case "tiff":
options := GetTiffOptions() options := GetTiffOptions()
LogTiffOptions(options) table.AddTiffOptions(options)
return tiff.Encode(output, img, options) return func(output io.Writer, img image.Image) error {
return tiff.Encode(output, img, options)
}, nil
case "avif": case "avif":
options := GetAvifOptions() options := GetAvifOptions()
LogAvifOptions(options) table.AddAvifOptions(options)
return avif.Encode(output, img, options) return func(output io.Writer, img image.Image) error {
return avif.Encode(output, img, options)
}, nil
case "jxl": case "jxl":
options := GetJxlOptions() options := GetJxlOptions()
LogJxlOptions(options) table.AddJxlOptions(options)
jpegxl.Encode(output, img, options) return func(output io.Writer, img image.Image) error {
return jpegxl.Encode(output, img, options)
}, nil
case "ico": case "ico":
return ico.Encode(output, img) table.Print()
return func(output io.Writer, img image.Image) error {
return ico.Encode(output, img)
}, nil
} }
return fmt.Errorf("unsupported output format: %s", format) return nil, fmt.Errorf("unsupported output format: %s", opts.Format)
} }

27
log.go
View File

@ -3,24 +3,39 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"github.com/coalaura/arguments"
) )
var ( var (
silent bool silent bool
) )
func info(fm string, args ...interface{}) { func debug(msg string) {
if silent { if silent {
return return
} }
fmt.Printf(fm, args...) b := arguments.NewBuilder(true)
fmt.Println()
b.Mute()
b.WriteString("# ")
b.WriteString(msg)
println(b.String())
} }
func fatalf(fm string, args ...interface{}) { func fatalf(format string, args ...interface{}) {
fmt.Printf("ERROR: "+fm, args...) must(fmt.Errorf(format, args...))
fmt.Println() }
func must(err error) {
if err == nil {
return
}
print("\033[38;5;160mERROR: \033[38;5;248m")
println(err.Error())
os.Exit(1) os.Exit(1)
} }

34
main.go
View File

@ -5,14 +5,16 @@ import (
) )
func main() { func main() {
header()
parse() parse()
info("Reading input image...") encoder, err := ResolveImageEncoder()
must(err)
debug("Reading input image...")
in, err := os.OpenFile(opts.Input, os.O_RDONLY, 0) in, err := os.OpenFile(opts.Input, os.O_RDONLY, 0)
if err != nil { must(err)
fatalf("Failed to open input file: %s", err)
}
defer in.Close() defer in.Close()
@ -24,27 +26,23 @@ func main() {
out = os.Stdout out = os.Stdout
} else { } else {
out, err = os.OpenFile(opts.Output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) out, err = os.OpenFile(opts.Output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil { must(err)
fatalf("Failed to open output file: %s", err)
}
defer out.Close() defer out.Close()
} }
info("Decoding input image...") debug("Decoding input image...")
img, err := ReadImage(in) img, err := ReadImage(in)
if err != nil { must(err)
fatalf("Failed to read image: %s", err)
}
info("Using output format: %s", opts.Format)
// Write image // Write image
info("Encoding output image...") debug("Encoding output image...")
err = WriteImage(out, img, opts.Format) img = Quantize(img)
if err != nil {
fatalf("Failed to write image: %s", err) err = encoder(out, img)
} must(err)
debug("Completed.")
} }

View File

@ -38,7 +38,7 @@ var opts = Options{
Output: "", Output: "",
Silent: false, Silent: false,
NumColors: 256, NumColors: 0,
Effort: 10, Effort: 10,
Format: "", Format: "",
Lossless: false, Lossless: false,
@ -59,14 +59,12 @@ func parse() {
// Common image options // Common image options
arguments.Register("quality", 'q', &opts.Quality).WithHelp("[avif|jpeg|jxl|webp] Quality level (1-100)") arguments.Register("quality", 'q', &opts.Quality).WithHelp("[avif|jpeg|jxl|webp] Quality level (1-100)")
arguments.Register("colors", 'c', &opts.NumColors).WithHelp("Number of colors to use when quantizing (0=no-quantizing, 256=max-colors)")
// AVIF // AVIF
arguments.Register("ratio", 'r', &opts.Ratio).WithHelp("[avif] YCbCr subsample-ratio (0=444, 1=422, 2=420, 3=440, 4=411, 5=410)") arguments.Register("ratio", 'r', &opts.Ratio).WithHelp("[avif] YCbCr subsample-ratio (0=444, 1=422, 2=420, 3=440, 4=411, 5=410)")
arguments.Register("speed", 'p', &opts.Speed).WithHelp("[avif] Encoder speed level (0=fast, 10=slower-better)") arguments.Register("speed", 'p', &opts.Speed).WithHelp("[avif] Encoder speed level (0=fast, 10=slower-better)")
// GIF
arguments.Register("colors", 'c', &opts.NumColors).WithHelp("[gif] Number of colors to use (1-256)")
// JXL // JXL
arguments.Register("effort", 'e', &opts.Effort).WithHelp("[jxl] Encoder effort level (0=fast, 10=slower-better)") arguments.Register("effort", 'e', &opts.Effort).WithHelp("[jxl] Encoder effort level (0=fast, 10=slower-better)")
@ -81,7 +79,7 @@ func parse() {
arguments.Register("lossless", 'l', &opts.Lossless).WithHelp("[webp] Use lossless compression") arguments.Register("lossless", 'l', &opts.Lossless).WithHelp("[webp] Use lossless compression")
arguments.Register("method", 'm', &opts.Method).WithHelp("[webp] Encoder method (0=fast, 6=slower-better)") arguments.Register("method", 'm', &opts.Method).WithHelp("[webp] Encoder method (0=fast, 6=slower-better)")
arguments.Parse() must(arguments.Parse())
help() help()
@ -116,9 +114,9 @@ func parse() {
opts.Format = "jpeg" opts.Format = "jpeg"
} }
// NumColors must be between 1 and 256 // NumColors must be between 0 and 256
if opts.NumColors < 1 || opts.NumColors > 256 { if opts.NumColors < 0 || opts.NumColors > 256 {
opts.NumColors = 256 opts.NumColors = 0
} }
// Effort must be between 0 and 10 // Effort must be between 0 and 10
@ -161,58 +159,30 @@ func GetWebPOptions() webp.Options {
} }
} }
func LogWebPOptions(options webp.Options) {
info("Using output options:")
info(" - lossless: %v", options.Lossless)
info(" - quality: %v", options.Quality)
info(" - method: %v", options.Method)
info(" - exact: %v", options.Exact)
}
func GetJpegOptions() *jpeg.Options { func GetJpegOptions() *jpeg.Options {
return &jpeg.Options{ return &jpeg.Options{
Quality: opts.Quality, Quality: opts.Quality,
} }
} }
func LogJpegOptions(options *jpeg.Options) {
info("Using output options:")
info(" - quality: %v", options.Quality)
}
func GetPNGOptions() *png.Encoder { func GetPNGOptions() *png.Encoder {
return &png.Encoder{ return &png.Encoder{
CompressionLevel: GetPNGCompressionLevel(), CompressionLevel: GetPNGCompressionLevel(),
} }
} }
func LogPNGOptions(encoder *png.Encoder) {
info("Using output options:")
info(" - level: %s", PNGCompressionLevelToString(encoder.CompressionLevel))
}
func GetGifOptions() *gif.Options { func GetGifOptions() *gif.Options {
return &gif.Options{ return &gif.Options{
NumColors: opts.NumColors, NumColors: opts.NumColors,
} }
} }
func LogGifOptions(options *gif.Options) {
info("Using output options:")
info(" - colors: %v", options.NumColors)
}
func GetTiffOptions() *tiff.Options { func GetTiffOptions() *tiff.Options {
return &tiff.Options{ return &tiff.Options{
Compression: GetTiffCompressionType(), Compression: GetTiffCompressionType(),
} }
} }
func LogTiffOptions(options *tiff.Options) {
info("Using output options:")
info(" - compression: %s", TiffCompressionTypeToString(options.Compression))
}
func GetAvifOptions() avif.Options { func GetAvifOptions() avif.Options {
return avif.Options{ return avif.Options{
Quality: opts.Quality, Quality: opts.Quality,
@ -222,14 +192,6 @@ func GetAvifOptions() avif.Options {
} }
} }
func LogAvifOptions(options avif.Options) {
info("Using output options:")
info(" - quality: %v", options.Quality)
info(" - quality-alpha: %v", options.QualityAlpha)
info(" - speed: %v", options.Speed)
info(" - chroma subsampling: %s", options.ChromaSubsampling.String())
}
func GetJxlOptions() jpegxl.Options { func GetJxlOptions() jpegxl.Options {
return jpegxl.Options{ return jpegxl.Options{
Quality: opts.Quality, Quality: opts.Quality,
@ -237,12 +199,6 @@ func GetJxlOptions() jpegxl.Options {
} }
} }
func LogJxlOptions(options jpegxl.Options) {
info("Using output options:")
info(" - quality: %v", options.Quality)
info(" - effort: %v", options.Effort)
}
func GetTiffCompressionType() tiff.CompressionType { func GetTiffCompressionType() tiff.CompressionType {
switch opts.Compression { switch opts.Compression {
case 0: case 0:

26
palette.go Normal file
View File

@ -0,0 +1,26 @@
package main
import (
"image"
"image/color"
"image/draw"
"github.com/ericpauley/go-quantize/quantize"
)
func Quantize(img image.Image) image.Image {
if opts.NumColors == 0 {
return img
}
bounds := img.Bounds()
q := quantize.MedianCutQuantizer{}
p := q.Quantize(make([]color.Color, 0, opts.NumColors), img)
paletted := image.NewPaletted(bounds, p)
draw.Draw(paletted, bounds, img, image.Point{}, draw.Src)
return paletted
}

134
table.go Normal file
View File

@ -0,0 +1,134 @@
package main
import (
"fmt"
"image/gif"
"image/jpeg"
"image/png"
"strings"
"github.com/coalaura/arguments"
"github.com/gen2brain/avif"
"github.com/gen2brain/jpegxl"
"github.com/gen2brain/webp"
"golang.org/x/image/tiff"
)
type Option struct {
Format string
Name string
Value interface{}
}
type OptionsTable struct {
max int
entries []Option
seen map[string]bool
}
func NewOptionsTable() *OptionsTable {
table := &OptionsTable{
entries: make([]Option, 0),
seen: make(map[string]bool),
}
table.Add("format", "%s", opts.Format)
if opts.NumColors != 0 {
table.Add("colors", "%d", opts.NumColors)
}
return table
}
func (t *OptionsTable) Add(name, format string, value interface{}) {
if t.seen[name] {
return
}
t.seen[name] = true
t.entries = append(t.entries, Option{
Format: format,
Name: name,
Value: value,
})
if len(name) > t.max {
t.max = len(name)
}
}
func (t *OptionsTable) Print() {
if opts.Silent {
return
}
b := arguments.NewBuilder(true)
b.Name()
b.WriteString("Options:")
for _, opt := range t.entries {
b.WriteRune('\n')
b.Mute()
b.WriteString(" - ")
b.Name()
b.WriteString(opt.Name)
b.WriteString(strings.Repeat(" ", t.max-len(opt.Name)))
b.Mute()
b.WriteString(": ")
b.Value()
b.WriteString(fmt.Sprintf(opt.Format, opt.Value))
}
println(b.String())
}
func (t *OptionsTable) AddWebPOptions(options webp.Options) {
t.Add("lossless", "%v", options.Lossless)
t.Add("quality", "%v", options.Quality)
t.Add("method", "%v", options.Method)
t.Add("exact", "%v", options.Exact)
t.Print()
}
func (t *OptionsTable) AddJpegOptions(options *jpeg.Options) {
t.Add("quality", "%v", options.Quality)
t.Print()
}
func (t *OptionsTable) AddPNGOptions(encoder *png.Encoder) {
t.Add("level", "%s", PNGCompressionLevelToString(encoder.CompressionLevel))
t.Print()
}
func (t *OptionsTable) AddGifOptions(options *gif.Options) {
t.Add("colors", "%v", options.NumColors)
t.Print()
}
func (t *OptionsTable) AddTiffOptions(options *tiff.Options) {
t.Add("compression", "%s", TiffCompressionTypeToString(options.Compression))
t.Print()
}
func (t *OptionsTable) AddAvifOptions(options avif.Options) {
t.Add("quality", "%v", options.Quality)
t.Add("speed", "%v", options.Speed)
t.Add("ratios", "%s", options.ChromaSubsampling.String())
t.Print()
}
func (t *OptionsTable) AddJxlOptions(options jpegxl.Options) {
t.Add("quality", "%v", options.Quality)
t.Add("effort", "%v", options.Effort)
t.Print()
}