1
0
mirror of https://github.com/coalaura/ffwebp.git synced 2025-07-17 22:04:35 +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) {
opts.Silent = true
opts.Format = "png"
encoder, _ := ResolveImageEncoder()
for _, file := range TestFiles {
log.Printf("Testing file: %s\n", file)
@ -47,7 +50,7 @@ func TestFFWebP(t *testing.T) {
var result bytes.Buffer
err = WriteImage(&result, img, "png")
err = encoder(&result, img)
if err != nil {
log.Fatalf("Failed to encode png image: %v", err)
}

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.23.4
require (
github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670
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/heic 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/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/go.mod h1:F5cdI+Gn1qi5K6qqvAdxdTD2TXkny+gTKU0o6NN1MlU=
github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
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/go.mod h1:tdL2sV6oOJXBZZvT5iP55VEM1X2c3/yJmYKMJTl8fXg=
github.com/gen2brain/heic v0.3.1 h1:ClY5YTdXdIanw7pe9ZVUM9XcsqH6CCCa5CZBlm58qOs=

24
help.go
View File

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

View File

@ -15,6 +15,8 @@ import (
"golang.org/x/image/tiff"
)
type Encoder func(io.Writer, image.Image) error
func ReadImage(input io.ReadSeeker) (image.Image, error) {
decoder, err := GetDecoderFromContent(input)
if err != nil {
@ -24,55 +26,77 @@ func ReadImage(input io.ReadSeeker) (image.Image, error) {
return decoder(input)
}
func WriteImage(output io.Writer, img image.Image, format string) error {
switch format {
func ResolveImageEncoder() (Encoder, error) {
table := NewOptionsTable()
switch opts.Format {
case "webp":
options := GetWebPOptions()
LogWebPOptions(options)
table.AddWebPOptions(options)
return func(output io.Writer, img image.Image) error {
return webp.Encode(output, img, options)
}, nil
case "jpeg":
options := GetJpegOptions()
LogJpegOptions(options)
table.AddJpegOptions(options)
return func(output io.Writer, img image.Image) error {
return jpeg.Encode(output, img, options)
}, nil
case "png":
encoder := GetPNGOptions()
LogPNGOptions(encoder)
table.AddPNGOptions(encoder)
return func(output io.Writer, img image.Image) error {
return encoder.Encode(output, img)
}, nil
case "gif":
options := GetGifOptions()
LogGifOptions(options)
table.AddGifOptions(options)
return func(output io.Writer, img image.Image) error {
return gif.Encode(output, img, options)
}, nil
case "bmp":
return func(output io.Writer, img image.Image) error {
return bmp.Encode(output, img)
}, nil
case "tiff":
options := GetTiffOptions()
LogTiffOptions(options)
table.AddTiffOptions(options)
return func(output io.Writer, img image.Image) error {
return tiff.Encode(output, img, options)
}, nil
case "avif":
options := GetAvifOptions()
LogAvifOptions(options)
table.AddAvifOptions(options)
return func(output io.Writer, img image.Image) error {
return avif.Encode(output, img, options)
}, nil
case "jxl":
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":
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 (
"fmt"
"os"
"github.com/coalaura/arguments"
)
var (
silent bool
)
func info(fm string, args ...interface{}) {
func debug(msg string) {
if silent {
return
}
fmt.Printf(fm, args...)
fmt.Println()
b := arguments.NewBuilder(true)
b.Mute()
b.WriteString("# ")
b.WriteString(msg)
println(b.String())
}
func fatalf(fm string, args ...interface{}) {
fmt.Printf("ERROR: "+fm, args...)
fmt.Println()
func fatalf(format string, args ...interface{}) {
must(fmt.Errorf(format, args...))
}
func must(err error) {
if err == nil {
return
}
print("\033[38;5;160mERROR: \033[38;5;248m")
println(err.Error())
os.Exit(1)
}

34
main.go
View File

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

View File

@ -38,7 +38,7 @@ var opts = Options{
Output: "",
Silent: false,
NumColors: 256,
NumColors: 0,
Effort: 10,
Format: "",
Lossless: false,
@ -59,14 +59,12 @@ func parse() {
// Common image options
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
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)")
// GIF
arguments.Register("colors", 'c', &opts.NumColors).WithHelp("[gif] Number of colors to use (1-256)")
// JXL
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("method", 'm', &opts.Method).WithHelp("[webp] Encoder method (0=fast, 6=slower-better)")
arguments.Parse()
must(arguments.Parse())
help()
@ -116,9 +114,9 @@ func parse() {
opts.Format = "jpeg"
}
// NumColors must be between 1 and 256
if opts.NumColors < 1 || opts.NumColors > 256 {
opts.NumColors = 256
// NumColors must be between 0 and 256
if opts.NumColors < 0 || opts.NumColors > 256 {
opts.NumColors = 0
}
// 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 {
return &jpeg.Options{
Quality: opts.Quality,
}
}
func LogJpegOptions(options *jpeg.Options) {
info("Using output options:")
info(" - quality: %v", options.Quality)
}
func GetPNGOptions() *png.Encoder {
return &png.Encoder{
CompressionLevel: GetPNGCompressionLevel(),
}
}
func LogPNGOptions(encoder *png.Encoder) {
info("Using output options:")
info(" - level: %s", PNGCompressionLevelToString(encoder.CompressionLevel))
}
func GetGifOptions() *gif.Options {
return &gif.Options{
NumColors: opts.NumColors,
}
}
func LogGifOptions(options *gif.Options) {
info("Using output options:")
info(" - colors: %v", options.NumColors)
}
func GetTiffOptions() *tiff.Options {
return &tiff.Options{
Compression: GetTiffCompressionType(),
}
}
func LogTiffOptions(options *tiff.Options) {
info("Using output options:")
info(" - compression: %s", TiffCompressionTypeToString(options.Compression))
}
func GetAvifOptions() avif.Options {
return avif.Options{
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 {
return jpegxl.Options{
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 {
switch opts.Compression {
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()
}