package canvas import ( "bytes" "errors" "image" _ "image/jpeg" // avoid users having to import when using image widget _ "image/png" // avoid the same for PNG images "io" "os" "path/filepath" "sync" "fyne.io/fyne/v2" "fyne.io/fyne/v2/internal/cache" "fyne.io/fyne/v2/internal/scale" "fyne.io/fyne/v2/internal/svg" "fyne.io/fyne/v2/storage" ) // ImageFill defines the different type of ways an image can stretch to fill its space. type ImageFill int const ( // ImageFillStretch will scale the image to match the Size() values. // This is the default and does not maintain aspect ratio. ImageFillStretch ImageFill = iota // ImageFillContain makes the image fit within the object Size(), // centrally and maintaining aspect ratio. // There may be transparent sections top and bottom or left and right. ImageFillContain // (Fit) // ImageFillOriginal ensures that the container grows to the pixel dimensions // required to fit the original image. The aspect of the image will be maintained so, // as with ImageFillContain there may be transparent areas around the image. // Note that the minSize may be smaller than the image dimensions if scale > 1. ImageFillOriginal ) // ImageScale defines the different scaling filters used to scaling images type ImageScale int32 const ( // ImageScaleSmooth will scale the image using ApproxBiLinear filter (or GL equivalent) ImageScaleSmooth ImageScale = iota // ImageScalePixels will scale the image using NearestNeighbor filter (or GL equivalent) ImageScalePixels // ImageScaleFastest will scale the image using hardware GPU if available // // Since: 2.0 ImageScaleFastest ) // Declare conformity with CanvasObject interface var _ fyne.CanvasObject = (*Image)(nil) // Image describes a drawable image area that can render in a Fyne canvas // The image may be a vector or a bitmap representation, it will fill the area. // The fill mode can be changed by setting FillMode to a different ImageFill. type Image struct { baseObject aspect float32 icon *svg.Decoder isSVG bool lock sync.Mutex // one of the following sources will provide our image data File string // Load the image from a file Resource fyne.Resource // Load the image from an in-memory resource Image image.Image // Specify a loaded image to use in this canvas object Translucency float64 // Set a translucency value > 0.0 to fade the image FillMode ImageFill // Specify how the image should expand to fill or fit the available space ScaleMode ImageScale // Specify the type of scaling interpolation applied to the image } // Alpha is a convenience function that returns the alpha value for an image // based on its Translucency value. The result is 1.0 - Translucency. func (i *Image) Alpha() float64 { return 1.0 - i.Translucency } // Aspect will return the original content aspect after it was last refreshed. // // Since: 2.4 func (i *Image) Aspect() float32 { if i.aspect == 0 { i.Refresh() } return i.aspect } // Hide will set this image to not be visible func (i *Image) Hide() { i.baseObject.Hide() repaint(i) } // MinSize returns the specified minimum size, if set, or {1, 1} otherwise. func (i *Image) MinSize() fyne.Size { if i.Image == nil || i.aspect == 0 { i.Refresh() } return i.baseObject.MinSize() } // Move the image object to a new position, relative to its parent top, left corner. func (i *Image) Move(pos fyne.Position) { i.baseObject.Move(pos) repaint(i) } // Refresh causes this image to be redrawn with its configured state. func (i *Image) Refresh() { i.lock.Lock() defer i.lock.Unlock() rc, err := i.updateReader() if err != nil { fyne.LogError("Failed to load image", err) return } if rc != nil { rcMem := rc defer rcMem.Close() } if i.File != "" || i.Resource != nil || i.Image != nil { r, err := i.updateAspectAndMinSize(rc) if err != nil { fyne.LogError("Failed to load image", err) return } rc = io.NopCloser(r) } if i.File != "" || i.Resource != nil { size := i.Size() width := size.Width height := size.Height if width == 0 || height == 0 { return } if i.isSVG { tex, err := i.renderSVG(width, height) if err != nil { fyne.LogError("Failed to render SVG", err) return } i.Image = tex } else { if rc == nil { return } img, _, err := image.Decode(rc) if err != nil { fyne.LogError("Failed to render image", err) return } i.Image = img } } Refresh(i) } // Resize on an image will scale the content or reposition it according to FillMode. // It will normally cause a Refresh to ensure the pixels are recalculated. func (i *Image) Resize(s fyne.Size) { if s == i.Size() { return } i.baseObject.Resize(s) if i.FillMode == ImageFillOriginal && i.size.Height > 2 { // we can just ask for a GPU redraw to align Refresh(i) return } i.baseObject.Resize(s) if i.isSVG || i.Image == nil { i.Refresh() // we need to rasterise at the new size } else { Refresh(i) // just re-size using GPU scaling } } // NewImageFromFile creates a new image from a local file. // Images returned from this method will scale to fit the canvas object. // The method for scaling can be set using the Fill field. func NewImageFromFile(file string) *Image { return &Image{File: file} } // NewImageFromURI creates a new image from named resource. // File URIs will read the file path and other schemes will download the data into a resource. // HTTP and HTTPs URIs will use the GET method by default to request the resource. // Images returned from this method will scale to fit the canvas object. // The method for scaling can be set using the Fill field. // // Since: 2.0 func NewImageFromURI(uri fyne.URI) *Image { if uri.Scheme() == "file" && len(uri.String()) > 7 { return NewImageFromFile(uri.Path()) } var read io.ReadCloser read, err := storage.Reader(uri) // attempt unknown / http file type if err != nil { fyne.LogError("Failed to open image URI", err) return &Image{} } defer read.Close() return NewImageFromReader(read, filepath.Base(uri.String())) } // NewImageFromReader creates a new image from a data stream. // The name parameter is required to uniquely identify this image (for caching etc.). // If the image in this io.Reader is an SVG, the name should end ".svg". // Images returned from this method will scale to fit the canvas object. // The method for scaling can be set using the Fill field. // // Since: 2.0 func NewImageFromReader(read io.Reader, name string) *Image { data, err := io.ReadAll(read) if err != nil { fyne.LogError("Unable to read image data", err) return nil } res := &fyne.StaticResource{ StaticName: name, StaticContent: data, } return NewImageFromResource(res) } // NewImageFromResource creates a new image by loading the specified resource. // Images returned from this method will scale to fit the canvas object. // The method for scaling can be set using the Fill field. func NewImageFromResource(res fyne.Resource) *Image { return &Image{Resource: res} } // NewImageFromImage returns a new Image instance that is rendered from the Go // image.Image passed in. // Images returned from this method will scale to fit the canvas object. // The method for scaling can be set using the Fill field. func NewImageFromImage(img image.Image) *Image { return &Image{Image: img} } func (i *Image) name() string { if i.Resource != nil { return i.Resource.Name() } else if i.File != "" { return i.File } return "" } func (i *Image) updateReader() (io.ReadCloser, error) { i.isSVG = false if i.Resource != nil { i.isSVG = svg.IsResourceSVG(i.Resource) return io.NopCloser(bytes.NewReader(i.Resource.Content())), nil } else if i.File != "" { var err error fd, err := os.Open(i.File) if err != nil { return nil, err } i.isSVG = svg.IsFileSVG(i.File) return fd, nil } return nil, nil } func (i *Image) updateAspectAndMinSize(reader io.Reader) (io.Reader, error) { var pixWidth, pixHeight int if reader != nil { r, width, height, aspect, err := i.imageDetailsFromReader(reader) if err != nil { return nil, err } reader = r i.aspect = aspect pixWidth, pixHeight = width, height } else if i.Image != nil { original := i.Image.Bounds().Size() i.aspect = float32(original.X) / float32(original.Y) pixWidth, pixHeight = original.X, original.Y } else { return nil, errors.New("no matching image source") } if i.FillMode == ImageFillOriginal { i.SetMinSize(scale.ToFyneSize(i, pixWidth, pixHeight)) } return reader, nil } func (i *Image) imageDetailsFromReader(source io.Reader) (reader io.Reader, width, height int, aspect float32, err error) { if source == nil { return nil, 0, 0, 0, errors.New("no matching reading reader") } if i.isSVG { var err error i.icon, err = svg.NewDecoder(source) if err != nil { return nil, 0, 0, 0, err } config := i.icon.Config() width, height = config.Width, config.Height aspect = config.Aspect } else { var buf bytes.Buffer tee := io.TeeReader(source, &buf) reader = io.MultiReader(&buf, source) config, _, err := image.DecodeConfig(tee) if err != nil { return nil, 0, 0, 0, err } width, height = config.Width, config.Height aspect = float32(width) / float32(height) } return } func (i *Image) renderSVG(width, height float32) (image.Image, error) { c := fyne.CurrentApp().Driver().CanvasForObject(i) screenWidth, screenHeight := int(width), int(height) if c != nil { // We want real output pixel count not just the screen coordinate space (i.e. macOS Retina) screenWidth, screenHeight = c.PixelCoordinateForPosition(fyne.Position{X: width, Y: height}) } tex := cache.GetSvg(i.name(), screenWidth, screenHeight) if tex != nil { return tex, nil } var err error tex, err = i.icon.Draw(screenWidth, screenHeight) if err != nil { return nil, err } cache.SetSvg(i.name(), tex, screenWidth, screenHeight) return tex, nil }