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. I am not familiar with thumbnailing outside of Linux using the xdg/fdo thumbnail manager spec. At a brief glance it would seem to use Window's system we would need to do it natively using syscalls. I would imagine we would need to do thumbnailing natively on ios/android as well due to storage locations and permissions. For now I 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?

Welcome to the Fyne wiki.

Project documentation

Getting involved

Platform details

Clone this wiki locally