image.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. package canvas
  2. import (
  3. "bytes"
  4. "errors"
  5. "image"
  6. _ "image/jpeg" // avoid users having to import when using image widget
  7. _ "image/png" // avoid the same for PNG images
  8. "io"
  9. "os"
  10. "path/filepath"
  11. "sync"
  12. "fyne.io/fyne/v2"
  13. "fyne.io/fyne/v2/internal/cache"
  14. "fyne.io/fyne/v2/internal/scale"
  15. "fyne.io/fyne/v2/internal/svg"
  16. "fyne.io/fyne/v2/storage"
  17. )
  18. // ImageFill defines the different type of ways an image can stretch to fill its space.
  19. type ImageFill int
  20. const (
  21. // ImageFillStretch will scale the image to match the Size() values.
  22. // This is the default and does not maintain aspect ratio.
  23. ImageFillStretch ImageFill = iota
  24. // ImageFillContain makes the image fit within the object Size(),
  25. // centrally and maintaining aspect ratio.
  26. // There may be transparent sections top and bottom or left and right.
  27. ImageFillContain // (Fit)
  28. // ImageFillOriginal ensures that the container grows to the pixel dimensions
  29. // required to fit the original image. The aspect of the image will be maintained so,
  30. // as with ImageFillContain there may be transparent areas around the image.
  31. // Note that the minSize may be smaller than the image dimensions if scale > 1.
  32. ImageFillOriginal
  33. )
  34. // ImageScale defines the different scaling filters used to scaling images
  35. type ImageScale int32
  36. const (
  37. // ImageScaleSmooth will scale the image using ApproxBiLinear filter (or GL equivalent)
  38. ImageScaleSmooth ImageScale = iota
  39. // ImageScalePixels will scale the image using NearestNeighbor filter (or GL equivalent)
  40. ImageScalePixels
  41. // ImageScaleFastest will scale the image using hardware GPU if available
  42. //
  43. // Since: 2.0
  44. ImageScaleFastest
  45. )
  46. // Declare conformity with CanvasObject interface
  47. var _ fyne.CanvasObject = (*Image)(nil)
  48. // Image describes a drawable image area that can render in a Fyne canvas
  49. // The image may be a vector or a bitmap representation, it will fill the area.
  50. // The fill mode can be changed by setting FillMode to a different ImageFill.
  51. type Image struct {
  52. baseObject
  53. aspect float32
  54. icon *svg.Decoder
  55. isSVG bool
  56. lock sync.Mutex
  57. // one of the following sources will provide our image data
  58. File string // Load the image from a file
  59. Resource fyne.Resource // Load the image from an in-memory resource
  60. Image image.Image // Specify a loaded image to use in this canvas object
  61. Translucency float64 // Set a translucency value > 0.0 to fade the image
  62. FillMode ImageFill // Specify how the image should expand to fill or fit the available space
  63. ScaleMode ImageScale // Specify the type of scaling interpolation applied to the image
  64. }
  65. // Alpha is a convenience function that returns the alpha value for an image
  66. // based on its Translucency value. The result is 1.0 - Translucency.
  67. func (i *Image) Alpha() float64 {
  68. return 1.0 - i.Translucency
  69. }
  70. // Aspect will return the original content aspect after it was last refreshed.
  71. //
  72. // Since: 2.4
  73. func (i *Image) Aspect() float32 {
  74. if i.aspect == 0 {
  75. i.Refresh()
  76. }
  77. return i.aspect
  78. }
  79. // Hide will set this image to not be visible
  80. func (i *Image) Hide() {
  81. i.baseObject.Hide()
  82. repaint(i)
  83. }
  84. // MinSize returns the specified minimum size, if set, or {1, 1} otherwise.
  85. func (i *Image) MinSize() fyne.Size {
  86. if i.Image == nil || i.aspect == 0 {
  87. i.Refresh()
  88. }
  89. return i.baseObject.MinSize()
  90. }
  91. // Move the image object to a new position, relative to its parent top, left corner.
  92. func (i *Image) Move(pos fyne.Position) {
  93. i.baseObject.Move(pos)
  94. repaint(i)
  95. }
  96. // Refresh causes this image to be redrawn with its configured state.
  97. func (i *Image) Refresh() {
  98. i.lock.Lock()
  99. defer i.lock.Unlock()
  100. rc, err := i.updateReader()
  101. if err != nil {
  102. fyne.LogError("Failed to load image", err)
  103. return
  104. }
  105. if rc != nil {
  106. rcMem := rc
  107. defer rcMem.Close()
  108. }
  109. if i.File != "" || i.Resource != nil || i.Image != nil {
  110. r, err := i.updateAspectAndMinSize(rc)
  111. if err != nil {
  112. fyne.LogError("Failed to load image", err)
  113. return
  114. }
  115. rc = io.NopCloser(r)
  116. }
  117. if i.File != "" || i.Resource != nil {
  118. size := i.Size()
  119. width := size.Width
  120. height := size.Height
  121. if width == 0 || height == 0 {
  122. return
  123. }
  124. if i.isSVG {
  125. tex, err := i.renderSVG(width, height)
  126. if err != nil {
  127. fyne.LogError("Failed to render SVG", err)
  128. return
  129. }
  130. i.Image = tex
  131. } else {
  132. if rc == nil {
  133. return
  134. }
  135. img, _, err := image.Decode(rc)
  136. if err != nil {
  137. fyne.LogError("Failed to render image", err)
  138. return
  139. }
  140. i.Image = img
  141. }
  142. }
  143. Refresh(i)
  144. }
  145. // Resize on an image will scale the content or reposition it according to FillMode.
  146. // It will normally cause a Refresh to ensure the pixels are recalculated.
  147. func (i *Image) Resize(s fyne.Size) {
  148. if s == i.Size() {
  149. return
  150. }
  151. i.baseObject.Resize(s)
  152. if i.FillMode == ImageFillOriginal && i.size.Height > 2 { // we can just ask for a GPU redraw to align
  153. Refresh(i)
  154. return
  155. }
  156. i.baseObject.Resize(s)
  157. if i.isSVG || i.Image == nil {
  158. i.Refresh() // we need to rasterise at the new size
  159. } else {
  160. Refresh(i) // just re-size using GPU scaling
  161. }
  162. }
  163. // NewImageFromFile creates a new image from a local file.
  164. // Images returned from this method will scale to fit the canvas object.
  165. // The method for scaling can be set using the Fill field.
  166. func NewImageFromFile(file string) *Image {
  167. return &Image{File: file}
  168. }
  169. // NewImageFromURI creates a new image from named resource.
  170. // File URIs will read the file path and other schemes will download the data into a resource.
  171. // HTTP and HTTPs URIs will use the GET method by default to request the resource.
  172. // Images returned from this method will scale to fit the canvas object.
  173. // The method for scaling can be set using the Fill field.
  174. //
  175. // Since: 2.0
  176. func NewImageFromURI(uri fyne.URI) *Image {
  177. if uri.Scheme() == "file" && len(uri.String()) > 7 {
  178. return NewImageFromFile(uri.Path())
  179. }
  180. var read io.ReadCloser
  181. read, err := storage.Reader(uri) // attempt unknown / http file type
  182. if err != nil {
  183. fyne.LogError("Failed to open image URI", err)
  184. return &Image{}
  185. }
  186. defer read.Close()
  187. return NewImageFromReader(read, filepath.Base(uri.String()))
  188. }
  189. // NewImageFromReader creates a new image from a data stream.
  190. // The name parameter is required to uniquely identify this image (for caching etc.).
  191. // If the image in this io.Reader is an SVG, the name should end ".svg".
  192. // Images returned from this method will scale to fit the canvas object.
  193. // The method for scaling can be set using the Fill field.
  194. //
  195. // Since: 2.0
  196. func NewImageFromReader(read io.Reader, name string) *Image {
  197. data, err := io.ReadAll(read)
  198. if err != nil {
  199. fyne.LogError("Unable to read image data", err)
  200. return nil
  201. }
  202. res := &fyne.StaticResource{
  203. StaticName: name,
  204. StaticContent: data,
  205. }
  206. return NewImageFromResource(res)
  207. }
  208. // NewImageFromResource creates a new image by loading the specified resource.
  209. // Images returned from this method will scale to fit the canvas object.
  210. // The method for scaling can be set using the Fill field.
  211. func NewImageFromResource(res fyne.Resource) *Image {
  212. return &Image{Resource: res}
  213. }
  214. // NewImageFromImage returns a new Image instance that is rendered from the Go
  215. // image.Image passed in.
  216. // Images returned from this method will scale to fit the canvas object.
  217. // The method for scaling can be set using the Fill field.
  218. func NewImageFromImage(img image.Image) *Image {
  219. return &Image{Image: img}
  220. }
  221. func (i *Image) name() string {
  222. if i.Resource != nil {
  223. return i.Resource.Name()
  224. } else if i.File != "" {
  225. return i.File
  226. }
  227. return ""
  228. }
  229. func (i *Image) updateReader() (io.ReadCloser, error) {
  230. i.isSVG = false
  231. if i.Resource != nil {
  232. i.isSVG = svg.IsResourceSVG(i.Resource)
  233. return io.NopCloser(bytes.NewReader(i.Resource.Content())), nil
  234. } else if i.File != "" {
  235. var err error
  236. fd, err := os.Open(i.File)
  237. if err != nil {
  238. return nil, err
  239. }
  240. i.isSVG = svg.IsFileSVG(i.File)
  241. return fd, nil
  242. }
  243. return nil, nil
  244. }
  245. func (i *Image) updateAspectAndMinSize(reader io.Reader) (io.Reader, error) {
  246. var pixWidth, pixHeight int
  247. if reader != nil {
  248. r, width, height, aspect, err := i.imageDetailsFromReader(reader)
  249. if err != nil {
  250. return nil, err
  251. }
  252. reader = r
  253. i.aspect = aspect
  254. pixWidth, pixHeight = width, height
  255. } else if i.Image != nil {
  256. original := i.Image.Bounds().Size()
  257. i.aspect = float32(original.X) / float32(original.Y)
  258. pixWidth, pixHeight = original.X, original.Y
  259. } else {
  260. return nil, errors.New("no matching image source")
  261. }
  262. if i.FillMode == ImageFillOriginal {
  263. i.SetMinSize(scale.ToFyneSize(i, pixWidth, pixHeight))
  264. }
  265. return reader, nil
  266. }
  267. func (i *Image) imageDetailsFromReader(source io.Reader) (reader io.Reader, width, height int, aspect float32, err error) {
  268. if source == nil {
  269. return nil, 0, 0, 0, errors.New("no matching reading reader")
  270. }
  271. if i.isSVG {
  272. var err error
  273. i.icon, err = svg.NewDecoder(source)
  274. if err != nil {
  275. return nil, 0, 0, 0, err
  276. }
  277. config := i.icon.Config()
  278. width, height = config.Width, config.Height
  279. aspect = config.Aspect
  280. } else {
  281. var buf bytes.Buffer
  282. tee := io.TeeReader(source, &buf)
  283. reader = io.MultiReader(&buf, source)
  284. config, _, err := image.DecodeConfig(tee)
  285. if err != nil {
  286. return nil, 0, 0, 0, err
  287. }
  288. width, height = config.Width, config.Height
  289. aspect = float32(width) / float32(height)
  290. }
  291. return
  292. }
  293. func (i *Image) renderSVG(width, height float32) (image.Image, error) {
  294. c := fyne.CurrentApp().Driver().CanvasForObject(i)
  295. screenWidth, screenHeight := int(width), int(height)
  296. if c != nil {
  297. // We want real output pixel count not just the screen coordinate space (i.e. macOS Retina)
  298. screenWidth, screenHeight = c.PixelCoordinateForPosition(fyne.Position{X: width, Y: height})
  299. }
  300. tex := cache.GetSvg(i.name(), screenWidth, screenHeight)
  301. if tex != nil {
  302. return tex, nil
  303. }
  304. var err error
  305. tex, err = i.icon.Draw(screenWidth, screenHeight)
  306. if err != nil {
  307. return nil, err
  308. }
  309. cache.SetSvg(i.name(), tex, screenWidth, screenHeight)
  310. return tex, nil
  311. }