mirror of
https://github.com/coalaura/ffwebp.git
synced 2025-07-17 22:04:35 +00:00
various improvements
This commit is contained in:
@ -1,10 +1,6 @@
|
|||||||
@echo off
|
@echo off
|
||||||
|
|
||||||
if not exist bin (
|
|
||||||
mkdir bin
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Building...
|
echo Building...
|
||||||
go build -o bin/ffwebp.exe
|
go build -o %USERPROFILE%/.bin/ffwebp.exe
|
||||||
|
|
||||||
echo Done
|
echo Done
|
||||||
|
@ -4,9 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"image/png"
|
"image/png"
|
||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,40 +26,43 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestFFWebP(t *testing.T) {
|
func TestFFWebP(t *testing.T) {
|
||||||
exe, err := filepath.Abs("bin/ffwebp.exe")
|
opts.Silent = true
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to get absolute path for ffwebp.exe: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range TestFiles {
|
for _, file := range TestFiles {
|
||||||
log.Printf("Testing file: %s\n", file)
|
log.Printf("Testing file: %s\n", file)
|
||||||
|
|
||||||
cmd := exec.Command(exe, "-i", file, "-f", "png", "-s")
|
in, err := os.OpenFile(file, os.O_RDONLY, 0)
|
||||||
|
|
||||||
// Capture the output (which is expected to be a PNG image)
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
|
|
||||||
// Run the command
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out := strings.TrimSpace(stdout.String())
|
log.Fatalf("Failed to read %s: %v", file, err)
|
||||||
|
|
||||||
log.Println(" - FAILED")
|
|
||||||
log.Fatalf("Test failed for file: %s (%s)\n", file, out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the captured stdout output as a PNG image
|
defer in.Close()
|
||||||
img, err := png.Decode(&stdout)
|
|
||||||
|
img, err := ReadImage(in)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to decode %s: %v", file, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
before := img.Bounds()
|
||||||
|
|
||||||
|
var result bytes.Buffer
|
||||||
|
|
||||||
|
err = WriteImage(&result, img, "png")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to encode png image: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err = png.Decode(&result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(" - FAILED")
|
log.Println(" - FAILED")
|
||||||
log.Fatalf("Failed to decode PNG image: %v\n", err)
|
log.Fatalf("Failed to decode PNG image: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if img == nil {
|
after := img.Bounds()
|
||||||
|
|
||||||
|
if before.Max.X != after.Max.X || before.Max.Y != after.Max.Y {
|
||||||
log.Println(" - FAILED")
|
log.Println(" - FAILED")
|
||||||
log.Fatalf("No image data returned for file: %s\n", file)
|
log.Fatalf("Invalid image (%dx%d != %dx%d) for file: %s\n", before.Max.X, before.Max.Y, after.Max.X, after.Max.Y, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println(" - PASSED")
|
log.Println(" - PASSED")
|
||||||
|
187
flags.go
187
flags.go
@ -1,187 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Argument struct {
|
|
||||||
IsNil bool
|
|
||||||
Name string
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Arguments struct {
|
|
||||||
Arguments map[string]Argument
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
arguments Arguments
|
|
||||||
)
|
|
||||||
|
|
||||||
// I don't like golang flags package
|
|
||||||
func init() {
|
|
||||||
arguments = Arguments{
|
|
||||||
Arguments: make(map[string]Argument),
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
arg string
|
|
||||||
val string
|
|
||||||
index int
|
|
||||||
|
|
||||||
current Argument
|
|
||||||
)
|
|
||||||
|
|
||||||
for i := 1; i < len(os.Args); i++ {
|
|
||||||
arg = os.Args[i]
|
|
||||||
|
|
||||||
if arg[0] == '-' && len(arg) > 1 {
|
|
||||||
if arg[1] == '-' {
|
|
||||||
index = strings.Index(arg[2:], "=")
|
|
||||||
|
|
||||||
if index >= 0 {
|
|
||||||
val = ""
|
|
||||||
|
|
||||||
if index+1 < len(arg) {
|
|
||||||
val = arg[2+index+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
arguments.Set(Argument{
|
|
||||||
Name: arg[2 : 2+index],
|
|
||||||
Value: val,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
arguments.Set(Argument{
|
|
||||||
Name: arg[2:],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
current = Argument{}
|
|
||||||
} else {
|
|
||||||
current = Argument{
|
|
||||||
Name: arg[1:],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current.Value = arg
|
|
||||||
|
|
||||||
arguments.Set(current)
|
|
||||||
|
|
||||||
current = Argument{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if current.Name != "" {
|
|
||||||
arguments.Set(current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arguments) Set(arg Argument) {
|
|
||||||
a.Arguments[arg.Name] = arg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arguments) Get(short, long string) Argument {
|
|
||||||
arg, ok := a.Arguments[short]
|
|
||||||
|
|
||||||
if !ok && long != short {
|
|
||||||
arg, ok = a.Arguments[long]
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return Argument{
|
|
||||||
IsNil: true,
|
|
||||||
Name: long,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return arg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arguments) GetString(short, long string) string {
|
|
||||||
return a.Get(short, long).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arguments) GetBool(short, long string, def bool) bool {
|
|
||||||
return a.Get(short, long).Bool(def)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arguments) GetInt64(short, long string, def, min, max int64) int64 {
|
|
||||||
return a.Get(short, long).Int64(def, min, max)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arguments) GetUint64(short, long string, def, min, max uint64) uint64 {
|
|
||||||
return a.Get(short, long).Uint64(def, min, max)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Arguments) GetFloat64(short, long string, def, min, max float64) float64 {
|
|
||||||
return a.Get(short, long).Float64(def, min, max)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Argument) String() string {
|
|
||||||
return a.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Argument) Bool(def bool) bool {
|
|
||||||
if a.IsNil {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.Value == "false" || a.Value == "0" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Argument) Int64(def, min, max int64) int64 {
|
|
||||||
if a.IsNil {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
i, err := strconv.ParseInt(a.Value, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
return minmax(i, min, max)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Argument) Uint64(def, min, max uint64) uint64 {
|
|
||||||
if a.IsNil {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
i, err := strconv.ParseUint(a.Value, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
return minmax(i, min, max)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Argument) Float64(def, min, max float64) float64 {
|
|
||||||
if a.IsNil {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
i, err := strconv.ParseFloat(a.Value, 64)
|
|
||||||
if err != nil {
|
|
||||||
return def
|
|
||||||
}
|
|
||||||
|
|
||||||
return minmax(i, min, max)
|
|
||||||
}
|
|
||||||
|
|
||||||
func minmax[T int64 | uint64 | float64](val, min, max T) T {
|
|
||||||
if min != 0 && val < min {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
|
|
||||||
if max != 0 && val > max {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
|
|
||||||
return val
|
|
||||||
}
|
|
47
format.go
47
format.go
@ -7,11 +7,10 @@ import (
|
|||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/biessek/golang-ico"
|
ico "github.com/biessek/golang-ico"
|
||||||
"github.com/gen2brain/avif"
|
"github.com/gen2brain/avif"
|
||||||
"github.com/gen2brain/heic"
|
"github.com/gen2brain/heic"
|
||||||
"github.com/gen2brain/jpegxl"
|
"github.com/gen2brain/jpegxl"
|
||||||
@ -22,35 +21,35 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
OutputFormats = []string{
|
OutputFormats = []string{
|
||||||
"jpeg",
|
|
||||||
"png",
|
|
||||||
"webp",
|
|
||||||
"gif",
|
|
||||||
"bmp",
|
|
||||||
"tiff",
|
|
||||||
"avif",
|
"avif",
|
||||||
"jxl",
|
"bmp",
|
||||||
|
"gif",
|
||||||
"ico",
|
"ico",
|
||||||
|
"jpeg",
|
||||||
|
"jxl",
|
||||||
|
"png",
|
||||||
|
"tiff",
|
||||||
|
"webp",
|
||||||
}
|
}
|
||||||
|
|
||||||
InputFormats = []string{
|
InputFormats = []string{
|
||||||
"jpeg",
|
|
||||||
"png",
|
|
||||||
"webp",
|
|
||||||
"gif",
|
|
||||||
"bmp",
|
|
||||||
"tiff",
|
|
||||||
"avif",
|
"avif",
|
||||||
"jxl",
|
"bmp",
|
||||||
"ico",
|
"gif",
|
||||||
"heic",
|
"heic",
|
||||||
"heif",
|
"heif",
|
||||||
|
"ico",
|
||||||
|
"jpeg",
|
||||||
|
"jxl",
|
||||||
|
"png",
|
||||||
|
"tiff",
|
||||||
|
"webp",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type Decoder func(io.Reader) (image.Image, error)
|
type Decoder func(io.Reader) (image.Image, error)
|
||||||
|
|
||||||
func GetDecoderFromContent(in *os.File) (Decoder, error) {
|
func GetDecoderFromContent(in io.ReadSeeker) (Decoder, error) {
|
||||||
buffer := make([]byte, 128)
|
buffer := make([]byte, 128)
|
||||||
|
|
||||||
_, err := in.Read(buffer)
|
_, err := in.Read(buffer)
|
||||||
@ -139,31 +138,31 @@ func IsJpegXL(buffer []byte) bool {
|
|||||||
return len(buffer) > 12 && string(buffer[:4]) == "JXL "
|
return len(buffer) > 12 && string(buffer[:4]) == "JXL "
|
||||||
}
|
}
|
||||||
|
|
||||||
func OutputFormatFromPath(path string) string {
|
func GetFormatFromPath(path string) string {
|
||||||
ext := strings.ToLower(filepath.Ext(path))
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
|
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".webp", ".riff":
|
case ".webp", ".riff":
|
||||||
return "webp"
|
return "webp"
|
||||||
case ".jpg", ".jpeg":
|
case ".jpg", ".jpeg", ".jpe", ".jif", ".jfif":
|
||||||
return "jpeg"
|
return "jpeg"
|
||||||
case ".png":
|
case ".png":
|
||||||
return "png"
|
return "png"
|
||||||
case ".gif":
|
case ".gif", ".giff":
|
||||||
return "gif"
|
return "gif"
|
||||||
case ".bmp":
|
case ".bmp", ".dib", ".rle":
|
||||||
return "bmp"
|
return "bmp"
|
||||||
case ".tiff", ".tif":
|
case ".tiff", ".tif":
|
||||||
return "tiff"
|
return "tiff"
|
||||||
case ".avif", ".avifs":
|
case ".avif", ".avifs":
|
||||||
return "avif"
|
return "avif"
|
||||||
case ".jxl":
|
case ".jxl", ".jxls":
|
||||||
return "jxl"
|
return "jxl"
|
||||||
case ".ico":
|
case ".ico":
|
||||||
return "ico"
|
return "ico"
|
||||||
}
|
}
|
||||||
|
|
||||||
return "webp"
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsValidOutputFormat(format string) bool {
|
func IsValidOutputFormat(format string) bool {
|
||||||
|
3
go.mod
3
go.mod
@ -1,9 +1,10 @@
|
|||||||
module ffwebp
|
module ffwebp
|
||||||
|
|
||||||
go 1.23.1
|
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/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
6
go.sum
@ -1,5 +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/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/gen2brain/avif v0.3.2 h1:XUR0CBl5n4ISFJE8/pc1RMEKt5KUVoW8InctN+M7+DQ=
|
github.com/gen2brain/avif v0.3.2 h1:XUR0CBl5n4ISFJE8/pc1RMEKt5KUVoW8InctN+M7+DQ=
|
||||||
|
73
help.go
73
help.go
@ -3,46 +3,63 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"strings"
|
"github.com/coalaura/arguments"
|
||||||
)
|
)
|
||||||
|
|
||||||
func help() {
|
func help() {
|
||||||
if !arguments.GetBool("h", "help", false) {
|
if !opts.Help {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info(" __ __ _")
|
println(" __ __ _")
|
||||||
info(" / _|/ _| | |")
|
println(" / _|/ _| | |")
|
||||||
info("| |_| |___ _____| |__ _ __")
|
println("| |_| |___ _____| |__ _ __")
|
||||||
info("| _| _\\ \\ /\\ / / _ \\ '_ \\| '_ \\")
|
println("| _| _\\ \\ /\\ / / _ \\ '_ \\| '_ \\")
|
||||||
info("| | | | \\ V V / __/ |_) | |_) |")
|
println("| | | | \\ V V / __/ |_) | |_) |")
|
||||||
info("|_| |_| \\_/\\_/ \\___|_.__/| .__/")
|
println("|_| |_| \\_/\\_/ \\___|_.__/| .__/")
|
||||||
info(" | |")
|
println(" | |")
|
||||||
info(" %s |_|", Version)
|
fmt.Printf(" %s |_|\n", Version)
|
||||||
|
|
||||||
info("\nffwebp -i <input> [output] [options]")
|
println("\nffwebp [options] <input> [output]\n")
|
||||||
|
|
||||||
var max int
|
arguments.ShowHelp(true)
|
||||||
|
|
||||||
for name := range options {
|
b := arguments.NewBuilder(true)
|
||||||
if len(name) > max {
|
|
||||||
max = len(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var formatted []string
|
b.WriteRune('\n')
|
||||||
|
b.Mute()
|
||||||
|
b.WriteString(" - ")
|
||||||
|
b.Name()
|
||||||
|
b.WriteString("Input formats")
|
||||||
|
b.Mute()
|
||||||
|
b.WriteString(": ")
|
||||||
|
values(b, InputFormats)
|
||||||
|
|
||||||
for name, help := range options {
|
b.WriteRune('\n')
|
||||||
formatted = append(formatted, fmt.Sprintf(" - %-*s: %s", max, name, help))
|
b.Mute()
|
||||||
}
|
b.WriteString(" - ")
|
||||||
|
b.Name()
|
||||||
|
b.WriteString("Output formats")
|
||||||
|
b.Mute()
|
||||||
|
b.WriteString(": ")
|
||||||
|
values(b, OutputFormats)
|
||||||
|
|
||||||
sort.Strings(formatted)
|
println(b.String())
|
||||||
|
|
||||||
info(strings.Join(formatted, "\n"))
|
|
||||||
|
|
||||||
info("\nInput formats: %s", strings.Join(InputFormats, ", "))
|
|
||||||
info("Output formats: %s", strings.Join(OutputFormats, ", "))
|
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func values(b *arguments.Builder, v []string) {
|
||||||
|
for i, value := range v {
|
||||||
|
if i > 0 {
|
||||||
|
b.Mute()
|
||||||
|
b.WriteString(", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Value()
|
||||||
|
b.WriteString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
}
|
||||||
|
31
image.go
31
image.go
@ -5,10 +5,9 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/gif"
|
"image/gif"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"io"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/biessek/golang-ico"
|
ico "github.com/biessek/golang-ico"
|
||||||
"github.com/gen2brain/avif"
|
"github.com/gen2brain/avif"
|
||||||
"github.com/gen2brain/jpegxl"
|
"github.com/gen2brain/jpegxl"
|
||||||
"github.com/gen2brain/webp"
|
"github.com/gen2brain/webp"
|
||||||
@ -16,23 +15,7 @@ import (
|
|||||||
"golang.org/x/image/tiff"
|
"golang.org/x/image/tiff"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func ReadImage(input io.ReadSeeker) (image.Image, error) {
|
||||||
options = map[string]string{
|
|
||||||
"c / colors": "Number of colors (1-256) (gif)",
|
|
||||||
"e / effort": "Encoder effort level (0-10) (jxl)",
|
|
||||||
"f / format": "Output format (avif, bmp, gif, jpeg, jxl, png, tiff, webp)",
|
|
||||||
"h / help": "Show this help page",
|
|
||||||
"l / lossless": "Use lossless compression (webp)",
|
|
||||||
"m / method": "Encoder method (0=fast, 6=slower-better) (webp)",
|
|
||||||
"r / ratio": "YCbCr subsample-ratio (0=444, 1=422, 2=420, 3=440, 4=411, 5=410) (avif)",
|
|
||||||
"s / silent": "Do not print any output",
|
|
||||||
"q / quality": "Set quality (0-100) (avif, jpeg, jxl, webp)",
|
|
||||||
"x / exact": "Preserve RGB values in transparent area (webp)",
|
|
||||||
"z / compression": "Compression type (0=uncompressed, 1=deflate, 2=lzw, 3=ccittgroup3, 4=ccittgroup4) (tiff)",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func ReadImage(input *os.File) (image.Image, error) {
|
|
||||||
decoder, err := GetDecoderFromContent(input)
|
decoder, err := GetDecoderFromContent(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -41,7 +24,7 @@ func ReadImage(input *os.File) (image.Image, error) {
|
|||||||
return decoder(input)
|
return decoder(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteImage(output *os.File, img image.Image, format string) error {
|
func WriteImage(output io.Writer, img image.Image, format string) error {
|
||||||
switch format {
|
switch format {
|
||||||
case "webp":
|
case "webp":
|
||||||
options := GetWebPOptions()
|
options := GetWebPOptions()
|
||||||
@ -56,7 +39,11 @@ func WriteImage(output *os.File, img image.Image, format string) error {
|
|||||||
|
|
||||||
return jpeg.Encode(output, img, options)
|
return jpeg.Encode(output, img, options)
|
||||||
case "png":
|
case "png":
|
||||||
return png.Encode(output, img)
|
encoder := GetPNGOptions()
|
||||||
|
|
||||||
|
LogPNGOptions(encoder)
|
||||||
|
|
||||||
|
return encoder.Encode(output, img)
|
||||||
case "gif":
|
case "gif":
|
||||||
options := GetGifOptions()
|
options := GetGifOptions()
|
||||||
|
|
||||||
|
10
log.go
10
log.go
@ -14,11 +14,13 @@ func info(fm string, args ...interface{}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf(fm+"\n", args...)
|
fmt.Printf(fm, args...)
|
||||||
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
func fatalf(code int, fm string, args ...interface{}) {
|
func fatalf(fm string, args ...interface{}) {
|
||||||
fmt.Printf("ERROR: "+fm+"\n", args...)
|
fmt.Printf("ERROR: "+fm, args...)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
os.Exit(code)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
69
main.go
69
main.go
@ -5,77 +5,46 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
help()
|
parse()
|
||||||
|
|
||||||
silent = arguments.GetBool("s", "silent", false)
|
info("Reading input image...")
|
||||||
|
|
||||||
// Read input file
|
in, err := os.OpenFile(opts.Input, os.O_RDONLY, 0)
|
||||||
input := arguments.GetString("i", "input")
|
|
||||||
|
|
||||||
var in *os.File
|
|
||||||
|
|
||||||
if input == "" {
|
|
||||||
in = os.Stdin
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
in, err = os.OpenFile(input, os.O_RDONLY, 0)
|
|
||||||
if err != nil {
|
|
||||||
fatalf(1, "Failed to open input file: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read image
|
|
||||||
if in == os.Stdin {
|
|
||||||
info("Decoding input from stdin...")
|
|
||||||
} else {
|
|
||||||
info("Decoding input image...")
|
|
||||||
}
|
|
||||||
|
|
||||||
img, err := ReadImage(in)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf(4, "Failed to read image: %s", err)
|
fatalf("Failed to open input file: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read output format
|
defer in.Close()
|
||||||
format := arguments.GetString("f", "format")
|
|
||||||
|
|
||||||
// Read output file
|
|
||||||
output := arguments.GetString("", "")
|
|
||||||
|
|
||||||
var out *os.File
|
var out *os.File
|
||||||
|
|
||||||
if output == "" {
|
if opts.Output == "" {
|
||||||
if format == "" {
|
opts.Silent = true
|
||||||
format = "webp"
|
|
||||||
}
|
|
||||||
|
|
||||||
out = os.Stdout
|
out = os.Stdout
|
||||||
silent = true
|
|
||||||
} else {
|
} else {
|
||||||
var err error
|
out, err = os.OpenFile(opts.Output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||||
|
|
||||||
if format == "" {
|
|
||||||
format = OutputFormatFromPath(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err = os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf(2, "Failed to open output file: %s", err)
|
fatalf("Failed to open output file: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer out.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !IsValidOutputFormat(format) {
|
info("Decoding input image...")
|
||||||
fatalf(3, "Invalid output format: %s", format)
|
|
||||||
|
img, err := ReadImage(in)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Failed to read image: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
info("Using output format: %s", format)
|
info("Using output format: %s", opts.Format)
|
||||||
|
|
||||||
// Write image
|
// Write image
|
||||||
info("Encoding output image...")
|
info("Encoding output image...")
|
||||||
|
|
||||||
err = WriteImage(out, img, format)
|
err = WriteImage(out, img, opts.Format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf(5, "Failed to write image: %s", err)
|
fatalf("Failed to write image: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
208
options.go
208
options.go
@ -4,19 +4,160 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/gif"
|
"image/gif"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
|
||||||
|
"github.com/coalaura/arguments"
|
||||||
"github.com/gen2brain/avif"
|
"github.com/gen2brain/avif"
|
||||||
"github.com/gen2brain/jpegxl"
|
"github.com/gen2brain/jpegxl"
|
||||||
"github.com/gen2brain/webp"
|
"github.com/gen2brain/webp"
|
||||||
"golang.org/x/image/tiff"
|
"golang.org/x/image/tiff"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Help bool
|
||||||
|
Input string
|
||||||
|
Output string
|
||||||
|
|
||||||
|
Silent bool
|
||||||
|
NumColors int
|
||||||
|
Effort int
|
||||||
|
Format string
|
||||||
|
Lossless bool
|
||||||
|
Method int
|
||||||
|
Ratio int
|
||||||
|
Quality int
|
||||||
|
Exact bool
|
||||||
|
Compression int
|
||||||
|
Level int
|
||||||
|
Speed int
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = Options{
|
||||||
|
Help: false,
|
||||||
|
Input: "",
|
||||||
|
Output: "",
|
||||||
|
|
||||||
|
Silent: false,
|
||||||
|
NumColors: 256,
|
||||||
|
Effort: 10,
|
||||||
|
Format: "",
|
||||||
|
Lossless: false,
|
||||||
|
Method: 6,
|
||||||
|
Ratio: 0,
|
||||||
|
Quality: 90,
|
||||||
|
Exact: false,
|
||||||
|
Compression: 2,
|
||||||
|
Level: 2,
|
||||||
|
Speed: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse() {
|
||||||
|
// General options
|
||||||
|
arguments.Register("help", 'h', &opts.Help).WithHelp("Show this help message")
|
||||||
|
arguments.Register("silent", 's', &opts.Silent).WithHelp("Do not print any output")
|
||||||
|
arguments.Register("format", 'f', &opts.Format).WithHelp("Output format (avif, bmp, gif, jpeg, jxl, png, tiff, webp, ico)")
|
||||||
|
|
||||||
|
// Common image options
|
||||||
|
arguments.Register("quality", 'q', &opts.Quality).WithHelp("[avif|jpeg|jxl|webp] Quality level (1-100)")
|
||||||
|
|
||||||
|
// 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)")
|
||||||
|
|
||||||
|
// PNG
|
||||||
|
arguments.Register("level", 'g', &opts.Level).WithHelp("[png] Compression level (0=no-compression, 1=best-speed, 2=best-compression)")
|
||||||
|
|
||||||
|
// TIFF
|
||||||
|
arguments.Register("compression", 't', &opts.Compression).WithHelp("[tiff] Compression type (0=uncompressed, 1=deflate, 2=lzw, 3=ccittgroup3, 4=ccittgroup4)")
|
||||||
|
|
||||||
|
// WebP
|
||||||
|
arguments.Register("exact", 'x', &opts.Exact).WithHelp("[webp] Preserve RGB values in transparent area")
|
||||||
|
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()
|
||||||
|
|
||||||
|
help()
|
||||||
|
|
||||||
|
if len(arguments.Args) < 1 {
|
||||||
|
fatalf("Missing input file")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Input = arguments.Args[0]
|
||||||
|
|
||||||
|
if len(arguments.Args) > 1 {
|
||||||
|
opts.Output = arguments.Args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Format != "" && !IsValidOutputFormat(opts.Format) {
|
||||||
|
fatalf("Invalid output format: %s", opts.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve format from output file
|
||||||
|
if opts.Format == "" && opts.Output != "" {
|
||||||
|
opts.Format = GetFormatFromPath(opts.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise resolve format from input file
|
||||||
|
if opts.Format == "" {
|
||||||
|
opts.Format = GetFormatFromPath(opts.Input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or default to webp
|
||||||
|
if opts.Format == "" {
|
||||||
|
opts.Format = "webp"
|
||||||
|
} else if opts.Format == "jpg" {
|
||||||
|
opts.Format = "jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumColors must be between 1 and 256
|
||||||
|
if opts.NumColors < 1 || opts.NumColors > 256 {
|
||||||
|
opts.NumColors = 256
|
||||||
|
}
|
||||||
|
|
||||||
|
// Effort must be between 0 and 10
|
||||||
|
if opts.Effort < 0 || opts.Effort > 10 {
|
||||||
|
opts.Effort = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method must be between 0 and 6
|
||||||
|
if opts.Method < 0 || opts.Method > 6 {
|
||||||
|
opts.Method = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quality must be between 1 and 100
|
||||||
|
if opts.Quality < 1 || opts.Quality > 100 {
|
||||||
|
opts.Quality = 90
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ratio must be between 0 and 5
|
||||||
|
if opts.Ratio < 0 || opts.Ratio > 5 {
|
||||||
|
opts.Ratio = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compression must be between 0 and 4
|
||||||
|
if opts.Compression < 0 || opts.Compression > 4 {
|
||||||
|
opts.Compression = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level must be between 0 and 2
|
||||||
|
if opts.Level < 0 || opts.Level > 2 {
|
||||||
|
opts.Level = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetWebPOptions() webp.Options {
|
func GetWebPOptions() webp.Options {
|
||||||
return webp.Options{
|
return webp.Options{
|
||||||
Lossless: arguments.GetBool("l", "lossless", false),
|
Lossless: opts.Lossless,
|
||||||
Quality: int(arguments.GetUint64("q", "quality", 100, 0, 100)),
|
Quality: opts.Quality,
|
||||||
Method: int(arguments.GetUint64("m", "method", 4, 0, 6)),
|
Method: opts.Method,
|
||||||
Exact: arguments.GetBool("x", "exact", false),
|
Exact: opts.Exact,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +171,7 @@ func LogWebPOptions(options webp.Options) {
|
|||||||
|
|
||||||
func GetJpegOptions() *jpeg.Options {
|
func GetJpegOptions() *jpeg.Options {
|
||||||
return &jpeg.Options{
|
return &jpeg.Options{
|
||||||
Quality: int(arguments.GetUint64("q", "quality", 100, 0, 100)),
|
Quality: opts.Quality,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,9 +180,20 @@ func LogJpegOptions(options *jpeg.Options) {
|
|||||||
info(" - quality: %v", options.Quality)
|
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 {
|
func GetGifOptions() *gif.Options {
|
||||||
return &gif.Options{
|
return &gif.Options{
|
||||||
NumColors: int(arguments.GetUint64("c", "colors", 256, 0, 256)),
|
NumColors: opts.NumColors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,9 +215,9 @@ func LogTiffOptions(options *tiff.Options) {
|
|||||||
|
|
||||||
func GetAvifOptions() avif.Options {
|
func GetAvifOptions() avif.Options {
|
||||||
return avif.Options{
|
return avif.Options{
|
||||||
Quality: int(arguments.GetUint64("q", "quality", 100, 0, 100)),
|
Quality: opts.Quality,
|
||||||
QualityAlpha: int(arguments.GetUint64("qa", "quality-alpha", 100, 0, 100)),
|
QualityAlpha: opts.Quality,
|
||||||
Speed: int(arguments.GetUint64("s", "speed", 6, 0, 10)),
|
Speed: opts.Speed,
|
||||||
ChromaSubsampling: GetAvifYCbCrSubsampleRatio(),
|
ChromaSubsampling: GetAvifYCbCrSubsampleRatio(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,8 +232,8 @@ func LogAvifOptions(options avif.Options) {
|
|||||||
|
|
||||||
func GetJxlOptions() jpegxl.Options {
|
func GetJxlOptions() jpegxl.Options {
|
||||||
return jpegxl.Options{
|
return jpegxl.Options{
|
||||||
Quality: int(arguments.GetUint64("q", "quality", 100, 0, 100)),
|
Quality: opts.Quality,
|
||||||
Effort: int(arguments.GetUint64("e", "effort", 7, 0, 10)),
|
Effort: opts.Effort,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,9 +244,7 @@ func LogJxlOptions(options jpegxl.Options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetTiffCompressionType() tiff.CompressionType {
|
func GetTiffCompressionType() tiff.CompressionType {
|
||||||
compression := arguments.GetUint64("z", "compression", 1, 0, 4)
|
switch opts.Compression {
|
||||||
|
|
||||||
switch compression {
|
|
||||||
case 0:
|
case 0:
|
||||||
return tiff.Uncompressed
|
return tiff.Uncompressed
|
||||||
case 1:
|
case 1:
|
||||||
@ -128,9 +278,7 @@ func TiffCompressionTypeToString(compression tiff.CompressionType) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetAvifYCbCrSubsampleRatio() image.YCbCrSubsampleRatio {
|
func GetAvifYCbCrSubsampleRatio() image.YCbCrSubsampleRatio {
|
||||||
sampleRatio := arguments.GetUint64("r", "sample-ratio", 0, 0, 5)
|
switch opts.Ratio {
|
||||||
|
|
||||||
switch sampleRatio {
|
|
||||||
case 0:
|
case 0:
|
||||||
return image.YCbCrSubsampleRatio444
|
return image.YCbCrSubsampleRatio444
|
||||||
case 1:
|
case 1:
|
||||||
@ -147,3 +295,29 @@ func GetAvifYCbCrSubsampleRatio() image.YCbCrSubsampleRatio {
|
|||||||
|
|
||||||
return image.YCbCrSubsampleRatio444
|
return image.YCbCrSubsampleRatio444
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetPNGCompressionLevel() png.CompressionLevel {
|
||||||
|
switch opts.Level {
|
||||||
|
case 0:
|
||||||
|
return png.NoCompression
|
||||||
|
case 1:
|
||||||
|
return png.BestSpeed
|
||||||
|
case 2:
|
||||||
|
return png.BestCompression
|
||||||
|
}
|
||||||
|
|
||||||
|
return png.BestCompression
|
||||||
|
}
|
||||||
|
|
||||||
|
func PNGCompressionLevelToString(level png.CompressionLevel) string {
|
||||||
|
switch level {
|
||||||
|
case png.NoCompression:
|
||||||
|
return "no-compression"
|
||||||
|
case png.BestSpeed:
|
||||||
|
return "best-speed"
|
||||||
|
case png.BestCompression:
|
||||||
|
return "best-compression"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
const Version = "development"
|
const Version = " dev"
|
||||||
|
Reference in New Issue
Block a user