mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-10-12 02:16:39 +00:00
fix(parser): drop hard image edge/pixel limit, add opt-in raster byte ceiling (#20745)
This commit is contained in:
@@ -160,8 +160,14 @@ func decodeDispatchImage(data []byte, decodeRaster bool) (image.Image, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parser: decode image: %w", err)
|
||||
}
|
||||
if config.Width <= 0 || config.Height <= 0 || config.Width > parser.MaxImageEdge || config.Height > parser.MaxImageEdge || int64(config.Width)*int64(config.Height) > parser.MaxImagePixels {
|
||||
return nil, fmt.Errorf("parser: image dimensions %dx%d exceed limits", config.Width, config.Height)
|
||||
if config.Width <= 0 || config.Height <= 0 {
|
||||
return nil, fmt.Errorf("parser: image has invalid dimensions %dx%d", config.Width, config.Height)
|
||||
}
|
||||
// Length/width are unbounded (mirrors Python's picture.py); the decoded
|
||||
// raster byte ceiling is opt-in via RAGFLOW_IMAGE_RASTER_MAX_BYTES (0 =
|
||||
// unlimited).
|
||||
if err := parser.CheckImageRasterLimit(config.Width, config.Height); err != nil {
|
||||
return nil, fmt.Errorf("parser: %w", err)
|
||||
}
|
||||
// VLM consumes the original bytes; only local OCR needs a decoded raster.
|
||||
if !decodeRaster {
|
||||
|
||||
@@ -159,7 +159,7 @@ func (c *visionPDFCropper) Crop(ctx context.Context, item map[string]any) (*visi
|
||||
if len(single) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
raster := util.CropSectionPositionsRasterLimited(positions, single, deepdoctype.DlaScale, maxVisionImagePixels)
|
||||
raster := util.CropSectionPositionsRasterLimited(positions, single, deepdoctype.DlaScale, maxVLMCropPixels)
|
||||
if raster == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -182,12 +182,12 @@ func pdfPagesRasterWithinVisionLimits(engine deepdoctype.PDFEngine, pageNums map
|
||||
if math.IsNaN(widthPixels) || math.IsInf(widthPixels, 0) ||
|
||||
math.IsNaN(heightPixels) || math.IsInf(heightPixels, 0) ||
|
||||
widthPixels <= 0 || heightPixels <= 0 ||
|
||||
widthPixels > maxVisionImageEdge || heightPixels > maxVisionImageEdge ||
|
||||
widthPixels*heightPixels > float64(maxVisionImagePixels) {
|
||||
widthPixels > maxVLMCropEdge || heightPixels > maxVLMCropEdge ||
|
||||
widthPixels*heightPixels > float64(maxVLMCropPixels) {
|
||||
return false
|
||||
}
|
||||
pagePixels := int64(widthPixels * heightPixels)
|
||||
if pagePixels > maxVisionImagePixels-totalPixels {
|
||||
if pagePixels > maxVLMCropPixels-totalPixels {
|
||||
return false
|
||||
}
|
||||
totalPixels += pagePixels
|
||||
|
||||
@@ -35,10 +35,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxVisionImageBytes = parserlib.MaxImagePayloadBytes
|
||||
maxVisionImagePixels = parserlib.MaxImagePixels
|
||||
maxVisionImageEdge = parserlib.MaxImageEdge
|
||||
maxVLMEncodedBytes = (maxVisionImageBytes+2)/3*4 + 256
|
||||
maxVisionImageBytes = parserlib.MaxImagePayloadBytes
|
||||
maxVLMEncodedBytes = (maxVisionImageBytes+2)/3*4 + 256
|
||||
// maxVLMCropPixels and maxVLMCropEdge bound the raster of PDF page crops
|
||||
// rendered for the vision model (see vision_enhancement_cgo.go). They are a
|
||||
// pixel budget for the VLM crop path and are intentionally separate from
|
||||
// the file/inline image parser, which now uses the opt-in raster-byte limit
|
||||
// (RAGFLOW_IMAGE_RASTER_MAX_BYTES, default 0 = unlimited). These keep their
|
||||
// historical values because a 0-byte ceiling would map to a 0-pixel crop
|
||||
// budget and reject every VLM crop.
|
||||
maxVLMCropPixels = 40_000_000
|
||||
maxVLMCropEdge = 12_000
|
||||
)
|
||||
|
||||
func materializeInlineVisionImage(raw string) (*visionImage, error) {
|
||||
@@ -96,8 +103,11 @@ func inspectVisionImagePayload(payload string) (string, error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if config.Width <= 0 || config.Height <= 0 || config.Width > maxVisionImageEdge || config.Height > maxVisionImageEdge || int64(config.Width)*int64(config.Height) > maxVisionImagePixels {
|
||||
return "", fmt.Errorf("vision image: image dimensions %dx%d exceed limits", config.Width, config.Height)
|
||||
if config.Width <= 0 || config.Height <= 0 {
|
||||
return "", fmt.Errorf("vision image: image has invalid dimensions %dx%d", config.Width, config.Height)
|
||||
}
|
||||
if err := parserlib.CheckImageRasterLimit(config.Width, config.Height); err != nil {
|
||||
return "", fmt.Errorf("vision image: %w", err)
|
||||
}
|
||||
return format, nil
|
||||
}
|
||||
@@ -165,7 +175,11 @@ func imageWithinVisionLimits(img image.Image) bool {
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
width, height := bounds.Dx(), bounds.Dy()
|
||||
return width > 0 && height > 0 && width <= maxVisionImageEdge && height <= maxVisionImageEdge && int64(width)*int64(height) <= maxVisionImagePixels
|
||||
if width <= 0 || height <= 0 {
|
||||
return false
|
||||
}
|
||||
// Edge is unbounded (mirrors Python); only the opt-in raster-byte ceiling applies.
|
||||
return parserlib.CheckImageRasterLimit(width, height) == nil
|
||||
}
|
||||
|
||||
func imageMIMEForFormat(format string) string {
|
||||
|
||||
@@ -25,11 +25,14 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMaterializeInlineVisionImageKeepsVLMForImageDimensionLimit(t *testing.T) {
|
||||
func TestMaterializeInlineVisionImageNoEdgeLimitKeepsVLM(t *testing.T) {
|
||||
var encoded bytes.Buffer
|
||||
large := image.NewRGBA(image.Rect(0, 0, maxVisionImageEdge+1, 1))
|
||||
if err := png.Encode(&encoded, large); err != nil {
|
||||
t.Fatalf("encode oversized image: %v", err)
|
||||
// Width exceeds the former 12000-edge guard. With the edge limit removed
|
||||
// (aligned to Python's picture.py, which imposes none) the inline image is
|
||||
// still materialized and its VLM payload preserved for the vision model.
|
||||
wide := image.NewRGBA(image.Rect(0, 0, 12001, 1))
|
||||
if err := png.Encode(&encoded, wide); err != nil {
|
||||
t.Fatalf("encode wide image: %v", err)
|
||||
}
|
||||
dataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(encoded.Bytes())
|
||||
|
||||
@@ -37,11 +40,8 @@ func TestMaterializeInlineVisionImageKeepsVLMForImageDimensionLimit(t *testing.T
|
||||
if err != nil {
|
||||
t.Fatalf("materializeInlineVisionImage: %v", err)
|
||||
}
|
||||
if materialized == nil || materialized.Raster != nil {
|
||||
t.Fatalf("raster = %#v, want OCR to skip the oversized image", materialized)
|
||||
}
|
||||
if materialized.VLMData != dataURI || !isUsableVisionImage(materialized.VLMData) {
|
||||
t.Fatal("oversized OCR input should preserve its valid VLM payload")
|
||||
if materialized == nil || materialized.VLMData != dataURI || !isUsableVisionImage(materialized.VLMData) {
|
||||
t.Fatalf("wide inline image must preserve its valid VLM payload (no edge limit): %#v", materialized)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ package parser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
deepdocpdf "ragflow/internal/deepdoc/parser/pdf"
|
||||
@@ -26,12 +30,71 @@ import (
|
||||
const (
|
||||
// MaxImagePayloadBytes bounds compressed image data accepted by media paths.
|
||||
MaxImagePayloadBytes = 32 << 20
|
||||
// MaxImagePixels bounds decoded image dimensions before allocating a raster.
|
||||
MaxImagePixels = 40_000_000
|
||||
// MaxImageEdge bounds either decoded image dimension before raster allocation.
|
||||
MaxImageEdge = 12_000
|
||||
)
|
||||
|
||||
// EnvImageRasterMaxBytes names the environment variable that sets the
|
||||
// decoded-raster byte ceiling for image parsing (media dispatch and inline
|
||||
// vision images). It mirrors Python's picture.py, which imposes no image size
|
||||
// guard at all: length/width are unbounded and the raster-byte ceiling is opt
|
||||
// in. A value of 0 (the default when unset, empty, or invalid/negative) means
|
||||
// unlimited. When set to N > 0 (bytes), a decoded raster larger than N bytes
|
||||
// (modelled as width * height * 4 for an 8-bit RGBA raster) is rejected.
|
||||
//
|
||||
// Operator guidance: with the default (unlimited) ceiling, a highly
|
||||
// compressible image can still exhaust the ingestion worker's heap during
|
||||
// decode. For untrusted input sources, set RAGFLOW_IMAGE_RASTER_MAX_BYTES to a
|
||||
// budget the deployment can absorb (bounded by DeepDocConcurrency). The 4
|
||||
// bytes/pixel model is an 8-bit RGBA estimate; 16-bit formats (e.g. NRGBA64)
|
||||
// allocate roughly twice that, so size the ceiling with headroom for such
|
||||
// input.
|
||||
const EnvImageRasterMaxBytes = "RAGFLOW_IMAGE_RASTER_MAX_BYTES"
|
||||
|
||||
var (
|
||||
imageRasterMaxBytesOnce sync.Once
|
||||
imageRasterMaxBytes int64
|
||||
)
|
||||
|
||||
// ImageRasterMaxBytes returns the decoded-raster byte ceiling. It is resolved
|
||||
// once from EnvImageRasterMaxBytes; negative or unparseable values fall back to
|
||||
// 0 (unlimited) so a misconfiguration never silently blocks every image.
|
||||
func ImageRasterMaxBytes() int64 {
|
||||
imageRasterMaxBytesOnce.Do(func() {
|
||||
imageRasterMaxBytes = resolveImageRasterMaxBytes()
|
||||
})
|
||||
return imageRasterMaxBytes
|
||||
}
|
||||
|
||||
func resolveImageRasterMaxBytes() int64 {
|
||||
v := strings.TrimSpace(os.Getenv(EnvImageRasterMaxBytes))
|
||||
if v == "" {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil || n < 0 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// CheckImageRasterLimit returns an error if a w-by-h image's decoded RGBA
|
||||
// raster would exceed the opt-in byte ceiling (ImageRasterMaxBytes). Edge
|
||||
// (length/width) limits are intentionally absent to match Python's picture.py.
|
||||
// A zero ceiling means unlimited and always passes.
|
||||
//
|
||||
// The raster is modelled as 4 bytes/pixel (8-bit RGBA); see EnvImageRasterMaxBytes
|
||||
// for the 16-bit headroom note. The product is computed in uint64 so extreme
|
||||
// dimensions cannot silently wrap to 0 under int64 signed overflow and bypass
|
||||
// a positive ceiling.
|
||||
func CheckImageRasterLimit(w, h int) error {
|
||||
if max := ImageRasterMaxBytes(); max > 0 {
|
||||
const bytesPerPixel = 4
|
||||
if uint64(w)*uint64(h)*bytesPerPixel > uint64(max) {
|
||||
return fmt.Errorf("image raster %dx%d exceeds %d-byte limit", w, h, max)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AcquireImageMedia limits concurrent image materialization across Parser
|
||||
// payload encoding and ingestion VLM crop paths. The native analyzer keeps
|
||||
// its own inference limit.
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// resetImageRasterMaxBytesForTest re-reads EnvImageRasterMaxBytes between
|
||||
// subtests by resetting the once guard. It lives in the test file so it is not
|
||||
// compiled into production; the parser package's unexported vars are reachable
|
||||
// because this is a same-package (white-box) test.
|
||||
func resetImageRasterMaxBytesForTest() {
|
||||
imageRasterMaxBytesOnce = sync.Once{}
|
||||
imageRasterMaxBytes = 0
|
||||
}
|
||||
|
||||
// TestImageRasterMaxBytesResolution covers env parsing: unset/zero/invalid/negative
|
||||
// all mean unlimited (0), while a positive integer is honoured.
|
||||
func TestImageRasterMaxBytesResolution(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
env string
|
||||
want int64
|
||||
}{
|
||||
{"unset means unlimited", "", 0},
|
||||
{"explicit zero means unlimited", "0", 0},
|
||||
{"positive value honoured", "123456789", 123456789},
|
||||
{"invalid falls back to unlimited", "not-a-number", 0},
|
||||
{"negative falls back to unlimited", "-5", 0},
|
||||
{"whitespace-trimmed zero", " 0 ", 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Setenv(EnvImageRasterMaxBytes, c.env)
|
||||
resetImageRasterMaxBytesForTest()
|
||||
t.Cleanup(resetImageRasterMaxBytesForTest)
|
||||
if got := ImageRasterMaxBytes(); got != c.want {
|
||||
t.Errorf("ImageRasterMaxBytes() with env %q = %d, want %d", c.env, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckImageRasterLimit covers the opt-in raster-byte ceiling against the
|
||||
// real 2692x16384 image that previously tripped the hard dimension guard.
|
||||
// raster bytes = 2692 * 16384 * 4 = 176,422,912.
|
||||
func TestCheckImageRasterLimit(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
w, h int
|
||||
env string
|
||||
wantErr bool
|
||||
}{
|
||||
{"default unlimited passes huge image", 2692, 16384, "", false},
|
||||
{"explicit zero passes huge image", 2692, 16384, "0", false},
|
||||
{"negative treated unlimited passes", 2692, 16384, "-1", false},
|
||||
{"invalid treated unlimited passes", 2692, 16384, "garbage", false},
|
||||
{"ceiling below raster blocks", 2692, 16384, "100000000", true},
|
||||
{"ceiling above raster passes", 2692, 16384, "200000000", false},
|
||||
{"ceiling exactly equal passes (not strictly over)", 2692, 16384, "176422912", false},
|
||||
{"ceiling one byte below blocks", 2692, 16384, "176422911", true},
|
||||
{"small image under generous ceiling passes", 100, 100, "100000", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Setenv(EnvImageRasterMaxBytes, c.env)
|
||||
resetImageRasterMaxBytesForTest()
|
||||
t.Cleanup(resetImageRasterMaxBytesForTest)
|
||||
err := CheckImageRasterLimit(c.w, c.h)
|
||||
if (err != nil) != c.wantErr {
|
||||
t.Errorf("CheckImageRasterLimit(%d,%d) env=%q: err=%v, wantErr=%v", c.w, c.h, c.env, err, c.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user