From 5588d95957100439f719ec18962901489ed3e643 Mon Sep 17 00:00:00 2001 From: foglar Date: Mon, 2 Jun 2025 16:53:32 +0200 Subject: [PATCH] added image rendering and resizing --- cmd/tui/main.go | 53 +++++---- pkg/cell_size/cell_size_unix.go | 48 ++++++++ pkg/cell_size/cell_size_win.go | 107 ++++++++++++++++++ pkg/icons/{iconunix.go => icon_unix.go} | 0 pkg/icons/{iconwin.go => icon_win.go} | 0 .../render_image.go | 16 +-- pkg/resize_image/resize_image.go | 33 ++++++ pkg/term_image/term_image.go | 66 +++++++++++ 8 files changed, 296 insertions(+), 27 deletions(-) create mode 100644 pkg/cell_size/cell_size_unix.go create mode 100644 pkg/cell_size/cell_size_win.go rename pkg/icons/{iconunix.go => icon_unix.go} (100%) rename pkg/icons/{iconwin.go => icon_win.go} (100%) rename pkg/{term_image => render_image}/render_image.go (75%) create mode 100644 pkg/resize_image/resize_image.go create mode 100644 pkg/term_image/term_image.go diff --git a/cmd/tui/main.go b/cmd/tui/main.go index 1c39581..158a834 100644 --- a/cmd/tui/main.go +++ b/cmd/tui/main.go @@ -4,14 +4,12 @@ import ( "fmt" _ "image/jpeg" _ "image/png" - "log" "strings" "github.com/jroimartin/gocui" - //"github.com/dolmen-go/kittyimg" - //"whspbrd/pkg/systray" - "whspbrd/pkg/term_image" + "whspbrd/pkg/cell_size" + "whspbrd/pkg/render_image" ) var messages []string @@ -85,7 +83,7 @@ func updateChatView(v *gocui.View) { //} // Print image directly to terminal (stdout) - term_image.RenderImage("kogami-rounded.png", i*3+2, 23) + render_image.RenderImage("kogami-rounded.png", i*3+2, 23, 50, 50) //err = kittyimg.Fprintln(os.Stdout, img) //if err != nil { // log.Println("Error rendering image:", err) @@ -196,24 +194,39 @@ func quit(g *gocui.Gui, v *gocui.View) error { } func main() { - fmt.Print("\033") - //systray.Systray() - g, err := gocui.NewGui(gocui.OutputNormal) + //g, err := gocui.NewGui(gocui.OutputNormal) + //if err != nil { + // log.Panicln(err) + //} + //defer g.Close() + // + //g.SetManagerFunc(layout) + // + //if err := keybindings(g); err != nil { + // log.Panicln(err) + //} + // + //if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + // log.Panicln(err) + //} + // and here is agrid making a grid of images + //for i := 0; i < 10; i++ { + // for j := 0; j < 10; j++ { + // render_image.RenderImage("kogami-pf-edit.jpg", i*3+2, j*7+23, 64, 64) + // } + //} + + //render_image.RenderImage("kogami-pf-edit.jpg", 0, 3, 750, 0) + + w, h, err := cell_size.GetTerminalCellSizePixels() if err != nil { - log.Panicln(err) + fmt.Println("Error getting terminal cell size:", err) + return } - defer g.Close() + width, height := cell_size.GetConsoleSize() - g.SetManagerFunc(layout) + fmt.Println("Terminal cell size in pixels:", w, "x", h) + fmt.Println("Console size in characters:", width, "x", height) - if err := keybindings(g); err != nil { - log.Panicln(err) - } - - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - log.Panicln(err) - } - - term_image.RenderImage("kogami-rounded.png", 20, 20) } diff --git a/pkg/cell_size/cell_size_unix.go b/pkg/cell_size/cell_size_unix.go new file mode 100644 index 0000000..128a642 --- /dev/null +++ b/pkg/cell_size/cell_size_unix.go @@ -0,0 +1,48 @@ +//go:build !windows + +package cell_size + +import ( + "fmt" + "os" + "syscall" + "unsafe" +) + +type winsize struct { + Rows uint16 + Cols uint16 + Xpixels uint16 + Ypixels uint16 +} + +func GetTerminalCellSizePixels() (widthPx int, heightPx int, err error) { + ws := &winsize{} + _, _, errno := syscall.Syscall( + syscall.SYS_IOCTL, + os.Stdout.Fd(), + uintptr(syscall.TIOCGWINSZ), + uintptr(unsafe.Pointer(ws)), + ) + if errno != 0 { + return 0, 0, errno + } + if ws.Cols == 0 || ws.Rows == 0 { + return 0, 0, fmt.Errorf("terminal rows or columns is zero") + } + widthPx = int(ws.Xpixels) / int(ws.Cols) + heightPx = int(ws.Ypixels) / int(ws.Rows) + return +} + +func GetConsoleSize() (int, int) { + var sz struct { + rows uint16 + cols uint16 + xpixels uint16 + ypixels uint16 + } + _, _, _ = syscall.Syscall(syscall.SYS_IOCTL, + uintptr(syscall.Stdout), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz))) + return int(sz.cols), int(sz.rows) +} diff --git a/pkg/cell_size/cell_size_win.go b/pkg/cell_size/cell_size_win.go new file mode 100644 index 0000000..805a9e1 --- /dev/null +++ b/pkg/cell_size/cell_size_win.go @@ -0,0 +1,107 @@ +//go:build windows + +package cell_size + +import ( + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +type coord struct { + X int16 + Y int16 +} + +type consoleFontInfoEx struct { + cbSize uint32 + nFont uint32 + dwFontSize coord + fontFamily uint32 + fontWeight uint32 + faceName [32]uint16 +} + +var ( + kernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGetCurrentConsoleFontEx = kernel32.NewProc("GetCurrentConsoleFontEx") +) + +func GetTerminalCellSizePixels() (widthPx int, heightPx int, err error) { + var fontInfo consoleFontInfoEx + fontInfo.cbSize = uint32(unsafe.Sizeof(fontInfo)) + + stdOutHandle := windows.Handle(syscall.Stdout) + ret, _, err := procGetCurrentConsoleFontEx.Call( + uintptr(stdOutHandle), + uintptr(0), // bMaximumWindow = false + uintptr(unsafe.Pointer(&fontInfo)), + ) + if ret == 0 { + return 0, 0, err + } + return int(fontInfo.dwFontSize.X), int(fontInfo.dwFontSize.Y), nil +} + +type ( + short int16 + word uint16 + smallRect struct { + Left short + Top short + Right short + Bottom short + } + consoleScreenBufferInfo struct { + Size coord + CursorPosition coord + Attributes word + Window smallRect + MaximumWindowSize coord + } +) + +var kernel32DLL = syscall.NewLazyDLL("kernel32.dll") +var getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") + +// GetConsoleSize returns the current number of columns and rows in the active console window. +// The return value of this function is in the order of cols, rows. +func GetConsoleSize() (int, int) { + stdoutHandle := getStdHandle(syscall.STD_OUTPUT_HANDLE) + var info, err = getConsoleScreenBufferInfo(stdoutHandle) + + if err != nil { + return 0, 0 + } + + return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1) +} + +func getError(r1, r2 uintptr, lastErr error) error { + // If the function fails, the return value is zero. + if r1 == 0 { + if lastErr != nil { + return lastErr + } + return syscall.EINVAL + } + return nil +} + +func getStdHandle(stdhandle int) uintptr { + handle, err := syscall.GetStdHandle(stdhandle) + if err != nil { + panic(fmt.Errorf("could not get standard io handle %d", stdhandle)) + } + return uintptr(handle) +} + +func getConsoleScreenBufferInfo(handle uintptr) (*consoleScreenBufferInfo, error) { + var info consoleScreenBufferInfo + if err := getError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil { + return nil, err + } + return &info, nil +} diff --git a/pkg/icons/iconunix.go b/pkg/icons/icon_unix.go similarity index 100% rename from pkg/icons/iconunix.go rename to pkg/icons/icon_unix.go diff --git a/pkg/icons/iconwin.go b/pkg/icons/icon_win.go similarity index 100% rename from pkg/icons/iconwin.go rename to pkg/icons/icon_win.go diff --git a/pkg/term_image/render_image.go b/pkg/render_image/render_image.go similarity index 75% rename from pkg/term_image/render_image.go rename to pkg/render_image/render_image.go index 4985093..497137f 100644 --- a/pkg/term_image/render_image.go +++ b/pkg/render_image/render_image.go @@ -1,4 +1,4 @@ -package term_image +package render_image import ( "encoding/base64" @@ -6,6 +6,8 @@ import ( "image" "image/draw" "os" + + "whspbrd/pkg/resize_image" ) func LoadImage(filePath string) (image.Image, error) { @@ -18,25 +20,26 @@ func LoadImage(filePath string) (image.Image, error) { return img, err } -func ConvertToRGBA(img image.Image) *image.RGBA { +func convertToRGBA(img image.Image) *image.RGBA { rgba := image.NewRGBA(img.Bounds()) draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src) return rgba } -func EncodeImageToBase64RGBA(rgba *image.RGBA) string { +func encodeImageToBase64RGBA(rgba *image.RGBA) string { return base64.StdEncoding.EncodeToString(rgba.Pix) } -func RenderImage(filepath string, row int, col int) { +func RenderImage(filepath string, row int, col int, width_ int, height_ int) { img, err := LoadImage(filepath) if err != nil { fmt.Printf("Error loading image: %v\n", err) return } - rgba := ConvertToRGBA(img) - encoded := EncodeImageToBase64RGBA(rgba) + rgba := convertToRGBA(img) + rgba, _ = resize_image.Resize(*rgba, width_, height_) + encoded := encodeImageToBase64RGBA(rgba) width := rgba.Rect.Dx() height := rgba.Rect.Dy() @@ -64,4 +67,3 @@ func RenderImage(filepath string, row int, col int) { } fmt.Print("\033[u") } - diff --git a/pkg/resize_image/resize_image.go b/pkg/resize_image/resize_image.go new file mode 100644 index 0000000..ccd236f --- /dev/null +++ b/pkg/resize_image/resize_image.go @@ -0,0 +1,33 @@ +package resize_image + +import ( + "image" + "image/color" +) + +func Resize(img image.RGBA, width int, height int) (*image.RGBA, error) { + if width <= 0 || height <= 0 { + return nil, nil + } + + newImg := image.NewRGBA(image.Rect(0, 0, width, height)) + scaleX := float64(img.Bounds().Dx()) / float64(width) + scaleY := float64(img.Bounds().Dy()) / float64(height) + + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + srcX := int(float64(x) * scaleX) + srcY := int(float64(y) * scaleY) + + r, g, b, a := getPixel(img, srcX, srcY) + newImg.Set(x, y, color.RGBA{r, g, b, a}) + } + } + + return newImg, nil +} + +func getPixel(img image.RGBA, x int, y int) (uint8, uint8, uint8, uint8) { + index := img.PixOffset(x, y) + return img.Pix[index], img.Pix[index+1], img.Pix[index+2], img.Pix[index+3] +} diff --git a/pkg/term_image/term_image.go b/pkg/term_image/term_image.go new file mode 100644 index 0000000..3a4374e --- /dev/null +++ b/pkg/term_image/term_image.go @@ -0,0 +1,66 @@ +package term_image + +import ( + "encoding/base64" + "fmt" + "image" + "image/draw" + "os" +) + +func LoadImage(filePath string) (image.Image, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + img, _, err := image.Decode(f) + return img, err +} + +func convertToRGBA(img image.Image) *image.RGBA { + rgba := image.NewRGBA(img.Bounds()) + draw.Draw(rgba, rgba.Bounds(), img, image.Point{0, 0}, draw.Src) + return rgba +} + +func encodeImageToBase64RGBA(rgba *image.RGBA) string { + return base64.StdEncoding.EncodeToString(rgba.Pix) +} + +func RenderImage(filepath string, row int, col int) { + img, err := LoadImage(filepath) + if err != nil { + fmt.Printf("Error loading image: %v\n", err) + return + } + + rgba := convertToRGBA(img) + encoded := encodeImageToBase64RGBA(rgba) + + width := rgba.Rect.Dx() + height := rgba.Rect.Dy() + + fmt.Printf("\033[s\033[%d;%dH", row, col) + + chunk_size := 4096 + pos := 0 + first := true + for pos < len(encoded) { + fmt.Print("\033_G") + if first { + fmt.Printf("q=2,a=T,f=32,s=%d,v=%d,", width, height) + first = false + } + chunk_len := len(encoded) - pos + if chunk_len > chunk_size { + chunk_len = chunk_size + } + if pos+chunk_len < len(encoded) { + fmt.Print("m=1") + } + fmt.Printf(";%s\033\\", encoded[pos:pos+chunk_len]) + pos += chunk_len + } + fmt.Print("\033[u") +}