From e5cda9d587cda476adc01e1ffebec302f06f2e7d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 14 Sep 2026 18:51:42 +0000 Subject: [PATCH] fix: accept 24 MP camera photographs in production A 6000x4000 PNG (~28 MiB, 24 MP) was rejected by the 10 MiB upload cap and the 20 megapixel decode ceiling. Raise those limits to 32 MiB and 40 megapixels so common camera photos can be analyzed. Co-authored-by: Torsten Dittmann --- README.md | 2 +- cmd/autogravity/main.go | 4 ++-- cmd/autogravity/main_test.go | 17 +++++++++++++++++ docs/src/routes/index.tsx | 2 +- internal/imageutil/image.go | 5 ++++- internal/imageutil/image_test.go | 12 +++++++++++- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index eea7171..08bb4a2 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ should not be compared across sources. Face detection does not perform identity recognition. Blurred, obscured, or highly stylized faces may use the saliency fallback. -Requests are limited to 10 MiB and decoded images to 20 megapixels. Separate +Requests are limited to 32 MiB and decoded images to 40 megapixels. Separate upload and analysis admission limits bound buffered-body and decoded-image memory without allowing slow uploads to reserve inference capacity. Both models are loaded once at startup and their inference sessions are reused safely diff --git a/cmd/autogravity/main.go b/cmd/autogravity/main.go index 743b01e..65d95b7 100644 --- a/cmd/autogravity/main.go +++ b/cmd/autogravity/main.go @@ -28,7 +28,7 @@ import ( ) const ( - maxRequestBytes = 10 << 20 // 10 MiB, including multipart overhead. + maxRequestBytes = 32 << 20 // 32 MiB, including multipart overhead. maxConcurrentUploads = 4 // Bounds buffered bodies without reserving inference. defaultIntraOpThreads = 1 // Avoid N requests multiplying ONNX worker threads. defaultFaceModelPath = "models/face_detection_yunet_2023mar.onnx" @@ -290,7 +290,7 @@ func (app *application) handleAnalyze(w http.ResponseWriter, r *http.Request) { outcome = "invalid_request" var tooLarge *http.MaxBytesError if errors.As(err, &tooLarge) { - writeError(w, http.StatusRequestEntityTooLarge, "image exceeds the 10 MiB request limit") + writeError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("image exceeds the %d MiB request limit", maxRequestBytes>>20)) return } writeError(w, http.StatusBadRequest, err.Error()) diff --git a/cmd/autogravity/main_test.go b/cmd/autogravity/main_test.go index 68242a1..0079646 100644 --- a/cmd/autogravity/main_test.go +++ b/cmd/autogravity/main_test.go @@ -18,6 +18,7 @@ import ( "time" "autogravity/internal/facedetection" + "autogravity/internal/imageutil" "autogravity/internal/saliency" ) @@ -302,6 +303,7 @@ func TestHandleAnalyzeErrors(t *testing.T) { {name: "unsupported format", method: http.MethodPost, contentType: "application/octet-stream", body: []byte("not an image"), wantStatus: http.StatusUnsupportedMediaType}, {name: "empty body", method: http.MethodPost, contentType: "image/png", wantStatus: http.StatusBadRequest}, {name: "request too large", method: http.MethodPost, contentType: "image/png", body: make([]byte, maxRequestBytes+1), wantStatus: http.StatusRequestEntityTooLarge}, + {name: "11 MiB body is within limit", method: http.MethodPost, contentType: "image/png", body: make([]byte, 11<<20), wantStatus: http.StatusUnsupportedMediaType}, } for _, tt := range tests { @@ -319,6 +321,21 @@ func TestHandleAnalyzeErrors(t *testing.T) { } } +func TestProductionPhotographFitsLimits(t *testing.T) { + const ( + productionPNGBytes = 28_026_017 + productionWidth = 6000 + productionHeight = 4000 + ) + if maxRequestBytes < productionPNGBytes { + t.Fatalf("maxRequestBytes = %d, production photograph is %d bytes", maxRequestBytes, productionPNGBytes) + } + pixels := int64(productionWidth) * int64(productionHeight) + if pixels > imageutil.MaxPixels { + t.Fatalf("MaxPixels = %d, production photograph is %d pixels", imageutil.MaxPixels, pixels) + } +} + func TestHandleAnalyzeBoundsConcurrentWork(t *testing.T) { analyzer := &fakeAnalyzer{delay: 10 * time.Millisecond} const concurrency = 2 diff --git a/docs/src/routes/index.tsx b/docs/src/routes/index.tsx index c3f7fde..55e6043 100644 --- a/docs/src/routes/index.tsx +++ b/docs/src/routes/index.tsx @@ -227,7 +227,7 @@ function DocsPage() {

- Requests are limited to 10 MiB and decoded images to 20 megapixels. + Requests are limited to 32 MiB and decoded images to 40 megapixels. Separate upload and analysis admission limits bound buffered-body and decoded-image memory without allowing slow uploads to reserve inference capacity. Both models are loaded once at startup and their diff --git a/internal/imageutil/image.go b/internal/imageutil/image.go index 1f54ced..a2abcd8 100644 --- a/internal/imageutil/image.go +++ b/internal/imageutil/image.go @@ -15,7 +15,10 @@ import ( _ "golang.org/x/image/webp" ) -const MaxPixels = 20_000_000 +// MaxPixels is the decoded width*height ceiling. 40 megapixels covers common +// 24 MP camera photographs (for example 6000x4000) while still rejecting +// decompression bombs before pixels are allocated. +const MaxPixels = 40_000_000 var ( ErrUnsupportedFormat = errors.New("unsupported image format") diff --git a/internal/imageutil/image_test.go b/internal/imageutil/image_test.go index 827901f..dc0ad9c 100644 --- a/internal/imageutil/image_test.go +++ b/internal/imageutil/image_test.go @@ -152,12 +152,22 @@ func TestDecodeRejectsUnknownFormat(t *testing.T) { } func TestDecodeRejectsExcessivePixelCount(t *testing.T) { - _, err := Decode(oversizedPNGHeader(5_000, 5_000)) + _, err := Decode(oversizedPNGHeader(8_000, 8_000)) if !errors.Is(err, ErrImageTooLarge) { t.Fatalf("Decode() error = %v, want ErrImageTooLarge", err) } } +func TestDecodeAccepts24MegapixelCameraPhoto(t *testing.T) { + _, err := Decode(oversizedPNGHeader(6000, 4000)) + if errors.Is(err, ErrImageTooLarge) { + t.Fatal("rejected 6000x4000 photograph that production must accept") + } + if err == nil { + t.Fatal("header-only fixture unexpectedly decoded") + } +} + func TestPrepareProducesNormalizedNCHW(t *testing.T) { img := image.NewRGBA(image.Rect(0, 0, 1, 1)) img.SetRGBA(0, 0, color.RGBA{R: 255, G: 128, B: 0, A: 255})