Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified container/testdata/doctabs/mobile/theme_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/color/dialog_expanded_theme_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/color/dialog_expanded_theme_ugly.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/color/dialog_recents_theme_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/color/dialog_recents_theme_ugly.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/color/dialog_theme_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/color/dialog_theme_ugly.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/dialog-confirm-blur.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/dialog-confirm-importance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/dialog-custom-confirm-importance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/form/hint_initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/form/hint_invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified dialog/testdata/form/hint_valid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified driver/software/testdata/button_important.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions internal/svg/svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"image"
"image/color"
"image/draw"
"io"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -80,7 +81,11 @@ func (d *Decoder) Draw(width, height int) (*image.NRGBA, error) {
x, y := svgOffset(d.icon, imgW, imgH)
d.icon.SetTarget(x, y, float64(imgW), float64(imgH))

img := image.NewNRGBA(image.Rect(0, 0, imgW, imgH))
// Rasterize to RGBA, then copy to NRGBA.
// We do this because golang.org/x/image has a fast path for RGBA dst image,
// and the fast path is ~69% faster, even with the overhead of copying the RGBA to
// NRGBA for returning.
img := image.NewRGBA(image.Rect(0, 0, imgW, imgH))
scanner := rasterx.NewScannerGV(config.Width, config.Height, img, img.Bounds())
raster := rasterx.NewDasher(width, height, scanner)

Expand All @@ -89,7 +94,10 @@ func (d *Decoder) Draw(width, height int) (*image.NRGBA, error) {
err = fmt.Errorf("SVG render error: %w", err)
return nil, err
}
return img, nil
bounds := img.Bounds()
out := image.NewNRGBA(bounds)
draw.Draw(out, bounds, img, bounds.Min, draw.Src)
return out, nil
}

