Skip to content

feat: add WebP + AVIF output encoding (pure-Go, no cgo) - #500

Open
trungnsbkvn wants to merge 13 commits into
willnorris:mainfrom
trungnsbkvn:main
Open

feat: add WebP + AVIF output encoding (pure-Go, no cgo)#500
trungnsbkvn wants to merge 13 commits into
willnorris:mainfrom
trungnsbkvn:main

Conversation

@trungnsbkvn

Copy link
Copy Markdown

Upstream imageproxy decodes WebP but only encodes JPEG/PNG/GIF/TIFF/BMP
(issue #114). This fork adds WebP + AVIF encoders via gen2brain/webp and
gen2brain/avif (libwebp/libaom compiled to WASM, run by wazero) so the
single static binary — built with CGO_ENABLED=0, native Windows — can emit
modern formats for luatsumienbac.vn's self-hosted /media resizer.

trungnsbkvn and others added 2 commits July 1, 2026 19:26
Upstream imageproxy decodes WebP but only encodes JPEG/PNG/GIF/TIFF/BMP
(issue willnorris#114). This fork adds WebP + AVIF encoders via gen2brain/webp and
gen2brain/avif (libwebp/libaom compiled to WASM, run by wazero) so the
single static binary — built with CGO_ENABLED=0, native Windows — can emit
modern formats for luatsumienbac.vn's self-hosted /media resizer.

- data.go: register webp/avif as parseable format options
- transform.go: webp/avif encode cases + contentTypeForFormat() + defaults
- imageproxy.go: set explicit Content-Type for known formats (Go's
  http.DetectContentType has no AVIF signature -> would 403 as octet-stream)
- go.mod: + gen2brain/webp, gen2brain/avif (pure-Go WASM, no cgo)

Verified: CGO_ENABLED=0 build + runtime smoke test emits valid
image/avif (ftypavif) and image/webp (RIFF/WEBP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build (native + cross-compile), run as a service (NSSM on Windows, systemd
on Linux with hardening), reverse proxy (IIS URL Rewrite+ARR, nginx, Caddy —
all stripping /img), disk + Cloudflare caching, optional HMAC signing, and a
troubleshooting table. Flags/behaviour verified against cmd/imageproxy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@willnorris

Copy link
Copy Markdown
Owner

Did you mean to include all the docs that look to be specific to your fork? I'm guess not?

There's some discussion about webp in #388, and I have an experimental webp branch using a different library. I'm not familiar with this one, but will take a look when I get a chance. I think the last one I tried ended up creating files much larger than their JPEG equivalent, so missed all the actual advatnages of webp.

@trungnsbkvn

Copy link
Copy Markdown
Author

My bad,

Not the docs. I was working with my Astro project and found your brilliant work.

trungnsbkvn and others added 11 commits July 2, 2026 10:59
- build.ps1: build build/imageproxy.exe (CGO_ENABLED=0, windows/amd64) + smoke test
- build/install-service.ps1, uninstall-service.ps1: NSSM service (PS 5.1+ compatible)
- build/run.ps1: foreground run for testing
- build/config.example.env: IMAGEPROXY_* env alternative to flags
- build/README.md: copy-to-server deploy checklist -> DEPLOY.md
- .gitignore: ignore built *.exe and *.log (scripts are tracked)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bwebp, WebP-only)

willnorris#393 is unmerged, WebP-only, and cgo-bound to C libwebp (breaks the
single-binary / native-Windows / no-deps goal). Our gen2brain WASM approach
stays CGO_ENABLED=0 and adds AVIF. Trade-off: slower encode, mitigated by cache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nssm is an external dependency (and its site can be down). Make the single
binary register ITSELF with the OS service manager instead:

- cmd/imageproxy/service.go (new): -service install|uninstall|start|stop|restart
  (Windows SCM / systemd / launchd) + -logFile (service stdout is discarded).
  Foreground behaviour unchanged; graceful shutdown on Stop.
- cmd/imageproxy/main.go: build the http.Server then runWithService(server).
- go.mod: + github.com/kardianos/service (pure Go, keeps CGO_ENABLED=0).
- build/install-service.ps1 + uninstall-service.ps1: rewritten to use the
  binary's -service (no nssm); install adds sc.exe crash-restart + auto-start.
- DEPLOY.md / build/README.md / FORK_NOTES.md: nssm -> native service.

Verified: CGO_ENABLED=0 windows build; foreground health-check OK; -service
control wired (lists valid actions); -logFile captures startup + errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nssm is available again, so keep it as an option alongside the native
self-install (native stays the default — no dependency).

- install-service.ps1: -Method native|nssm (default native). nssm branch
  quotes paths, sets AppExit Restart + auto-start; native branch unchanged.
- uninstall-service.ps1: generic sc.exe stop+delete — removes either kind.
- DEPLOY.md / build/README.md: document both methods; FORK_NOTES clarifies
  nssm still works as an external wrapper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…poll)

