check / check (push) Canceled after 0s
The fetcher accepts image/svg+xml again and internal/magic detects SVG. A raster format renders the SVG with librsvg at the density that makes it cover the output size, so an enlarged SVG stays sharp; the rendered size is held to MaxInputDimension. 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, as an SVG's root element can come after a prolog. Tests that pinned SVG as refused now expect it to be read. Model: opus-5-5
223 lines
6.8 KiB
Go
223 lines
6.8 KiB
Go
package imageprocessor
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/davidbyttow/govips/v2/vips"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
)
|
|
|
|
// testSVG is a 16x12 SVG, black on its left half and white on its right.
|
|
const testSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="12">` +
|
|
`<rect width="16" height="12" fill="white"/>` +
|
|
`<rect width="8" height="12" fill="black"/></svg>`
|
|
|
|
// newSVGTestProcessor returns a processor for the SVG tests.
|
|
func newSVGTestProcessor() *ImageProcessor {
|
|
return New(Params{MaxInputBytes: 1 << 20, MaxConcurrentProcessing: 1})
|
|
}
|
|
|
|
// processSVG processes svg, as an SVG source, with transform.
|
|
func processSVG(
|
|
t *testing.T, svg string, transform imagerequest.Transform,
|
|
) (*Result, []byte, error) {
|
|
t.Helper()
|
|
|
|
result, err := newSVGTestProcessor().Process(t.Context(),
|
|
bytes.NewReader([]byte(svg)), imagerequest.FormatSVG, &transform)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
output, err := io.ReadAll(result.Content)
|
|
if err != nil {
|
|
t.Fatalf("reading the output: %v", err)
|
|
}
|
|
|
|
return result, output, nil
|
|
}
|
|
|
|
// TestImageProcessor_SVGOrigIsTheSVG checks that orig of an SVG at the orig
|
|
// size is the SVG itself, never another format, and that orig at another
|
|
// size is ErrSVGNotOriginalSize.
|
|
func TestImageProcessor_SVGOrigIsTheSVG(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
result, output, err := processSVG(t, testSVG,
|
|
imagerequest.Transform{Format: imagerequest.FormatOriginal})
|
|
if err != nil {
|
|
t.Fatalf("orig of the SVG: %v", err)
|
|
}
|
|
|
|
t.Logf("orig of the SVG: %s, %dx%d", result.ContentType, result.Width,
|
|
result.Height)
|
|
|
|
if result.ContentType != "image/svg+xml" || string(output) != testSVG {
|
|
t.Errorf("orig of the SVG = %s %q, want image/svg+xml %q",
|
|
result.ContentType, output, testSVG)
|
|
}
|
|
|
|
_, _, err = processSVG(t, testSVG, imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 32, Height: 24},
|
|
Format: imagerequest.FormatOriginal,
|
|
})
|
|
if !errors.Is(err, ErrSVGNotOriginalSize) {
|
|
t.Errorf("orig of the SVG at 32x24: error = %v, want %v", err,
|
|
ErrSVGNotOriginalSize)
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_RendersSVGToEachRasterFormat checks that an SVG is
|
|
// rendered to each raster format at the requested size.
|
|
func TestImageProcessor_RendersSVGToEachRasterFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, format := range []imagerequest.Format{
|
|
imagerequest.FormatJPEG, imagerequest.FormatPNG, imagerequest.FormatGIF,
|
|
imagerequest.FormatWebP, imagerequest.FormatAVIF, imagerequest.FormatJXL,
|
|
} {
|
|
result, output, err := processSVG(t, testSVG, imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 40, Height: 30},
|
|
Format: format,
|
|
})
|
|
if err != nil {
|
|
t.Errorf("SVG as %s: %v", format, err)
|
|
|
|
continue
|
|
}
|
|
|
|
t.Logf("SVG as %s: %s, %dx%d, %d bytes", format, result.ContentType,
|
|
result.Width, result.Height, len(output))
|
|
|
|
if result.ContentType != format.MIMEType() ||
|
|
vips.DetermineImageType(output) != vipsImageType(format) ||
|
|
result.Width != 40 || result.Height != 30 {
|
|
t.Errorf("SVG as %s = %s, %dx%d; want %s, 40x30", format,
|
|
result.ContentType, result.Width, result.Height, format.MIMEType())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_RendersSVGSharpWhenEnlarged checks that a 16x12 SVG
|
|
// asked for at 1600x1200 is rendered at that size, not rendered at 16x12 and
|
|
// enlarged: 10 pixels either side of the edge between its black and white
|
|
// halves are black and white, where an enlarged image is grey there.
|
|
func TestImageProcessor_RendersSVGSharpWhenEnlarged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, output, err := processSVG(t, testSVG, imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 1600, Height: 1200},
|
|
Format: imagerequest.FormatPNG,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SVG as PNG at 1600x1200: %v", err)
|
|
}
|
|
|
|
img, err := vips.NewImageFromBuffer(output)
|
|
if err != nil {
|
|
t.Fatalf("decoding the PNG: %v", err)
|
|
}
|
|
|
|
t.Cleanup(img.Close)
|
|
|
|
for x, want := range map[int]float64{790: 0, 810: 255} {
|
|
point, err := img.GetPoint(x, 600)
|
|
if err != nil {
|
|
t.Fatalf("GetPoint(%d, 600): %v", x, err)
|
|
}
|
|
|
|
t.Logf("pixel at %d,600: %v", x, point)
|
|
|
|
if point[0] != want {
|
|
t.Errorf("pixel at %d,600 = %v, want %v", x, point[0], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_SVGFetchesNothing checks that rendering an SVG that
|
|
// refers to an image and an external entity on a local server makes no
|
|
// request to that server.
|
|
func TestImageProcessor_SVGFetchesNothing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var requests atomic.Int64
|
|
|
|
png := createTestPNG(t, 4, 4)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
requests.Add(1)
|
|
t.Logf("request for %s", r.URL)
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write(png)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
|
|
svg := `<?xml version="1.0"?>` +
|
|
`<!DOCTYPE svg [<!ENTITY outside SYSTEM "` + server.URL + `/entity">]>` +
|
|
`<svg xmlns="http://www.w3.org/2000/svg" ` +
|
|
`xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="12">` +
|
|
`<image href="` + server.URL + `/image.png" width="16" height="12"/>` +
|
|
`<image xlink:href="` + server.URL + `/xlink.png" width="16" height="12"/>` +
|
|
`<text y="10">&outside;</text></svg>`
|
|
|
|
_, _, err := processSVG(t, svg, imagerequest.Transform{
|
|
Format: imagerequest.FormatPNG,
|
|
})
|
|
t.Logf("SVG as PNG: error = %v", err)
|
|
|
|
if requests.Load() != 0 {
|
|
t.Errorf("rendering the SVG made %d requests, want none", requests.Load())
|
|
}
|
|
}
|
|
|
|
// TestImageProcessor_RefusesSVGTooLarge checks that an SVG wider than
|
|
// MaxInputDimension is ErrInputTooLarge, as a raster source is.
|
|
func TestImageProcessor_RefusesSVGTooLarge(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, _, err := processSVG(t,
|
|
`<svg xmlns="http://www.w3.org/2000/svg" width="10000" height="10"/>`,
|
|
imagerequest.Transform{
|
|
Size: imagerequest.Size{Width: 100, Height: 100},
|
|
Format: imagerequest.FormatPNG,
|
|
})
|
|
if !errors.Is(err, ErrInputTooLarge) {
|
|
t.Errorf("SVG of 10000x10: error = %v, want %v", err, ErrInputTooLarge)
|
|
}
|
|
}
|
|
|
|
// TestSVGDensity checks the density at which an SVG is rendered to cover
|
|
// an output size, and that it is held to MaxInputDimension.
|
|
func TestSVGDensity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
width, height int
|
|
outputWidth, outputHeight int
|
|
want int
|
|
}{
|
|
{"own size", 16, 12, 16, 12, svgDefaultDensity},
|
|
{"100 times larger", 16, 12, 1600, 1200, 7200},
|
|
{"wider than the SVG's proportions", 16, 12, 160, 12, 720},
|
|
{"10 times smaller", 100, 100, 10, 10, 8},
|
|
{"held to MaxInputDimension", 8192, 1, 8192, 8192, svgDefaultDensity},
|
|
{"held to MaxInputDimension when enlarged", 1024, 1, 8192, 8192, 576},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := svgDensity(tt.width, tt.height, tt.outputWidth, tt.outputHeight)
|
|
if got != tt.want {
|
|
t.Errorf("%s: svgDensity(%d, %d, %d, %d) = %d, want %d", tt.name,
|
|
tt.width, tt.height, tt.outputWidth, tt.outputHeight, got, tt.want)
|
|
}
|
|
}
|
|
}
|