Skip to content
Open
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
3 changes: 1 addition & 2 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ jobs:
os:
- ubuntu-latest
- macos-15
# 1.25 is the floor imposed by golang.org/x/image; keep in sync with go.mod.
# 1.26 is the floor imposed by github.com/gen2brain/h265; keep in sync with go.mod.
go-version:
- '1.25'
- '1.26'

steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.26.0'
go-version: '1.26.4'

- name: Test
run: make test
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased
- Add HEIC/HEIF decoding and encoding via `imgio.HEICEncoder` and `.heic`/`.heif` CLI output, backed by the pure Go `github.com/gen2brain/h265` codec
- Bump minimum supported Go version to 1.26

## 0.17.1
- Fix transparent pixels when resizing with the Box filter by @youdie006 in https://github.com/anthonynsimon/bild/pull/115
- Bump golang.org/x/image from 0.44.0 to 0.45.0 to address CVE-2026-46603 in the VP8L decoder, reported in https://github.com/anthonynsimon/bild/pull/116
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ bild imgio encode input.png output.webp

## Install package

bild requires Go version 1.25 or greater.
bild requires Go version 1.26 or greater.

```bash
go get github.com/anthonynsimon/bild/...
Expand Down Expand Up @@ -120,14 +120,19 @@ func main() {

## Supported formats

`imgio.Open` decodes PNG, JPEG, BMP and WebP images. The following encoders are available:
`imgio.Open` decodes PNG, JPEG, BMP, WebP and HEIC/HEIF images. The following encoders are available:

- `imgio.PNGEncoder()`
- `imgio.JPEGEncoder(quality)`
- `imgio.BMPEncoder()`
- `imgio.WEBPEncoder(options)` — pass `nil` for defaults
- `imgio.HEICEncoder(options)` — pass `nil` for defaults

The CLI selects the encoder from the output file extension (`.png`, `.jpg`/`.jpeg`, `.bmp`, `.webp`).
The CLI selects the encoder from the output file extension (`.png`, `.jpg`/`.jpeg`, `.bmp`, `.webp`, `.heic`/`.heif`).

HEIC support is pure Go and adds no transitive dependencies, but note that HEVC is
covered by patents; distributing software that decodes or encodes it may require a
licence from the patent holders.

# Output examples
## Adjustment
Expand Down
7 changes: 7 additions & 0 deletions cmd/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var jpgExtensions = []string{".jpg", ".jpeg"}
var pngExtensions = []string{".png"}
var bmpExtensions = []string{".bmp"}
var webpExtensions = []string{".webp"}
var heicExtensions = []string{".heic", ".heif"}

var (
// ErrWrongSize is thrown when the provided size string does not match the expected form.
Expand Down Expand Up @@ -58,6 +59,12 @@ func resolveEncoder(outputfile string, defaultEncoding imgio.Encoder) imgio.Enco
}
}

for _, ext := range heicExtensions {
if strings.HasSuffix(lower, ext) {
return imgio.HEICEncoder(nil)
}
}

return defaultEncoding
}

Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module github.com/anthonynsimon/bild

go 1.25.0
go 1.26.4

require (
github.com/HugoSmits86/nativewebp v1.3.0
github.com/gen2brain/h265 v0.2.2
github.com/spf13/cobra v1.10.2
golang.org/x/image v0.45.0
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/HugoSmits86/nativewebp v1.3.0 h1:n1egtEzSV4KwFtealr7dzdYq1wI/uj/bOQ/QcTcIyVE=
github.com/HugoSmits86/nativewebp v1.3.0/go.mod h1:YNQuWenlVmSUUASVNhTDwf4d7FwYQGbGhklC8p72Vr8=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/gen2brain/h265 v0.2.2 h1:rnpfo8I4PFhohbij8ySYZlJFn07TTwV+/uWBrKsOEas=
github.com/gen2brain/h265 v0.2.2/go.mod h1:kefpLxLhGpNCUG/q/aabJcD6GqB9Nhet3fNi9xX9Wzw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down
12 changes: 12 additions & 0 deletions imgio/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"

"github.com/HugoSmits86/nativewebp"
"github.com/gen2brain/h265/heic"
"golang.org/x/image/bmp"
)

Expand Down Expand Up @@ -65,6 +66,17 @@ func WEBPEncoder(o *nativewebp.Options) Encoder {
}
}

// HEICEncoder returns an encoder to HEIC. Pass nil to encode with the
// default options, which are lossy 4:2:0 at 8 bits.
func HEICEncoder(o *heic.EncodeOptions) Encoder {
return func(w io.Writer, img image.Image) error {
if o == nil {
return heic.Encode(w, img)
}
return heic.Encode(w, img, *o)
}
}

// Save creates a file and writes to it an image using the provided encoder.
//
// Usage example:
Expand Down
42 changes: 42 additions & 0 deletions imgio/io_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"image"
"strings"
"testing"

"github.com/anthonynsimon/bild/clone"
"github.com/anthonynsimon/bild/util"
)

func TestEncode(t *testing.T) {
Expand Down Expand Up @@ -52,6 +55,19 @@ func TestEncode(t *testing.T) {
},
},
},
{
format: "heic",
encoder: HEICEncoder(nil),
value: &image.RGBA{
Rect: image.Rect(0, 0, 3, 3),
Stride: 3 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
},
},
},
{
format: "webp",
encoder: WEBPEncoder(nil),
Expand Down Expand Up @@ -79,3 +95,29 @@ func TestEncode(t *testing.T) {
}
}
}

func TestOpenHEIC(t *testing.T) {
// A 4x4 image of four solid 2x2 quadrants: red, green, blue, white,
// written losslessly at 4:4:4. The coded samples are exact, but the
// RGB to YCbCr conversion either way rounds, hence the tolerance.
expected := &image.RGBA{
Rect: image.Rect(0, 0, 4, 4),
Stride: 4 * 4,
Pix: []uint8{
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
},
}

img, err := Open("testdata/quadrants.heic")
if err != nil {
t.Fatal(err)
}

actual := clone.AsRGBA(img)
if !util.RGBAImageApproxEqual(actual, expected, 1) {
t.Errorf("%s: expected: %#v, actual: %#v", "Open HEIC", expected, actual)
}
}
Binary file added imgio/testdata/quadrants.heic
Binary file not shown.