func IsFileSVG(path string) bool {
Expand Down
260 changes: 139 additions & 121 deletions internal/svg/svg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,128 +18,129 @@ import (
"fyne.io/fyne/v2/internal/test"
)

var tests = map[string]struct {
svgFile string
color color.Color
wantImage string
}{
"paths": {
svgFile: "cancel_Paths.svg",
color: color.NRGBA{R: 100, G: 100, A: 200},
wantImage: "colorized/paths.png",
},
"circles": {
svgFile: "circles.svg",
color: color.NRGBA{R: 100, B: 100, A: 200},
wantImage: "colorized/circles.png",
},
"polygons": {
svgFile: "polygons.svg",
color: color.NRGBA{G: 100, B: 100, A: 200},
wantImage: "colorized/polygons.png",
},
"rects": {
svgFile: "rects.svg",
color: color.NRGBA{R: 100, G: 100, B: 100, A: 200},
wantImage: "colorized/rects.png",
},
"negative rects": {
svgFile: "rects-negative.svg",
color: color.NRGBA{R: 100, G: 100, B: 100, A: 200},
wantImage: "colorized/rects.png",
},
"group of paths": {
svgFile: "check_GroupPaths.svg",
color: color.NRGBA{R: 100, G: 100, A: 100},
wantImage: "colorized/group_paths.png",
},
"group of circles": {
svgFile: "group_circles.svg",
color: color.NRGBA{R: 100, B: 100, A: 100},
wantImage: "colorized/group_circles.png",
},
"group of polygons": {
svgFile: "warning_GroupPolygons.svg",
color: color.NRGBA{G: 100, B: 100, A: 100},
wantImage: "colorized/group_polygons.png",
},
"group of rects": {
svgFile: "info_GroupRects.svg",
color: color.NRGBA{R: 100, G: 100, B: 100, A: 100},
wantImage: "colorized/group_rects.png",
},
"NRGBA64": {
svgFile: "circles.svg",
// If the low 8 bits of each component were used, this would look cyan instead of yellow.
// When the MSB is used instead, it correctly looks yellow.
color: color.NRGBA64{R: 0xff00, G: 0xffff, B: 0x00ff, A: 0xffff},
wantImage: "colorized/circles_yellow.png",
},
"translucent NRGBA64": {
svgFile: "circles.svg",
color: color.NRGBA64{R: 0xff00, G: 0xffff, B: 0x00ff, A: 0x7fff},
wantImage: "colorized/circles_yellow_translucent.png",
},
"RGBA": {
svgFile: "circles.svg",
color: color.RGBAModel.Convert(color.NRGBA{R: 0xff, G: 0xff, B: 0x00, A: 0xff}),
wantImage: "colorized/circles_yellow.png",
},
"transluscent RGBA": {
svgFile: "circles.svg",
color: color.RGBAModel.Convert(color.NRGBA{R: 0xff, G: 0xff, B: 0x00, A: 0x7f}),
wantImage: "colorized/circles_yellow_translucent.png",
},
"RGBA64": {
svgFile: "circles.svg",
// If the least significant byte of each component was being used, this would look cyan instead of yellow.
// Since alpha=0xffff, unmultiplyAlpha knows it does not need to unmultiply anything, and so it just
// returns the MSB of each component.
color: color.RGBA64Model.Convert(color.NRGBA64{R: 0xff00, G: 0xffff, B: 0x00ff, A: 0xffff}),
wantImage: "colorized/circles_yellow.png",
},
"transluscent RGBA64": {
svgFile: "circles.svg",
// Since alpha!=0xffff, if we were to use R:0xff00, G:0xffff, B:0x00ff like before,
// this would end up being drawn with 0xfeff00 instead of 0xffff00, and we would need a separate image to test for that.
// Instead, we use R:0xfff0, G:0xfff0, B:0x000f, A:0x7fff instead, which unmultiplyAlpha returns as 0xff, 0xff, 0x00, 0x7f,
// so that we correctly get 0xffff00 with alpha 0x7f when ToRGBA is used.
// The RGBA64's contents are 0x7ff7, 0x7ff7, 0x0007, 0x7fff, so:
// If ToRGBA wasn't being called and instead the LSB of each component was being read, this would show up as 0xf7f707 with alpha 0xff.
// If the MSB was being read without umultiplication, this would show up as 0x7f7f00 with alpha 0x7f.
color: color.RGBA64Model.Convert(color.NRGBA64{R: 0xfff0, G: 0xfff0, B: 0x000f, A: 0x7fff}),
wantImage: "colorized/circles_yellow_translucent.png",
},
"Alpha": {
svgFile: "circles.svg",
color: color.Alpha{A: 0x7f},
wantImage: "colorized/circles_white_translucent.png",
},
"Alpha16": {
svgFile: "circles.svg",
// If the LSB from components returned by RGBA() was being used, this would be black.
// If the MSB from components returned by RGBA() was being used, this would be grey.
// It is white when either we bypass RGBA() and directly make a 0xffffff color with the alpha's MSB (which is what ToRGBA does),
// or if we call RBGA(), un-multiply the alpha from the non-alpha components, and use their MSB to get white (Or something very near it like 0xfefefe).
color: color.Alpha16{A: 0x7f00},
wantImage: "colorized/circles_white_translucent.png",
},
"Gray": {
svgFile: "circles.svg",
color: color.Gray{Y: 0xff},
wantImage: "colorized/circles_white.png",
},
"Gray16": {
svgFile: "circles.svg",
// If the LSB from components returned by RGBA() was being used, this would be black.
// It is white when either we bypass RGBA() and directly make a 0xffffff color with the alpha's MSB (which is what ToRGBA does),
// or if we call RBGA(), un-multiply the alpha from the non-alpha components, and use their MSB to get white (Or something very near it like 0xfefefe),
// or if the MSB from components returned by RGBA() was being used (because Gray and Gray16 do not have alpha values).
color: color.Gray16{Y: 0xff00},
wantImage: "colorized/circles_white.png",
},
}

func TestColorize(t *testing.T) {
tests := map[string]struct {
svgFile string
color color.Color
wantImage string
}{
"paths": {
svgFile: "cancel_Paths.svg",
color: color.NRGBA{R: 100, G: 100, A: 200},
wantImage: "colorized/paths.png",
},
"circles": {
svgFile: "circles.svg",
color: color.NRGBA{R: 100, B: 100, A: 200},
wantImage: "colorized/circles.png",
},
"polygons": {
svgFile: "polygons.svg",
color: color.NRGBA{G: 100, B: 100, A: 200},
wantImage: "colorized/polygons.png",
},
"rects": {
svgFile: "rects.svg",
color: color.NRGBA{R: 100, G: 100, B: 100, A: 200},
wantImage: "colorized/rects.png",
},
"negative rects": {
svgFile: "rects-negative.svg",
color: color.NRGBA{R: 100, G: 100, B: 100, A: 200},
wantImage: "colorized/rects.png",
},
"group of paths": {
svgFile: "check_GroupPaths.svg",
color: color.NRGBA{R: 100, G: 100, A: 100},
wantImage: "colorized/group_paths.png",
},
"group of circles": {
svgFile: "group_circles.svg",
color: color.NRGBA{R: 100, B: 100, A: 100},
wantImage: "colorized/group_circles.png",
},
"group of polygons": {
svgFile: "warning_GroupPolygons.svg",
color: color.NRGBA{G: 100, B: 100, A: 100},
wantImage: "colorized/group_polygons.png",
},
"group of rects": {
svgFile: "info_GroupRects.svg",
color: color.NRGBA{R: 100, G: 100, B: 100, A: 100},
wantImage: "colorized/group_rects.png",
},
"NRGBA64": {
svgFile: "circles.svg",
// If the low 8 bits of each component were used, this would look cyan instead of yellow.
// When the MSB is used instead, it correctly looks yellow.
color: color.NRGBA64{R: 0xff00, G: 0xffff, B: 0x00ff, A: 0xffff},
wantImage: "colorized/circles_yellow.png",
},
"translucent NRGBA64": {
svgFile: "circles.svg",
color: color.NRGBA64{R: 0xff00, G: 0xffff, B: 0x00ff, A: 0x7fff},
wantImage: "colorized/circles_yellow_translucent.png",
},
"RGBA": {
svgFile: "circles.svg",
color: color.RGBAModel.Convert(color.NRGBA{R: 0xff, G: 0xff, B: 0x00, A: 0xff}),
wantImage: "colorized/circles_yellow.png",
},
"transluscent RGBA": {
svgFile: "circles.svg",
color: color.RGBAModel.Convert(color.NRGBA{R: 0xff, G: 0xff, B: 0x00, A: 0x7f}),
wantImage: "colorized/circles_yellow_translucent.png",
},
"RGBA64": {
svgFile: "circles.svg",
// If the least significant byte of each component was being used, this would look cyan instead of yellow.
// Since alpha=0xffff, unmultiplyAlpha knows it does not need to unmultiply anything, and so it just
// returns the MSB of each component.
color: color.RGBA64Model.Convert(color.NRGBA64{R: 0xff00, G: 0xffff, B: 0x00ff, A: 0xffff}),
wantImage: "colorized/circles_yellow.png",
},
"transluscent RGBA64": {
svgFile: "circles.svg",
// Since alpha!=0xffff, if we were to use R:0xff00, G:0xffff, B:0x00ff like before,
// this would end up being drawn with 0xfeff00 instead of 0xffff00, and we would need a separate image to test for that.
// Instead, we use R:0xfff0, G:0xfff0, B:0x000f, A:0x7fff instead, which unmultiplyAlpha returns as 0xff, 0xff, 0x00, 0x7f,
// so that we correctly get 0xffff00 with alpha 0x7f when ToRGBA is used.
// The RGBA64's contents are 0x7ff7, 0x7ff7, 0x0007, 0x7fff, so:
// If ToRGBA wasn't being called and instead the LSB of each component was being read, this would show up as 0xf7f707 with alpha 0xff.
// If the MSB was being read without umultiplication, this would show up as 0x7f7f00 with alpha 0x7f.
color: color.RGBA64Model.Convert(color.NRGBA64{R: 0xfff0, G: 0xfff0, B: 0x000f, A: 0x7fff}),
wantImage: "colorized/circles_yellow_translucent.png",
},
"Alpha": {
svgFile: "circles.svg",
color: color.Alpha{A: 0x7f},
wantImage: "colorized/circles_white_translucent.png",
},
"Alpha16": {
svgFile: "circles.svg",
// If the LSB from components returned by RGBA() was being used, this would be black.
// If the MSB from components returned by RGBA() was being used, this would be grey.
// It is white when either we bypass RGBA() and directly make a 0xffffff color with the alpha's MSB (which is what ToRGBA does),
// or if we call RBGA(), un-multiply the alpha from the non-alpha components, and use their MSB to get white (Or something very near it like 0xfefefe).
color: color.Alpha16{A: 0x7f00},
wantImage: "colorized/circles_white_translucent.png",
},
"Gray": {
svgFile: "circles.svg",
color: color.Gray{Y: 0xff},
wantImage: "colorized/circles_white.png",
},
"Gray16": {
svgFile: "circles.svg",
// If the LSB from components returned by RGBA() was being used, this would be black.
// It is white when either we bypass RGBA() and directly make a 0xffffff color with the alpha's MSB (which is what ToRGBA does),
// or if we call RBGA(), un-multiply the alpha from the non-alpha components, and use their MSB to get white (Or something very near it like 0xfefefe),
// or if the MSB from components returned by RGBA() was being used (because Gray and Gray16 do not have alpha values).
color: color.Gray16{Y: 0xff00},
wantImage: "colorized/circles_white.png",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
bytes, err := os.ReadFile(filepath.Join("testdata", tt.svgFile))
Expand Down Expand Up @@ -206,6 +207,23 @@ func TestSVG_ReplaceFillColor_Ellipse(t *testing.T) {
assert.True(t, strings.Contains(string(res), "#ff0000"))
}

func BenchmarkSvg(b *testing.B) {
for name, tt := range tests {
data, err := os.ReadFile(filepath.Join("testdata", tt.svgFile))
require.NoError(b, err)
content, _ := Colorize(data, tt.color)
icon, err := NewDecoder(bytes.NewReader(content))
require.NoError(b, err, "failed to read SVG data")

b.Run(name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := icon.Draw(500, 500)
require.NoError(b, err)
}
})
}
}

func helperDrawSVG(t *testing.T, data []byte) image.Image {
icon, err := oksvg.ReadIconStream(bytes.NewReader(data))
require.NoError(t, err, "failed to read SVG data")
Expand Down
Binary file modified widget/testdata/button/disabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/button/high_importance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/button/high_importance_hovered.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/button/low_importance_disabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/button/success_importance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/entry/validation_set_invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/form/disable_disabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/form/disable_initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/form/disable_re_enabled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/form/disable_validation_disabled_invalid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/form/disable_validation_disabled_valid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified widget/testdata/form/disable_validation_enabled_invalid.png
Binary file modified widget/testdata/form/disable_validation_enabled_valid.png
Binary file modified widget/testdata/form/disable_validation_initial.png
Binary file modified widget/testdata/form/hint_initial.png
Binary file modified widget/testdata/form/hint_invalid.png
Binary file modified widget/testdata/form/hint_valid.png
Binary file modified widget/testdata/form/theme_changed.png
Binary file modified widget/testdata/form/theme_initial.png
Binary file modified widget/testdata/form/validation_entry_first_type_initial.png
Binary file modified widget/testdata/form/validation_entry_first_type_invalid.png
Binary file modified widget/testdata/form/validation_entry_first_type_valid.png
Binary file modified widget/testdata/form/validation_initial.png
Binary file modified widget/testdata/form/validation_invalid.png
Binary file modified widget/testdata/form/validation_valid.png
Loading