Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 45 additions & 0 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"

"github.com/anthonynsimon/bild/imgio"
"github.com/anthonynsimon/bild/transform"
)

var jpgExtensions = []string{".jpg", ".jpeg"}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
}
183 changes: 183 additions & 0 deletions cmd/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,196 @@ 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",
Short: "apply geometric transformations to images",
}

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
}
Loading