The fetcher accepts image/svg+xml again and internal/magic detects SVG. A raster format renders the SVG at the least size that covers the output, held to 8192 on a side, through libvips' thumbnail, which renders at any scale, so an enlarged SVG stays sharp. An SVG's own size is not limited; one with a side more than 8192 times the other is ErrSVGTooThin, answered 415. orig of an SVG at the orig size is the SVG unchanged, cached as a variant; at any other size it is ErrSVGNotOriginalSize, answered 400. The cache reads up to 1024 first bytes of a file to find its format. Render time and memory grow with what the SVG draws; their bound is the owner's decision on #68. Tests that pinned SVG as refused now expect it to be read. Model: opus-5-5
This commit is contained in:
1 parent
1379497897
commit
98b79c2e27
19 files changed
+796
-82
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -113,10 +114,21 @@ var ErrUnsupportedOutputFormat = errors.New("unsupported output format")
|
||||
// ErrUnsupportedInputFormat is returned, before the input is decoded, when
|
||||
// libvips reads the input's first bytes as another format than its source
|
||||
// format, or as none. An SVG whose first bytes pass the first-bytes check for
|
||||
// AVIF is one example. A source format other than the six pixa reads is also
|
||||
// refused with it.
|
||||
// AVIF is one example. A source format other than the seven pixa reads is
|
||||
// also refused with it.
|
||||
var ErrUnsupportedInputFormat = errors.New("unsupported input format")
|
||||
|
||||
// ErrSVGNotOriginalSize is returned when orig is asked of an SVG source at a
|
||||
// size other than orig. pixa returns an SVG only as it is; a raster format
|
||||
// gives it a size.
|
||||
var ErrSVGNotOriginalSize = errors.New(
|
||||
"an SVG is returned as SVG only at the orig size")
|
||||
|
||||
// ErrSVGTooThin is returned, before rendering, when an SVG source has one
|
||||
// side more than MaxInputDimension times the other. Rendered with its longer
|
||||
// side at most MaxInputDimension, its shorter side would be under 1 pixel.
|
||||
var ErrSVGTooThin = errors.New("SVG source too thin to render")
|
||||
|
||||
// ErrTooManyImages is returned when MaxConcurrentProcessing images are being
|
||||
// processed and none finishes within ProcessingWaitTimeout.
|
||||
var ErrTooManyImages = errors.New("too many images being processed at once")
|
||||
@@ -164,7 +176,9 @@ func New(params Params) *ImageProcessor {
|
||||
// encodes, it returns ctx's error and gives the slot back. govips gives pixa
|
||||
// no way to stop libvips once an encode has begun, so an encode keeps its
|
||||
// slot until it finishes. An AVIF output of more than MaxAVIFOutputPixels is
|
||||
// refused with ErrAVIFOutputTooLarge before anything is decoded.
|
||||
// refused with ErrAVIFOutputTooLarge before anything is decoded. orig of an
|
||||
// SVG source is the SVG as it is, since libvips cannot save SVG; any other
|
||||
// format renders it at the output size.
|
||||
func (p *ImageProcessor) Process(
|
||||
ctx context.Context,
|
||||
input io.Reader,
|
||||
@@ -190,27 +204,17 @@ func (p *ImageProcessor) Process(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode image
|
||||
img, err := vips.NewImageFromBuffer(data)
|
||||
img, err := loadImage(data, sourceFormat)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode image: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
defer img.Close()
|
||||
|
||||
// Turn the image upright now: encode strips the EXIF orientation tag,
|
||||
// and sizes below must be worked out on the upright image.
|
||||
err = img.AutoRotate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to auto-rotate: %w", err)
|
||||
}
|
||||
origWidth, origHeight := img.Width(), img.Height()
|
||||
outputFormat := resolveOutputFormat(transform.Format, sourceFormat)
|
||||
|
||||
// Get original dimensions
|
||||
origWidth := img.Width()
|
||||
origHeight := img.Height()
|
||||
|
||||
// Validate input dimensions to prevent DoS via decompression bombs
|
||||
if origWidth > MaxInputDimension || origHeight > MaxInputDimension {
|
||||
return nil, ErrInputTooLarge
|
||||
if outputFormat == imagerequest.FormatSVG && sourceFormat == imagerequest.FormatSVG {
|
||||
return svgAsItIs(data, transform.Size, origWidth, origHeight)
|
||||
}
|
||||
|
||||
// Determine target dimensions; too large ones are refused before resizing
|
||||
@@ -220,12 +224,6 @@ func (p *ImageProcessor) Process(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// orig is the source's own format; encode refuses an empty format
|
||||
outputFormat := transform.Format
|
||||
if outputFormat == imagerequest.FormatOriginal {
|
||||
outputFormat = sourceFormat
|
||||
}
|
||||
|
||||
// Determine output dimensions; too large ones are refused before
|
||||
// resizing, which with FitCover already decodes the input
|
||||
outputWidth, outputHeight, err := outputDimensions(origWidth, origHeight,
|
||||
@@ -234,6 +232,19 @@ func (p *ImageProcessor) Process(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// An SVG is rendered again at about the output size, to stay sharp
|
||||
if sourceFormat == imagerequest.FormatSVG {
|
||||
rendered, err := renderSVG(data, origWidth, origHeight,
|
||||
outputWidth, outputHeight)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rendered.Close()
|
||||
|
||||
img = rendered
|
||||
}
|
||||
|
||||
// Resize if needed
|
||||
if targetWidth != origWidth || targetHeight != origHeight {
|
||||
err := p.resize(img, outputWidth, outputHeight, transform.FitMode)
|
||||
@@ -290,6 +301,110 @@ func targetDimensions(
|
||||
return width, height, nil
|
||||
}
|
||||
|
||||
// resolveOutputFormat returns the format of the output for the requested
|
||||
// format: sourceFormat for orig, and format itself for any other. encode
|
||||
// refuses an empty format.
|
||||
func resolveOutputFormat(
|
||||
format, sourceFormat imagerequest.Format,
|
||||
) imagerequest.Format {
|
||||
if format == imagerequest.FormatOriginal {
|
||||
return sourceFormat
|
||||
}
|
||||
|
||||
return format
|
||||
}
|
||||
|
||||
// loadImage decodes data and turns the image upright: encode strips the EXIF
|
||||
// orientation tag, and sizes must be worked out on the upright image. An
|
||||
// image with a side above MaxInputDimension is ErrInputTooLarge, which guards
|
||||
// against decompression bombs. An SVG is not checked: only its header has
|
||||
// been read, and the output checks limit the size it is rendered at.
|
||||
func loadImage(
|
||||
data []byte, sourceFormat imagerequest.Format,
|
||||
) (*vips.ImageRef, error) {
|
||||
img, err := vips.NewImageFromBuffer(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
err = img.AutoRotate()
|
||||
if err != nil {
|
||||
img.Close()
|
||||
|
||||
return nil, fmt.Errorf("failed to auto-rotate: %w", err)
|
||||
}
|
||||
|
||||
if sourceFormat != imagerequest.FormatSVG &&
|
||||
(img.Width() > MaxInputDimension || img.Height() > MaxInputDimension) {
|
||||
img.Close()
|
||||
|
||||
return nil, ErrInputTooLarge
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// svgAsItIs returns the SVG data unchanged, which is orig of an SVG source
|
||||
// at the orig size. width x height is the SVG's own size. Any other size is
|
||||
// ErrSVGNotOriginalSize.
|
||||
func svgAsItIs(
|
||||
data []byte, size imagerequest.Size, width, height int,
|
||||
) (*Result, error) {
|
||||
if !size.OriginalSize() {
|
||||
return nil, fmt.Errorf("%w: asked for %dx%d", ErrSVGNotOriginalSize,
|
||||
size.Width, size.Height)
|
||||
}
|
||||
|
||||
return &Result{
|
||||
Content: io.NopCloser(bytes.NewReader(data)),
|
||||
ContentLength: int64(len(data)),
|
||||
ContentType: imagerequest.FormatSVG.MIMEType(),
|
||||
Width: width,
|
||||
Height: height,
|
||||
InputWidth: width,
|
||||
InputHeight: height,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// svgRenderSize returns the least size in the proportions of width x height,
|
||||
// an SVG's own size, that covers outputWidth x outputHeight, so that resize
|
||||
// hardly changes it. Neither side is above MaxInputDimension.
|
||||
func svgRenderSize(width, height, outputWidth, outputHeight int) (int, int) {
|
||||
scale := min(
|
||||
max(float64(outputWidth)/float64(width),
|
||||
float64(outputHeight)/float64(height)),
|
||||
float64(MaxInputDimension)/float64(max(width, height)))
|
||||
|
||||
return int(math.Round(float64(width) * scale)),
|
||||
int(math.Round(float64(height) * scale))
|
||||
}
|
||||
|
||||
// renderSVG renders the SVG data, whose own size is width x height, at
|
||||
// svgRenderSize for outputWidth x outputHeight. An SVG with one side more
|
||||
// than MaxInputDimension times the other is ErrSVGTooThin. libvips'
|
||||
// thumbnail has librsvg render the SVG at that size directly, at any scale:
|
||||
// govips takes a density only in whole dots per inch, which cannot render
|
||||
// an SVG below 1/72 of its own size. Given the SVG as bytes with no file
|
||||
// name, librsvg loads no file or URL the SVG refers to.
|
||||
func renderSVG(
|
||||
data []byte, width, height, outputWidth, outputHeight int,
|
||||
) (*vips.ImageRef, error) {
|
||||
if max(width, height) > MaxInputDimension*min(width, height) {
|
||||
return nil, fmt.Errorf("%w: %dx%d", ErrSVGTooThin, width, height)
|
||||
}
|
||||
|
||||
renderWidth, renderHeight := svgRenderSize(width, height,
|
||||
outputWidth, outputHeight)
|
||||
|
||||
img, err := vips.NewThumbnailWithSizeFromBuffer(data,
|
||||
renderWidth, renderHeight, vips.InterestingNone, vips.SizeBoth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to render SVG: %w", err)
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// WaitForProcessing waits until no image is being processed, or until ctx
|
||||
// ends, and returns how many images were still being processed then. It
|
||||
// waits by taking each slot in processingSemaphore as it frees up until it
|
||||
@@ -397,6 +512,8 @@ func vipsImageType(format imagerequest.Format) vips.ImageType {
|
||||
return vips.ImageTypeAVIF
|
||||
case imagerequest.FormatJXL:
|
||||
return vips.ImageTypeJXL
|
||||
case imagerequest.FormatSVG:
|
||||
return vips.ImageTypeSVG
|
||||
case imagerequest.FormatOriginal, imagerequest.FormatAuto:
|
||||
return vips.ImageTypeUnknown
|
||||
default:
|
||||
@@ -575,7 +692,9 @@ func (p *ImageProcessor) encode(
|
||||
case imagerequest.FormatJXL:
|
||||
return exportJXL(img, quality)
|
||||
|
||||
case imagerequest.FormatOriginal, imagerequest.FormatAuto:
|
||||
// libvips cannot save SVG; Process returns an SVG source as it is
|
||||
case imagerequest.FormatSVG, imagerequest.FormatOriginal,
|
||||
imagerequest.FormatAuto:
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedOutputFormat, format)
|
||||
|
||||
default:
|
||||
|
||||
Reference in new issue
Block a user