image.go 5.1 KB

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