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 = `` + `` + `` // 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 := `` + `]>` + `` + `` + `` + `&outside;` _, _, 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, ``, 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) } } }