image.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. package painter
  2. import (
  3. "bytes"
  4. "fmt"
  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. "strings"
  12. "golang.org/x/image/draw"
  13. "fyne.io/fyne/v2"
  14. "fyne.io/fyne/v2/canvas"
  15. "fyne.io/fyne/v2/internal"
  16. "fyne.io/fyne/v2/internal/cache"
  17. "fyne.io/fyne/v2/internal/svg"
  18. )
  19. var aspects = make(map[interface{}]float32, 16)
  20. // GetAspect looks up an aspect ratio of an image
  21. func GetAspect(img *canvas.Image) float32 {
  22. aspect := float32(0.0)
  23. if img.Resource != nil {
  24. aspect = aspects[img.Resource.Name()]
  25. } else if img.File != "" {
  26. aspect = aspects[img.File]
  27. } else if img.Image != nil {
  28. // HOTFIX until Fyne 2.4 proper fix:
  29. // we are not storing the aspect ratio in the map for the image.Image case
  30. size := img.Image.Bounds().Size()
  31. return float32(size.X) / float32(size.Y)
  32. }
  33. if aspect == 0 {
  34. aspect = aspects[img]
  35. }
  36. return aspect
  37. }
  38. // PaintImage renders a given fyne Image to a Go standard image
  39. // If a fyne.Canvas is given and the image’s fill mode is “fill original” the image’s min size has
  40. // to fit its original size. If it doesn’t, PaintImage does not paint the image but adjusts its min size.
  41. // The image will then be painted on the next frame because of the min size change.
  42. func PaintImage(img *canvas.Image, c fyne.Canvas, width, height int) image.Image {
  43. var wantOrigW, wantOrigH int
  44. wantOrigSize := false
  45. if img.FillMode == canvas.ImageFillOriginal && c != nil {
  46. wantOrigW = internal.ScaleInt(c, img.MinSize().Width)
  47. wantOrigH = internal.ScaleInt(c, img.MinSize().Height)
  48. wantOrigSize = true
  49. }
  50. dst, origW, origH, err := paintImage(img, width, height, wantOrigSize, wantOrigW, wantOrigH)
  51. if err != nil {
  52. fyne.LogError("failed to paint image", err)
  53. return nil
  54. }
  55. if wantOrigSize && dst == nil {
  56. dpSize := fyne.NewSize(internal.UnscaleInt(c, origW), internal.UnscaleInt(c, origH))
  57. img.SetMinSize(dpSize)
  58. canvas.Refresh(img) // force the initial size to be respected
  59. }
  60. return dst
  61. }
  62. func paintImage(img *canvas.Image, width, height int, wantOrigSize bool, wantOrigW, wantOrigH int) (dst image.Image, origW, origH int, err error) {
  63. if (width <= 0 || height <= 0) && !wantOrigSize {
  64. return
  65. }
  66. var aspectCacheKey interface{} = img
  67. checkSize := func(origW, origH int) bool {
  68. aspect := float32(origW) / float32(origH)
  69. // this is used by our render code, so let's set it to the file aspect
  70. aspects[aspectCacheKey] = aspect
  71. return !wantOrigSize || (wantOrigW == origW && wantOrigH == origH)
  72. }
  73. switch {
  74. case img.File != "" || img.Resource != nil:
  75. var (
  76. file io.Reader
  77. name string
  78. isSVG bool
  79. )
  80. if img.Resource != nil {
  81. name = img.Resource.Name()
  82. file = bytes.NewReader(img.Resource.Content())
  83. isSVG = IsResourceSVG(img.Resource)
  84. } else {
  85. name = img.File
  86. var handle *os.File
  87. handle, err = os.Open(img.File)
  88. if err != nil {
  89. err = fmt.Errorf("image load error: %w", err)
  90. return
  91. }
  92. defer handle.Close()
  93. file = handle
  94. isSVG = isFileSVG(img.File)
  95. }
  96. aspectCacheKey = name
  97. if isSVG {
  98. tex := cache.GetSvg(name, width, height)
  99. if tex == nil {
  100. // Not in cache, so load the item and add to cache
  101. tex, err = svg.ToImage(file, width, height, checkSize)
  102. if err != nil {
  103. return
  104. }
  105. cache.SetSvg(name, tex, width, height)
  106. }
  107. dst = tex
  108. } else {
  109. var pixels image.Image
  110. pixels, _, err = image.Decode(file)
  111. if err != nil {
  112. err = fmt.Errorf("failed to decode image: %w", err)
  113. return
  114. }
  115. origSize := pixels.Bounds().Size()
  116. origW, origH = origSize.X, origSize.Y
  117. if checkSize(origSize.X, origSize.Y) {
  118. dst = scaleImage(pixels, width, height, img.ScaleMode)
  119. }
  120. }
  121. case img.Image != nil:
  122. origSize := img.Image.Bounds().Size()
  123. origW, origH = origSize.X, origSize.Y
  124. // HOTFIX until Fyne 2.4: don't store aspect ratio in map, as checkSize(x, y) does.
  125. // Doing so leaks a reference to the image.Image data
  126. if !wantOrigSize || (wantOrigW == origW && wantOrigH == origH) {
  127. dst = scaleImage(img.Image, width, height, img.ScaleMode)
  128. }
  129. default:
  130. dst = image.NewNRGBA(image.Rect(0, 0, 1, 1))
  131. }
  132. return
  133. }
  134. func scaleImage(pixels image.Image, scaledW, scaledH int, scale canvas.ImageScale) image.Image {
  135. if scale == canvas.ImageScaleFastest || scale == canvas.ImageScalePixels {
  136. // do not perform software scaling
  137. return pixels
  138. }
  139. pixW := int(fyne.Min(float32(scaledW), float32(pixels.Bounds().Dx()))) // don't push more pixels than we have to
  140. pixH := int(fyne.Min(float32(scaledH), float32(pixels.Bounds().Dy()))) // the GL calls will scale this up on GPU.
  141. scaledBounds := image.Rect(0, 0, pixW, pixH)
  142. tex := image.NewNRGBA(scaledBounds)
  143. switch scale {
  144. case canvas.ImageScalePixels:
  145. draw.NearestNeighbor.Scale(tex, scaledBounds, pixels, pixels.Bounds(), draw.Over, nil)
  146. default:
  147. if scale != canvas.ImageScaleSmooth {
  148. fyne.LogError("Invalid canvas.ImageScale value, using canvas.ImageScaleSmooth", nil)
  149. }
  150. draw.CatmullRom.Scale(tex, scaledBounds, pixels, pixels.Bounds(), draw.Over, nil)
  151. }
  152. return tex
  153. }
  154. func isFileSVG(path string) bool {
  155. return strings.ToLower(filepath.Ext(path)) == ".svg"
  156. }
  157. // IsResourceSVG checks if the resource is an SVG or not.
  158. func IsResourceSVG(res fyne.Resource) bool {
  159. if strings.ToLower(filepath.Ext(res.Name())) == ".svg" {
  160. return true
  161. }
  162. if len(res.Content()) < 5 {
  163. return false
  164. }
  165. switch strings.ToLower(string(res.Content()[:5])) {
  166. case "<!doc", "<?xml", "<svg ":
  167. return true
  168. }
  169. return false
  170. }