package painter import ( "bytes" "fmt" "image" _ "image/jpeg" // avoid users having to import when using image widget _ "image/png" // avoid the same for PNG images "io" "os" "path/filepath" "strings" "golang.org/x/image/draw" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/internal" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/svg" ) var aspects = make(map[interface{}]float32, 16) // GetAspect looks up an aspect ratio of an image func GetAspect(img *canvas.Image) float32 { aspect := float32(0.0) if img.Resource != nil { aspect = aspects[img.Resource.Name()] } else if img.File != "" { aspect = aspects[img.File] } else if img.Image != nil { // HOTFIX until Fyne 2.4 proper fix: // we are not storing the aspect ratio in the map for the image.Image case size := img.Image.Bounds().Size() return float32(size.X) / float32(size.Y) } if aspect == 0 { aspect = aspects[img] } return aspect } // PaintImage renders a given fyne Image to a Go standard image // If a fyne.Canvas is given and the image’s fill mode is “fill original” the image’s min size has // to fit its original size. If it doesn’t, PaintImage does not paint the image but adjusts its min size. // The image will then be painted on the next frame because of the min size change. func PaintImage(img *canvas.Image, c fyne.Canvas, width, height int) image.Image { var wantOrigW, wantOrigH int wantOrigSize := false if img.FillMode == canvas.ImageFillOriginal && c != nil { wantOrigW = internal.ScaleInt(c, img.MinSize().Width) wantOrigH = internal.ScaleInt(c, img.MinSize().Height) wantOrigSize = true } dst, origW, origH, err := paintImage(img, width, height, wantOrigSize, wantOrigW, wantOrigH) if err != nil { fyne.LogError("failed to paint image", err) return nil } if wantOrigSize && dst == nil { dpSize := fyne.NewSize(internal.UnscaleInt(c, origW), internal.UnscaleInt(c, origH)) img.SetMinSize(dpSize) canvas.Refresh(img) // force the initial size to be respected } return dst } func paintImage(img *canvas.Image, width, height int, wantOrigSize bool, wantOrigW, wantOrigH int) (dst image.Image, origW, origH int, err error) { if (width <= 0 || height <= 0) && !wantOrigSize { return } var aspectCacheKey interface{} = img checkSize := func(origW, origH int) bool { aspect := float32(origW) / float32(origH) // this is used by our render code, so let's set it to the file aspect aspects[aspectCacheKey] = aspect return !wantOrigSize || (wantOrigW == origW && wantOrigH == origH) } switch { case img.File != "" || img.Resource != nil: var ( file io.Reader name string isSVG bool ) if img.Resource != nil { name = img.Resource.Name() file = bytes.NewReader(img.Resource.Content()) isSVG = IsResourceSVG(img.Resource) } else { name = img.File var handle *os.File handle, err = os.Open(img.File) if err != nil { err = fmt.Errorf("image load error: %w", err) return } defer handle.Close() file = handle isSVG = isFileSVG(img.File) } aspectCacheKey = name if isSVG { tex := cache.GetSvg(name, width, height) if tex == nil { // Not in cache, so load the item and add to cache tex, err = svg.ToImage(file, width, height, checkSize) if err != nil { return } cache.SetSvg(name, tex, width, height) } dst = tex } else { var pixels image.Image pixels, _, err = image.Decode(file) if err != nil { err = fmt.Errorf("failed to decode image: %w", err) return } origSize := pixels.Bounds().Size() origW, origH = origSize.X, origSize.Y if checkSize(origSize.X, origSize.Y) { dst = scaleImage(pixels, width, height, img.ScaleMode) } } case img.Image != nil: origSize := img.Image.Bounds().Size() origW, origH = origSize.X, origSize.Y // HOTFIX until Fyne 2.4: don't store aspect ratio in map, as checkSize(x, y) does. // Doing so leaks a reference to the image.Image data if !wantOrigSize || (wantOrigW == origW && wantOrigH == origH) { dst = scaleImage(img.Image, width, height, img.ScaleMode) } default: dst = image.NewNRGBA(image.Rect(0, 0, 1, 1)) } return } func scaleImage(pixels image.Image, scaledW, scaledH int, scale canvas.ImageScale) image.Image { if scale == canvas.ImageScaleFastest || scale == canvas.ImageScalePixels { // do not perform software scaling return pixels } pixW := int(fyne.Min(float32(scaledW), float32(pixels.Bounds().Dx()))) // don't push more pixels than we have to pixH := int(fyne.Min(float32(scaledH), float32(pixels.Bounds().Dy()))) // the GL calls will scale this up on GPU. scaledBounds := image.Rect(0, 0, pixW, pixH) tex := image.NewNRGBA(scaledBounds) switch scale { case canvas.ImageScalePixels: draw.NearestNeighbor.Scale(tex, scaledBounds, pixels, pixels.Bounds(), draw.Over, nil) default: if scale != canvas.ImageScaleSmooth { fyne.LogError("Invalid canvas.ImageScale value, using canvas.ImageScaleSmooth", nil) } draw.CatmullRom.Scale(tex, scaledBounds, pixels, pixels.Bounds(), draw.Over, nil) } return tex } func isFileSVG(path string) bool { return strings.ToLower(filepath.Ext(path)) == ".svg" } // IsResourceSVG checks if the resource is an SVG or not. func IsResourceSVG(res fyne.Resource) bool { if strings.ToLower(filepath.Ext(res.Name())) == ".svg" { return true } if len(res.Content()) < 5 { return false } switch strings.ToLower(string(res.Content()[:5])) { case "