The fetcher accepts image/svg+xml; internal/magic detects SVG. orig of an SVG at the orig size is the SVG unchanged, cached, with a Content-Security-Policy allowing only its styles and data: images; at another size, ErrSVGNotOriginalSize (400). Any other format renders it at the least size covering the output, at most 8192 on a side. Rendering refuses an SVG with a side over 8192 times the other (ErrSVGTooThin, 415), and the orig size of one above 8192 on a side (ErrSVGTooLargeForOrigSize, 400). Child processes of pixad read an SVG's size and render it, killed after 20 seconds and limited to 1 GiB; any failure there is ErrSVGRenderFailed (415). Tests pinning the SVG refusal now expect SVG. Model: opus-5-5
This commit was merged in pull request #351.
This commit is contained in:
1 parent
20874ba2af
commit
c2e32d987b
22 files changed
+1525
-131
No files matched your search
@@ -0,0 +1,503 @@
|
||||
package imageprocessor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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 draws no
|
||||
// image it refers to: one at a URL on a local server, by href or xlink:href,
|
||||
// or a local file that exists. Each SVG renders without error, white where
|
||||
// the image would be. An SVG that uses an external entity at that server is
|
||||
// a separate case. No SVG makes a request to the server.
|
||||
func TestImageProcessor_SVGFetchesNothing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var requests atomic.Int64
|
||||
|
||||
// Blue is 128 in every pixel of this PNG.
|
||||
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)
|
||||
|
||||
pngPath := filepath.Join(t.TempDir(), "image.png")
|
||||
|
||||
err := os.WriteFile(pngPath, png, 0o600)
|
||||
if err != nil {
|
||||
t.Fatalf("writing the PNG file: %v", err)
|
||||
}
|
||||
|
||||
for _, reference := range []string{
|
||||
`href="` + server.URL + `/image.png"`,
|
||||
`xlink:href="` + server.URL + `/xlink.png"`,
|
||||
`href="file://` + pngPath + `"`,
|
||||
} {
|
||||
svg := `<svg xmlns="http://www.w3.org/2000/svg" ` +
|
||||
`xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="12">` +
|
||||
`<rect width="16" height="12" fill="white"/>` +
|
||||
`<image ` + reference + ` width="16" height="12"/></svg>`
|
||||
|
||||
_, output, err := processSVG(t, svg, imagerequest.Transform{
|
||||
Format: imagerequest.FormatPNG,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("SVG with image %s as PNG: %v", reference, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
img, err := vips.NewImageFromBuffer(output)
|
||||
if err != nil {
|
||||
t.Fatalf("decoding the PNG: %v", err)
|
||||
}
|
||||
|
||||
point, err := img.GetPoint(8, 6)
|
||||
img.Close()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("GetPoint(8, 6): %v", err)
|
||||
}
|
||||
|
||||
t.Logf("SVG with image %s: pixel at 8,6 = %v", reference, point)
|
||||
|
||||
if point[2] != 255 {
|
||||
t.Errorf("SVG with image %s: blue at 8,6 = %v, want 255: the "+
|
||||
"image is drawn", reference, point[2])
|
||||
}
|
||||
}
|
||||
|
||||
svg := `<?xml version="1.0"?>` +
|
||||
`<!DOCTYPE svg [<!ENTITY outside SYSTEM "` + server.URL + `/entity">]>` +
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="12">` +
|
||||
`<text y="10">&outside;</text></svg>`
|
||||
|
||||
// librsvg loads no external entity, so it refuses this SVG: outside is
|
||||
// not defined.
|
||||
_, _, err = processSVG(t, svg, imagerequest.Transform{
|
||||
Format: imagerequest.FormatPNG,
|
||||
})
|
||||
t.Logf("SVG with an external entity as PNG: error = %v", err)
|
||||
|
||||
if requests.Load() != 0 {
|
||||
t.Errorf("rendering the SVGs made %d requests, want none",
|
||||
requests.Load())
|
||||
}
|
||||
}
|
||||
|
||||
// TestImageProcessor_RendersSVGLargerThanMaxInputDimension checks that an
|
||||
// SVG whose own size is above MaxInputDimension is limited only by the
|
||||
// output checks: it is rendered at a raster size, orig of it is the SVG
|
||||
// itself, and a raster format at its own size is ErrSVGTooLargeForOrigSize.
|
||||
// The 1000000x1000000 one is rendered at a ten-thousandth of its own size.
|
||||
func TestImageProcessor_RendersSVGLargerThanMaxInputDimension(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, svg := range []string{
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="10000" height="10">` +
|
||||
`<rect width="10000" height="10"/></svg>`,
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="1000000" ` +
|
||||
`height="1000000"><rect width="1000000" height="1000000"/></svg>`,
|
||||
} {
|
||||
result, _, err := processSVG(t, svg, imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 100, Height: 100},
|
||||
Format: imagerequest.FormatPNG,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("%s as PNG at 100x100: %v", svg, err)
|
||||
} else {
|
||||
t.Logf("%s as PNG at 100x100: %dx%d", svg, result.Width, result.Height)
|
||||
|
||||
if result.Width != 100 || result.Height != 100 {
|
||||
t.Errorf("%s as PNG at 100x100 = %dx%d, want 100x100", svg,
|
||||
result.Width, result.Height)
|
||||
}
|
||||
}
|
||||
|
||||
result, output, err := processSVG(t, svg,
|
||||
imagerequest.Transform{Format: imagerequest.FormatOriginal})
|
||||
if err != nil || result.ContentType != imagerequest.FormatSVG.MIMEType() ||
|
||||
string(output) != svg {
|
||||
t.Errorf("orig of %s: error = %v, want the SVG itself", svg, err)
|
||||
}
|
||||
|
||||
_, _, err = processSVG(t, svg,
|
||||
imagerequest.Transform{Format: imagerequest.FormatPNG})
|
||||
if !errors.Is(err, ErrSVGTooLargeForOrigSize) {
|
||||
t.Errorf("%s as PNG at its own size: error = %v, want %v", svg,
|
||||
err, ErrSVGTooLargeForOrigSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestImageProcessor_RefusesSVGTooThin checks that an SVG with one side more
|
||||
// than MaxInputDimension times the other is ErrSVGTooThin at a raster size,
|
||||
// that one at exactly that proportion is rendered, and that orig of either
|
||||
// is the SVG itself.
|
||||
func TestImageProcessor_RefusesSVGTooThin(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tooThin := `<svg xmlns="http://www.w3.org/2000/svg" width="81930" ` +
|
||||
`height="10"><rect width="81930" height="10"/></svg>`
|
||||
atTheLimit := `<svg xmlns="http://www.w3.org/2000/svg" width="81920" ` +
|
||||
`height="10"><rect width="81920" height="10"/></svg>`
|
||||
size := imagerequest.Size{Width: 100, Height: 100}
|
||||
|
||||
_, _, err := processSVG(t, tooThin,
|
||||
imagerequest.Transform{Size: size, Format: imagerequest.FormatPNG})
|
||||
if !errors.Is(err, ErrSVGTooThin) {
|
||||
t.Errorf("81930x10 SVG as PNG at 100x100: error = %v, want %v", err,
|
||||
ErrSVGTooThin)
|
||||
}
|
||||
|
||||
result, _, err := processSVG(t, atTheLimit,
|
||||
imagerequest.Transform{Size: size, Format: imagerequest.FormatPNG})
|
||||
if err != nil {
|
||||
t.Errorf("81920x10 SVG as PNG at 100x100: %v", err)
|
||||
} else if result.Width != 100 || result.Height != 100 {
|
||||
t.Errorf("81920x10 SVG as PNG at 100x100 = %dx%d, want 100x100",
|
||||
result.Width, result.Height)
|
||||
}
|
||||
|
||||
for _, svg := range []string{tooThin, atTheLimit} {
|
||||
result, output, err := processSVG(t, svg,
|
||||
imagerequest.Transform{Format: imagerequest.FormatOriginal})
|
||||
if err != nil || result.ContentType != imagerequest.FormatSVG.MIMEType() ||
|
||||
string(output) != svg {
|
||||
t.Errorf("orig of %s: error = %v, want the SVG itself", svg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// testSVGRenderTimeout is the time limit of the SVG time limit tests, short
|
||||
// so that they finish in a few seconds. Under the race detector a child
|
||||
// process takes about a second to start and read an SVG's size.
|
||||
const testSVGRenderTimeout = 3 * time.Second
|
||||
|
||||
// processSVGTimed has processor process svg, as an SVG source, with transform,
|
||||
// and returns the error and how long it took.
|
||||
func processSVGTimed(
|
||||
t *testing.T, processor *ImageProcessor, svg string,
|
||||
transform imagerequest.Transform,
|
||||
) (time.Duration, error) {
|
||||
t.Helper()
|
||||
|
||||
start := time.Now()
|
||||
_, err := processor.Process(t.Context(), strings.NewReader(svg),
|
||||
imagerequest.FormatSVG, &transform)
|
||||
|
||||
return time.Since(start), err
|
||||
}
|
||||
|
||||
// checkProcessesAnotherImage checks that processor, after refusing an SVG,
|
||||
// processes a PNG.
|
||||
func checkProcessesAnotherImage(t *testing.T, processor *ImageProcessor) {
|
||||
t.Helper()
|
||||
|
||||
_, err := processor.Process(t.Context(),
|
||||
bytes.NewReader(createTestPNG(t, 64, 48)), imagerequest.FormatPNG,
|
||||
&imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 32, Height: 24},
|
||||
Format: imagerequest.FormatJPEG,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("a PNG after the SVG: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestImageProcessor_StopsSlowSVGRender checks that an SVG that takes about
|
||||
// 12 seconds to render, 600 shapes each drawn with an feTurbulence filter
|
||||
// effect, is ErrSVGRenderFailed soon after a time limit of
|
||||
// testSVGRenderTimeout, not when the render would end. The processor then
|
||||
// processes another image.
|
||||
func TestImageProcessor_StopsSlowSVGRender(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svg := `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">` +
|
||||
`<filter id="noise">` +
|
||||
`<feTurbulence baseFrequency="0.05" numOctaves="4"/></filter>` +
|
||||
strings.Repeat(`<rect width="256" height="256" filter="url(#noise)"/>`,
|
||||
600) +
|
||||
`</svg>`
|
||||
|
||||
processor := newSVGTestProcessor()
|
||||
processor.svgRenderTimeout = testSVGRenderTimeout
|
||||
|
||||
elapsed, err := processSVGTimed(t, processor, svg, imagerequest.Transform{
|
||||
Size: imagerequest.Size{Width: 256, Height: 256},
|
||||
Format: imagerequest.FormatPNG,
|
||||
})
|
||||
t.Logf("slow SVG as PNG: error %v after %v", err, elapsed)
|
||||
|
||||
if !errors.Is(err, ErrSVGRenderFailed) {
|
||||
t.Errorf("slow SVG as PNG: error = %v, want %v", err,
|
||||
ErrSVGRenderFailed)
|
||||
}
|
||||
|
||||
if elapsed > testSVGRenderTimeout+2*time.Second {
|
||||
t.Errorf("slow SVG as PNG: refused after %v, want soon after %v",
|
||||
elapsed, testSVGRenderTimeout)
|
||||
}
|
||||
|
||||
checkProcessesAnotherImage(t, processor)
|
||||
}
|
||||
|
||||
// TestImageProcessor_StopsSlowSVGSizeRead checks that orig of an SVG whose
|
||||
// size takes about 10 seconds to read is ErrSVGRenderFailed soon after a time
|
||||
// limit of testSVGRenderTimeout. To read the size, librsvg matches each of the
|
||||
// SVG's 2500 CSS rules against each of its 2500 shapes, walking up the 75
|
||||
// groups around it. The processor then processes another image.
|
||||
func TestImageProcessor_StopsSlowSVGSizeRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var rules strings.Builder
|
||||
|
||||
for i := range 2500 {
|
||||
fmt.Fprintf(&rules, ".group%d rect { fill: red }", i)
|
||||
}
|
||||
|
||||
svg := `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="12">` +
|
||||
`<style>` + rules.String() + `</style>` + strings.Repeat(`<g>`, 75) +
|
||||
strings.Repeat(`<rect width="1" height="1"/>`, 2500) +
|
||||
strings.Repeat(`</g>`, 75) + `</svg>`
|
||||
|
||||
processor := newSVGTestProcessor()
|
||||
processor.svgRenderTimeout = testSVGRenderTimeout
|
||||
|
||||
elapsed, err := processSVGTimed(t, processor, svg,
|
||||
imagerequest.Transform{Format: imagerequest.FormatOriginal})
|
||||
t.Logf("orig of the SVG: error %v after %v", err, elapsed)
|
||||
|
||||
if !errors.Is(err, ErrSVGRenderFailed) {
|
||||
t.Errorf("orig of the SVG: error = %v, want %v", err,
|
||||
ErrSVGRenderFailed)
|
||||
}
|
||||
|
||||
if elapsed > testSVGRenderTimeout+2*time.Second {
|
||||
t.Errorf("orig of the SVG: refused after %v, want soon after %v",
|
||||
elapsed, testSVGRenderTimeout)
|
||||
}
|
||||
|
||||
checkProcessesAnotherImage(t, processor)
|
||||
}
|
||||
|
||||
// TestImageProcessor_StopsSVGRenderOverMemoryLimit checks that an SVG that
|
||||
// needs about 500 MiB more than a child process's own memory to render, a
|
||||
// chain of 2000 blurs that each keep their result, is ErrSVGRenderFailed with
|
||||
// a memory limit of 512 MiB, and that a plain SVG renders under the same
|
||||
// limit. A child process needs about 130 MiB of its own, and about 350 MiB
|
||||
// under the race detector, so the chain also renders under MaxSVGRenderMemory.
|
||||
func TestImageProcessor_StopsSVGRenderOverMemoryLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var blurs strings.Builder
|
||||
|
||||
blurs.WriteString(`<feGaussianBlur stdDeviation="3" result="blur0"/>`)
|
||||
|
||||
for i := 1; i < 2000; i++ {
|
||||
fmt.Fprintf(&blurs,
|
||||
`<feGaussianBlur in="blur%d" stdDeviation="3" result="blur%d"/>`,
|
||||
i-1, i)
|
||||
}
|
||||
|
||||
svg := `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">` +
|
||||
`<filter id="blurs" color-interpolation-filters="sRGB">` +
|
||||
blurs.String() + `</filter>` +
|
||||
`<rect width="256" height="256" fill="red" filter="url(#blurs)"/></svg>`
|
||||
size := imagerequest.Size{Width: 256, Height: 256}
|
||||
|
||||
processor := newSVGTestProcessor()
|
||||
processor.maxSVGRenderMemory = 512 << 20
|
||||
|
||||
elapsed, err := processSVGTimed(t, processor, svg,
|
||||
imagerequest.Transform{Size: size, Format: imagerequest.FormatPNG})
|
||||
t.Logf("SVG of 2000 blurs as PNG: error %v after %v", err, elapsed)
|
||||
|
||||
if !errors.Is(err, ErrSVGRenderFailed) {
|
||||
t.Errorf("SVG of 2000 blurs as PNG: error = %v, want %v", err,
|
||||
ErrSVGRenderFailed)
|
||||
}
|
||||
|
||||
_, err = processSVGTimed(t, processor, testSVG,
|
||||
imagerequest.Transform{Size: size, Format: imagerequest.FormatPNG})
|
||||
if err != nil {
|
||||
t.Errorf("plain SVG as PNG after it: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSVGRenderSize checks the size at which an SVG is rendered to cover an
|
||||
// output size, and that it is held to MaxInputDimension.
|
||||
func TestSVGRenderSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
width, height int
|
||||
outputWidth, outputHeight int
|
||||
wantWidth, wantHeight int
|
||||
}{
|
||||
{"own size", 16, 12, 16, 12, 16, 12},
|
||||
{"100 times larger", 16, 12, 1600, 1200, 1600, 1200},
|
||||
{"wider than the SVG's proportions", 16, 12, 160, 12, 160, 120},
|
||||
{"10 times smaller", 100, 100, 10, 10, 10, 10},
|
||||
{"held to MaxInputDimension", 8192, 1, 8192, 8192, 8192, 1},
|
||||
{"held to MaxInputDimension when enlarged", 1024, 1, 8192, 8192, 8192, 8},
|
||||
{"far above MaxInputDimension", 1000000, 1000000, 100, 100, 100, 100},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
gotWidth, gotHeight := svgRenderSize(tt.width, tt.height,
|
||||
tt.outputWidth, tt.outputHeight)
|
||||
if gotWidth != tt.wantWidth || gotHeight != tt.wantHeight {
|
||||
t.Errorf("%s: svgRenderSize(%d, %d, %d, %d) = %dx%d, want %dx%d",
|
||||
tt.name, tt.width, tt.height, tt.outputWidth, tt.outputHeight,
|
||||
gotWidth, gotHeight, tt.wantWidth, tt.wantHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user