aboutsummaryrefslogtreecommitdiffstats
path: root/convert.go
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-06-12 21:08:16 -0700
committerRose Hogenson <rosehogenson@posteo.net>2026-06-12 21:08:16 -0700
commitc64c27668fd923e3c29abf1df7831d7830360142 (patch)
treecb72cbe29114df240307db05215f3954ad93b216 /convert.go
parente384a47e508a114bdfdda9b5356f4887ba853335 (diff)
parente005a8d182df6ce62cf83f6ef71537c3c113e61d (diff)
downloadimgutil-c64c27668fd923e3c29abf1df7831d7830360142.tar.zst
Add convert to imgutil
Diffstat (limited to 'convert.go')
-rw-r--r--convert.go149
1 files changed, 149 insertions, 0 deletions
diff --git a/convert.go b/convert.go
new file mode 100644
index 0000000..d0523ba
--- /dev/null
+++ b/convert.go
@@ -0,0 +1,149 @@
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "image"
+ "image/gif"
+ "image/jpeg"
+ "image/png"
+ "os"
+ "strings"
+
+ "golang.org/x/image/bmp"
+ "golang.org/x/image/draw"
+ "golang.org/x/image/tiff"
+ _ "golang.org/x/image/webp"
+)
+
+var (
+ x = flag.Int("x", 0, "new image width")
+ y = flag.Int("y", 0, "new image height")
+ stretch = flag.Bool("stretch", false, "if resizing, whether to stretch the image rather than cropping (the default)")
+ format = flag.String("format", "", "output image format, default is to use the file extension")
+)
+
+func load(filename string) (image.Image, error) {
+ file := os.Stdin
+ if filename != "-" {
+ var err error
+ file, err = os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ }
+
+ img, _, err := image.Decode(file)
+ if err != nil {
+ return nil, fmt.Errorf("decode %q: %s", filename, err)
+ }
+
+ return img, nil
+}
+
+func divRound(n, d int) int {
+ return (n + d/2) / d
+}
+
+func resize(img image.Image) image.Image {
+ x, y := *x, *y
+ if x == 0 {
+ x = divRound(y*img.Bounds().Dx(), img.Bounds().Dy())
+ }
+ if y == 0 {
+ y = divRound(x*img.Bounds().Dy(), img.Bounds().Dx())
+ }
+ output := image.NewRGBA64(image.Rect(0, 0, x, y))
+ srcRect := img.Bounds()
+ if !*stretch {
+ if img.Bounds().Dx()*y >= img.Bounds().Dy()*x {
+ scaledX := divRound(img.Bounds().Dy()*x, y)
+ xOffset := (img.Bounds().Dx() - scaledX) / 2
+ srcRect.Min.X += xOffset
+ srcRect.Max.X = srcRect.Min.X + scaledX
+ } else {
+ scaledY := divRound(img.Bounds().Dx()*y, x)
+ yOffset := (img.Bounds().Dy() - scaledY) / 2
+ srcRect.Min.Y += yOffset
+ srcRect.Max.Y = srcRect.Min.Y + scaledY
+ }
+ }
+ draw.CatmullRom.Scale(output, output.Bounds(), img, srcRect, draw.Src, nil)
+ return output
+}
+
+func run() error {
+ args := flag.Args()
+ if len(args) != 2 {
+ return errors.New("usage: convert input-file output-file")
+ }
+ inputFileName, outputFileName := args[0], args[1]
+
+ ext := *format
+ if ext == "" {
+ n := strings.LastIndex(outputFileName, ".")
+ if n < 0 {
+ return errors.New("unknown output file extension")
+ }
+ ext = outputFileName[n+1:]
+ }
+ switch ext {
+ case "gif", "jpeg", "jpg", "png", "bmp", "tiff":
+ default:
+ return fmt.Errorf("unknown output file extension %q", ext)
+ }
+
+ input, err := load(inputFileName)
+ if err != nil {
+ return err
+ }
+
+ if *x != 0 || *y != 0 {
+ input = resize(input)
+ }
+
+ outputFile, err := os.Create(outputFileName)
+ if err != nil {
+ return err
+ }
+ defer outputFile.Close()
+ switch ext {
+ case "gif":
+ err = gif.Encode(outputFile, input, nil)
+ case "jpeg", "jpg":
+ err = jpeg.Encode(outputFile, input, &jpeg.Options{Quality: 100})
+ case "png":
+ err = png.Encode(outputFile, input)
+ case "bmp":
+ err = bmp.Encode(outputFile, input)
+ case "tiff":
+ err = tiff.Encode(outputFile, input, nil)
+ }
+ if err != nil {
+ return err
+ }
+ return outputFile.Close()
+}
+
+func main() {
+ flag.Usage = func() {
+ fmt.Fprint(os.Stderr, `Usage: convert input-file output-file
+Supported formats:
+ png
+ jpeg
+ bmp
+ tiff
+ webp (input only)
+
+`)
+ flag.PrintDefaults()
+ }
+ flag.Parse()
+
+ if err := run(); err != nil {
+ fmt.Fprintf(os.Stderr, "convert: %s\n", err)
+ os.Exit(1)
+ }
+}