mirror of
https://codeberg.org/gigirassy/cuber
synced 2026-08-30 23:37:42 +00:00
214 lines
4.1 KiB
Go
214 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"image"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/image/webp"
|
|
)
|
|
|
|
func main() {
|
|
quality := flag.Int("q", 95, "")
|
|
outFmt := flag.String("out", "", "")
|
|
workers := flag.Int("workers", runtime.NumCPU(), "")
|
|
overwrite := flag.Bool("overwrite", false, "")
|
|
flag.Usage = func() {}
|
|
flag.Parse()
|
|
|
|
files := flag.Args()
|
|
if len(files) == 0 {
|
|
os.Exit(1)
|
|
}
|
|
|
|
var forcedFmt string
|
|
if *outFmt != "" {
|
|
f := strings.ToLower(*outFmt)
|
|
if f == "jpeg" {
|
|
f = "jpg"
|
|
}
|
|
if f != "jpg" && f != "png" {
|
|
log.Fatalf("unsupported --out format: %s", *outFmt)
|
|
}
|
|
forcedFmt = f
|
|
}
|
|
|
|
jobs := make(chan string)
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < *workers; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for path := range jobs {
|
|
if err := processFile(path, forcedFmt, *quality, *overwrite); err != nil {
|
|
log.Printf("%s: %v\n", path, err)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
for _, f := range files {
|
|
jobs <- f
|
|
}
|
|
close(jobs)
|
|
wg.Wait()
|
|
}
|
|
|
|
func processFile(path, forcedFmt string, quality int, overwrite bool) error {
|
|
inF, err := os.Open(path)
|
|
if err != nil {
|
|
return fmt.Errorf("open: %w", err)
|
|
}
|
|
defer inF.Close()
|
|
|
|
ext := strings.ToLower(filepath.Ext(path))
|
|
ext = strings.TrimPrefix(ext, ".")
|
|
var img image.Image
|
|
|
|
if ext == "webp" {
|
|
img, err = webp.Decode(inF)
|
|
if err != nil {
|
|
return fmt.Errorf("decode webp: %w", err)
|
|
}
|
|
} else {
|
|
img, _, err = image.Decode(inF)
|
|
if err != nil {
|
|
if try, _ := inF.Seek(0, io.SeekStart); try == 0 {
|
|
if dec, werr := webp.Decode(inF); werr == nil {
|
|
img = dec
|
|
} else {
|
|
return fmt.Errorf("decode: %w (webp fallback: %v)", err, werr)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("decode: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
var target string
|
|
if forcedFmt != "" {
|
|
target = forcedFmt
|
|
} else {
|
|
switch ext {
|
|
case "jpg", "jpeg":
|
|
target = "jpg"
|
|
case "png":
|
|
target = "png"
|
|
case "webp":
|
|
target = "jpg"
|
|
default:
|
|
target = "jpg"
|
|
}
|
|
}
|
|
|
|
dir := filepath.Dir(path)
|
|
base := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
|
outName := fmt.Sprintf("%s-opt.%s", base, target)
|
|
outPath := filepath.Join(dir, outName)
|
|
if overwrite {
|
|
outPath = path
|
|
}
|
|
|
|
tmpPath := outPath + ".tmp"
|
|
outF, err := os.Create(tmpPath)
|
|
if err != nil {
|
|
return fmt.Errorf("create output: %w", err)
|
|
}
|
|
|
|
if target == "jpg" {
|
|
if hasAlpha(img) {
|
|
img = removeAlpha(img)
|
|
}
|
|
opts := &jpeg.Options{Quality: clampQuality(quality)}
|
|
if err := jpeg.Encode(outF, img, opts); err != nil {
|
|
outF.Close()
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("encode jpeg: %w", err)
|
|
}
|
|
} else if target == "png" {
|
|
enc := png.Encoder{CompressionLevel: png.BestCompression}
|
|
if err := enc.Encode(outF, img); err != nil {
|
|
outF.Close()
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("encode png: %w", err)
|
|
}
|
|
} else {
|
|
outF.Close()
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("unsupported target format: %s", target)
|
|
}
|
|
outF.Close()
|
|
|
|
if overwrite {
|
|
if err := os.Rename(tmpPath, outPath); err != nil {
|
|
return fmt.Errorf("rename: %w", err)
|
|
}
|
|
} else {
|
|
final := outPath
|
|
i := 1
|
|
for {
|
|
if _, err := os.Stat(final); os.IsNotExist(err) {
|
|
break
|
|
}
|
|
final = filepath.Join(dir, fmt.Sprintf("%s-opt-%d.%s", base, i, target))
|
|
i++
|
|
}
|
|
if err := os.Rename(tmpPath, final); err != nil {
|
|
return fmt.Errorf("rename: %w", err)
|
|
}
|
|
outPath = final
|
|
}
|
|
|
|
fmt.Printf("%s -> %s\n", path, outPath)
|
|
return nil
|
|
}
|
|
|
|
func clampQuality(q int) int {
|
|
if q < 1 {
|
|
return 1
|
|
}
|
|
if q > 100 {
|
|
return 100
|
|
}
|
|
return q
|
|
}
|
|
|
|
func hasAlpha(img image.Image) bool {
|
|
switch img.(type) {
|
|
case *image.NRGBA, *image.NRGBA64, *image.RGBA, *image.RGBA64:
|
|
return true
|
|
default:
|
|
b := img.Bounds()
|
|
if b.Empty() {
|
|
return false
|
|
}
|
|
r, g, b2, a := img.At(b.Min.X, b.Min.Y).RGBA()
|
|
return a != 0xffff || r != g || g != b2
|
|
}
|
|
}
|
|
|
|
func removeAlpha(src image.Image) image.Image {
|
|
b := src.Bounds()
|
|
dst := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
|
for y := 0; y < b.Dy(); y++ {
|
|
for x := 0; x < b.Dx(); x++ {
|
|
dst.Set(x, y, image.White)
|
|
}
|
|
}
|
|
for y := b.Min.Y; y < b.Max.Y; y++ {
|
|
for x := b.Min.X; x < b.Max.X; x++ {
|
|
dst.Set(x-b.Min.X, y-b.Min.Y, src.At(x, y))
|
|
}
|
|
}
|
|
return dst
|
|
} |