From 575a17967fc83fda24ecc8a0c0651e682cc750ab Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 20 Oct 2026 17:58:01 +0200 Subject: [PATCH] fix(thumbnails): decode one frame of a png asked for as a still An animated png carried every frame through imaging.Decode, which only the gif path guarded: a 465 KB file with 200 full-canvas frames allocated 226 MB, now 10 MB. png.Decode reads IDAT and ignores an animation's fdAT chunks. --- .../thumbnails/pkg/service/http/v0/push_imaging.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/services/thumbnails/pkg/service/http/v0/push_imaging.go b/services/thumbnails/pkg/service/http/v0/push_imaging.go index 097a84fdcb..f954fd318f 100644 --- a/services/thumbnails/pkg/service/http/v0/push_imaging.go +++ b/services/thumbnails/pkg/service/http/v0/push_imaging.go @@ -70,12 +70,18 @@ func extForSourceFormatImaging(src *imagingSource) string { } } -// decodeStill reads the pixels of the one frame that is resized here. An -// animated gif only reaches this path when a still format was asked for, and -// imaging.Decode would hold every frame of it in memory to return the first. +// decodeStill reads the pixels of the one frame that is resized here. +// imaging.Decode is DecodeAll plus SingleFrame, so for an animation it holds +// every frame in memory to return the first, and the frame guard only sees a +// gif. The stdlib decoders read the first frame alone: gif.Decode stops after +// it, png.Decode reads IDAT and ignores an animation's later fdAT chunks. The +// rest keeps the imaging path, which is also what reads a JPEG's orientation. func decodeStill(src *imagingSource) (image.Image, error) { - if src.format == "gif" { + switch src.format { + case "gif": return gif.Decode(bytes.NewReader(src.data)) + case "png": + return png.Decode(bytes.NewReader(src.data)) } return imaging.Decode(bytes.NewReader(src.data), imaging.AutoOrientation(true))