detached 513887c

Merge pull request #346 from bnema/feat/webp-favicons

feat(favicon): decode WebP favicons safely
commit
513887caaed76906369620157f1c762fb4b16793
author
bnema · · 2026-07-14 17:26 UTC
committer
GitHub · · 2026-07-14 17:26 UTC
merge of
45b5ee0 + d921a00 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
19 files · +1902 -11
M README.md +3 -2
@@ -134,7 +134,7 @@ Dumber uses Chromium Embedded Framework by default. WebKitGTK is available as a
134134 On Arch Linux, install the CEF runtime with:
135135
136136 ```bash
137-sudo pacman -S cef
137+sudo pacman -S cef libwebp
138138 ```
139139
140140 For hardware video decoding, `cef-vaapi` is also available from the AUR as an optional CEF build.
@@ -179,7 +179,7 @@ CEF OSR frame rate adapts to the active Wayland monitor refresh rate when both `
179179 **Arch Linux:**
180180
181181 ```bash
182-sudo pacman -S cef webkitgtk-6.0 gtk4 gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugin-va
182+sudo pacman -S cef webkitgtk-6.0 gtk4 libwebp gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugin-va
183183 ```
184184
185185 **Debian/Ubuntu:**
@@ -213,6 +213,7 @@ Set `ENV=dev` to use `.dev/dumber/` for config and data instead of XDG paths.
213213 - CEF runtime (default backend)
214214 - WebKitGTK 6.0 development/runtime packages (fallback backend and runtime checks)
215215 - Brotli for compressed systemviews assets
216+- libwebp runtime for WebP favicon decoding (for example `libwebp` on Arch)
216217
217218 Systemviews assets are generated with `go tool templ` and Go's `js/wasm` toolchain; no root Node toolchain is required.
218219
@@ -134,7 +134,7 @@ Dumber uses Chromium Embedded Framework by default. WebKitGTK is available as a
134 On Arch Linux, install the CEF runtime with:134 On Arch Linux, install the CEF runtime with:
135 135
136 ```bash136 ```bash
137-sudo pacman -S cef137+sudo pacman -S cef libwebp
138 ```138 ```
139 139
140 For hardware video decoding, `cef-vaapi` is also available from the AUR as an optional CEF build.140 For hardware video decoding, `cef-vaapi` is also available from the AUR as an optional CEF build.
@@ -179,7 +179,7 @@ CEF OSR frame rate adapts to the active Wayland monitor refresh rate when both `
179 **Arch Linux:**179 **Arch Linux:**
180 180
181 ```bash181 ```bash
182-sudo pacman -S cef webkitgtk-6.0 gtk4 gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugin-va182+sudo pacman -S cef webkitgtk-6.0 gtk4 libwebp gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugin-va
183 ```183 ```
184 184
185 **Debian/Ubuntu:**185 **Debian/Ubuntu:**
@@ -213,6 +213,7 @@ Set `ENV=dev` to use `.dev/dumber/` for config and data instead of XDG paths.
213 - CEF runtime (default backend)213 - CEF runtime (default backend)
214 - WebKitGTK 6.0 development/runtime packages (fallback backend and runtime checks)214 - WebKitGTK 6.0 development/runtime packages (fallback backend and runtime checks)
215 - Brotli for compressed systemviews assets215 - Brotli for compressed systemviews assets
216+- libwebp runtime for WebP favicon decoding (for example `libwebp` on Arch)
216 217
217 Systemviews assets are generated with `go tool templ` and Go's `js/wasm` toolchain; no root Node toolchain is required.218 Systemviews assets are generated with `go tool templ` and Go's `js/wasm` toolchain; no root Node toolchain is required.
218 219
M aur/dumber-browser-bin/.SRCINFO +1 -0
@@ -8,6 +8,7 @@ pkgbase = dumber-browser-bin
88 depends = gtk4
99 depends = cef
1010 depends = webkitgtk-6.0
11+ depends = libwebp
1112 optdepends = gst-plugins-base: Base media codecs
1213 optdepends = gst-plugins-good: Additional media codecs
1314 optdepends = gst-plugins-bad: Extended media codecs
@@ -8,6 +8,7 @@ pkgbase = dumber-browser-bin
8 depends = gtk48 depends = gtk4
9 depends = cef9 depends = cef
10 depends = webkitgtk-6.010 depends = webkitgtk-6.0
11+ depends = libwebp
11 optdepends = gst-plugins-base: Base media codecs12 optdepends = gst-plugins-base: Base media codecs
12 optdepends = gst-plugins-good: Additional media codecs13 optdepends = gst-plugins-good: Additional media codecs
13 optdepends = gst-plugins-bad: Extended media codecs14 optdepends = gst-plugins-bad: Extended media codecs
M aur/dumber-browser-bin/PKGBUILD +1 -1
@@ -6,7 +6,7 @@ pkgdesc="A minimal keyboard-driven browser for tiling WMs"
66 arch=('x86_64')
77 url="https://github.com/bnema/dumber"
88 license=('MIT')
9-depends=('gtk4' 'cef' 'webkitgtk-6.0')
9+depends=('gtk4' 'cef' 'webkitgtk-6.0' 'libwebp')
1010 optdepends=(
1111 'gst-plugins-base: Base media codecs'
1212 'gst-plugins-good: Additional media codecs'
@@ -6,7 +6,7 @@ pkgdesc="A minimal keyboard-driven browser for tiling WMs"
6 arch=('x86_64')6 arch=('x86_64')
7 url="https://github.com/bnema/dumber"7 url="https://github.com/bnema/dumber"
8 license=('MIT')8 license=('MIT')
9-depends=('gtk4' 'cef' 'webkitgtk-6.0')9+depends=('gtk4' 'cef' 'webkitgtk-6.0' 'libwebp')
10 optdepends=(10 optdepends=(
11 'gst-plugins-base: Base media codecs'11 'gst-plugins-base: Base media codecs'
12 'gst-plugins-good: Additional media codecs'12 'gst-plugins-good: Additional media codecs'
M aur/dumber-browser-git/.SRCINFO +1 -0
@@ -11,6 +11,7 @@ pkgbase = dumber-browser-git
1111 depends = gtk4
1212 depends = cef
1313 depends = webkitgtk-6.0
14+ depends = libwebp
1415 optdepends = gst-plugins-base: Base media codecs
1516 optdepends = gst-plugins-good: Additional media codecs
1617 optdepends = gst-plugins-bad: Extended media codecs
@@ -11,6 +11,7 @@ pkgbase = dumber-browser-git
11 depends = gtk411 depends = gtk4
12 depends = cef12 depends = cef
13 depends = webkitgtk-6.013 depends = webkitgtk-6.0
14+ depends = libwebp
14 optdepends = gst-plugins-base: Base media codecs15 optdepends = gst-plugins-base: Base media codecs
15 optdepends = gst-plugins-good: Additional media codecs16 optdepends = gst-plugins-good: Additional media codecs
16 optdepends = gst-plugins-bad: Extended media codecs17 optdepends = gst-plugins-bad: Extended media codecs
M aur/dumber-browser-git/PKGBUILD +1 -1
@@ -7,7 +7,7 @@ arch=('x86_64')
77 url="https://github.com/bnema/dumber"
88 license=('MIT')
99 makedepends=('go>=1.26' 'git' 'brotli')
10-depends=('gtk4' 'cef' 'webkitgtk-6.0')
10+depends=('gtk4' 'cef' 'webkitgtk-6.0' 'libwebp')
1111 optdepends=(
1212 'gst-plugins-base: Base media codecs'
1313 'gst-plugins-good: Additional media codecs'
@@ -7,7 +7,7 @@ arch=('x86_64')
7 url="https://github.com/bnema/dumber"7 url="https://github.com/bnema/dumber"
8 license=('MIT')8 license=('MIT')
9 makedepends=('go>=1.26' 'git' 'brotli')9 makedepends=('go>=1.26' 'git' 'brotli')
10-depends=('gtk4' 'cef' 'webkitgtk-6.0')10+depends=('gtk4' 'cef' 'webkitgtk-6.0' 'libwebp')
11 optdepends=(11 optdepends=(
12 'gst-plugins-base: Base media codecs'12 'gst-plugins-base: Base media codecs'
13 'gst-plugins-good: Additional media codecs'13 'gst-plugins-good: Additional media codecs'
M flatpak/dev.bnema.Dumber.yml +1 -0
@@ -1,6 +1,7 @@
11 id: dev.bnema.Dumber
22 runtime: org.gnome.Platform
33 runtime-version: '49'
4+# org.gnome.Platform provides libwebp at runtime for purego WebP favicon decoding.
45 sdk: org.gnome.Sdk
56 command: dumber
67
@@ -1,6 +1,7 @@
1 id: dev.bnema.Dumber1 id: dev.bnema.Dumber
2 runtime: org.gnome.Platform2 runtime: org.gnome.Platform
3 runtime-version: '49'3 runtime-version: '49'
4+# org.gnome.Platform provides libwebp at runtime for purego WebP favicon decoding.
4 sdk: org.gnome.Sdk5 sdk: org.gnome.Sdk
5 command: dumber6 command: dumber
6 7
M go.mod +1 -0
@@ -10,6 +10,7 @@ require (
1010 github.com/bnema/purego-cef2gtk v0.8.4-0.20260714160309-bbd397409ebe
1111 github.com/bnema/purego-pipewire v0.1.5-0.20260714124450-6504b9cdf02a
1212 github.com/bnema/purego-sqlite v0.1.4-0.20260714131321-3614fe683b9c
13+ github.com/bnema/purego-webp v0.2.1-0.20260714152238-7bc9c402fdf6
1314 github.com/bnema/puregotk v0.7.1-0.20260714170452-0b6b9b901234
1415 github.com/charmbracelet/bubbles v1.0.0
1516 github.com/charmbracelet/bubbletea v1.3.10
@@ -10,6 +10,7 @@ require (
10 github.com/bnema/purego-cef2gtk v0.8.4-0.20260714160309-bbd397409ebe10 github.com/bnema/purego-cef2gtk v0.8.4-0.20260714160309-bbd397409ebe
11 github.com/bnema/purego-pipewire v0.1.5-0.20260714124450-6504b9cdf02a11 github.com/bnema/purego-pipewire v0.1.5-0.20260714124450-6504b9cdf02a
12 github.com/bnema/purego-sqlite v0.1.4-0.20260714131321-3614fe683b9c12 github.com/bnema/purego-sqlite v0.1.4-0.20260714131321-3614fe683b9c
13+ github.com/bnema/purego-webp v0.2.1-0.20260714152238-7bc9c402fdf6
13 github.com/bnema/puregotk v0.7.1-0.20260714170452-0b6b9b90123414 github.com/bnema/puregotk v0.7.1-0.20260714170452-0b6b9b901234
14 github.com/charmbracelet/bubbles v1.0.015 github.com/charmbracelet/bubbles v1.0.0
15 github.com/charmbracelet/bubbletea v1.3.1016 github.com/charmbracelet/bubbletea v1.3.10
M go.sum +2 -0 generated file: collapsed
M internal/application/usecase/favicon_test.go +19 -0
@@ -404,6 +404,25 @@ func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T)
404404 }
405405 }
406406
407+func TestFaviconObserveUnavailableWebPPreservesCachedAssets(t *testing.T) {
408+ fx := newFaviconFixture(t)
409+ fx.seedOriginal([]byte("old"))
410+ fx.blobs.png["example.com"] = []byte("old-png")
411+ fx.blobs.sized["example.com"] = map[int][]byte{32: []byte("old-32")}
412+ fx.converter.errByContentType = map[string]error{"image/webp": ErrFaviconMiss}
413+
414+ _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://example.com/favicon.webp", []byte("webp"), favicon.SourceEngine, "image/webp")
415+ if !errors.Is(err, ErrFaviconMiss) {
416+ t.Fatalf("Observe err=%v, want favicon miss", err)
417+ }
418+ if string(fx.blobs.original["example.com"]) != "old" || string(fx.blobs.png["example.com"]) != "old-png" || string(fx.blobs.sized["example.com"][32]) != "old-32" {
419+ t.Fatalf("cached assets changed after unavailable WebP decoder")
420+ }
421+ if fx.blobs.writeOriginals != 0 || fx.blobs.removeDerived != 0 || fx.invalidators.calls != 0 {
422+ t.Fatalf("unavailable decoder mutated cache: writes=%d removes=%d invalidations=%d", fx.blobs.writeOriginals, fx.blobs.removeDerived, fx.invalidators.calls)
423+ }
424+}
425+
407426 func TestFaviconEnsureSizedPropagatesReadSizedErrors(t *testing.T) {
408427 fx := newFaviconFixture(t)
409428 fx.seedOriginal([]byte("old"))
@@ -404,6 +404,25 @@ func TestFaviconObserveConversionErrorDoesNotMutateExistingAssets(t *testing.T)
404 }404 }
405 }405 }
406 406
407+func TestFaviconObserveUnavailableWebPPreservesCachedAssets(t *testing.T) {
408+ fx := newFaviconFixture(t)
409+ fx.seedOriginal([]byte("old"))
410+ fx.blobs.png["example.com"] = []byte("old-png")
411+ fx.blobs.sized["example.com"] = map[int][]byte{32: []byte("old-32")}
412+ fx.converter.errByContentType = map[string]error{"image/webp": ErrFaviconMiss}
413+
414+ _, err := fx.uc.Observe(context.Background(), "https://example.com", "https://example.com/favicon.webp", []byte("webp"), favicon.SourceEngine, "image/webp")
415+ if !errors.Is(err, ErrFaviconMiss) {
416+ t.Fatalf("Observe err=%v, want favicon miss", err)
417+ }
418+ if string(fx.blobs.original["example.com"]) != "old" || string(fx.blobs.png["example.com"]) != "old-png" || string(fx.blobs.sized["example.com"][32]) != "old-32" {
419+ t.Fatalf("cached assets changed after unavailable WebP decoder")
420+ }
421+ if fx.blobs.writeOriginals != 0 || fx.blobs.removeDerived != 0 || fx.invalidators.calls != 0 {
422+ t.Fatalf("unavailable decoder mutated cache: writes=%d removes=%d invalidations=%d", fx.blobs.writeOriginals, fx.blobs.removeDerived, fx.invalidators.calls)
423+ }
424+}
425+
407 func TestFaviconEnsureSizedPropagatesReadSizedErrors(t *testing.T) {426 func TestFaviconEnsureSizedPropagatesReadSizedErrors(t *testing.T) {
408 fx := newFaviconFixture(t)427 fx := newFaviconFixture(t)
409 fx.seedOriginal([]byte("old"))428 fx.seedOriginal([]byte("old"))
M internal/infrastructure/favicon/converter.go +19 -7
@@ -16,17 +16,25 @@ import (
1616 )
1717
1818 // ImageConverter converts safe favicon image formats into normalized PNGs.
19-type ImageConverter struct{}
19+type ImageConverter struct {
20+ webp *webPDecoder
21+}
22+
23+func NewImageConverter() *ImageConverter { return &ImageConverter{webp: newDefaultWebPDecoder()} }
2024
21-func NewImageConverter() *ImageConverter { return &ImageConverter{} }
25+// newImageConverterWithWebPRuntime provides an infrastructure test seam while
26+// application ports remain independent of the libwebp implementation.
27+func newImageConverterWithWebPRuntime(runtime webpRuntime) *ImageConverter {
28+ return &ImageConverter{webp: newWebPDecoder(runtime)}
29+}
2230
23-func (*ImageConverter) Convert(ctx context.Context, original []byte, contentType string, sizes []int) (*appport.ConvertedFavicon, error) {
31+func (c *ImageConverter) Convert(ctx context.Context, original []byte, contentType string, sizes []int) (*appport.ConvertedFavicon, error) {
2432 _ = ctx
2533 if len(original) == 0 {
2634 return nil, fmt.Errorf("%w: empty image", appport.ErrFaviconMiss)
2735 }
2836
29- img, err := decodeFaviconImage(original, normalizeContentType(contentType))
37+ img, err := decodeFaviconImage(original, normalizeContentType(contentType), c.webp)
3038 if err != nil {
3139 return nil, err
3240 }
@@ -48,7 +56,7 @@ func (*ImageConverter) Convert(ctx context.Context, original []byte, contentType
4856 return out, nil
4957 }
5058
51-func decodeFaviconImage(data []byte, ct string) (image.Image, error) {
59+func decodeFaviconImage(data []byte, ct string, webp *webPDecoder) (image.Image, error) {
5260 switch ct {
5361 case "image/svg+xml":
5462 return nil, fmt.Errorf("%w: svg favicon conversion is disabled", appport.ErrFaviconMiss)
@@ -59,9 +67,13 @@ func decodeFaviconImage(data []byte, ct string) (image.Image, error) {
5967 }
6068 return img, nil
6169 case "image/webp":
62- return nil, fmt.Errorf("%w: webp favicon conversion is disabled", appport.ErrFaviconMiss)
70+ img, err := webp.Decode(data)
71+ if err != nil {
72+ return nil, fmt.Errorf("%w: decode webp favicon: %w", appport.ErrFaviconMiss, err)
73+ }
74+ return img, nil
6375 }
64- if ct != "" && ct != "image/png" && ct != "image/jpeg" && ct != "image/gif" && ct != "image/webp" {
76+ if ct != "" && ct != "image/png" && ct != "image/jpeg" && ct != "image/gif" {
6577 return nil, fmt.Errorf("%w: unsupported favicon content type %s", appport.ErrFaviconMiss, ct)
6678 }
6779 img, _, err := image.Decode(bytes.NewReader(data))
@@ -16,17 +16,25 @@ import (
16 )16 )
17 17
18 // ImageConverter converts safe favicon image formats into normalized PNGs.18 // ImageConverter converts safe favicon image formats into normalized PNGs.
19-type ImageConverter struct{}19+type ImageConverter struct {
20+ webp *webPDecoder
21+}
22+
23+func NewImageConverter() *ImageConverter { return &ImageConverter{webp: newDefaultWebPDecoder()} }
20 24
21-func NewImageConverter() *ImageConverter { return &ImageConverter{} }25+// newImageConverterWithWebPRuntime provides an infrastructure test seam while
26+// application ports remain independent of the libwebp implementation.
27+func newImageConverterWithWebPRuntime(runtime webpRuntime) *ImageConverter {
28+ return &ImageConverter{webp: newWebPDecoder(runtime)}
29+}
22 30
23-func (*ImageConverter) Convert(ctx context.Context, original []byte, contentType string, sizes []int) (*appport.ConvertedFavicon, error) {31+func (c *ImageConverter) Convert(ctx context.Context, original []byte, contentType string, sizes []int) (*appport.ConvertedFavicon, error) {
24 _ = ctx32 _ = ctx
25 if len(original) == 0 {33 if len(original) == 0 {
26 return nil, fmt.Errorf("%w: empty image", appport.ErrFaviconMiss)34 return nil, fmt.Errorf("%w: empty image", appport.ErrFaviconMiss)
27 }35 }
28 36
29- img, err := decodeFaviconImage(original, normalizeContentType(contentType))37+ img, err := decodeFaviconImage(original, normalizeContentType(contentType), c.webp)
30 if err != nil {38 if err != nil {
31 return nil, err39 return nil, err
32 }40 }
@@ -48,7 +56,7 @@ func (*ImageConverter) Convert(ctx context.Context, original []byte, contentType
48 return out, nil56 return out, nil
49 }57 }
50 58
51-func decodeFaviconImage(data []byte, ct string) (image.Image, error) {59+func decodeFaviconImage(data []byte, ct string, webp *webPDecoder) (image.Image, error) {
52 switch ct {60 switch ct {
53 case "image/svg+xml":61 case "image/svg+xml":
54 return nil, fmt.Errorf("%w: svg favicon conversion is disabled", appport.ErrFaviconMiss)62 return nil, fmt.Errorf("%w: svg favicon conversion is disabled", appport.ErrFaviconMiss)
@@ -59,9 +67,13 @@ func decodeFaviconImage(data []byte, ct string) (image.Image, error) {
59 }67 }
60 return img, nil68 return img, nil
61 case "image/webp":69 case "image/webp":
62- return nil, fmt.Errorf("%w: webp favicon conversion is disabled", appport.ErrFaviconMiss)70+ img, err := webp.Decode(data)
71+ if err != nil {
72+ return nil, fmt.Errorf("%w: decode webp favicon: %w", appport.ErrFaviconMiss, err)
73+ }
74+ return img, nil
63 }75 }
64- if ct != "" && ct != "image/png" && ct != "image/jpeg" && ct != "image/gif" && ct != "image/webp" {76+ if ct != "" && ct != "image/png" && ct != "image/jpeg" && ct != "image/gif" {
65 return nil, fmt.Errorf("%w: unsupported favicon content type %s", appport.ErrFaviconMiss, ct)77 return nil, fmt.Errorf("%w: unsupported favicon content type %s", appport.ErrFaviconMiss, ct)
66 }78 }
67 img, _, err := image.Decode(bytes.NewReader(data))79 img, _, err := image.Decode(bytes.NewReader(data))
A internal/infrastructure/favicon/testdata/alpha.webp +0 -0 binary file: no text diff

binary file: no text diff

A internal/infrastructure/favicon/webp_decoder.go +93 -0
@@ -0,0 +1,93 @@
1+package favicon
2+
3+import (
4+ "errors"
5+ "image"
6+ "math"
7+
8+ "github.com/bnema/purego-webp/libwebp"
9+)
10+
11+const (
12+ // maxWebPInputBytes bounds untrusted compressed favicon input before it is
13+ // handed to libwebp.
14+ maxWebPInputBytes = 5 << 20
15+ // maxWebPDimension and maxWebPPixels bound the final RGBA allocation.
16+ maxWebPDimension = 4096
17+ maxWebPPixels = 4 << 20
18+)
19+
20+var (
21+ errWebPInputTooLarge = errors.New("webp favicon input exceeds size limit")
22+ errWebPImageTooLarge = errors.New("webp favicon dimensions exceed size limit")
23+ errWebPInvalidMetadata = errors.New("invalid webp favicon metadata")
24+)
25+
26+// webpRuntime is the narrow infrastructure seam around the dynamically loaded
27+// libwebp binding. It deliberately exposes only Go values, keeping backend
28+// types out of ports and the domain.
29+type webpRuntime interface {
30+ Info(data []byte) (width, height int, err error)
31+ DecodeRGBAInto(data, pix []byte, stride, width, height int) error
32+}
33+
34+type libwebpRuntime struct{}
35+
36+func (libwebpRuntime) Info(data []byte) (int, int, error) {
37+ width, height, ok, err := libwebp.WebPGetInfo(data)
38+ if err != nil {
39+ return 0, 0, err
40+ }
41+ if !ok {
42+ return 0, 0, errWebPInvalidMetadata
43+ }
44+ return width, height, nil
45+}
46+
47+func (libwebpRuntime) DecodeRGBAInto(data, pix []byte, stride, width, height int) error {
48+ return libwebp.WebPDecodeRGBAIntoWithInfo(data, pix, stride, width, height)
49+}
50+
51+// webPDecoder owns the failure boundary for the optional libwebp runtime.
52+type webPDecoder struct{ runtime webpRuntime }
53+
54+func newWebPDecoder(runtime webpRuntime) *webPDecoder { return &webPDecoder{runtime: runtime} }
55+
56+func newDefaultWebPDecoder() *webPDecoder { return newWebPDecoder(libwebpRuntime{}) }
57+
58+func (d *webPDecoder) Decode(data []byte) (*image.NRGBA, error) {
59+ if len(data) == 0 {
60+ return nil, errWebPInvalidMetadata
61+ }
62+ if len(data) > maxWebPInputBytes {
63+ return nil, errWebPInputTooLarge
64+ }
65+ if d == nil || d.runtime == nil {
66+ return nil, errors.New("webp decoder unavailable")
67+ }
68+ width, height, err := d.runtime.Info(data)
69+ if err != nil {
70+ return nil, err
71+ }
72+ if width <= 0 || height <= 0 {
73+ return nil, errWebPInvalidMetadata
74+ }
75+ if width > maxWebPDimension || height > maxWebPDimension || width > maxWebPPixels/height {
76+ return nil, errWebPImageTooLarge
77+ }
78+ if width > math.MaxInt/4 {
79+ return nil, errWebPImageTooLarge
80+ }
81+ stride := width * 4
82+ if height > math.MaxInt/stride {
83+ return nil, errWebPImageTooLarge
84+ }
85+
86+ // Allocate exactly the destination buffer used by libwebp; no native image
87+ // buffer or second decoded copy crosses this adapter boundary.
88+ img := image.NewNRGBA(image.Rect(0, 0, width, height))
89+ if err := d.runtime.DecodeRGBAInto(data, img.Pix, img.Stride, width, height); err != nil {
90+ return nil, err
91+ }
92+ return img, nil
93+}
@@ -0,0 +1,93 @@
1+package favicon
2+
3+import (
4+ "errors"
5+ "image"
6+ "math"
7+
8+ "github.com/bnema/purego-webp/libwebp"
9+)
10+
11+const (
12+ // maxWebPInputBytes bounds untrusted compressed favicon input before it is
13+ // handed to libwebp.
14+ maxWebPInputBytes = 5 << 20
15+ // maxWebPDimension and maxWebPPixels bound the final RGBA allocation.
16+ maxWebPDimension = 4096
17+ maxWebPPixels = 4 << 20
18+)
19+
20+var (
21+ errWebPInputTooLarge = errors.New("webp favicon input exceeds size limit")
22+ errWebPImageTooLarge = errors.New("webp favicon dimensions exceed size limit")
23+ errWebPInvalidMetadata = errors.New("invalid webp favicon metadata")
24+)
25+
26+// webpRuntime is the narrow infrastructure seam around the dynamically loaded
27+// libwebp binding. It deliberately exposes only Go values, keeping backend
28+// types out of ports and the domain.
29+type webpRuntime interface {
30+ Info(data []byte) (width, height int, err error)
31+ DecodeRGBAInto(data, pix []byte, stride, width, height int) error
32+}
33+
34+type libwebpRuntime struct{}
35+
36+func (libwebpRuntime) Info(data []byte) (int, int, error) {
37+ width, height, ok, err := libwebp.WebPGetInfo(data)
38+ if err != nil {
39+ return 0, 0, err
40+ }
41+ if !ok {
42+ return 0, 0, errWebPInvalidMetadata
43+ }
44+ return width, height, nil
45+}
46+
47+func (libwebpRuntime) DecodeRGBAInto(data, pix []byte, stride, width, height int) error {
48+ return libwebp.WebPDecodeRGBAIntoWithInfo(data, pix, stride, width, height)
49+}
50+
51+// webPDecoder owns the failure boundary for the optional libwebp runtime.
52+type webPDecoder struct{ runtime webpRuntime }
53+
54+func newWebPDecoder(runtime webpRuntime) *webPDecoder { return &webPDecoder{runtime: runtime} }
55+
56+func newDefaultWebPDecoder() *webPDecoder { return newWebPDecoder(libwebpRuntime{}) }
57+
58+func (d *webPDecoder) Decode(data []byte) (*image.NRGBA, error) {
59+ if len(data) == 0 {
60+ return nil, errWebPInvalidMetadata
61+ }
62+ if len(data) > maxWebPInputBytes {
63+ return nil, errWebPInputTooLarge
64+ }
65+ if d == nil || d.runtime == nil {
66+ return nil, errors.New("webp decoder unavailable")
67+ }
68+ width, height, err := d.runtime.Info(data)
69+ if err != nil {
70+ return nil, err
71+ }
72+ if width <= 0 || height <= 0 {
73+ return nil, errWebPInvalidMetadata
74+ }
75+ if width > maxWebPDimension || height > maxWebPDimension || width > maxWebPPixels/height {
76+ return nil, errWebPImageTooLarge
77+ }
78+ if width > math.MaxInt/4 {
79+ return nil, errWebPImageTooLarge
80+ }
81+ stride := width * 4
82+ if height > math.MaxInt/stride {
83+ return nil, errWebPImageTooLarge
84+ }
85+
86+ // Allocate exactly the destination buffer used by libwebp; no native image
87+ // buffer or second decoded copy crosses this adapter boundary.
88+ img := image.NewNRGBA(image.Rect(0, 0, width, height))
89+ if err := d.runtime.DecodeRGBAInto(data, img.Pix, img.Stride, width, height); err != nil {
90+ return nil, err
91+ }
92+ return img, nil
93+}
A internal/infrastructure/favicon/webp_decoder_test.go +92 -0
@@ -0,0 +1,92 @@
1+package favicon
2+
3+import (
4+ "bytes"
5+ "context"
6+ "errors"
7+ "image"
8+ "image/color"
9+ "image/png"
10+ "os"
11+ "testing"
12+
13+ appport "github.com/bnema/dumber/internal/application/port"
14+)
15+
16+func TestConverterWebPConvertsAlphaFixture(t *testing.T) {
17+ data, err := os.ReadFile("testdata/alpha.webp")
18+ if err != nil {
19+ t.Fatal(err)
20+ }
21+ conv := NewImageConverter()
22+ out, err := conv.Convert(context.Background(), data, "image/webp", []int{32})
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ img, err := png.Decode(bytes.NewReader(out.PNG))
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ if got := color.NRGBAModel.Convert(img.At(0, 0)).(color.NRGBA); got.A != 64 || got.R != 255 {
31+ t.Fatalf("alpha pixel = %#v, want red alpha 64", got)
32+ }
33+ if sized, err := png.Decode(bytes.NewReader(out.SizedPNG[32])); err != nil || sized.Bounds() != image.Rect(0, 0, 32, 32) {
34+ t.Fatalf("sized WebP = %v, %v", sized.Bounds(), err)
35+ }
36+}
37+
38+func TestConverterWebPFailuresAreFaviconMisses(t *testing.T) {
39+ for _, tc := range []struct {
40+ name string
41+ data []byte
42+ dec webpRuntime
43+ }{
44+ {name: "truncated", data: []byte("RIFF\x04\x00\x00\x00WEBPVP8 "), dec: &fakeWebPRuntime{infoErr: errors.New("truncated")}},
45+ {name: "unavailable", data: []byte("webp"), dec: &fakeWebPRuntime{infoErr: errors.New("libwebp unavailable")}},
46+ } {
47+ t.Run(tc.name, func(t *testing.T) {
48+ conv := newImageConverterWithWebPRuntime(tc.dec)
49+ if _, err := conv.Convert(context.Background(), tc.data, "image/webp", nil); !errors.Is(err, appport.ErrFaviconMiss) {
50+ t.Fatalf("err = %v, want favicon miss", err)
51+ }
52+ })
53+ }
54+}
55+
56+func TestWebPDecoderRejectsInputAndDecodedLimitsBeforeAllocation(t *testing.T) {
57+ for _, tc := range []struct {
58+ name string
59+ data []byte
60+ width, height int
61+ }{
62+ {name: "input", data: bytes.Repeat([]byte("x"), maxWebPInputBytes+1), width: 1, height: 1},
63+ {name: "dimension", data: []byte("webp"), width: maxWebPDimension + 1, height: 1},
64+ {name: "pixels", data: []byte("webp"), width: maxWebPDimension, height: maxWebPDimension},
65+ } {
66+ t.Run(tc.name, func(t *testing.T) {
67+ runtime := &fakeWebPRuntime{width: tc.width, height: tc.height}
68+ _, err := newWebPDecoder(runtime).Decode(tc.data)
69+ if err == nil {
70+ t.Fatal("Decode succeeded")
71+ }
72+ if runtime.decodeCalls != 0 {
73+ t.Fatalf("decode calls = %d, want no allocation/decode", runtime.decodeCalls)
74+ }
75+ })
76+ }
77+}
78+
79+type fakeWebPRuntime struct {
80+ width, height int
81+ infoErr error
82+ decodeCalls int
83+}
84+
85+func (r *fakeWebPRuntime) Info(_ []byte) (int, int, error) { return r.width, r.height, r.infoErr }
86+func (r *fakeWebPRuntime) DecodeRGBAInto(_, pix []byte, _, _, _ int) error {
87+ r.decodeCalls++
88+ for i := range pix {
89+ pix[i] = 0xff
90+ }
91+ return nil
92+}
@@ -0,0 +1,92 @@
1+package favicon
2+
3+import (
4+ "bytes"
5+ "context"
6+ "errors"
7+ "image"
8+ "image/color"
9+ "image/png"
10+ "os"
11+ "testing"
12+
13+ appport "github.com/bnema/dumber/internal/application/port"
14+)
15+
16+func TestConverterWebPConvertsAlphaFixture(t *testing.T) {
17+ data, err := os.ReadFile("testdata/alpha.webp")
18+ if err != nil {
19+ t.Fatal(err)
20+ }
21+ conv := NewImageConverter()
22+ out, err := conv.Convert(context.Background(), data, "image/webp", []int{32})
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ img, err := png.Decode(bytes.NewReader(out.PNG))
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ if got := color.NRGBAModel.Convert(img.At(0, 0)).(color.NRGBA); got.A != 64 || got.R != 255 {
31+ t.Fatalf("alpha pixel = %#v, want red alpha 64", got)
32+ }
33+ if sized, err := png.Decode(bytes.NewReader(out.SizedPNG[32])); err != nil || sized.Bounds() != image.Rect(0, 0, 32, 32) {
34+ t.Fatalf("sized WebP = %v, %v", sized.Bounds(), err)
35+ }
36+}
37+
38+func TestConverterWebPFailuresAreFaviconMisses(t *testing.T) {
39+ for _, tc := range []struct {
40+ name string
41+ data []byte
42+ dec webpRuntime
43+ }{
44+ {name: "truncated", data: []byte("RIFF\x04\x00\x00\x00WEBPVP8 "), dec: &fakeWebPRuntime{infoErr: errors.New("truncated")}},
45+ {name: "unavailable", data: []byte("webp"), dec: &fakeWebPRuntime{infoErr: errors.New("libwebp unavailable")}},
46+ } {
47+ t.Run(tc.name, func(t *testing.T) {
48+ conv := newImageConverterWithWebPRuntime(tc.dec)
49+ if _, err := conv.Convert(context.Background(), tc.data, "image/webp", nil); !errors.Is(err, appport.ErrFaviconMiss) {
50+ t.Fatalf("err = %v, want favicon miss", err)
51+ }
52+ })
53+ }
54+}
55+
56+func TestWebPDecoderRejectsInputAndDecodedLimitsBeforeAllocation(t *testing.T) {
57+ for _, tc := range []struct {
58+ name string
59+ data []byte
60+ width, height int
61+ }{
62+ {name: "input", data: bytes.Repeat([]byte("x"), maxWebPInputBytes+1), width: 1, height: 1},
63+ {name: "dimension", data: []byte("webp"), width: maxWebPDimension + 1, height: 1},
64+ {name: "pixels", data: []byte("webp"), width: maxWebPDimension, height: maxWebPDimension},
65+ } {
66+ t.Run(tc.name, func(t *testing.T) {
67+ runtime := &fakeWebPRuntime{width: tc.width, height: tc.height}
68+ _, err := newWebPDecoder(runtime).Decode(tc.data)
69+ if err == nil {
70+ t.Fatal("Decode succeeded")
71+ }
72+ if runtime.decodeCalls != 0 {
73+ t.Fatalf("decode calls = %d, want no allocation/decode", runtime.decodeCalls)
74+ }
75+ })
76+ }
77+}
78+
79+type fakeWebPRuntime struct {
80+ width, height int
81+ infoErr error
82+ decodeCalls int
83+}
84+
85+func (r *fakeWebPRuntime) Info(_ []byte) (int, int, error) { return r.width, r.height, r.infoErr }
86+func (r *fakeWebPRuntime) DecodeRGBAInto(_, pix []byte, _, _, _ int) error {
87+ r.decodeCalls++
88+ for i := range pix {
89+ pix[i] = 0xff
90+ }
91+ return nil
92+}
A vendor/github.com/bnema/purego-webp/LICENSE +21 -0 generated file: collapsed
A vendor/github.com/bnema/purego-webp/internal/libwebp/generated_symbols.go +513 -0 generated file: collapsed
A vendor/github.com/bnema/purego-webp/internal/libwebp/loader.go +86 -0 generated file: collapsed
A vendor/github.com/bnema/purego-webp/internal/libwebp/types.go +164 -0 generated file: collapsed
A vendor/github.com/bnema/purego-webp/libwebp/libwebp.go +880 -0
@@ -0,0 +1,880 @@
1+// Package libwebp exposes C-first bindings to libwebp using purego.
2+//
3+// The API mirrors libwebp function naming and behavior as closely as possible,
4+// while using Go slices and errors for safety in common call paths.
5+package libwebp
6+
7+import (
8+ "errors"
9+ "fmt"
10+ "math"
11+ "unsafe"
12+
13+ lowlevel "github.com/bnema/purego-webp/internal/libwebp"
14+)
15+
16+var (
17+ // ErrInvalidData indicates invalid or empty input bitstream/data.
18+ ErrInvalidData = errors.New("libwebp: invalid webp data")
19+ // ErrDecodeFailed indicates libwebp decode failure.
20+ ErrDecodeFailed = errors.New("libwebp: decode failed")
21+ // ErrEncodeFailed indicates libwebp encode failure.
22+ ErrEncodeFailed = errors.New("libwebp: encode failed")
23+ // ErrInvalidDimension indicates invalid image width/height.
24+ ErrInvalidDimension = errors.New("libwebp: invalid dimensions")
25+ // ErrInvalidStride indicates invalid row stride for the provided buffer.
26+ ErrInvalidStride = errors.New("libwebp: invalid stride")
27+ // ErrBufferTooSmall indicates the destination buffer cannot hold output.
28+ ErrBufferTooSmall = errors.New("libwebp: output buffer too small")
29+ // ErrNotAvailable indicates the function is not available in the loaded
30+ // libwebp version. Use the corresponding Available() helper to check first.
31+ ErrNotAvailable = errors.New("libwebp: function not available in loaded library version")
32+)
33+
34+// VP8StatusCode is the status enum used by libwebp decode APIs.
35+type VP8StatusCode int32
36+
37+const (
38+ VP8StatusOK VP8StatusCode = 0
39+ VP8StatusOutOfMemory VP8StatusCode = 1
40+ VP8StatusInvalidParam VP8StatusCode = 2
41+ VP8StatusBitstreamError VP8StatusCode = 3
42+ VP8StatusUnsupportedFeat VP8StatusCode = 4
43+ VP8StatusSuspended VP8StatusCode = 5
44+ VP8StatusUserAbort VP8StatusCode = 6
45+ VP8StatusNotEnoughData VP8StatusCode = 7
46+)
47+
48+type BitstreamFeatures struct {
49+ Width int
50+ Height int
51+ HasAlpha bool
52+ HasAnimation bool
53+ Format int
54+}
55+
56+// DecBuffer is the low-level decode output buffer struct from libwebp.
57+type DecBuffer = lowlevel.WebPDecBuffer
58+
59+// DecoderOptions is the low-level decoder options struct from libwebp.
60+type DecoderOptions = lowlevel.WebPDecoderOptions
61+
62+// DecoderConfig is the low-level decoder config struct from libwebp.
63+type DecoderConfig = lowlevel.WebPDecoderConfig
64+
65+// Config is the low-level encoder config struct from libwebp.
66+type Config = lowlevel.WebPConfig
67+
68+// MemoryWriter is libwebp's in-memory writer struct.
69+type MemoryWriter = lowlevel.WebPMemoryWriter
70+
71+// Picture is the low-level picture struct used by advanced encode APIs.
72+type Picture = lowlevel.WebPPicture
73+
74+const (
75+ // Encoder presets used by WebPConfigPreset.
76+ PresetDefault = 0
77+ PresetPicture = 1
78+ PresetPhoto = 2
79+ PresetDrawing = 3
80+ PresetIcon = 4
81+ PresetText = 5
82+
83+ HintDefault = 0
84+ HintPicture = 1
85+ HintPhoto = 2
86+ HintGraph = 3
87+ HintLast = 4
88+
89+ // Decode output colorspace/mode constants from decode.h.
90+ ModeRGB = 0
91+ ModeRGBA = 1
92+ ModeBGR = 2
93+ ModeBGRA = 3
94+ ModeARGB = 4
95+ ModeRGBA4444 = 5
96+ ModeRGB565 = 6
97+ ModergbA = 7
98+ ModebgrA = 8
99+ ModeArgb = 9
100+ ModergbA4444 = 10
101+ ModeYUV = 11
102+ ModeYUVA = 12
103+ ModeLast = 13
104+)
105+
106+// Available reports whether libwebp can be loaded in the current environment.
107+func Available() bool {
108+ return lowlevel.Available()
109+}
110+
111+// Version returns decoder and encoder library versions (packed hex format).
112+func Version() (decoder uint32, encoder uint32, err error) {
113+ if err := lowlevel.EnsureLoaded(); err != nil {
114+ return 0, 0, err
115+ }
116+
117+ return uint32(lowlevel.WebPGetDecoderVersion()), uint32(lowlevel.WebPGetEncoderVersion()), nil
118+}
119+
120+// WebPGetInfo validates the bitstream and returns image dimensions.
121+func WebPGetInfo(data []byte) (width, height int, ok bool, err error) {
122+ if err := lowlevel.EnsureLoaded(); err != nil {
123+ return 0, 0, false, err
124+ }
125+ if len(data) == 0 {
126+ return 0, 0, false, nil
127+ }
128+
129+ var w, h int32
130+ ret := lowlevel.WebPGetInfo(&data[0], uintptr(len(data)), &w, &h)
131+ return int(w), int(h), ret != 0, nil
132+}
133+
134+// WebPGetFeatures returns parsed bitstream features and decode status.
135+func WebPGetFeatures(data []byte) (features BitstreamFeatures, status VP8StatusCode, err error) {
136+ if err := lowlevel.EnsureLoaded(); err != nil {
137+ return BitstreamFeatures{}, 0, err
138+ }
139+ if len(data) == 0 {
140+ return BitstreamFeatures{}, VP8StatusNotEnoughData, nil
141+ }
142+
143+ var raw lowlevel.WebPBitstreamFeatures
144+ status = VP8StatusCode(lowlevel.WebPGetFeaturesInternal(&data[0], uintptr(len(data)), &raw, lowlevel.WebPDecoderABIVersion))
145+
146+ return BitstreamFeatures{
147+ Width: int(raw.Width),
148+ Height: int(raw.Height),
149+ HasAlpha: raw.HasAlpha != 0,
150+ HasAnimation: raw.HasAnimation != 0,
151+ Format: int(raw.Format),
152+ }, status, nil
153+}
154+
155+// WebPInitDecBuffer initializes a decode buffer with ABI-checked defaults.
156+func WebPInitDecBuffer(buffer *DecBuffer) (ok bool, err error) {
157+ if err := lowlevel.EnsureLoaded(); err != nil {
158+ return false, err
159+ }
160+ if buffer == nil {
161+ return false, ErrInvalidData
162+ }
163+
164+ return lowlevel.WebPInitDecBufferInternal(buffer, lowlevel.WebPDecoderABIVersion) != 0, nil
165+}
166+
167+// WebPFreeDecBuffer releases any memory owned by the decode buffer.
168+func WebPFreeDecBuffer(buffer *DecBuffer) error {
169+ if err := lowlevel.EnsureLoaded(); err != nil {
170+ return err
171+ }
172+ if buffer == nil {
173+ return nil
174+ }
175+
176+ lowlevel.WebPFreeDecBuffer(buffer)
177+ return nil
178+}
179+
180+// WebPInitDecoderConfig initializes a decoder config with ABI-checked defaults.
181+func WebPInitDecoderConfig(config *DecoderConfig) (ok bool, err error) {
182+ if err := lowlevel.EnsureLoaded(); err != nil {
183+ return false, err
184+ }
185+ if config == nil {
186+ return false, ErrInvalidData
187+ }
188+
189+ return lowlevel.WebPInitDecoderConfigInternal(config, lowlevel.WebPDecoderABIVersion) != 0, nil
190+}
191+
192+// WebPValidateDecoderConfigAvailable reports whether WebPValidateDecoderConfig
193+// is available in the loaded libwebp. The symbol was added in libwebp 1.6.0
194+// (released 2025-03); most distributions still ship an earlier version.
195+func WebPValidateDecoderConfigAvailable() bool {
196+ return lowlevel.ValidateDecoderConfigAvailable()
197+}
198+
199+// WebPValidateDecoderConfig validates decoder config values.
200+// It returns ErrNotAvailable if the loaded libwebp predates 1.6.0.
201+func WebPValidateDecoderConfig(config *DecoderConfig) (ok bool, err error) {
202+ if err := lowlevel.EnsureLoaded(); err != nil {
203+ return false, err
204+ }
205+ if !lowlevel.ValidateDecoderConfigAvailable() {
206+ return false, ErrNotAvailable
207+ }
208+ if config == nil {
209+ return false, ErrInvalidData
210+ }
211+
212+ return lowlevel.WebPValidateDecoderConfig(config) != 0, nil
213+}
214+
215+// WebPConfigInit initializes encoder config to default preset/quality.
216+func WebPConfigInit(config *Config) (ok bool, err error) {
217+ if err := lowlevel.EnsureLoaded(); err != nil {
218+ return false, err
219+ }
220+ if config == nil {
221+ return false, ErrInvalidData
222+ }
223+
224+ return lowlevel.WebPConfigInitInternal(config, PresetDefault, 75, lowlevel.WebPEncoderABIVersion) != 0, nil
225+}
226+
227+// WebPConfigPreset initializes encoder config with the given preset and quality.
228+func WebPConfigPreset(config *Config, preset int32, quality float32) (ok bool, err error) {
229+ if err := lowlevel.EnsureLoaded(); err != nil {
230+ return false, err
231+ }
232+ if config == nil {
233+ return false, ErrInvalidData
234+ }
235+
236+ return lowlevel.WebPConfigInitInternal(config, preset, quality, lowlevel.WebPEncoderABIVersion) != 0, nil
237+}
238+
239+// WebPConfigLosslessPreset applies a built-in lossless level preset.
240+func WebPConfigLosslessPreset(config *Config, level int32) (ok bool, err error) {
241+ if err := lowlevel.EnsureLoaded(); err != nil {
242+ return false, err
243+ }
244+ if config == nil {
245+ return false, ErrInvalidData
246+ }
247+
248+ return lowlevel.WebPConfigLosslessPreset(config, level) != 0, nil
249+}
250+
251+// WebPValidateConfig validates encoder config values.
252+func WebPValidateConfig(config *Config) (ok bool, err error) {
253+ if err := lowlevel.EnsureLoaded(); err != nil {
254+ return false, err
255+ }
256+ if config == nil {
257+ return false, ErrInvalidData
258+ }
259+
260+ return lowlevel.WebPValidateConfig(config) != 0, nil
261+}
262+
263+// WebPPictureInit initializes a picture struct with ABI-checked defaults.
264+func WebPPictureInit(picture *Picture) (ok bool, err error) {
265+ if err := lowlevel.EnsureLoaded(); err != nil {
266+ return false, err
267+ }
268+ if picture == nil {
269+ return false, ErrInvalidData
270+ }
271+
272+ return lowlevel.WebPPictureInitInternal(picture, lowlevel.WebPEncoderABIVersion) != 0, nil
273+}
274+
275+// WebPMemoryWriterInit initializes a memory writer instance.
276+func WebPMemoryWriterInit(writer *MemoryWriter) error {
277+ if err := lowlevel.EnsureLoaded(); err != nil {
278+ return err
279+ }
280+ if writer == nil {
281+ return ErrInvalidData
282+ }
283+
284+ lowlevel.WebPMemoryWriterInit(writer)
285+ return nil
286+}
287+
288+// WebPMemoryWriterClear frees memory owned by a memory writer.
289+func WebPMemoryWriterClear(writer *MemoryWriter) error {
290+ if err := lowlevel.EnsureLoaded(); err != nil {
291+ return err
292+ }
293+ if writer == nil {
294+ return nil
295+ }
296+
297+ lowlevel.WebPMemoryWriterClear(writer)
298+ return nil
299+}
300+
301+// WebPEncode runs advanced encoding with explicit config and picture structs.
302+func WebPEncode(config *Config, picture *Picture) (ok bool, err error) {
303+ if err := lowlevel.EnsureLoaded(); err != nil {
304+ return false, err
305+ }
306+ if config == nil || picture == nil {
307+ return false, ErrInvalidData
308+ }
309+
310+ return lowlevel.WebPEncode(config, picture) != 0, nil
311+}
312+
313+// WebPINewDecoder creates an incremental decoder using the provided output buffer.
314+func WebPINewDecoder(outputBuffer *DecBuffer) (uintptr, error) {
315+ if err := lowlevel.EnsureLoaded(); err != nil {
316+ return 0, err
317+ }
318+
319+ idec := lowlevel.WebPINewDecoder(outputBuffer)
320+ if idec == 0 {
321+ return 0, ErrDecodeFailed
322+ }
323+
324+ return idec, nil
325+}
326+
327+// WebPINewRGB creates an incremental decoder producing packed RGB-family output.
328+func WebPINewRGB(csp int32, outputBuffer []byte, outputStride int32) (uintptr, error) {
329+ if err := lowlevel.EnsureLoaded(); err != nil {
330+ return 0, err
331+ }
332+
333+ ptr, size := ptrAndSize(outputBuffer)
334+ idec := lowlevel.WebPINewRGB(csp, ptr, size, outputStride)
335+ if idec == 0 {
336+ return 0, ErrDecodeFailed
337+ }
338+
339+ return idec, nil
340+}
341+
342+// WebPINewYUV creates an incremental decoder producing YUV planes.
343+func WebPINewYUV(luma []byte, lumaStride int32, u []byte, uStride int32, v []byte, vStride int32) (uintptr, error) {
344+ if err := lowlevel.EnsureLoaded(); err != nil {
345+ return 0, err
346+ }
347+
348+ lumaPtr, lumaSize := ptrAndSize(luma)
349+ uPtr, uSize := ptrAndSize(u)
350+ vPtr, vSize := ptrAndSize(v)
351+
352+ idec := lowlevel.WebPINewYUV(lumaPtr, lumaSize, lumaStride, uPtr, uSize, uStride, vPtr, vSize, vStride)
353+ if idec == 0 {
354+ return 0, ErrDecodeFailed
355+ }
356+
357+ return idec, nil
358+}
359+
360+// WebPINewYUVA creates an incremental decoder producing YUVA planes.
361+func WebPINewYUVA(luma []byte, lumaStride int32, u []byte, uStride int32, v []byte, vStride int32, a []byte, aStride int32) (uintptr, error) {
362+ if err := lowlevel.EnsureLoaded(); err != nil {
363+ return 0, err
364+ }
365+
366+ lumaPtr, lumaSize := ptrAndSize(luma)
367+ uPtr, uSize := ptrAndSize(u)
368+ vPtr, vSize := ptrAndSize(v)
369+ aPtr, aSize := ptrAndSize(a)
370+
371+ idec := lowlevel.WebPINewYUVA(lumaPtr, lumaSize, lumaStride, uPtr, uSize, uStride, vPtr, vSize, vStride, aPtr, aSize, aStride)
372+ if idec == 0 {
373+ return 0, ErrDecodeFailed
374+ }
375+
376+ return idec, nil
377+}
378+
379+// WebPIDelete destroys an incremental decoder instance.
380+func WebPIDelete(idec uintptr) error {
381+ if err := lowlevel.EnsureLoaded(); err != nil {
382+ return err
383+ }
384+ if idec == 0 {
385+ return nil
386+ }
387+
388+ lowlevel.WebPIDelete(idec)
389+ return nil
390+}
391+
392+// WebPIAppend appends data to an incremental decoder.
393+func WebPIAppend(idec uintptr, data []byte) (VP8StatusCode, error) {
394+ if err := lowlevel.EnsureLoaded(); err != nil {
395+ return 0, err
396+ }
397+ if idec == 0 || len(data) == 0 {
398+ return VP8StatusInvalidParam, ErrInvalidData
399+ }
400+
401+ return VP8StatusCode(lowlevel.WebPIAppend(idec, &data[0], uintptr(len(data)))), nil
402+}
403+
404+// WebPIUpdate updates an incremental decoder with a full prefix buffer.
405+func WebPIUpdate(idec uintptr, data []byte) (VP8StatusCode, error) {
406+ if err := lowlevel.EnsureLoaded(); err != nil {
407+ return 0, err
408+ }
409+ if idec == 0 || len(data) == 0 {
410+ return VP8StatusInvalidParam, ErrInvalidData
411+ }
412+
413+ return VP8StatusCode(lowlevel.WebPIUpdate(idec, &data[0], uintptr(len(data)))), nil
414+}
415+
416+// WebPIDecode creates an incremental decoder with optional config and input.
417+func WebPIDecode(data []byte, config *DecoderConfig) (uintptr, error) {
418+ if err := lowlevel.EnsureLoaded(); err != nil {
419+ return 0, err
420+ }
421+ if len(data) == 0 {
422+ return 0, ErrInvalidData
423+ }
424+
425+ idec := lowlevel.WebPIDecode(&data[0], uintptr(len(data)), config)
426+ if idec == 0 {
427+ return 0, ErrDecodeFailed
428+ }
429+
430+ return idec, nil
431+}
432+
433+// WebPIDecodedArea returns decoded displayable area metadata.
434+func WebPIDecodedArea(idec uintptr, left, top, width, height *int32) (*DecBuffer, error) {
435+ if err := lowlevel.EnsureLoaded(); err != nil {
436+ return nil, err
437+ }
438+ if idec == 0 {
439+ return nil, ErrInvalidData
440+ }
441+
442+ buf := lowlevel.WebPIDecodedArea(idec, left, top, width, height)
443+ if buf == nil {
444+ return nil, ErrDecodeFailed
445+ }
446+
447+ return buf, nil
448+}
449+
450+// WebPIDecGetRGB returns current incremental packed RGB-family output pointer.
451+func WebPIDecGetRGB(idec uintptr, lastY, width, height, stride *int32) (uintptr, error) {
452+ if err := lowlevel.EnsureLoaded(); err != nil {
453+ return 0, err
454+ }
455+ if idec == 0 {
456+ return 0, ErrInvalidData
457+ }
458+
459+ ptr := lowlevel.WebPIDecGetRGB(idec, lastY, width, height, stride)
460+ if ptr == nil {
461+ return 0, ErrDecodeFailed
462+ }
463+
464+ return uintptr(unsafe.Pointer(ptr)), nil
465+}
466+
467+// WebPIDecGetYUVA returns current incremental YUVA luma pointer.
468+func WebPIDecGetYUVA(idec uintptr, lastY *int32, u, v, a **byte, width, height, stride, uvStride, aStride *int32) (uintptr, error) {
469+ if err := lowlevel.EnsureLoaded(); err != nil {
470+ return 0, err
471+ }
472+ if idec == 0 {
473+ return 0, ErrInvalidData
474+ }
475+
476+ ptr := lowlevel.WebPIDecGetYUVA(idec, lastY, u, v, a, width, height, stride, uvStride, aStride)
477+ if ptr == nil {
478+ return 0, ErrDecodeFailed
479+ }
480+
481+ return uintptr(unsafe.Pointer(ptr)), nil
482+}
483+
484+// WebPIDecGetYUV is a compatibility wrapper for YUV output without alpha.
485+func WebPIDecGetYUV(idec uintptr, lastY *int32, u, v **byte, width, height, stride, uvStride *int32) (uintptr, error) {
486+ var a *byte
487+ return WebPIDecGetYUVA(idec, lastY, u, v, &a, width, height, stride, uvStride, nil)
488+}
489+
490+// WebPDecode performs advanced one-shot decode using DecoderConfig.
491+func WebPDecode(data []byte, config *DecoderConfig) (status VP8StatusCode, err error) {
492+ if err := lowlevel.EnsureLoaded(); err != nil {
493+ return 0, err
494+ }
495+ if len(data) == 0 {
496+ return VP8StatusNotEnoughData, nil
497+ }
498+ if config == nil {
499+ return VP8StatusInvalidParam, ErrInvalidData
500+ }
501+
502+ return VP8StatusCode(lowlevel.WebPDecode(&data[0], uintptr(len(data)), config)), nil
503+}
504+
505+// WebPDecodeRGBA decodes to packed RGBA and returns an owned Go buffer.
506+func WebPDecodeRGBA(data []byte) (pix []byte, width, height, stride int, err error) {
507+ return decodeToOwnedBuffer(data, 4, lowlevel.WebPDecodeRGBA)
508+}
509+
510+// WebPDecodeARGB decodes to packed ARGB and returns an owned Go buffer.
511+func WebPDecodeARGB(data []byte) (pix []byte, width, height, stride int, err error) {
512+ return decodeToOwnedBuffer(data, 4, lowlevel.WebPDecodeARGB)
513+}
514+
515+// WebPDecodeBGRA decodes to packed BGRA and returns an owned Go buffer.
516+func WebPDecodeBGRA(data []byte) (pix []byte, width, height, stride int, err error) {
517+ return decodeToOwnedBuffer(data, 4, lowlevel.WebPDecodeBGRA)
518+}
519+
520+// WebPDecodeRGB decodes to packed RGB and returns an owned Go buffer.
521+func WebPDecodeRGB(data []byte) (pix []byte, width, height, stride int, err error) {
522+ return decodeToOwnedBuffer(data, 3, lowlevel.WebPDecodeRGB)
523+}
524+
525+// WebPDecodeBGR decodes to packed BGR and returns an owned Go buffer.
526+func WebPDecodeBGR(data []byte) (pix []byte, width, height, stride int, err error) {
527+ return decodeToOwnedBuffer(data, 3, lowlevel.WebPDecodeBGR)
528+}
529+
530+// WebPDecodeRGBAInto decodes into a caller-provided RGBA buffer.
531+func WebPDecodeRGBAInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
532+ return decodeInto(data, outputBuffer, outputStride, 4, lowlevel.WebPDecodeRGBAInto)
533+}
534+
535+// WebPDecodeRGBAIntoWithInfo decodes into a caller-provided RGBA buffer using
536+// dimensions already validated by WebPGetInfo, avoiding a second parse.
537+func WebPDecodeRGBAIntoWithInfo(data []byte, outputBuffer []byte, outputStride, width, height int) error {
538+ return decodeIntoWithInfo(data, outputBuffer, outputStride, width, height, 4, lowlevel.WebPDecodeRGBAInto)
539+}
540+
541+// WebPDecodeARGBInto decodes into a caller-provided ARGB buffer.
542+func WebPDecodeARGBInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
543+ return decodeInto(data, outputBuffer, outputStride, 4, lowlevel.WebPDecodeARGBInto)
544+}
545+
546+// WebPDecodeBGRAInto decodes into a caller-provided BGRA buffer.
547+func WebPDecodeBGRAInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
548+ return decodeInto(data, outputBuffer, outputStride, 4, lowlevel.WebPDecodeBGRAInto)
549+}
550+
551+// WebPDecodeRGBInto decodes into a caller-provided RGB buffer.
552+func WebPDecodeRGBInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
553+ return decodeInto(data, outputBuffer, outputStride, 3, lowlevel.WebPDecodeRGBInto)
554+}
555+
556+// WebPDecodeBGRInto decodes into a caller-provided BGR buffer.
557+func WebPDecodeBGRInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
558+ return decodeInto(data, outputBuffer, outputStride, 3, lowlevel.WebPDecodeBGRInto)
559+}
560+
561+// WebPDecodeYUV decodes to planar YUV and returns owned Go buffers.
562+func WebPDecodeYUV(data []byte) (y, u, v []byte, width, height, yStride, uvStride int, err error) {
563+ if err := lowlevel.EnsureLoaded(); err != nil {
564+ return nil, nil, nil, 0, 0, 0, 0, err
565+ }
566+ if len(data) == 0 {
567+ return nil, nil, nil, 0, 0, 0, 0, ErrInvalidData
568+ }
569+
570+ var w, h int32
571+ var uPtr, vPtr *byte
572+ var ys, uvs int32
573+ yPtr := lowlevel.WebPDecodeYUV(&data[0], uintptr(len(data)), &w, &h, &uPtr, &vPtr, &ys, &uvs)
574+ if yPtr == nil {
575+ return nil, nil, nil, 0, 0, 0, 0, ErrDecodeFailed
576+ }
577+ defer lowlevel.WebPFree(uintptr(unsafe.Pointer(yPtr)))
578+
579+ width = int(w)
580+ height = int(h)
581+ if width <= 0 || height <= 0 {
582+ return nil, nil, nil, 0, 0, 0, 0, ErrInvalidDimension
583+ }
584+
585+ yStride = int(ys)
586+ uvStride = int(uvs)
587+ uvHeight := (height + 1) / 2
588+
589+ y = make([]byte, yStride*height)
590+ u = make([]byte, uvStride*uvHeight)
591+ v = make([]byte, uvStride*uvHeight)
592+
593+ copy(y, unsafe.Slice(yPtr, len(y)))
594+ copy(u, unsafe.Slice(uPtr, len(u)))
595+ copy(v, unsafe.Slice(vPtr, len(v)))
596+
597+ return y, u, v, width, height, yStride, uvStride, nil
598+}
599+
600+// WebPDecodeYUVInto decodes into caller-provided Y, U and V planes.
601+func WebPDecodeYUVInto(data []byte, luma []byte, lumaStride int, u []byte, uStride int, v []byte, vStride int) (width, height int, err error) {
602+ if err := lowlevel.EnsureLoaded(); err != nil {
603+ return 0, 0, err
604+ }
605+ if len(data) == 0 {
606+ return 0, 0, ErrInvalidData
607+ }
608+ if len(luma) == 0 || len(u) == 0 || len(v) == 0 {
609+ return 0, 0, ErrBufferTooSmall
610+ }
611+
612+ w, h, ok, err := WebPGetInfo(data)
613+ if err != nil {
614+ return 0, 0, err
615+ }
616+ if !ok {
617+ return 0, 0, ErrInvalidData
618+ }
619+ if lumaStride < w {
620+ return 0, 0, ErrInvalidStride
621+ }
622+ uvWidth := (w + 1) / 2
623+ uvHeight := (h + 1) / 2
624+ if uStride < uvWidth || vStride < uvWidth {
625+ return 0, 0, ErrInvalidStride
626+ }
627+ if len(luma) < lumaStride*h || len(u) < uStride*uvHeight || len(v) < vStride*uvHeight {
628+ return 0, 0, ErrBufferTooSmall
629+ }
630+
631+ ptr := lowlevel.WebPDecodeYUVInto(
632+ &data[0],
633+ uintptr(len(data)),
634+ &luma[0],
635+ uintptr(len(luma)),
636+ int32(lumaStride),
637+ &u[0],
638+ uintptr(len(u)),
639+ int32(uStride),
640+ &v[0],
641+ uintptr(len(v)),
642+ int32(vStride),
643+ )
644+ if ptr == nil {
645+ return 0, 0, ErrDecodeFailed
646+ }
647+
648+ return w, h, nil
649+}
650+
651+// WebPEncodeRGBA encodes packed RGBA pixels with lossy quality.
652+func WebPEncodeRGBA(rgba []byte, width, height, stride int, quality float32) ([]byte, error) {
653+ return encodeWithQuality(rgba, width, height, stride, 4, quality, lowlevel.WebPEncodeRGBA)
654+}
655+
656+// WebPEncodeBGRA encodes packed BGRA pixels with lossy quality.
657+func WebPEncodeBGRA(bgra []byte, width, height, stride int, quality float32) ([]byte, error) {
658+ return encodeWithQuality(bgra, width, height, stride, 4, quality, lowlevel.WebPEncodeBGRA)
659+}
660+
661+// WebPEncodeRGB encodes packed RGB pixels with lossy quality.
662+func WebPEncodeRGB(rgb []byte, width, height, stride int, quality float32) ([]byte, error) {
663+ return encodeWithQuality(rgb, width, height, stride, 3, quality, lowlevel.WebPEncodeRGB)
664+}
665+
666+// WebPEncodeBGR encodes packed BGR pixels with lossy quality.
667+func WebPEncodeBGR(bgr []byte, width, height, stride int, quality float32) ([]byte, error) {
668+ return encodeWithQuality(bgr, width, height, stride, 3, quality, lowlevel.WebPEncodeBGR)
669+}
670+
671+// WebPEncodeLosslessRGBA encodes packed RGBA pixels in lossless mode.
672+func WebPEncodeLosslessRGBA(rgba []byte, width, height, stride int) ([]byte, error) {
673+ return encodeLossless(rgba, width, height, stride, 4, lowlevel.WebPEncodeLosslessRGBA)
674+}
675+
676+// WebPEncodeLosslessBGRA encodes packed BGRA pixels in lossless mode.
677+func WebPEncodeLosslessBGRA(bgra []byte, width, height, stride int) ([]byte, error) {
678+ return encodeLossless(bgra, width, height, stride, 4, lowlevel.WebPEncodeLosslessBGRA)
679+}
680+
681+// WebPEncodeLosslessRGB encodes packed RGB pixels in lossless mode.
682+func WebPEncodeLosslessRGB(rgb []byte, width, height, stride int) ([]byte, error) {
683+ return encodeLossless(rgb, width, height, stride, 3, lowlevel.WebPEncodeLosslessRGB)
684+}
685+
686+// WebPEncodeLosslessBGR encodes packed BGR pixels in lossless mode.
687+func WebPEncodeLosslessBGR(bgr []byte, width, height, stride int) ([]byte, error) {
688+ return encodeLossless(bgr, width, height, stride, 3, lowlevel.WebPEncodeLosslessBGR)
689+}
690+
691+type decodeFunc func(data *byte, dataSize uintptr, width *int32, height *int32) *byte
692+
693+type decodeIntoFunc func(data *byte, dataSize uintptr, outputBuffer *byte, outputBufferSize uintptr, outputStride int32) *byte
694+
695+func decodeInto(data []byte, outputBuffer []byte, outputStride int, bytesPerPixel int, fn decodeIntoFunc) (width, height int, err error) {
696+ if err := lowlevel.EnsureLoaded(); err != nil {
697+ return 0, 0, err
698+ }
699+ if len(data) == 0 {
700+ return 0, 0, ErrInvalidData
701+ }
702+ if len(outputBuffer) == 0 {
703+ return 0, 0, ErrBufferTooSmall
704+ }
705+ w, h, ok, err := WebPGetInfo(data)
706+ if err != nil {
707+ return 0, 0, err
708+ }
709+ if !ok {
710+ return 0, 0, ErrInvalidData
711+ }
712+ if err := decodeIntoWithInfo(data, outputBuffer, outputStride, w, h, bytesPerPixel, fn); err != nil {
713+ return 0, 0, err
714+ }
715+ return w, h, nil
716+}
717+
718+func decodeIntoWithInfo(data []byte, outputBuffer []byte, outputStride, width, height, bytesPerPixel int, fn decodeIntoFunc) error {
719+ if err := lowlevel.EnsureLoaded(); err != nil {
720+ return err
721+ }
722+ if len(data) == 0 {
723+ return ErrInvalidData
724+ }
725+ if len(outputBuffer) == 0 {
726+ return ErrBufferTooSmall
727+ }
728+ minimumStride, _, err := checkedDecodeLayout(width, height, bytesPerPixel)
729+ if err != nil {
730+ return err
731+ }
732+ if outputStride < minimumStride || outputStride > math.MaxInt32 {
733+ return ErrInvalidStride
734+ }
735+ required, ok := checkedProduct(outputStride, height)
736+ if !ok || len(outputBuffer) < required {
737+ return ErrBufferTooSmall
738+ }
739+ if fn(&data[0], uintptr(len(data)), &outputBuffer[0], uintptr(len(outputBuffer)), int32(outputStride)) == nil {
740+ return ErrDecodeFailed
741+ }
742+ return nil
743+}
744+
745+func decodeToOwnedBuffer(data []byte, bytesPerPixel int, fn decodeFunc) (pix []byte, width, height, stride int, err error) {
746+ if err := lowlevel.EnsureLoaded(); err != nil {
747+ return nil, 0, 0, 0, err
748+ }
749+ if len(data) == 0 {
750+ return nil, 0, 0, 0, ErrInvalidData
751+ }
752+
753+ var w, h int32
754+ ptr := fn(&data[0], uintptr(len(data)), &w, &h)
755+ if ptr == nil {
756+ return nil, 0, 0, 0, ErrDecodeFailed
757+ }
758+ defer lowlevel.WebPFree(uintptr(unsafe.Pointer(ptr)))
759+
760+ width = int(w)
761+ height = int(h)
762+ stride, bufLen, err := checkedDecodeLayout(width, height, bytesPerPixel)
763+ if err != nil {
764+ return nil, 0, 0, 0, err
765+ }
766+ pix = make([]byte, bufLen)
767+ copy(pix, unsafe.Slice(ptr, bufLen))
768+
769+ return pix, width, height, stride, nil
770+}
771+
772+type encodeLossyFunc func(pix *byte, width int32, height int32, stride int32, quality float32, output **byte) uintptr
773+
774+func encodeWithQuality(pix []byte, width, height, stride, bytesPerPixel int, quality float32, fn encodeLossyFunc) ([]byte, error) {
775+ if err := lowlevel.EnsureLoaded(); err != nil {
776+ return nil, err
777+ }
778+ if err := validatePixelInput(pix, width, height, stride, bytesPerPixel); err != nil {
779+ return nil, err
780+ }
781+
782+ var out *byte
783+ size := fn(&pix[0], int32(width), int32(height), int32(stride), quality, &out)
784+ if size == 0 || out == nil {
785+ return nil, ErrEncodeFailed
786+ }
787+ defer lowlevel.WebPFree(uintptr(unsafe.Pointer(out)))
788+
789+ b := make([]byte, int(size))
790+ copy(b, unsafe.Slice(out, int(size)))
791+
792+ return b, nil
793+}
794+
795+type encodeLosslessFunc func(pix *byte, width int32, height int32, stride int32, output **byte) uintptr
796+
797+func encodeLossless(pix []byte, width, height, stride, bytesPerPixel int, fn encodeLosslessFunc) ([]byte, error) {
798+ if err := lowlevel.EnsureLoaded(); err != nil {
799+ return nil, err
800+ }
801+ if err := validatePixelInput(pix, width, height, stride, bytesPerPixel); err != nil {
802+ return nil, err
803+ }
804+
805+ var out *byte
806+ size := fn(&pix[0], int32(width), int32(height), int32(stride), &out)
807+ if size == 0 || out == nil {
808+ return nil, ErrEncodeFailed
809+ }
810+ defer lowlevel.WebPFree(uintptr(unsafe.Pointer(out)))
811+
812+ b := make([]byte, int(size))
813+ copy(b, unsafe.Slice(out, int(size)))
814+
815+ return b, nil
816+}
817+
818+func validatePixelInput(pix []byte, width, height, stride, bytesPerPixel int) error {
819+ minimumStride, _, err := checkedDecodeLayout(width, height, bytesPerPixel)
820+ if err != nil {
821+ return err
822+ }
823+ if stride < minimumStride || stride > math.MaxInt32 {
824+ return ErrInvalidStride
825+ }
826+ required, ok := checkedProduct(stride, height)
827+ if !ok {
828+ return ErrInvalidDimension
829+ }
830+ if len(pix) < required {
831+ return fmt.Errorf("libwebp: pixel buffer too small: got=%d need>=%d", len(pix), required)
832+ }
833+ return nil
834+}
835+
836+// checkedDecodeLayout validates the dimensions and packed output layout before
837+// values are passed to libwebp, whose output stride parameter is a C int.
838+func checkedDecodeLayout(width, height, bytesPerPixel int) (stride, size int, err error) {
839+ if width <= 0 || height <= 0 {
840+ return 0, 0, ErrInvalidDimension
841+ }
842+ if bytesPerPixel <= 0 || width > math.MaxInt32/bytesPerPixel {
843+ return 0, 0, ErrInvalidStride
844+ }
845+ stride = width * bytesPerPixel
846+ size, ok := checkedProduct(stride, height)
847+ if !ok {
848+ return 0, 0, ErrInvalidDimension
849+ }
850+ return stride, size, nil
851+}
852+
853+func checkedProduct(a, b int) (int, bool) {
854+ if a < 0 || b < 0 || (a != 0 && b > int(^uint(0)>>1)/a) {
855+ return 0, false
856+ }
857+ return a * b, true
858+}
859+
860+// WebPIsPremultipliedMode reports whether the decode colorspace is premultiplied.
861+func WebPIsPremultipliedMode(mode int) bool {
862+ return mode == ModergbA || mode == ModebgrA || mode == ModeArgb || mode == ModergbA4444
863+}
864+
865+// WebPIsAlphaMode reports whether the decode colorspace contains alpha.
866+func WebPIsAlphaMode(mode int) bool {
867+ return mode == ModeRGBA || mode == ModeBGRA || mode == ModeARGB || mode == ModeRGBA4444 || mode == ModeYUVA || WebPIsPremultipliedMode(mode)
868+}
869+
870+// WebPIsRGBMode reports whether the decode colorspace is RGB-family.
871+func WebPIsRGBMode(mode int) bool {
872+ return mode < ModeYUV
873+}
874+
875+func ptrAndSize(b []byte) (*byte, uintptr) {
876+ if len(b) == 0 {
877+ return nil, 0
878+ }
879+ return &b[0], uintptr(len(b))
880+}
@@ -0,0 +1,880 @@
1+// Package libwebp exposes C-first bindings to libwebp using purego.
2+//
3+// The API mirrors libwebp function naming and behavior as closely as possible,
4+// while using Go slices and errors for safety in common call paths.
5+package libwebp
6+
7+import (
8+ "errors"
9+ "fmt"
10+ "math"
11+ "unsafe"
12+
13+ lowlevel "github.com/bnema/purego-webp/internal/libwebp"
14+)
15+
16+var (
17+ // ErrInvalidData indicates invalid or empty input bitstream/data.
18+ ErrInvalidData = errors.New("libwebp: invalid webp data")
19+ // ErrDecodeFailed indicates libwebp decode failure.
20+ ErrDecodeFailed = errors.New("libwebp: decode failed")
21+ // ErrEncodeFailed indicates libwebp encode failure.
22+ ErrEncodeFailed = errors.New("libwebp: encode failed")
23+ // ErrInvalidDimension indicates invalid image width/height.
24+ ErrInvalidDimension = errors.New("libwebp: invalid dimensions")
25+ // ErrInvalidStride indicates invalid row stride for the provided buffer.
26+ ErrInvalidStride = errors.New("libwebp: invalid stride")
27+ // ErrBufferTooSmall indicates the destination buffer cannot hold output.
28+ ErrBufferTooSmall = errors.New("libwebp: output buffer too small")
29+ // ErrNotAvailable indicates the function is not available in the loaded
30+ // libwebp version. Use the corresponding Available() helper to check first.
31+ ErrNotAvailable = errors.New("libwebp: function not available in loaded library version")
32+)
33+
34+// VP8StatusCode is the status enum used by libwebp decode APIs.
35+type VP8StatusCode int32
36+
37+const (
38+ VP8StatusOK VP8StatusCode = 0
39+ VP8StatusOutOfMemory VP8StatusCode = 1
40+ VP8StatusInvalidParam VP8StatusCode = 2
41+ VP8StatusBitstreamError VP8StatusCode = 3
42+ VP8StatusUnsupportedFeat VP8StatusCode = 4
43+ VP8StatusSuspended VP8StatusCode = 5
44+ VP8StatusUserAbort VP8StatusCode = 6
45+ VP8StatusNotEnoughData VP8StatusCode = 7
46+)
47+
48+type BitstreamFeatures struct {
49+ Width int
50+ Height int
51+ HasAlpha bool
52+ HasAnimation bool
53+ Format int
54+}
55+
56+// DecBuffer is the low-level decode output buffer struct from libwebp.
57+type DecBuffer = lowlevel.WebPDecBuffer
58+
59+// DecoderOptions is the low-level decoder options struct from libwebp.
60+type DecoderOptions = lowlevel.WebPDecoderOptions
61+
62+// DecoderConfig is the low-level decoder config struct from libwebp.
63+type DecoderConfig = lowlevel.WebPDecoderConfig
64+
65+// Config is the low-level encoder config struct from libwebp.
66+type Config = lowlevel.WebPConfig
67+
68+// MemoryWriter is libwebp's in-memory writer struct.
69+type MemoryWriter = lowlevel.WebPMemoryWriter
70+
71+// Picture is the low-level picture struct used by advanced encode APIs.
72+type Picture = lowlevel.WebPPicture
73+
74+const (
75+ // Encoder presets used by WebPConfigPreset.
76+ PresetDefault = 0
77+ PresetPicture = 1
78+ PresetPhoto = 2
79+ PresetDrawing = 3
80+ PresetIcon = 4
81+ PresetText = 5
82+
83+ HintDefault = 0
84+ HintPicture = 1
85+ HintPhoto = 2
86+ HintGraph = 3
87+ HintLast = 4
88+
89+ // Decode output colorspace/mode constants from decode.h.
90+ ModeRGB = 0
91+ ModeRGBA = 1
92+ ModeBGR = 2
93+ ModeBGRA = 3
94+ ModeARGB = 4
95+ ModeRGBA4444 = 5
96+ ModeRGB565 = 6
97+ ModergbA = 7
98+ ModebgrA = 8
99+ ModeArgb = 9
100+ ModergbA4444 = 10
101+ ModeYUV = 11
102+ ModeYUVA = 12
103+ ModeLast = 13
104+)
105+
106+// Available reports whether libwebp can be loaded in the current environment.
107+func Available() bool {
108+ return lowlevel.Available()
109+}
110+
111+// Version returns decoder and encoder library versions (packed hex format).
112+func Version() (decoder uint32, encoder uint32, err error) {
113+ if err := lowlevel.EnsureLoaded(); err != nil {
114+ return 0, 0, err
115+ }
116+
117+ return uint32(lowlevel.WebPGetDecoderVersion()), uint32(lowlevel.WebPGetEncoderVersion()), nil
118+}
119+
120+// WebPGetInfo validates the bitstream and returns image dimensions.
121+func WebPGetInfo(data []byte) (width, height int, ok bool, err error) {
122+ if err := lowlevel.EnsureLoaded(); err != nil {
123+ return 0, 0, false, err
124+ }
125+ if len(data) == 0 {
126+ return 0, 0, false, nil
127+ }
128+
129+ var w, h int32
130+ ret := lowlevel.WebPGetInfo(&data[0], uintptr(len(data)), &w, &h)
131+ return int(w), int(h), ret != 0, nil
132+}
133+
134+// WebPGetFeatures returns parsed bitstream features and decode status.
135+func WebPGetFeatures(data []byte) (features BitstreamFeatures, status VP8StatusCode, err error) {
136+ if err := lowlevel.EnsureLoaded(); err != nil {
137+ return BitstreamFeatures{}, 0, err
138+ }
139+ if len(data) == 0 {
140+ return BitstreamFeatures{}, VP8StatusNotEnoughData, nil
141+ }
142+
143+ var raw lowlevel.WebPBitstreamFeatures
144+ status = VP8StatusCode(lowlevel.WebPGetFeaturesInternal(&data[0], uintptr(len(data)), &raw, lowlevel.WebPDecoderABIVersion))
145+
146+ return BitstreamFeatures{
147+ Width: int(raw.Width),
148+ Height: int(raw.Height),
149+ HasAlpha: raw.HasAlpha != 0,
150+ HasAnimation: raw.HasAnimation != 0,
151+ Format: int(raw.Format),
152+ }, status, nil
153+}
154+
155+// WebPInitDecBuffer initializes a decode buffer with ABI-checked defaults.
156+func WebPInitDecBuffer(buffer *DecBuffer) (ok bool, err error) {
157+ if err := lowlevel.EnsureLoaded(); err != nil {
158+ return false, err
159+ }
160+ if buffer == nil {
161+ return false, ErrInvalidData
162+ }
163+
164+ return lowlevel.WebPInitDecBufferInternal(buffer, lowlevel.WebPDecoderABIVersion) != 0, nil
165+}
166+
167+// WebPFreeDecBuffer releases any memory owned by the decode buffer.
168+func WebPFreeDecBuffer(buffer *DecBuffer) error {
169+ if err := lowlevel.EnsureLoaded(); err != nil {
170+ return err
171+ }
172+ if buffer == nil {
173+ return nil
174+ }
175+
176+ lowlevel.WebPFreeDecBuffer(buffer)
177+ return nil
178+}
179+
180+// WebPInitDecoderConfig initializes a decoder config with ABI-checked defaults.
181+func WebPInitDecoderConfig(config *DecoderConfig) (ok bool, err error) {
182+ if err := lowlevel.EnsureLoaded(); err != nil {
183+ return false, err
184+ }
185+ if config == nil {
186+ return false, ErrInvalidData
187+ }
188+
189+ return lowlevel.WebPInitDecoderConfigInternal(config, lowlevel.WebPDecoderABIVersion) != 0, nil
190+}
191+
192+// WebPValidateDecoderConfigAvailable reports whether WebPValidateDecoderConfig
193+// is available in the loaded libwebp. The symbol was added in libwebp 1.6.0
194+// (released 2025-03); most distributions still ship an earlier version.
195+func WebPValidateDecoderConfigAvailable() bool {
196+ return lowlevel.ValidateDecoderConfigAvailable()
197+}
198+
199+// WebPValidateDecoderConfig validates decoder config values.
200+// It returns ErrNotAvailable if the loaded libwebp predates 1.6.0.
201+func WebPValidateDecoderConfig(config *DecoderConfig) (ok bool, err error) {
202+ if err := lowlevel.EnsureLoaded(); err != nil {
203+ return false, err
204+ }
205+ if !lowlevel.ValidateDecoderConfigAvailable() {
206+ return false, ErrNotAvailable
207+ }
208+ if config == nil {
209+ return false, ErrInvalidData
210+ }
211+
212+ return lowlevel.WebPValidateDecoderConfig(config) != 0, nil
213+}
214+
215+// WebPConfigInit initializes encoder config to default preset/quality.
216+func WebPConfigInit(config *Config) (ok bool, err error) {
217+ if err := lowlevel.EnsureLoaded(); err != nil {
218+ return false, err
219+ }
220+ if config == nil {
221+ return false, ErrInvalidData
222+ }
223+
224+ return lowlevel.WebPConfigInitInternal(config, PresetDefault, 75, lowlevel.WebPEncoderABIVersion) != 0, nil
225+}
226+
227+// WebPConfigPreset initializes encoder config with the given preset and quality.
228+func WebPConfigPreset(config *Config, preset int32, quality float32) (ok bool, err error) {
229+ if err := lowlevel.EnsureLoaded(); err != nil {
230+ return false, err
231+ }
232+ if config == nil {
233+ return false, ErrInvalidData
234+ }
235+
236+ return lowlevel.WebPConfigInitInternal(config, preset, quality, lowlevel.WebPEncoderABIVersion) != 0, nil
237+}
238+
239+// WebPConfigLosslessPreset applies a built-in lossless level preset.
240+func WebPConfigLosslessPreset(config *Config, level int32) (ok bool, err error) {
241+ if err := lowlevel.EnsureLoaded(); err != nil {
242+ return false, err
243+ }
244+ if config == nil {
245+ return false, ErrInvalidData
246+ }
247+
248+ return lowlevel.WebPConfigLosslessPreset(config, level) != 0, nil
249+}
250+
251+// WebPValidateConfig validates encoder config values.
252+func WebPValidateConfig(config *Config) (ok bool, err error) {
253+ if err := lowlevel.EnsureLoaded(); err != nil {
254+ return false, err
255+ }
256+ if config == nil {
257+ return false, ErrInvalidData
258+ }
259+
260+ return lowlevel.WebPValidateConfig(config) != 0, nil
261+}
262+
263+// WebPPictureInit initializes a picture struct with ABI-checked defaults.
264+func WebPPictureInit(picture *Picture) (ok bool, err error) {
265+ if err := lowlevel.EnsureLoaded(); err != nil {
266+ return false, err
267+ }
268+ if picture == nil {
269+ return false, ErrInvalidData
270+ }
271+
272+ return lowlevel.WebPPictureInitInternal(picture, lowlevel.WebPEncoderABIVersion) != 0, nil
273+}
274+
275+// WebPMemoryWriterInit initializes a memory writer instance.
276+func WebPMemoryWriterInit(writer *MemoryWriter) error {
277+ if err := lowlevel.EnsureLoaded(); err != nil {
278+ return err
279+ }
280+ if writer == nil {
281+ return ErrInvalidData
282+ }
283+
284+ lowlevel.WebPMemoryWriterInit(writer)
285+ return nil
286+}
287+
288+// WebPMemoryWriterClear frees memory owned by a memory writer.
289+func WebPMemoryWriterClear(writer *MemoryWriter) error {
290+ if err := lowlevel.EnsureLoaded(); err != nil {
291+ return err
292+ }
293+ if writer == nil {
294+ return nil
295+ }
296+
297+ lowlevel.WebPMemoryWriterClear(writer)
298+ return nil
299+}
300+
301+// WebPEncode runs advanced encoding with explicit config and picture structs.
302+func WebPEncode(config *Config, picture *Picture) (ok bool, err error) {
303+ if err := lowlevel.EnsureLoaded(); err != nil {
304+ return false, err
305+ }
306+ if config == nil || picture == nil {
307+ return false, ErrInvalidData
308+ }
309+
310+ return lowlevel.WebPEncode(config, picture) != 0, nil
311+}
312+
313+// WebPINewDecoder creates an incremental decoder using the provided output buffer.
314+func WebPINewDecoder(outputBuffer *DecBuffer) (uintptr, error) {
315+ if err := lowlevel.EnsureLoaded(); err != nil {
316+ return 0, err
317+ }
318+
319+ idec := lowlevel.WebPINewDecoder(outputBuffer)
320+ if idec == 0 {
321+ return 0, ErrDecodeFailed
322+ }
323+
324+ return idec, nil
325+}
326+
327+// WebPINewRGB creates an incremental decoder producing packed RGB-family output.
328+func WebPINewRGB(csp int32, outputBuffer []byte, outputStride int32) (uintptr, error) {
329+ if err := lowlevel.EnsureLoaded(); err != nil {
330+ return 0, err
331+ }
332+
333+ ptr, size := ptrAndSize(outputBuffer)
334+ idec := lowlevel.WebPINewRGB(csp, ptr, size, outputStride)
335+ if idec == 0 {
336+ return 0, ErrDecodeFailed
337+ }
338+
339+ return idec, nil
340+}
341+
342+// WebPINewYUV creates an incremental decoder producing YUV planes.
343+func WebPINewYUV(luma []byte, lumaStride int32, u []byte, uStride int32, v []byte, vStride int32) (uintptr, error) {
344+ if err := lowlevel.EnsureLoaded(); err != nil {
345+ return 0, err
346+ }
347+
348+ lumaPtr, lumaSize := ptrAndSize(luma)
349+ uPtr, uSize := ptrAndSize(u)
350+ vPtr, vSize := ptrAndSize(v)
351+
352+ idec := lowlevel.WebPINewYUV(lumaPtr, lumaSize, lumaStride, uPtr, uSize, uStride, vPtr, vSize, vStride)
353+ if idec == 0 {
354+ return 0, ErrDecodeFailed
355+ }
356+
357+ return idec, nil
358+}
359+
360+// WebPINewYUVA creates an incremental decoder producing YUVA planes.
361+func WebPINewYUVA(luma []byte, lumaStride int32, u []byte, uStride int32, v []byte, vStride int32, a []byte, aStride int32) (uintptr, error) {
362+ if err := lowlevel.EnsureLoaded(); err != nil {
363+ return 0, err
364+ }
365+
366+ lumaPtr, lumaSize := ptrAndSize(luma)
367+ uPtr, uSize := ptrAndSize(u)
368+ vPtr, vSize := ptrAndSize(v)
369+ aPtr, aSize := ptrAndSize(a)
370+
371+ idec := lowlevel.WebPINewYUVA(lumaPtr, lumaSize, lumaStride, uPtr, uSize, uStride, vPtr, vSize, vStride, aPtr, aSize, aStride)
372+ if idec == 0 {
373+ return 0, ErrDecodeFailed
374+ }
375+
376+ return idec, nil
377+}
378+
379+// WebPIDelete destroys an incremental decoder instance.
380+func WebPIDelete(idec uintptr) error {
381+ if err := lowlevel.EnsureLoaded(); err != nil {
382+ return err
383+ }
384+ if idec == 0 {
385+ return nil
386+ }
387+
388+ lowlevel.WebPIDelete(idec)
389+ return nil
390+}
391+
392+// WebPIAppend appends data to an incremental decoder.
393+func WebPIAppend(idec uintptr, data []byte) (VP8StatusCode, error) {
394+ if err := lowlevel.EnsureLoaded(); err != nil {
395+ return 0, err
396+ }
397+ if idec == 0 || len(data) == 0 {
398+ return VP8StatusInvalidParam, ErrInvalidData
399+ }
400+
401+ return VP8StatusCode(lowlevel.WebPIAppend(idec, &data[0], uintptr(len(data)))), nil
402+}
403+
404+// WebPIUpdate updates an incremental decoder with a full prefix buffer.
405+func WebPIUpdate(idec uintptr, data []byte) (VP8StatusCode, error) {
406+ if err := lowlevel.EnsureLoaded(); err != nil {
407+ return 0, err
408+ }
409+ if idec == 0 || len(data) == 0 {
410+ return VP8StatusInvalidParam, ErrInvalidData
411+ }
412+
413+ return VP8StatusCode(lowlevel.WebPIUpdate(idec, &data[0], uintptr(len(data)))), nil
414+}
415+
416+// WebPIDecode creates an incremental decoder with optional config and input.
417+func WebPIDecode(data []byte, config *DecoderConfig) (uintptr, error) {
418+ if err := lowlevel.EnsureLoaded(); err != nil {
419+ return 0, err
420+ }
421+ if len(data) == 0 {
422+ return 0, ErrInvalidData
423+ }
424+
425+ idec := lowlevel.WebPIDecode(&data[0], uintptr(len(data)), config)
426+ if idec == 0 {
427+ return 0, ErrDecodeFailed
428+ }
429+
430+ return idec, nil
431+}
432+
433+// WebPIDecodedArea returns decoded displayable area metadata.
434+func WebPIDecodedArea(idec uintptr, left, top, width, height *int32) (*DecBuffer, error) {
435+ if err := lowlevel.EnsureLoaded(); err != nil {
436+ return nil, err
437+ }
438+ if idec == 0 {
439+ return nil, ErrInvalidData
440+ }
441+
442+ buf := lowlevel.WebPIDecodedArea(idec, left, top, width, height)
443+ if buf == nil {
444+ return nil, ErrDecodeFailed
445+ }
446+
447+ return buf, nil
448+}
449+
450+// WebPIDecGetRGB returns current incremental packed RGB-family output pointer.
451+func WebPIDecGetRGB(idec uintptr, lastY, width, height, stride *int32) (uintptr, error) {
452+ if err := lowlevel.EnsureLoaded(); err != nil {
453+ return 0, err
454+ }
455+ if idec == 0 {
456+ return 0, ErrInvalidData
457+ }
458+
459+ ptr := lowlevel.WebPIDecGetRGB(idec, lastY, width, height, stride)
460+ if ptr == nil {
461+ return 0, ErrDecodeFailed
462+ }
463+
464+ return uintptr(unsafe.Pointer(ptr)), nil
465+}
466+
467+// WebPIDecGetYUVA returns current incremental YUVA luma pointer.
468+func WebPIDecGetYUVA(idec uintptr, lastY *int32, u, v, a **byte, width, height, stride, uvStride, aStride *int32) (uintptr, error) {
469+ if err := lowlevel.EnsureLoaded(); err != nil {
470+ return 0, err
471+ }
472+ if idec == 0 {
473+ return 0, ErrInvalidData
474+ }
475+
476+ ptr := lowlevel.WebPIDecGetYUVA(idec, lastY, u, v, a, width, height, stride, uvStride, aStride)
477+ if ptr == nil {
478+ return 0, ErrDecodeFailed
479+ }
480+
481+ return uintptr(unsafe.Pointer(ptr)), nil
482+}
483+
484+// WebPIDecGetYUV is a compatibility wrapper for YUV output without alpha.
485+func WebPIDecGetYUV(idec uintptr, lastY *int32, u, v **byte, width, height, stride, uvStride *int32) (uintptr, error) {
486+ var a *byte
487+ return WebPIDecGetYUVA(idec, lastY, u, v, &a, width, height, stride, uvStride, nil)
488+}
489+
490+// WebPDecode performs advanced one-shot decode using DecoderConfig.
491+func WebPDecode(data []byte, config *DecoderConfig) (status VP8StatusCode, err error) {
492+ if err := lowlevel.EnsureLoaded(); err != nil {
493+ return 0, err
494+ }
495+ if len(data) == 0 {
496+ return VP8StatusNotEnoughData, nil
497+ }
498+ if config == nil {
499+ return VP8StatusInvalidParam, ErrInvalidData
500+ }
501+
502+ return VP8StatusCode(lowlevel.WebPDecode(&data[0], uintptr(len(data)), config)), nil
503+}
504+
505+// WebPDecodeRGBA decodes to packed RGBA and returns an owned Go buffer.
506+func WebPDecodeRGBA(data []byte) (pix []byte, width, height, stride int, err error) {
507+ return decodeToOwnedBuffer(data, 4, lowlevel.WebPDecodeRGBA)
508+}
509+
510+// WebPDecodeARGB decodes to packed ARGB and returns an owned Go buffer.
511+func WebPDecodeARGB(data []byte) (pix []byte, width, height, stride int, err error) {
512+ return decodeToOwnedBuffer(data, 4, lowlevel.WebPDecodeARGB)
513+}
514+
515+// WebPDecodeBGRA decodes to packed BGRA and returns an owned Go buffer.
516+func WebPDecodeBGRA(data []byte) (pix []byte, width, height, stride int, err error) {
517+ return decodeToOwnedBuffer(data, 4, lowlevel.WebPDecodeBGRA)
518+}
519+
520+// WebPDecodeRGB decodes to packed RGB and returns an owned Go buffer.
521+func WebPDecodeRGB(data []byte) (pix []byte, width, height, stride int, err error) {
522+ return decodeToOwnedBuffer(data, 3, lowlevel.WebPDecodeRGB)
523+}
524+
525+// WebPDecodeBGR decodes to packed BGR and returns an owned Go buffer.
526+func WebPDecodeBGR(data []byte) (pix []byte, width, height, stride int, err error) {
527+ return decodeToOwnedBuffer(data, 3, lowlevel.WebPDecodeBGR)
528+}
529+
530+// WebPDecodeRGBAInto decodes into a caller-provided RGBA buffer.
531+func WebPDecodeRGBAInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
532+ return decodeInto(data, outputBuffer, outputStride, 4, lowlevel.WebPDecodeRGBAInto)
533+}
534+
535+// WebPDecodeRGBAIntoWithInfo decodes into a caller-provided RGBA buffer using
536+// dimensions already validated by WebPGetInfo, avoiding a second parse.
537+func WebPDecodeRGBAIntoWithInfo(data []byte, outputBuffer []byte, outputStride, width, height int) error {
538+ return decodeIntoWithInfo(data, outputBuffer, outputStride, width, height, 4, lowlevel.WebPDecodeRGBAInto)
539+}
540+
541+// WebPDecodeARGBInto decodes into a caller-provided ARGB buffer.
542+func WebPDecodeARGBInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
543+ return decodeInto(data, outputBuffer, outputStride, 4, lowlevel.WebPDecodeARGBInto)
544+}
545+
546+// WebPDecodeBGRAInto decodes into a caller-provided BGRA buffer.
547+func WebPDecodeBGRAInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
548+ return decodeInto(data, outputBuffer, outputStride, 4, lowlevel.WebPDecodeBGRAInto)
549+}
550+
551+// WebPDecodeRGBInto decodes into a caller-provided RGB buffer.
552+func WebPDecodeRGBInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
553+ return decodeInto(data, outputBuffer, outputStride, 3, lowlevel.WebPDecodeRGBInto)
554+}
555+
556+// WebPDecodeBGRInto decodes into a caller-provided BGR buffer.
557+func WebPDecodeBGRInto(data []byte, outputBuffer []byte, outputStride int) (width, height int, err error) {
558+ return decodeInto(data, outputBuffer, outputStride, 3, lowlevel.WebPDecodeBGRInto)
559+}
560+
561+// WebPDecodeYUV decodes to planar YUV and returns owned Go buffers.
562+func WebPDecodeYUV(data []byte) (y, u, v []byte, width, height, yStride, uvStride int, err error) {
563+ if err := lowlevel.EnsureLoaded(); err != nil {
564+ return nil, nil, nil, 0, 0, 0, 0, err
565+ }
566+ if len(data) == 0 {
567+ return nil, nil, nil, 0, 0, 0, 0, ErrInvalidData
568+ }
569+
570+ var w, h int32
571+ var uPtr, vPtr *byte
572+ var ys, uvs int32
573+ yPtr := lowlevel.WebPDecodeYUV(&data[0], uintptr(len(data)), &w, &h, &uPtr, &vPtr, &ys, &uvs)
574+ if yPtr == nil {
575+ return nil, nil, nil, 0, 0, 0, 0, ErrDecodeFailed
576+ }
577+ defer lowlevel.WebPFree(uintptr(unsafe.Pointer(yPtr)))
578+
579+ width = int(w)
580+ height = int(h)
581+ if width <= 0 || height <= 0 {
582+ return nil, nil, nil, 0, 0, 0, 0, ErrInvalidDimension
583+ }
584+
585+ yStride = int(ys)
586+ uvStride = int(uvs)
587+ uvHeight := (height + 1) / 2
588+
589+ y = make([]byte, yStride*height)
590+ u = make([]byte, uvStride*uvHeight)
591+ v = make([]byte, uvStride*uvHeight)
592+
593+ copy(y, unsafe.Slice(yPtr, len(y)))
594+ copy(u, unsafe.Slice(uPtr, len(u)))
595+ copy(v, unsafe.Slice(vPtr, len(v)))
596+
597+ return y, u, v, width, height, yStride, uvStride, nil
598+}
599+
600+// WebPDecodeYUVInto decodes into caller-provided Y, U and V planes.
601+func WebPDecodeYUVInto(data []byte, luma []byte, lumaStride int, u []byte, uStride int, v []byte, vStride int) (width, height int, err error) {
602+ if err := lowlevel.EnsureLoaded(); err != nil {
603+ return 0, 0, err
604+ }
605+ if len(data) == 0 {
606+ return 0, 0, ErrInvalidData
607+ }
608+ if len(luma) == 0 || len(u) == 0 || len(v) == 0 {
609+ return 0, 0, ErrBufferTooSmall
610+ }
611+
612+ w, h, ok, err := WebPGetInfo(data)
613+ if err != nil {
614+ return 0, 0, err
615+ }
616+ if !ok {
617+ return 0, 0, ErrInvalidData
618+ }
619+ if lumaStride < w {
620+ return 0, 0, ErrInvalidStride
621+ }
622+ uvWidth := (w + 1) / 2
623+ uvHeight := (h + 1) / 2
624+ if uStride < uvWidth || vStride < uvWidth {
625+ return 0, 0, ErrInvalidStride
626+ }
627+ if len(luma) < lumaStride*h || len(u) < uStride*uvHeight || len(v) < vStride*uvHeight {
628+ return 0, 0, ErrBufferTooSmall
629+ }
630+
631+ ptr := lowlevel.WebPDecodeYUVInto(
632+ &data[0],
633+ uintptr(len(data)),
634+ &luma[0],
635+ uintptr(len(luma)),
636+ int32(lumaStride),
637+ &u[0],
638+ uintptr(len(u)),
639+ int32(uStride),
640+ &v[0],
641+ uintptr(len(v)),
642+ int32(vStride),
643+ )
644+ if ptr == nil {
645+ return 0, 0, ErrDecodeFailed
646+ }
647+
648+ return w, h, nil
649+}
650+
651+// WebPEncodeRGBA encodes packed RGBA pixels with lossy quality.
652+func WebPEncodeRGBA(rgba []byte, width, height, stride int, quality float32) ([]byte, error) {
653+ return encodeWithQuality(rgba, width, height, stride, 4, quality, lowlevel.WebPEncodeRGBA)
654+}
655+
656+// WebPEncodeBGRA encodes packed BGRA pixels with lossy quality.
657+func WebPEncodeBGRA(bgra []byte, width, height, stride int, quality float32) ([]byte, error) {
658+ return encodeWithQuality(bgra, width, height, stride, 4, quality, lowlevel.WebPEncodeBGRA)
659+}
660+
661+// WebPEncodeRGB encodes packed RGB pixels with lossy quality.
662+func WebPEncodeRGB(rgb []byte, width, height, stride int, quality float32) ([]byte, error) {
663+ return encodeWithQuality(rgb, width, height, stride, 3, quality, lowlevel.WebPEncodeRGB)
664+}
665+
666+// WebPEncodeBGR encodes packed BGR pixels with lossy quality.
667+func WebPEncodeBGR(bgr []byte, width, height, stride int, quality float32) ([]byte, error) {
668+ return encodeWithQuality(bgr, width, height, stride, 3, quality, lowlevel.WebPEncodeBGR)
669+}
670+
671+// WebPEncodeLosslessRGBA encodes packed RGBA pixels in lossless mode.
672+func WebPEncodeLosslessRGBA(rgba []byte, width, height, stride int) ([]byte, error) {
673+ return encodeLossless(rgba, width, height, stride, 4, lowlevel.WebPEncodeLosslessRGBA)
674+}
675+
676+// WebPEncodeLosslessBGRA encodes packed BGRA pixels in lossless mode.
677+func WebPEncodeLosslessBGRA(bgra []byte, width, height, stride int) ([]byte, error) {
678+ return encodeLossless(bgra, width, height, stride, 4, lowlevel.WebPEncodeLosslessBGRA)
679+}
680+
681+// WebPEncodeLosslessRGB encodes packed RGB pixels in lossless mode.
682+func WebPEncodeLosslessRGB(rgb []byte, width, height, stride int) ([]byte, error) {
683+ return encodeLossless(rgb, width, height, stride, 3, lowlevel.WebPEncodeLosslessRGB)
684+}
685+
686+// WebPEncodeLosslessBGR encodes packed BGR pixels in lossless mode.
687+func WebPEncodeLosslessBGR(bgr []byte, width, height, stride int) ([]byte, error) {
688+ return encodeLossless(bgr, width, height, stride, 3, lowlevel.WebPEncodeLosslessBGR)
689+}
690+
691+type decodeFunc func(data *byte, dataSize uintptr, width *int32, height *int32) *byte
692+
693+type decodeIntoFunc func(data *byte, dataSize uintptr, outputBuffer *byte, outputBufferSize uintptr, outputStride int32) *byte
694+
695+func decodeInto(data []byte, outputBuffer []byte, outputStride int, bytesPerPixel int, fn decodeIntoFunc) (width, height int, err error) {
696+ if err := lowlevel.EnsureLoaded(); err != nil {
697+ return 0, 0, err
698+ }
699+ if len(data) == 0 {
700+ return 0, 0, ErrInvalidData
701+ }
702+ if len(outputBuffer) == 0 {
703+ return 0, 0, ErrBufferTooSmall
704+ }
705+ w, h, ok, err := WebPGetInfo(data)
706+ if err != nil {
707+ return 0, 0, err
708+ }
709+ if !ok {
710+ return 0, 0, ErrInvalidData
711+ }
712+ if err := decodeIntoWithInfo(data, outputBuffer, outputStride, w, h, bytesPerPixel, fn); err != nil {
713+ return 0, 0, err
714+ }
715+ return w, h, nil
716+}
717+
718+func decodeIntoWithInfo(data []byte, outputBuffer []byte, outputStride, width, height, bytesPerPixel int, fn decodeIntoFunc) error {
719+ if err := lowlevel.EnsureLoaded(); err != nil {
720+ return err
721+ }
722+ if len(data) == 0 {
723+ return ErrInvalidData
724+ }
725+ if len(outputBuffer) == 0 {
726+ return ErrBufferTooSmall
727+ }
728+ minimumStride, _, err := checkedDecodeLayout(width, height, bytesPerPixel)
729+ if err != nil {
730+ return err
731+ }
732+ if outputStride < minimumStride || outputStride > math.MaxInt32 {
733+ return ErrInvalidStride
734+ }
735+ required, ok := checkedProduct(outputStride, height)
736+ if !ok || len(outputBuffer) < required {
737+ return ErrBufferTooSmall
738+ }
739+ if fn(&data[0], uintptr(len(data)), &outputBuffer[0], uintptr(len(outputBuffer)), int32(outputStride)) == nil {
740+ return ErrDecodeFailed
741+ }
742+ return nil
743+}
744+
745+func decodeToOwnedBuffer(data []byte, bytesPerPixel int, fn decodeFunc) (pix []byte, width, height, stride int, err error) {
746+ if err := lowlevel.EnsureLoaded(); err != nil {
747+ return nil, 0, 0, 0, err
748+ }
749+ if len(data) == 0 {
750+ return nil, 0, 0, 0, ErrInvalidData
751+ }
752+
753+ var w, h int32
754+ ptr := fn(&data[0], uintptr(len(data)), &w, &h)
755+ if ptr == nil {
756+ return nil, 0, 0, 0, ErrDecodeFailed
757+ }
758+ defer lowlevel.WebPFree(uintptr(unsafe.Pointer(ptr)))
759+
760+ width = int(w)
761+ height = int(h)
762+ stride, bufLen, err := checkedDecodeLayout(width, height, bytesPerPixel)
763+ if err != nil {
764+ return nil, 0, 0, 0, err
765+ }
766+ pix = make([]byte, bufLen)
767+ copy(pix, unsafe.Slice(ptr, bufLen))
768+
769+ return pix, width, height, stride, nil
770+}
771+
772+type encodeLossyFunc func(pix *byte, width int32, height int32, stride int32, quality float32, output **byte) uintptr
773+
774+func encodeWithQuality(pix []byte, width, height, stride, bytesPerPixel int, quality float32, fn encodeLossyFunc) ([]byte, error) {
775+ if err := lowlevel.EnsureLoaded(); err != nil {
776+ return nil, err
777+ }
778+ if err := validatePixelInput(pix, width, height, stride, bytesPerPixel); err != nil {
779+ return nil, err
780+ }
781+
782+ var out *byte
783+ size := fn(&pix[0], int32(width), int32(height), int32(stride), quality, &out)
784+ if size == 0 || out == nil {
785+ return nil, ErrEncodeFailed
786+ }
787+ defer lowlevel.WebPFree(uintptr(unsafe.Pointer(out)))
788+
789+ b := make([]byte, int(size))
790+ copy(b, unsafe.Slice(out, int(size)))
791+
792+ return b, nil
793+}
794+
795+type encodeLosslessFunc func(pix *byte, width int32, height int32, stride int32, output **byte) uintptr
796+
797+func encodeLossless(pix []byte, width, height, stride, bytesPerPixel int, fn encodeLosslessFunc) ([]byte, error) {
798+ if err := lowlevel.EnsureLoaded(); err != nil {
799+ return nil, err
800+ }
801+ if err := validatePixelInput(pix, width, height, stride, bytesPerPixel); err != nil {
802+ return nil, err
803+ }
804+
805+ var out *byte
806+ size := fn(&pix[0], int32(width), int32(height), int32(stride), &out)
807+ if size == 0 || out == nil {
808+ return nil, ErrEncodeFailed
809+ }
810+ defer lowlevel.WebPFree(uintptr(unsafe.Pointer(out)))
811+
812+ b := make([]byte, int(size))
813+ copy(b, unsafe.Slice(out, int(size)))
814+
815+ return b, nil
816+}
817+
818+func validatePixelInput(pix []byte, width, height, stride, bytesPerPixel int) error {
819+ minimumStride, _, err := checkedDecodeLayout(width, height, bytesPerPixel)
820+ if err != nil {
821+ return err
822+ }
823+ if stride < minimumStride || stride > math.MaxInt32 {
824+ return ErrInvalidStride
825+ }
826+ required, ok := checkedProduct(stride, height)
827+ if !ok {
828+ return ErrInvalidDimension
829+ }
830+ if len(pix) < required {
831+ return fmt.Errorf("libwebp: pixel buffer too small: got=%d need>=%d", len(pix), required)
832+ }
833+ return nil
834+}
835+
836+// checkedDecodeLayout validates the dimensions and packed output layout before
837+// values are passed to libwebp, whose output stride parameter is a C int.
838+func checkedDecodeLayout(width, height, bytesPerPixel int) (stride, size int, err error) {
839+ if width <= 0 || height <= 0 {
840+ return 0, 0, ErrInvalidDimension
841+ }
842+ if bytesPerPixel <= 0 || width > math.MaxInt32/bytesPerPixel {
843+ return 0, 0, ErrInvalidStride
844+ }
845+ stride = width * bytesPerPixel
846+ size, ok := checkedProduct(stride, height)
847+ if !ok {
848+ return 0, 0, ErrInvalidDimension
849+ }
850+ return stride, size, nil
851+}
852+
853+func checkedProduct(a, b int) (int, bool) {
854+ if a < 0 || b < 0 || (a != 0 && b > int(^uint(0)>>1)/a) {
855+ return 0, false
856+ }
857+ return a * b, true
858+}
859+
860+// WebPIsPremultipliedMode reports whether the decode colorspace is premultiplied.
861+func WebPIsPremultipliedMode(mode int) bool {
862+ return mode == ModergbA || mode == ModebgrA || mode == ModeArgb || mode == ModergbA4444
863+}
864+
865+// WebPIsAlphaMode reports whether the decode colorspace contains alpha.
866+func WebPIsAlphaMode(mode int) bool {
867+ return mode == ModeRGBA || mode == ModeBGRA || mode == ModeARGB || mode == ModeRGBA4444 || mode == ModeYUVA || WebPIsPremultipliedMode(mode)
868+}
869+
870+// WebPIsRGBMode reports whether the decode colorspace is RGB-family.
871+func WebPIsRGBMode(mode int) bool {
872+ return mode < ModeYUV
873+}
874+
875+func ptrAndSize(b []byte) (*byte, uintptr) {
876+ if len(b) == 0 {
877+ return nil, 0
878+ }
879+ return &b[0], uintptr(len(b))
880+}
M vendor/modules.txt +4 -0 generated file: collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close