218 lines
4.9 KiB
Go
218 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"fmt"
|
|
"image"
|
|
"image/draw"
|
|
"image/jpeg"
|
|
"image/png"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"time"
|
|
|
|
tk "modernc.org/tk9.0"
|
|
)
|
|
|
|
// this is a default image
|
|
//
|
|
//go:embed noise.png
|
|
var noise []byte
|
|
|
|
var validFileTypes = []tk.FileType{
|
|
{
|
|
TypeName: "JPEG",
|
|
Extensions: []string{".jpg", ".jpeg"},
|
|
},
|
|
{
|
|
TypeName: "GIF",
|
|
Extensions: []string{".gif"},
|
|
},
|
|
{
|
|
TypeName: "PNG",
|
|
Extensions: []string{".png"},
|
|
},
|
|
{
|
|
TypeName: "SVG",
|
|
Extensions: []string{".svg"},
|
|
},
|
|
}
|
|
|
|
var metaActive bool
|
|
|
|
var directoryState struct {
|
|
currentDirectory string
|
|
currentFile string
|
|
images []string
|
|
}
|
|
|
|
func isImage(entry os.DirEntry) bool {
|
|
if entry.IsDir() {
|
|
return false
|
|
}
|
|
ext := filepath.Ext(entry.Name())
|
|
for _, ft := range validFileTypes {
|
|
if slices.Contains(ft.Extensions, ext) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isType(filename string, desired string) bool {
|
|
ext := filepath.Ext(filename)
|
|
if ext == "" {
|
|
return false
|
|
}
|
|
for _, ft := range validFileTypes {
|
|
if slices.Contains(ft.Extensions, ext) && ft.TypeName == desired {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func jpegToPng(in io.Reader) (*bytes.Buffer, error) {
|
|
start := time.Now()
|
|
buf := new(bytes.Buffer)
|
|
|
|
img, err := jpeg.Decode(in)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode jpeg: %w", err)
|
|
}
|
|
|
|
b := img.Bounds()
|
|
dst := image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
|
draw.Draw(dst, dst.Bounds(), img, b.Min, draw.Src)
|
|
|
|
if err := (&png.Encoder{
|
|
CompressionLevel: -2,
|
|
}).Encode(buf, dst); err != nil {
|
|
return nil, fmt.Errorf("unable to encode png: %w", err)
|
|
}
|
|
end := time.Now()
|
|
log.Printf("jpeg to png took %v", end.Sub(start))
|
|
return buf, nil
|
|
}
|
|
|
|
func newFileInDirectory(img *tk.LabelWidget) func() {
|
|
return func() {
|
|
files := tk.GetOpenFile(tk.Filetypes(validFileTypes))
|
|
if len(files) < 1 || files[0] == "" {
|
|
log.Println("no file chosen")
|
|
return
|
|
}
|
|
directoryState.currentFile = filepath.Base(files[0])
|
|
directoryState.currentDirectory = filepath.Dir(files[0])
|
|
dirfiles, err := os.ReadDir(directoryState.currentDirectory)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
directoryState.images = []string{}
|
|
for _, v := range dirfiles {
|
|
if isImage(v) {
|
|
directoryState.images = append(directoryState.images, v.Name())
|
|
}
|
|
}
|
|
updateImage(files[0], img)
|
|
}
|
|
}
|
|
|
|
func newDirectory(img *tk.LabelWidget) func() {
|
|
return func() {
|
|
dir := tk.ChooseDirectory()
|
|
if dir == "" {
|
|
log.Println("no directory chosen")
|
|
return
|
|
}
|
|
directoryState.currentDirectory = dir
|
|
dirfiles, err := os.ReadDir(directoryState.currentDirectory)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
directoryState.images = []string{}
|
|
for _, v := range dirfiles {
|
|
if isImage(v) {
|
|
directoryState.images = append(directoryState.images, v.Name())
|
|
}
|
|
}
|
|
updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[0]), img)
|
|
}
|
|
}
|
|
|
|
func updateImage(file string, img *tk.LabelWidget) {
|
|
f, err := os.Open(file)
|
|
if err != nil {
|
|
log.Println(err.Error())
|
|
return
|
|
}
|
|
var r io.Reader
|
|
if isType(file, "JPEG") {
|
|
r, err = jpegToPng(f)
|
|
if err != nil {
|
|
log.Println(err.Error())
|
|
return
|
|
}
|
|
} else {
|
|
r = f
|
|
}
|
|
i, err := io.ReadAll(r)
|
|
if err != nil {
|
|
log.Println(err.Error())
|
|
return
|
|
}
|
|
img.Configure(tk.Image(tk.NewPhoto(tk.Data(i))))
|
|
directoryState.currentFile = filepath.Base(file)
|
|
}
|
|
|
|
func main() {
|
|
img := tk.Label(tk.Image(tk.NewPhoto(tk.Data(noise))))
|
|
|
|
menubar := tk.Menu()
|
|
fileMenu := menubar.Menu()
|
|
fileMenu.AddCommand(tk.Lbl("Open File"), tk.Underline(0), tk.Accelerator("Meta+O"), tk.Command(newFileInDirectory(img)))
|
|
fileMenu.AddCommand(tk.Lbl("Open Directory"), tk.Underline(0), tk.Command(newDirectory(img)))
|
|
menubar.AddCascade(tk.Lbl("File"), tk.Underline(0), tk.Mnu(fileMenu))
|
|
|
|
// TODO: if someone presses the Meta key again after the openfile dialog box closes,
|
|
// it triggers a _release_ event instead of a press event. The second time afterward, it works correctly.
|
|
tk.Bind(tk.App, "<KeyPress>", tk.Command(func(e *tk.Event) {
|
|
curr := slices.Index(directoryState.images, directoryState.currentFile)
|
|
switch e.Keysym {
|
|
case ".":
|
|
log.Printf("state: %+v", directoryState)
|
|
case "Meta_L", "Meta_R":
|
|
metaActive = true
|
|
case "o":
|
|
if metaActive {
|
|
newFileInDirectory(img)()
|
|
}
|
|
case "Up":
|
|
if curr > 0 {
|
|
updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[curr-1]), img)
|
|
}
|
|
case "Down":
|
|
if curr < len(directoryState.images)-1 && curr != -1 {
|
|
updateImage(filepath.Join(directoryState.currentDirectory, directoryState.images[curr+1]), img)
|
|
}
|
|
}
|
|
}))
|
|
tk.Bind(tk.App, "<KeyRelease>", tk.Command(func(e *tk.Event) {
|
|
switch e.Keysym {
|
|
case "Meta_L", "Meta_R":
|
|
metaActive = false
|
|
}
|
|
}))
|
|
// todo: resize image based on scroll events
|
|
// tk.Bind(tk.App, "<TouchpadScroll>", tk.Command(func(e *tk.Event) {
|
|
// log.Printf("%v, %v", int16(e.Delta>>16), int16(e.Delta&0xFFFF))
|
|
// }))
|
|
|
|
tk.Pack(img)
|
|
tk.App.Configure(tk.Mnu(menubar)).Center().Wait()
|
|
}
|