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.
-
2022 February 01 16:38 stuartscott 2480299¤
The dominant colors of the January edition cover image were several shades of grey and two shades of gold. I chose the two gold shades for the digest's color scheme. I have not figured out a way to automate such a subjective decision yet.
Convey is made available by Aletheia Ware under the Terms of Service and Privacy Policy.
Convey is an open-source project released under the Apache 2.0 License and hosted on Github.
© 2021 Aletheia Ware LLC. All rights reserved.