2026年 01月 12日

golang で AVIF 対応 image.Decode

標準ないし準標準 (golang.org/x/image/webp みたいな) にはないので面倒っぽそうだなあとなんとなく思ってたけど意外にも簡単に対応できた。

CGo を許容するかで今のところ2択っぽい

  1. _ "github.com/vegidio/avif-go" CGo
  2. _ "github.com/gen2brain/avif" libavif を WASM にコンパイルし、wazero で実行

どっちもブランクインポートするだけでつかえる。ベンチ的には CGo のほうが10倍早い

こういう検証とベンチをむちゃくちゃサクっとやれるのすごいいいよなあ。Agentic Coding。脳の負荷が位置が変わっているのを感じる。

検証

vegidio/avif-go

# register_vegidio_test.go
package main

import (
	"bytes"
	"image"
	"os"
	"testing"

	_ "github.com/vegidio/avif-go"
)

func TestRegisterVegidio(t *testing.T) {
	data, err := os.ReadFile("../../static/fixtures/sample.avif")
	if err != nil {
		t.Fatal(err)
	}

	img, format, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		t.Fatalf("Vegidio registration failed: %v", err)
	}

	t.Logf("Successfully decoded %dx%d %s image using vegidio", img.Bounds().Dx(), img.Bounds().Dy(), format)
	if format != "avif" {
		t.Errorf("Expected format avif, got %s", format)
	}
}
go test -v register_vegidio_test.go   
=== RUN   TestRegisterVegidio
    register_vegidio_test.go:23: Successfully decoded 2886x2164 avif image using vegidio
--- PASS: TestRegisterVegidio (0.08s)
PASS
ok      command-line-arguments  0.313s

github.com/gen2brain/avif

# register_gen2brain_test.go
package main

import (
	"bytes"
	"image"
	"os"
	"testing"

	_ "github.com/gen2brain/avif"
)

func TestRegisterGen2brain(t *testing.T) {
	data, err := os.ReadFile("../../static/fixtures/sample.avif")
	if err != nil {
		t.Fatal(err)
	}

	img, format, err := image.Decode(bytes.NewReader(data))
	if err != nil {
		t.Fatalf("Gen2brain registration failed: %v", err)
	}

	t.Logf("Successfully decoded %dx%d %s image using gen2brain", img.Bounds().Dx(), img.Bounds().Dy(), format)
	if format != "avif" {
		t.Errorf("Expected format avif, got %s", format)
	}
}
go test -v register_gen2brain_test.go 
=== RUN   TestRegisterGen2brain
    register_gen2brain_test.go:23: Successfully decoded 2886x2164 avif image using gen2brain
--- PASS: TestRegisterGen2brain (0.73s)
PASS
ok      command-line-arguments  1.119s

Cloudflare R2を使ってみる

容量的には 10GB までは無料で、そのあと従量課金になってもそれほど高額ではなさそう。

ただクラスB操作(参照系)がコントロールしにくいのでちょっと怖い。CDN経由のキャッシュミスだけが問題なのでファイル数が十分に少なければ恐るることはなさそう。現状ではこのサイトの全GETリクエスト(ほとんどクローラーだけど)がR2に飛んだとしても無料枠に収まる。

まず新規アップロードを R2 にするようにしてある。様子を見つつ過去分もアップロードするつもりではあるけど、その前に JPEG ファイルを全ファイル AVIF にするということをしたいのでまだやれてない。

もともと Cloudflare Registrar 契約してるので使いはじめに抵抗がない感じ。

CDNエッジでジオシティーズ的なやつを作ってみる

半年以上前に Cloudflare Workers上で Honoフレームワーク使いつつ懐しい構成のサイトを作ってみたやつ、日記に書いてなかった。

中身がシーディーエンヌのエッジのワーカーで動いてるけど見てくれがジオシティーズだったら面白いなと思って、当時は Github Copilot 使いながら作った記憶。

いわゆる6hotサイトみたいのは現状の Worker の無料枠で全然問題なかろうという気がするし、そういう意味では現代のジオシティーズなんじゃと思ったのだった。

Durable Objects まわり仕様が変化していくのでエーアイに書かせて困った思い出がある。作りっぱなし

AVIFすごすぎるな

過去のをちまちま変換してるけど、1/10 になってかつ見た目がまったく変わらないのも多い。JPEGはやめよう!

リンク切れした画像の一部を復活させた

過去画像の AVIF 化にともなって、Picasa → Google Photos → セルフホストと画像移行する過程で、うまくファイル名一致で移行できずにリンク切れになっていたファイルを、Lightroom Classic から再現像することである程度は復元した。

エントリの中にはあるがファイルシステムにないものを抽出して、

2026/01/12 17:38:39 AVIF変換を検証中...
2026/01/12 17:38:39 [欠落] エントリID:19390 (2012/04/20/1) 画像: IMG_0191-2048.jpg (AVIF:なし)
2026/01/12 17:38:39 [欠落] エントリID:19392 (2012/04/21/1) 画像: IMG_0123-2048.jpg (AVIF:なし)
2026/01/12 17:38:39 [欠落] エントリID:19393 (2012/04/22/1) 画像: IMG_0170-2048.jpg (AVIF:なし)
...
|<

この情報をGeminiに投げつけながらワンライナーでファイル名の一部を抽出してもらい、Lightroom の検索窓に投げんだ。あきらかに無関係なファイル以外をクイックコレクションに追加してすべて書き出た。

ただ、カメラのファイル名はあんまり一意ではない(買い替えのタイミングとかでリセットされている)。なので exiftool で撮影日時を取得し、↑ のログの日付と、因果律が崩れない範囲でファイル名をマッチングさせるコードを書いてもらった (なぜか Ruby で書いてくれた)

そのうえで何度か漏れたファイルを出力した。1件だけどうしても見つからないファイルがあったけどもういいかなという感じ。

過去画像を R2 に移行した

AVIF変換おわらせたあとガッと移行させた。

これで写真の表示が早くなるといいな。なんだかんだ Google Photos から移行してきてからは表示の遅さゆえに写真アップロードするモチベが落ちていたなと思う。

事前処理

もともとのファイル郡と拡張子ごとのサイズ

$ find . -type f -printf "%s %f\n" | awk '{ext=$NF; if(ext ~ /\./) {sub(/.*\./,"",ext); ext=tolower(ext)} else {ext="no_ext"}; sum[ext]+=$1} END {for(e in sum) print sum[e], e}' | sort -nr | numfmt --to=iec --field=1
5.5G jpg
298M png
60M webp
56M gif
1.5M avif
1.1M jpeg
219K ds_store

oxipng と avif への変換後

$ find . -type f -printf "%s %f\n" | awk '{ext=$NF; if(ext ~ /\./) {sub(/.*\./,"",ext); ext=tolower(ext)} else {ext="no_ext"}; sum[ext]+=$1} END {for(e in sum) print sum[e], e}' | sort -nr | numfmt --to=iec --field=1
3.3G avif
450M jpg
240M png
60M webp
56M gif

この450MBの jpg については参照されてないものっぽいのでGCすることに。