From 0dfdc8110c71c67649f1ebf73aceef727c0cbc96 Mon Sep 17 00:00:00 2001 From: anthonynsimon Date: Mon, 2 Mar 2026 23:30:17 +0100 Subject: [PATCH] Add support for all transform cmds --- CHANGELOG.md | 1 + README.md | 15 ++++ cmd/helpers.go | 45 ++++++++++++ cmd/transform.go | 183 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 244 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7d72e2..1017e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## next +- Add CLI support for all transform commands - New feature transform.Zoom with CLI support (`bild transform zoom`) - Support Go versions from 1.21 to 1.26 - Fix flaky tests on arm64 diff --git a/README.md b/README.md index 7dc181a..cd489d6 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,21 @@ For example, to apply a median effect with a radius of 1.5 on the image `input.p bild effect median --radius 1.5 input.png output.png ``` +To resize an image to 800x600 using the lanczos filter: +``` +bild transform resize --width 800 --height 600 --filter lanczos input.png output.png +``` + +To rotate an image 90 degrees clockwise, expanding bounds to fit: +``` +bild transform rotate --angle 90 --resize-bounds input.png output.png +``` + +To crop an image to a specific region: +``` +bild transform crop --rect 0x0+512x256 input.png output.png +``` + ## Install package diff --git a/cmd/helpers.go b/cmd/helpers.go index ffe36b2..0f131ac 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/anthonynsimon/bild/imgio" + "github.com/anthonynsimon/bild/transform" ) var jpgExtensions = []string{".jpg", ".jpeg"} @@ -18,6 +19,10 @@ var bmpExtensions = []string{".bmp"} var ( // ErrWrongSize is thrown when the provided size string does not match the expected form. errWrongSize = errors.New("size must be of form [width]x[height], i.e. 400x200") + // errWrongRect is thrown when the provided rect string does not match the expected form. + errWrongRect = errors.New("rect must be of form [x0]x[y0]+[x1]x[y1], i.e. 0x0+512x256") + // errUnknownFilter is thrown when an unknown resample filter name is provided. + errUnknownFilter = errors.New("unknown filter, options: nearestneighbor, box, linear, gaussian, mitchellnetravali, catmullrom, lanczos") ) type size struct { @@ -104,3 +109,43 @@ func parseSizeStr(sizestr string) (*size, error) { Height: h, }, nil } + +func parseRectStr(rectstr string) (image.Rectangle, error) { + parts := strings.SplitN(rectstr, "+", 2) + if len(parts) != 2 { + return image.Rectangle{}, errWrongRect + } + + min, err := parseSizeStr(parts[0]) + if err != nil { + return image.Rectangle{}, errWrongRect + } + + max, err := parseSizeStr(parts[1]) + if err != nil { + return image.Rectangle{}, errWrongRect + } + + return image.Rect(min.Width, min.Height, max.Width, max.Height), nil +} + +func parseResampleFilter(name string) (transform.ResampleFilter, error) { + switch strings.ToLower(name) { + case "nearestneighbor": + return transform.NearestNeighbor, nil + case "box": + return transform.Box, nil + case "linear": + return transform.Linear, nil + case "gaussian": + return transform.Gaussian, nil + case "mitchellnetravali": + return transform.MitchellNetravali, nil + case "catmullrom": + return transform.CatmullRom, nil + case "lanczos": + return transform.Lanczos, nil + default: + return transform.ResampleFilter{}, errUnknownFilter + } +} diff --git a/cmd/transform.go b/cmd/transform.go index 22d9471..9da301f 100644 --- a/cmd/transform.go +++ b/cmd/transform.go @@ -38,6 +38,181 @@ func zoom() *cobra.Command { return cmd } +func rotate() *cobra.Command { + var angle float64 + var pivot string + var resizeBounds bool + + var cmd = &cobra.Command{ + Use: "rotate", + Short: "rotate an image by the given angle in degrees", + Args: cobra.ExactArgs(2), + Example: "rotate --angle 90 --resize-bounds input.jpg output.jpg", + Run: func(cmd *cobra.Command, args []string) { + fin := args[0] + fout := args[1] + + var opts *transform.RotationOptions + if pivot != "" || resizeBounds { + opts = &transform.RotationOptions{ResizeBounds: resizeBounds} + if pivot != "" { + s, err := parseSizeStr(pivot) + exitIfNotNil(err) + opts.Pivot = &image.Point{X: s.Width, Y: s.Height} + } + } + + apply(fin, fout, func(img image.Image) (image.Image, error) { + return transform.Rotate(img, angle, opts), nil + }) + }} + + cmd.Flags().Float64VarP(&angle, "angle", "a", 0, "rotation angle in degrees (clockwise)") + cmd.Flags().StringVarP(&pivot, "pivot", "p", "", "pivot point as XxY (e.g. 100x100)") + cmd.Flags().BoolVar(&resizeBounds, "resize-bounds", false, "resize image bounds to fit rotated content") + + return cmd +} + +func fliph() *cobra.Command { + return &cobra.Command{ + Use: "fliph", + Short: "flip an image horizontally", + Args: cobra.ExactArgs(2), + Example: "fliph input.jpg output.jpg", + Run: func(cmd *cobra.Command, args []string) { + apply(args[0], args[1], func(img image.Image) (image.Image, error) { + return transform.FlipH(img), nil + }) + }, + } +} + +func flipv() *cobra.Command { + return &cobra.Command{ + Use: "flipv", + Short: "flip an image vertically", + Args: cobra.ExactArgs(2), + Example: "flipv input.jpg output.jpg", + Run: func(cmd *cobra.Command, args []string) { + apply(args[0], args[1], func(img image.Image) (image.Image, error) { + return transform.FlipV(img), nil + }) + }, + } +} + +func resize() *cobra.Command { + var width, height int + var filter string + + var cmd = &cobra.Command{ + Use: "resize", + Short: "resize an image to the given dimensions", + Args: cobra.ExactArgs(2), + Example: "resize --width 800 --height 600 --filter lanczos input.jpg output.jpg", + Run: func(cmd *cobra.Command, args []string) { + fin := args[0] + fout := args[1] + + f, err := parseResampleFilter(filter) + exitIfNotNil(err) + + apply(fin, fout, func(img image.Image) (image.Image, error) { + return transform.Resize(img, width, height, f), nil + }) + }} + + cmd.Flags().IntVarP(&width, "width", "w", 0, "target width in pixels") + cmd.Flags().IntVarP(&height, "height", "h", 0, "target height in pixels") + cmd.Flags().StringVarP(&filter, "filter", "f", "linear", "resampling filter (nearestneighbor, box, linear, gaussian, mitchellnetravali, catmullrom, lanczos)") + + return cmd +} + +func crop() *cobra.Command { + var rect string + + var cmd = &cobra.Command{ + Use: "crop", + Short: "crop an image to the given rectangle", + Args: cobra.ExactArgs(2), + Example: "crop --rect 0x0+512x256 input.jpg output.jpg", + Run: func(cmd *cobra.Command, args []string) { + fin := args[0] + fout := args[1] + + r, err := parseRectStr(rect) + exitIfNotNil(err) + + apply(fin, fout, func(img image.Image) (image.Image, error) { + return transform.Crop(img, r), nil + }) + }} + + cmd.Flags().StringVarP(&rect, "rect", "r", "", "crop rectangle as X0xY0+X1xY1 (e.g. 0x0+512x256)") + + return cmd +} + +func translate() *cobra.Command { + var dx, dy int + + var cmd = &cobra.Command{ + Use: "translate", + Short: "shift an image by dx and dy pixels", + Args: cobra.ExactArgs(2), + Example: "translate --dx 100 --dy 50 input.jpg output.jpg", + Run: func(cmd *cobra.Command, args []string) { + apply(args[0], args[1], func(img image.Image) (image.Image, error) { + return transform.Translate(img, dx, dy), nil + }) + }} + + cmd.Flags().IntVar(&dx, "dx", 0, "horizontal shift in pixels (positive moves right)") + cmd.Flags().IntVar(&dy, "dy", 0, "vertical shift in pixels (positive moves up)") + + return cmd +} + +func shearh() *cobra.Command { + var angle float64 + + var cmd = &cobra.Command{ + Use: "shearh", + Short: "apply horizontal shear transformation", + Args: cobra.ExactArgs(2), + Example: "shearh --angle 30 input.jpg output.jpg", + Run: func(cmd *cobra.Command, args []string) { + apply(args[0], args[1], func(img image.Image) (image.Image, error) { + return transform.ShearH(img, angle), nil + }) + }} + + cmd.Flags().Float64VarP(&angle, "angle", "a", 0, "shear angle in degrees") + + return cmd +} + +func shearv() *cobra.Command { + var angle float64 + + var cmd = &cobra.Command{ + Use: "shearv", + Short: "apply vertical shear transformation", + Args: cobra.ExactArgs(2), + Example: "shearv --angle 30 input.jpg output.jpg", + Run: func(cmd *cobra.Command, args []string) { + apply(args[0], args[1], func(img image.Image) (image.Image, error) { + return transform.ShearV(img, angle), nil + }) + }} + + cmd.Flags().Float64VarP(&angle, "angle", "a", 0, "shear angle in degrees") + + return cmd +} + func createTransform() *cobra.Command { var transformCmd = &cobra.Command{ Use: "transform", @@ -45,6 +220,14 @@ func createTransform() *cobra.Command { } transformCmd.AddCommand(zoom()) + transformCmd.AddCommand(rotate()) + transformCmd.AddCommand(fliph()) + transformCmd.AddCommand(flipv()) + transformCmd.AddCommand(resize()) + transformCmd.AddCommand(crop()) + transformCmd.AddCommand(translate()) + transformCmd.AddCommand(shearh()) + transformCmd.AddCommand(shearv()) return transformCmd }