diff --git a/go.mod b/go.mod index 66b39d6b..4b17d3a5 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.23.0 require ( github.com/corona10/goimagehash v1.1.0 - github.com/disintegration/imaging v1.6.2 github.com/flopp/go-findfont v0.1.0 github.com/fogleman/gg v1.3.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 diff --git a/go.sum b/go.sum index ba7e77b0..076f32ae 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI= github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI= -github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= -github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/goccy/go-findfont v0.0.0-20250109093214-c2e12b298c75 h1:XIuIPArJ/7VuKj7uygrjl7yBYP+sTa58ljJzOxU4qDc= @@ -12,9 +10,7 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8= github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU= -golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= diff --git a/gvc/image_renderer.go b/gvc/image_renderer.go index 2a88f771..51e5b843 100644 --- a/gvc/image_renderer.go +++ b/gvc/image_renderer.go @@ -11,11 +11,11 @@ import ( "strings" "sync" - "github.com/disintegration/imaging" "github.com/flopp/go-findfont" "github.com/fogleman/gg" "github.com/goccy/go-graphviz/internal/wasm" "github.com/golang/freetype/truetype" + "golang.org/x/image/draw" "golang.org/x/image/font" "golang.org/x/image/font/gofont/goregular" "golang.org/x/image/font/opentype" @@ -395,7 +395,7 @@ func (r *ImageRenderer) LoadImage(ctx context.Context, job *Job, shape *UserShap bottomRightY := bf.UR().Y() width := bottomRightX - topLeftX height := bottomRightY - topLeftY - img = imaging.Resize(img, int(width), int(height), imaging.Lanczos) + img = resizeImage(img, int(width), int(height)) xPAD := defaultXPAD / 2.0 yPAD := defaultYPAD / 2.0 posX := (topLeftX + xPAD) * job.Scale().X() @@ -410,6 +410,26 @@ func (r *ImageRenderer) LoadImage(ctx context.Context, job *Job, shape *UserShap return nil } +// resizeImage scales img to width x height with a Catmull-Rom kernel. A +// zero width or height is derived from the other one so the aspect ratio +// is preserved; when both are zero the image is returned as is. +func resizeImage(img image.Image, width, height int) image.Image { + srcBounds := img.Bounds() + srcW, srcH := srcBounds.Dx(), srcBounds.Dy() + if srcW == 0 || srcH == 0 || (width <= 0 && height <= 0) { + return img + } + if width <= 0 { + width = max(1, int(float64(height)*float64(srcW)/float64(srcH)+0.5)) + } + if height <= 0 { + height = max(1, int(float64(width)*float64(srcH)/float64(srcW)+0.5)) + } + dst := image.NewNRGBA(image.Rect(0, 0, width, height)) + draw.CatmullRom.Scale(dst, dst.Bounds(), img, srcBounds, draw.Src, nil) + return dst +} + type FontLoader func(ctx context.Context, job *Job, font *TextFont) (font.Face, error) var ( diff --git a/gvc/image_renderer_test.go b/gvc/image_renderer_test.go new file mode 100644 index 00000000..8a0b0d5e --- /dev/null +++ b/gvc/image_renderer_test.go @@ -0,0 +1,44 @@ +package gvc + +import ( + "image" + "image/color" + "testing" +) + +func TestResizeImage(t *testing.T) { + src := image.NewNRGBA(image.Rect(0, 0, 40, 20)) + for y := 0; y < 20; y++ { + for x := 0; x < 40; x++ { + src.Set(x, y, color.NRGBA{R: 200, G: 30, B: 30, A: 255}) + } + } + + tests := []struct { + name string + width int + height int + wantW, wantH int + }{ + {name: "both dimensions", width: 10, height: 10, wantW: 10, wantH: 10}, + {name: "width only keeps the aspect ratio", width: 20, height: 0, wantW: 20, wantH: 10}, + {name: "height only keeps the aspect ratio", width: 0, height: 5, wantW: 10, wantH: 5}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resizeImage(src, tt.width, tt.height) + b := got.Bounds() + if b.Dx() != tt.wantW || b.Dy() != tt.wantH { + t.Fatalf("size = %dx%d, want %dx%d", b.Dx(), b.Dy(), tt.wantW, tt.wantH) + } + c := color.NRGBAModel.Convert(got.At(b.Min.X+b.Dx()/2, b.Min.Y+b.Dy()/2)).(color.NRGBA) + if c.R < 190 || c.A != 255 { + t.Fatalf("center pixel = %+v, want the source colour", c) + } + }) + } + + if got := resizeImage(src, 0, 0); got != image.Image(src) { + t.Fatal("a zero size must return the image unchanged") + } +}