Skip to content

Fyne Thumbnail Proposal

Stephen M Houston edited this page Aug 13, 2020 · 9 revisions

At some point in the near future Fyne will need the ability to create thumbnails and load them as well as load them from cache. This code follows thumbnailing using the xdg/fdo thumbnail manager spec. At a brief glance it would seem to Windows would need to do it natively using syscalls. Thumbnailing natively on ios/android as well due to storage locations and permissions will likely be required. For now we have a working solution in FyFoto that follows the xdg/fdo spec and adapts to fallbacks for cross platform. Thumbnailing is done in a go routine and this is what the current code looks like:

package main

import (
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"image"
	"image/png"
	"math"
	"os"
	"os/user"
	"time"

	"fyne.io/fyne"
	"fyne.io/fyne/canvas"

	"golang.org/x/image/draw"
)

func thumbnail(ff *FyFoto, thumbQueue <-chan gridImage, quitQueue <-chan string) {
	for {
		select {
		case <-quitQueue:
			return
		case gi := <-thumbQueue:
			file := gi.imageFile
			base := ""
			xdgcache := os.Getenv("XDG_CACHE_HOME")
			if xdgcache == "" {
				usr, err := user.Current()
				if err != nil {
					fmt.Println("Could not find the current user")
					return
				}
				base = usr.HomeDir + "/.cache"
			} else {
				base = xdgcache
			}
			os.Mkdir(base, 0700)
			thumbDir := base + "/thumbnails"
			os.Mkdir(thumbDir, 0700)
			thumbDir += "/normal"
			os.Mkdir(thumbDir, 0700)

			uri := []byte("file://" + file)
			newfileMD5 := md5.New()
			newfileMD5.Write(uri)
			newfile := hex.EncodeToString(newfileMD5.Sum(nil))
			destfile := thumbDir + "/" + newfile + ".png"

			needThumb := 1
			th, err := os.Stat(destfile)
			if !os.IsNotExist(err) {
				orig, _ := os.Stat(file)
				modThumb := th.ModTime()
				modOrig := orig.ModTime()
				diff := modThumb.Sub(modOrig)
				if diff > (time.Duration(0) * time.Second) {
					needThumb = 0
				}
			}
			if needThumb == 1 {
				img, err := os.Open(file)
				if err != nil {
					fmt.Println("Could not open image to thumbnail")
					img.Close()
					break
				}
				src, _, err := image.Decode(img)
				if err != nil {
					fmt.Println("Could not decode source image for thumbnail")
					img.Close()
					break
				}
				img.Close()
				img, err = os.Open(file)
				if err != nil {
					fmt.Println("Could not open image to thumbnail")
					img.Close()
					break
				}
				cfg, _, err := image.DecodeConfig(img)
				if err != nil {
					fmt.Println("Could not get original image size")
					img.Close()
					break
				}
				img.Close()
				newWidth, newHeight := 128, 128
				if cfg.Width > cfg.Height {
					scale := float64(cfg.Width) / float64(cfg.Height)
					newHeight = int(math.Round(float64(newWidth) / float64(scale)))
				} else if cfg.Height > cfg.Width {
					scale := float64(cfg.Height) / float64(cfg.Width)
					newWidth = int(math.Round(float64(newHeight) / float64(scale)))
				}
				dest := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
				draw.NearestNeighbor.Scale(dest, dest.Bounds(), src, src.Bounds(), draw.Src, nil)

				out, err := os.Create(destfile)
				if err != nil {
					fmt.Println("Could not create thumbnail destination file")
				}
				err = png.Encode(out, dest)
				if err != nil {
					fmt.Println("Could not encode png for thumbnail")
				}
			}
			if &gi != nil && gi.imageDir == ff.currentDir {
				gi.imageObject = canvas.NewImageFromFile(destfile)
				gi.imageObject.FillMode = canvas.ImageFillContain
				size := int(math.Floor(float64(128 * ff.window.Canvas().Scale())))
				gi.imageObject.SetMinSize(fyne.NewSize(size, size))
				gi.Append(gi.imageObject)

				ff.images.AddObject(&gi)
				canvas.Refresh(ff.images)
			}
		}
	}
}

This would not be hard to adapt into canvas/image or widget/icon. What would be needed:

  • A new API such as SetThumbnail(thumbnail bool) that if true would take the image resource and thumbnail it/get thumbnail from cache and use that as the displayed imaged.

Considerations:

  • Is this cross platform enough where we take the trade off of potentially adding additional storage/duplication of thumbnails on platforms other than linux by adapting the fdo/xdg spec cross platform?
  • Should we consider doing the thumbnails natively on platforms that require them be done that way including syscalls?
  • Should we add in an API to switch the thumbnail size between the specs NORMAL and LARGE choices or should we just stick to NORMAL?
  • What milestone would this need to be a part of?
  • This code is taken from fyfoto and will need to better handle thumbnail storage paths/locations on platforms other than linux - We have a where to store API already in progress that this would use.