commit fbb5e12442316bb8e2e724d1a9bb891d0fc174df Author: Laura Date: Sun Sep 8 00:32:52 2024 +0200 Initial commit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bfadae3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,75 @@ +name: Build and Release + +on: + push: + tags: + - 'v*' + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + goos: [windows, linux, darwin] + goarch: [amd64, arm64] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.1' + + - name: Set up Environment + run: | + mkdir -p build + echo "package main" > version.go + echo "const Version = \"${{ github.ref_name }}\"" >> version.go + go install mvdan.cc/garble@latest + + - name: Build for ${{ matrix.goos }}_${{ matrix.goarch }} + run: | + if [ "${{ matrix.goos }}" = "windows" ]; then EXT=".exe"; else EXT=""; fi + GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} garble -tiny build -o build/ffwebp_${{ github.ref_name }}_${{ matrix.goos }}_${{ matrix.goarch }}$EXT -ldflags -w + + - name: Upload build artifact + uses: actions/upload-artifact@v3 + with: + name: ffwebp_${{ github.ref_name }}_${{ matrix.goos }}_${{ matrix.goarch }} + path: ./build/ffwebp_${{ github.ref_name }}_${{ matrix.goos }}_${{ matrix.goarch }}* + + release: + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + path: ./build + + - name: Upload all built binaries to release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./build/ + asset_name: ffwebp_${{ github.ref_name }} + asset_content_type: application/octet-stream diff --git a/ffwebp_test.go b/ffwebp_test.go new file mode 100644 index 0000000..c0c7ea1 --- /dev/null +++ b/ffwebp_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "bytes" + "image/png" + "log" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +var ( + TestFiles = []string{ + "test/image.avif", + "test/image.bmp", + "test/image.gif", + "test/image.heic", + "test/image.heif", + "test/image.ico", + "test/image.jpg", + "test/image.png", + "test/image.tif", + "test/image.tiff", + "test/image.webp", + "test/image.jxl", + } +) + +func TestFFWebP(t *testing.T) { + exe, err := filepath.Abs("bin/ffwebp.exe") + if err != nil { + log.Fatalf("Failed to get absolute path for ffwebp.exe: %v\n", err) + } + + for _, file := range TestFiles { + log.Printf("Testing file: %s\n", file) + + cmd := exec.Command(exe, "-i", file, "-f", "png", "-s") + + // 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 { + out := strings.TrimSpace(stdout.String()) + + log.Println(" - FAILED") + log.Fatalf("Test failed for file: %s (%s)\n", file, out) + } + + // Decode the captured stdout output as a PNG image + img, err := png.Decode(&stdout) + if err != nil { + log.Println(" - FAILED") + log.Fatalf("Failed to decode PNG image: %v\n", err) + } + + if img == nil { + log.Println(" - FAILED") + log.Fatalf("No image data returned for file: %s\n", file) + } + + log.Println(" - PASSED") + } +} diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..bfbf545 --- /dev/null +++ b/flags.go @@ -0,0 +1,187 @@ +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 +} diff --git a/format.go b/format.go new file mode 100644 index 0000000..21fce30 --- /dev/null +++ b/format.go @@ -0,0 +1,177 @@ +package main + +import ( + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "io" + "os" + "path/filepath" + "strings" + + "github.com/biessek/golang-ico" + "github.com/gen2brain/avif" + "github.com/gen2brain/heic" + "github.com/gen2brain/jpegxl" + "github.com/gen2brain/webp" + "golang.org/x/image/bmp" + "golang.org/x/image/tiff" +) + +var ( + OutputFormats = []string{ + "jpeg", + "png", + "webp", + "gif", + "bmp", + "tiff", + "avif", + "jxl", + "ico", + } + + InputFormats = []string{ + "jpeg", + "png", + "webp", + "gif", + "bmp", + "tiff", + "avif", + "jxl", + "ico", + "heic", + "heif", + } +) + +type Decoder func(io.Reader) (image.Image, error) + +func GetDecoderFromContent(in *os.File) (Decoder, error) { + buffer := make([]byte, 128) + + _, err := in.Read(buffer) + if err != nil { + return nil, err + } + + if _, err := in.Seek(0, io.SeekStart); err != nil { + return nil, err + } + + if IsJPEG(buffer) { + return jpeg.Decode, nil + } else if IsPNG(buffer) { + return png.Decode, nil + } else if IsGIF(buffer) { + return gif.Decode, nil + } else if IsBMP(buffer) { + return bmp.Decode, nil + } else if IsWebP(buffer) { + return webp.Decode, nil + } else if IsTIFF(buffer) { + return tiff.Decode, nil + } else if IsICO(buffer) { + return ico.Decode, nil + } else if IsHEIC(buffer) { + return heic.Decode, nil + } else if IsAVIF(buffer) { + return avif.Decode, nil + } else if IsJpegXL(buffer) { + return jpegxl.Decode, nil + } + + return nil, fmt.Errorf("unsupported input format") +} + +func IsJPEG(buffer []byte) bool { + return len(buffer) > 2 && buffer[0] == 0xFF && buffer[1] == 0xD8 +} + +func IsPNG(buffer []byte) bool { + return len(buffer) > 8 && string(buffer[:8]) == "\x89PNG\r\n\x1a\n" +} + +func IsGIF(buffer []byte) bool { + return len(buffer) > 6 && (string(buffer[:6]) == "GIF87a" || string(buffer[:6]) == "GIF89a") +} + +func IsBMP(buffer []byte) bool { + return len(buffer) > 2 && string(buffer[:2]) == "BM" +} + +func IsICO(buffer []byte) bool { + return len(buffer) > 4 && buffer[0] == 0x00 && buffer[1] == 0x00 && buffer[2] == 0x01 && buffer[3] == 0x00 +} + +func IsWebP(buffer []byte) bool { + // Check if its VP8L + if len(buffer) > 16 && string(buffer[12:16]) == "VP8L" { + return true + } + + // Check if its WebP or RIFF WEBP + return len(buffer) > 12 && string(buffer[:4]) == "RIFF" && string(buffer[8:12]) == "WEBP" +} + +func IsAVIF(buffer []byte) bool { + return len(buffer) > 12 && string(buffer[4:8]) == "ftyp" && string(buffer[8:12]) == "avif" +} + +func IsTIFF(buffer []byte) bool { + return len(buffer) > 4 && (string(buffer[:4]) == "II*\x00" || string(buffer[:4]) == "MM\x00*") +} + +func IsHEIC(buffer []byte) bool { + return len(buffer) > 12 && string(buffer[4:8]) == "ftyp" && (string(buffer[8:12]) == "heic" || string(buffer[8:12]) == "heix") +} + +func IsJpegXL(buffer []byte) bool { + // Check for JPEG XL codestream (starts with 0xFF 0x0A) + if len(buffer) > 2 && buffer[0] == 0xFF && buffer[1] == 0x0A { + return true + } + + // Check for JPEG XL container (starts with "JXL ") + return len(buffer) > 12 && string(buffer[:4]) == "JXL " +} + +func OutputFormatFromPath(path string) string { + ext := strings.ToLower(filepath.Ext(path)) + + switch ext { + case ".webp", ".riff": + return "webp" + case ".jpg", ".jpeg": + return "jpeg" + case ".png": + return "png" + case ".gif": + return "gif" + case ".bmp": + return "bmp" + case ".tiff", ".tif": + return "tiff" + case ".avif", ".avifs": + return "avif" + case ".jxl": + return "jxl" + case ".ico": + return "ico" + } + + return "webp" +} + +func IsValidOutputFormat(format string) bool { + for _, f := range OutputFormats { + if f == format { + return true + } + } + + return false +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..821dd33 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module ffwebp + +go 1.23.1 + +require ( + github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670 + github.com/gen2brain/avif v0.3.2 + github.com/gen2brain/heic v0.3.1 + github.com/gen2brain/jpegxl v0.3.1 + github.com/gen2brain/webp v0.4.5 + golang.org/x/image v0.20.0 +) + +require ( + github.com/ebitengine/purego v0.7.1 // indirect + github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect + github.com/tetratelabs/wazero v1.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1ab44a6 --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +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/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= +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/go.mod h1:tdL2sV6oOJXBZZvT5iP55VEM1X2c3/yJmYKMJTl8fXg= +github.com/gen2brain/heic v0.3.1 h1:ClY5YTdXdIanw7pe9ZVUM9XcsqH6CCCa5CZBlm58qOs= +github.com/gen2brain/heic v0.3.1/go.mod h1:m2sVIf02O7wfO8mJm+PvE91lnq4QYJy2hseUon7So10= +github.com/gen2brain/jpegxl v0.3.1 h1:QAcs68WXQUQRABPVu5p5MineuqfqnVd/JRiI+s7AEE4= +github.com/gen2brain/jpegxl v0.3.1/go.mod h1:jLh4Fl9QaHkc1RsOJu4S2r20x+gSzjnuM+K8jOm4DEo= +github.com/gen2brain/webp v0.4.5 h1:wolsWSKnYfnYaWUtGLx3EfXhLWVvVx9yZGof+JNGYgY= +github.com/gen2brain/webp v0.4.5/go.mod h1:giUCZaJt7D8ae9AjSq4gC3QKUuA9SD8LZy0o2zcWxMI= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= +github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/help.go b/help.go new file mode 100644 index 0000000..794c510 --- /dev/null +++ b/help.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "os" + "sort" + "strings" +) + +func help() { + if !arguments.GetBool("h", "help", false) { + return + } + + info(" __ __ _") + info(" / _|/ _| | |") + info("| |_| |___ _____| |__ _ __") + info("| _| _\\ \\ /\\ / / _ \\ '_ \\| '_ \\") + info("| | | | \\ V V / __/ |_) | |_) |") + info("|_| |_| \\_/\\_/ \\___|_.__/| .__/") + info(" | |") + info(" %s |_|", Version) + + info("\nffwebp -i [output] [options]") + + var max int + + for name := range options { + if len(name) > max { + max = len(name) + } + } + + var formatted []string + + for name, help := range options { + formatted = append(formatted, fmt.Sprintf(" - %-*s: %s", max, name, help)) + } + + sort.Strings(formatted) + + info(strings.Join(formatted, "\n")) + + info("\nInput formats: %s", strings.Join(InputFormats, ", ")) + info("Output formats: %s", strings.Join(OutputFormats, ", ")) + + os.Exit(0) +} diff --git a/image.go b/image.go new file mode 100644 index 0000000..664a656 --- /dev/null +++ b/image.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + "os" + + "github.com/biessek/golang-ico" + "github.com/gen2brain/avif" + "github.com/gen2brain/jpegxl" + "github.com/gen2brain/webp" + "golang.org/x/image/bmp" + "golang.org/x/image/tiff" +) + +var ( + 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) + if err != nil { + return nil, err + } + + return decoder(input) +} + +func WriteImage(output *os.File, img image.Image, format string) error { + switch format { + case "webp": + options := GetWebPOptions() + + LogWebPOptions(options) + + return webp.Encode(output, img, options) + case "jpeg": + options := GetJpegOptions() + + LogJpegOptions(options) + + return jpeg.Encode(output, img, options) + case "png": + return png.Encode(output, img) + case "gif": + options := GetGifOptions() + + LogGifOptions(options) + + return gif.Encode(output, img, options) + case "bmp": + return bmp.Encode(output, img) + case "tiff": + options := GetTiffOptions() + + LogTiffOptions(options) + + return tiff.Encode(output, img, options) + case "avif": + options := GetAvifOptions() + + LogAvifOptions(options) + + return avif.Encode(output, img, options) + case "jxl": + options := GetJxlOptions() + + LogJxlOptions(options) + + jpegxl.Encode(output, img, options) + case "ico": + return ico.Encode(output, img) + } + + return fmt.Errorf("unsupported output format: %s", format) +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..e8d1109 --- /dev/null +++ b/log.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + "os" +) + +var ( + silent bool +) + +func info(fm string, args ...interface{}) { + if silent { + return + } + + fmt.Printf(fm+"\n", args...) +} + +func fatalf(code int, fm string, args ...interface{}) { + fmt.Printf("ERROR: "+fm+"\n", args...) + + os.Exit(code) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..481cdac --- /dev/null +++ b/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "os" +) + +func main() { + help() + + silent = arguments.GetBool("s", "silent", false) + + // Read input file + 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 { + fatalf(4, "Failed to read image: %s", err) + } + + // Read output format + format := arguments.GetString("f", "format") + + // Read output file + output := arguments.GetString("", "") + + var out *os.File + + if output == "" { + if format == "" { + format = "webp" + } + + out = os.Stdout + silent = true + } else { + var err error + + if format == "" { + format = OutputFormatFromPath(output) + } + + out, err = os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + fatalf(2, "Failed to open output file: %s", err) + } + } + + if !IsValidOutputFormat(format) { + fatalf(3, "Invalid output format: %s", format) + } + + info("Using output format: %s", format) + + // Write image + info("Encoding output image...") + + err = WriteImage(out, img, format) + if err != nil { + fatalf(5, "Failed to write image: %s", err) + } +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..24860c9 --- /dev/null +++ b/options.go @@ -0,0 +1,149 @@ +package main + +import ( + "image" + "image/gif" + "image/jpeg" + + "github.com/gen2brain/avif" + "github.com/gen2brain/jpegxl" + "github.com/gen2brain/webp" + "golang.org/x/image/tiff" +) + +func GetWebPOptions() webp.Options { + return webp.Options{ + Lossless: arguments.GetBool("l", "lossless", false), + Quality: int(arguments.GetUint64("q", "quality", 100, 0, 100)), + Method: int(arguments.GetUint64("m", "method", 4, 0, 6)), + Exact: arguments.GetBool("x", "exact", false), + } +} + +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: int(arguments.GetUint64("q", "quality", 100, 0, 100)), + } +} + +func LogJpegOptions(options *jpeg.Options) { + info("Using output options:") + info(" - quality: %v", options.Quality) +} + +func GetGifOptions() *gif.Options { + return &gif.Options{ + NumColors: int(arguments.GetUint64("c", "colors", 256, 0, 256)), + } +} + +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: int(arguments.GetUint64("q", "quality", 100, 0, 100)), + QualityAlpha: int(arguments.GetUint64("qa", "quality-alpha", 100, 0, 100)), + Speed: int(arguments.GetUint64("s", "speed", 6, 0, 10)), + ChromaSubsampling: GetAvifYCbCrSubsampleRatio(), + } +} + +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: int(arguments.GetUint64("q", "quality", 100, 0, 100)), + Effort: int(arguments.GetUint64("e", "effort", 7, 0, 10)), + } +} + +func LogJxlOptions(options jpegxl.Options) { + info("Using output options:") + info(" - quality: %v", options.Quality) + info(" - effort: %v", options.Effort) +} + +func GetTiffCompressionType() tiff.CompressionType { + compression := arguments.GetUint64("z", "compression", 1, 0, 4) + + switch compression { + case 0: + return tiff.Uncompressed + case 1: + return tiff.Deflate + case 2: + return tiff.LZW + case 3: + return tiff.CCITTGroup3 + case 4: + return tiff.CCITTGroup4 + } + + return tiff.Deflate +} + +func TiffCompressionTypeToString(compression tiff.CompressionType) string { + switch compression { + case tiff.Uncompressed: + return "uncompressed" + case tiff.Deflate: + return "deflate" + case tiff.LZW: + return "lzw" + case tiff.CCITTGroup3: + return "ccittgroup3" + case tiff.CCITTGroup4: + return "ccittgroup4" + default: + return "unknown" + } +} + +func GetAvifYCbCrSubsampleRatio() image.YCbCrSubsampleRatio { + sampleRatio := arguments.GetUint64("r", "sample-ratio", 0, 0, 5) + + switch sampleRatio { + case 0: + return image.YCbCrSubsampleRatio444 + case 1: + return image.YCbCrSubsampleRatio422 + case 2: + return image.YCbCrSubsampleRatio420 + case 3: + return image.YCbCrSubsampleRatio440 + case 4: + return image.YCbCrSubsampleRatio411 + case 5: + return image.YCbCrSubsampleRatio410 + } + + return image.YCbCrSubsampleRatio444 +} diff --git a/test/image.avif b/test/image.avif new file mode 100644 index 0000000..f8e029c Binary files /dev/null and b/test/image.avif differ diff --git a/test/image.bmp b/test/image.bmp new file mode 100644 index 0000000..b255eb0 Binary files /dev/null and b/test/image.bmp differ diff --git a/test/image.gif b/test/image.gif new file mode 100644 index 0000000..ac78ba9 Binary files /dev/null and b/test/image.gif differ diff --git a/test/image.heic b/test/image.heic new file mode 100644 index 0000000..dd76335 Binary files /dev/null and b/test/image.heic differ diff --git a/test/image.heif b/test/image.heif new file mode 100644 index 0000000..23cd868 Binary files /dev/null and b/test/image.heif differ diff --git a/test/image.ico b/test/image.ico new file mode 100644 index 0000000..147bdcb Binary files /dev/null and b/test/image.ico differ diff --git a/test/image.jpg b/test/image.jpg new file mode 100644 index 0000000..2d6a14b Binary files /dev/null and b/test/image.jpg differ diff --git a/test/image.jxl b/test/image.jxl new file mode 100644 index 0000000..d1a5a7d Binary files /dev/null and b/test/image.jxl differ diff --git a/test/image.png b/test/image.png new file mode 100644 index 0000000..d633ae1 Binary files /dev/null and b/test/image.png differ diff --git a/test/image.tif b/test/image.tif new file mode 100644 index 0000000..442041a Binary files /dev/null and b/test/image.tif differ diff --git a/test/image.tiff b/test/image.tiff new file mode 100644 index 0000000..e8a135c Binary files /dev/null and b/test/image.tiff differ diff --git a/test/image.webp b/test/image.webp new file mode 100644 index 0000000..122741b Binary files /dev/null and b/test/image.webp differ diff --git a/version.go b/version.go new file mode 100644 index 0000000..15baba7 --- /dev/null +++ b/version.go @@ -0,0 +1,3 @@ +package main + +const Version = "development"