CONVEY

Dominant Colors GUI

2022 February 01 16:21 stuartscott 1473754¤ 1240149¤

You may have noticed that the January edition of the Convey Digest looks a little different from the previous ones - the color scheme is now based on the dominant colors of the cover image!

Cenk Alti's dominantcolor is an open source Go library for calculating the dominant colors of an image, which I used to create a simple command line app. However I found myself continuously copying RGB codes from the terminal into an online color tool to see the actual colors.

In the true programmers' spirit of spending more time automating a task than it would take to actually complete it manually, I decided to spend an hour building a GUI using Fyne.

I started by creating a simple app with a single window containing the image being sampled and a rectangle showing the dominant color. I used a split container to lay the window content out side-by-side.

package main

import (
	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
)

type DominantColor struct {
	Window    fyne.Window
	Image     *canvas.Image
	Rectangle *canvas.Rectangle
}

func main() {
	dc := &DominantColor{
		Window: app.New().NewWindow("Dominant Color"),
		Image: &canvas.Image{},
		Rectangle: &canvas.Rectangle{},
	}
	dc.Image.FillMode = canvas.ImageFillContain
	// ...
	dc.Window.SetContent(container.NewHSplit(dc.Image, dc.Rectangle))
	dc.Window.CenterOnScreen()
	dc.Window.Resize(fyne.NewSize(800, 600))
	dc.Window.ShowAndRun()
}

If an argument was passed through the command line, try to load it as an image.

	if len(os.Args) > 1 {
		f, err := os.Open(os.Args[1])
		if err != nil {
			dialog.ShowError(err, dc.Window)
			return
		}
		dc.Open(f.Name(), f)
	}

The Open function updates the window title, loads the image, and calculates dominant color.

func (dc *DominantColor) Open(name string, reader io.ReadCloser) {
	dc.Window.SetTitle("Dominant Color - " + name)
	defer reader.Close()
	i, _, err := image.Decode(reader)
	if err != nil {
		dialog.ShowError(err, dc.Window)
		return
	}
	dc.Image.Image = i
	dc.Image.Refresh()
	dc.Rectangle.FillColor = dominantcolor.Find(i)
	dc.Rectangle.Refresh()
}

In addition to selecting an image through the command line, I wanted the option to select an image through Fyne's builtin file dialog so I added a toolbar with an open file button that triggers the dialog and opens the selected file. I used a border layout to place the toolbar at the top of the window, and the split container in the center.

	dc.Window.SetContent(container.NewBorder(widget.NewToolbar(widget.NewToolbarAction(theme.FileIcon(), func() {
		fd := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) {
			if err != nil {
				dialog.ShowError(err, dc.Window)
				return
			}
			if reader != nil {
				dc.Open(reader.URI().Name(), reader)
			}
		}, dc.Window)
		fd.SetFilter(storage.NewExtensionFileFilter([]string{".jpg", ".jpeg", ".png"}))
		fd.Show()
	})), nil, nil, nil, container.NewHSplit(dc.Image, dc.Rectangle)))

Next, I extended the GUI to display the image's 6 most dominant colors by replacing the single rectangle in the struct with a list and adding a field to hold the colors.

type DominantColor struct {
	// ...
	List      *widget.List
	Colors    []color.RGBA
}

The list displays as many elements as there are colors in the slice, and each element has a rectangle to show the color and a text box containing the RGB color code.

	dc := &DominantColor{
		// ...
		List: &widget.List{},
	}
	// ...
	dc.List.Length = func() int {
		return len(dc.Colors)
	}
	dc.List.CreateItem = func() fyne.CanvasObject {
		r := &canvas.Rectangle{}
		r.SetMinSize(fyne.NewSize(64, 64))
		t := &canvas.Text{}
		t.Alignment = fyne.TextAlignCenter
		t.Text = "#FFFFFF"
		return container.NewBorder(nil, nil, r, nil, t)
	}
	dc.List.UpdateItem = func(id widget.ListItemID, obj fyne.CanvasObject) {
		t := obj.(*fyne.Container).Objects[0].(*canvas.Text)
		r := obj.(*fyne.Container).Objects[1].(*canvas.Rectangle)
		c := dc.Colors[id]
		t.Text = dominantcolor.Hex(c)
		r.FillColor = c
		t.Refresh()
		r.Refresh()
	}

The Open function was modified to use dominantcolor.FindN to find the 6 dominant colors.

func (dc *DominantColor) Open(name string, reader io.ReadCloser) {
	// ...
	dc.Colors = dominantcolor.FindN(i, 6)
	dc.List.Refresh()
}

Finally, to make my workflow a little easier I added a button next to each color that copies the RGB code to the clipboard.

	dc.List.CreateItem = func() fyne.CanvasObject {
		// ...
		b := &widget.Button{}
		b.Icon = theme.ContentCopyIcon()
		b.Importance = widget.LowImportance
		b.OnTapped = func() {
			dc.Window.Clipboard().SetContent(t.Text)
		}
		return container.NewBorder(nil, nil, r, b, t)
	}

And that's it! A working GUI in under 100 lines of code which, as always, is open source and hosted on GitHub.

Sort: Cost Yield Time

Sign Up