Handle AVIF source images in thumb pipeline
Decode AVIF via avifdec when Go's image.Decode can't, and pass-through small AVIFs (<=800px wide) by caching the original bytes instead of re-encoding.
This commit is contained in:
@@ -150,6 +150,18 @@ func buildAvifThumb(src, outPath string) error {
|
||||
return fmt.Errorf("image too big (>%d bytes)", thumbMaxBytes)
|
||||
}
|
||||
|
||||
if isAvif(body) {
|
||||
// If the source AVIF is already within our width budget, skip the
|
||||
// decode/resize/re-encode roundtrip and cache the original bytes.
|
||||
w, _, infoErr := avifInfo(body)
|
||||
if infoErr == nil && w > 0 && w <= thumbWidth {
|
||||
return writeFile(outPath, body)
|
||||
}
|
||||
body, err = avifToPNG(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("avifdec: %w", err)
|
||||
}
|
||||
}
|
||||
src2, _, err := image.Decode(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode: %w", err)
|
||||
@@ -202,3 +214,89 @@ func buildAvifThumb(src, outPath string) error {
|
||||
}
|
||||
return os.Rename(stagedOut, outPath)
|
||||
}
|
||||
|
||||
// isAvif reports whether body looks like an AVIF file via the ISO-BMFF "ftyp"
|
||||
// brand at bytes 4..12 ("ftypavif"/"ftypavis").
|
||||
func isAvif(body []byte) bool {
|
||||
if len(body) < 12 {
|
||||
return false
|
||||
}
|
||||
if string(body[4:8]) != "ftyp" {
|
||||
return false
|
||||
}
|
||||
brand := string(body[8:12])
|
||||
return brand == "avif" || brand == "avis"
|
||||
}
|
||||
|
||||
// avifInfo returns the width and height of an AVIF by shelling out to
|
||||
// `avifdec --info`. It still decodes the image internally but doesn't write
|
||||
// pixels to disk, so it's cheaper than full avifToPNG.
|
||||
func avifInfo(body []byte) (int, int, error) {
|
||||
in, err := os.CreateTemp("", "pete-avif-info-*.avif")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
inPath := in.Name()
|
||||
defer os.Remove(inPath)
|
||||
if _, err := in.Write(body); err != nil {
|
||||
in.Close()
|
||||
return 0, 0, err
|
||||
}
|
||||
if err := in.Close(); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
out, err := exec.Command("avifdec", "--info", inPath).CombinedOutput()
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
rest, ok := strings.CutPrefix(line, "* Resolution")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_, dims, ok := strings.Cut(rest, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var w, h int
|
||||
if _, err := fmt.Sscanf(strings.TrimSpace(dims), "%dx%d", &w, &h); err == nil {
|
||||
return w, h, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, fmt.Errorf("no resolution in avifdec output")
|
||||
}
|
||||
|
||||
// writeFile atomically writes body to path via a sibling temp + rename.
|
||||
func writeFile(path string, body []byte) error {
|
||||
staged := path + ".tmp"
|
||||
if err := os.WriteFile(staged, body, 0o644); err != nil {
|
||||
os.Remove(staged)
|
||||
return err
|
||||
}
|
||||
return os.Rename(staged, path)
|
||||
}
|
||||
|
||||
// avifToPNG decodes AVIF bytes to PNG bytes by shelling out to avifdec.
|
||||
func avifToPNG(body []byte) ([]byte, error) {
|
||||
in, err := os.CreateTemp("", "pete-avif-in-*.avif")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inPath := in.Name()
|
||||
defer os.Remove(inPath)
|
||||
if _, err := in.Write(body); err != nil {
|
||||
in.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := in.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outPath := inPath + ".png"
|
||||
defer os.Remove(outPath)
|
||||
cmd := exec.Command("avifdec", inPath, outPath)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return os.ReadFile(outPath)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user