The old trailing-& background job raced the health check ('(smoke test
skipped)') and could leak the process. Now starts hidden with -PassThru,
polls /health-check up to 6s, and stops that exact process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- install-service.ps1: add optional -NssmPath so nssm need not be on PATH.
- DEPLOY.md: acquisition (choco/scoop/manual zip -> win64\nssm.exe),
  by-hand configuration + what each nssm setting means, graceful-stop and
  run-as-account notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ssm args

Reported: 'nssm.exe : Can't open service!' aborted install-service.ps1 on a
fresh install. Root cause: PowerShell 7.4+ throws on a native command's
non-zero exit under ErrorActionPreference='Stop', so the pre-cleanup 'nssm
stop' of a not-yet-installed service was fatal.

- install/uninstall/build .ps1: set $PSNativeCommandUseErrorActionPreference
  = $false (harmless on PS 5.1); keep explicit $LASTEXITCODE checks.
- install-service.ps1: pass runtime flags to nssm as an ARRAY
  () instead of a hand-quoted AppParameters
  string — fixes spaced/& paths (e.g. 'D:\Webs\2. Youth & Partners\...').
  Cleanup now Get-Service-guarded + sc.exe; verify Running at the end.
- DEPLOY.md: by-hand nssm uses the array form; troubleshooting rows for the
  NativeCommandError and spaced paths (space-free path recommended).

Verified: reproduced the throw with sc.exe on a nonexistent service and
confirmed the flag prevents it; all scripts parse clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- install-service.ps1 / run.ps1 / config.example.env: cache dir ->
  "D:/Webs/2. Youth & Partners/media/luatsumienbac/_imgcache" (real path;
  spaces + & handled by the svcArgs array-splat / quoting)
- add -baseURL https://luatsumienbac.vn/media/ so /img/<opts>/<file>
  resolves to a readable, SEO-friendly, CDN-cacheable path (no base64)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Windows PowerShell 5.1 reads UTF-8 files without a BOM as CP1252, mangling
em-dashes (—) and box-drawing (─) into `â€"` and breaking string/brace parsing
(e.g. install-service.ps1 line 97 threw "Unexpected token"). Replace all
non-ASCII with ASCII equivalents so the scripts parse under PS 5.1 and 7+.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nssm stores AppParameters unquoted, so the spaced/& media cache path
(D:/Webs/2. Youth & Partners/...) truncated -cache at the first space and Go's
flag parser dropped every flag after it — silently killing -baseURL, so readable
URLs 400'd ("must provide absolute remote URL").

- $CacheDir -> D:/imgcache/luatsumienbac (no spaces; cache is scratch, regenerates)
- install-service.ps1: assert no svcArg contains a space on the nssm path (fail
  loudly instead of installing a silently-broken service)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@willnorris

Copy link
Copy Markdown
Owner

okay, I finally got around to testing the webp and avif implementations, and this is pretty impressive! At least in terms of image quality compared to my previous attempts (#393). It looks to add about 9MB to the total binary size, which isn't great, but it's a 15% increase for supporting modern image formats, so that's not terrible. I still feel a little bit uncomfortable with embedding a wasm runtime in imageproxy, but it's also not the craziest approach for this kind of thing.

I've been spending some time recently on breaking imageproxy into smaller includable modules, so we can make this an optional plugin somehow, so both the increased size and wasm nature are worth a shot here.

One of the first things we'll need to do though is get this PR off of your main branch so that it doesn't include all of these unrelated changes. When I have some time (hopefully tomorrow?) to work on getting this ready to merge, I may just open a new clean PR with just the relevant changes.

so anyway, just wanted to say thanks for the contribution, and I hope to look at it more closely soon. It might have to wait for some of the module work to land, but this is definitely good motivation for me to get that going.

@willnorris

Copy link
Copy Markdown
Owner

I just wanted to follow up from my comment yesterday to say that I did get this reworked on top of the modules work. You can see it in the modules branch (this commit specifically).

I even went so far as to update my personal website to that branch and add a global config option to encode all of my images as AVIF, at which point it crashed my little webhost by running out of memory. I haven't dug into it much yet, but it reminded me about one of the reasons I wanted to add module support. While I have a strong preference for no CGO in the core imageproxy build, individual modules could use CGO if needed and users could choose to link them in or not. So that could open up things like using libvips, which is significantly faster.

So, for now you're welcome to try the modules branch if you'd like and see how it performs for you. I definitely need to clean up the modules API quite a bit before I'm ready to merge it into main, and I will probably hold off for a bit on merging this webp/avif implementation. I'd like to look at adding some real benchmarks for image transformation, and do some comparisons of these libraries using purego versus not, as well as some other CGO libraries (now that those are sort of an option).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants