Commit 35139825 authored by Lukas's avatar Lukas
Browse files

Initial Commit

parent 23538c06
This diff is collapsed.
package main
import (
"image"
"image/gif"
_ "image/jpeg"
_ "image/png"
"log"
"os"
)
func DecodeImage(path string) image.Image {
file, err := os.Open(path)
defer file.Close()
if err != nil {
log.Fatal(err)
}
img, _, err := image.Decode(file)
if err != nil {
log.Fatal(err)
}
return img
}
func DecodeGIF(path string) *gif.GIF {
file, err := os.Open(path)
defer file.Close()
if err != nil {
log.Fatal(err)
}
img, err := gif.DecodeAll(file)
if err != nil {
log.Fatal(err)
}
return img
}
module github.com/wwared/img2term
require (
github.com/disintegration/imaging v1.6.2
github.com/exrook/drawille-go v0.0.0-20180117021400-68d036fca70a
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/stretchr/testify v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
)
go 1.15
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/exrook/drawille-go v0.0.0-20180117021400-68d036fca70a h1:cqI+C4VYK7KhzITlcQYMYiQk4Wy7pKApvZGChenYml0=
github.com/exrook/drawille-go v0.0.0-20180117021400-68d036fca70a/go.mod h1:PttwnPNwT0bEDftn684HBM62kdAwQYKPlSZVNppFQEA=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 h1:5hpz5aRr+W1erYCL5JRhSUBJRph7l9XkNveoExlrKYk=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime"
"runtime/pprof"
"golang.org/x/crypto/ssh/terminal"
)
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
func main() {
flagIRC := flag.Bool("irc", false, "Output IRC color codes")
flagMOTD := flag.Bool("motd", false, "Output IRC MOTD for Ergo")
flagIRC16 := flag.Bool("irc16", false, "Output IRC colors codes (compatibility mode)")
flagMOTD16 := flag.Bool("motd16", false, "Output IRC MOTD for Ergo (compatibility mode)")
flag256 := flag.Bool("256", false, "Use 256 colors")
flag24bit := flag.Bool("24bit", false, "Use 24-bit colors")
flagBraille := flag.Bool("braille", false, "Use braille characters") // TODO add color support
// flagAnimated := flag.Bool("animated", false, "Animated GIF playback")
flagSpaces := flag.Bool("spaces", false, "Use 2 spaces per pixel instead of fitting two pixels in ▀")
flagGrayscale := flag.Bool("gray", false, "Make the image grayscale")
flagInvert := flag.Bool("invert", false, "Invert the image colors (useful with -braille)")
flagAutocrop := flag.Bool("crop", false, "Automatically crop out same-color or transparent borders")
flagAutoresize := flag.Bool("autoresize", false, "Automatically downscale image so it fits your terminal")
flagResizeW := flag.Int("width", 0, "Downscale image if greater than width")
flagResizeH := flag.Int("height", 0, "Downscale image if greater than height")
flag.Parse()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
mode := term16
setMode := func(m RenderMode) {
if mode != term16 {
fmt.Print("Only one of -irc, -irc16, -256 or -24bit must be given")
os.Exit(1)
}
mode = m
}
if *flag256 {
setMode(term256)
}
if *flag24bit {
setMode(term24bit)
}
if *flagIRC {
setMode(irc)
}
if *flagMOTD {
setMode(motd)
}
if *flagIRC16 {
setMode(irc16)
}
if *flagMOTD16 {
setMode(motd16)
}
if *flagBraille {
setMode(braille)
}
w, h := *flagResizeW, *flagResizeH
if *flagAutoresize {
var err error
w, h, err = terminal.GetSize(int(os.Stdout.Fd()))
if err != nil {
log.Fatal(err)
}
h -= 3 // Some vertical padding for shell prompts
}
if *flagSpaces {
w /= 2
} else {
h *= 2
}
for _, file := range flag.Args() {
img := DecodeImage(file)
res := RenderToText(img, *flagGrayscale, *flagInvert, *flagAutocrop, *flagSpaces, w, h, mode)
fmt.Print(res)
}
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
defer f.Close()
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("could not write memory profile: ", err)
}
}
}
package main
import (
"bytes"
"fmt"
"image"
"image/color"
"strconv"
"strings"
"github.com/disintegration/imaging"
"github.com/exrook/drawille-go"
"github.com/lucasb-eyer/go-colorful"
)
type Pixel struct {
color colorful.Color
alpha uint32
}
// Rendering entrypoint
func RenderToText(img image.Image, grayscale bool, invert bool, autocrop bool, use_spaces bool, width int, height int, mode RenderMode) string {
if grayscale || mode == braille {
img = Grayscale(img)
}
if invert {
img = imaging.Invert(img)
}
if autocrop {
img = CropBorders(img)
}
if width != 0 || height != 0 {
if height != 0 && img.Bounds().Size().Y > height {
img = imaging.Resize(img, 0, height, imaging.NearestNeighbor)
}
if width != 0 && img.Bounds().Size().X > width {
img = imaging.Resize(img, width, 0, imaging.NearestNeighbor)
}
}
if mode == braille {
return RenderBraille(GetPixels(img))
}
return Render(mode, use_spaces, GetPixels(img))
}
//
// Preprocessing filters
//
func Grayscale(img image.Image) image.Image {
bounds := img.Bounds()
gray := image.NewGray16(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if !IsTransparent(img.At(x, y)) {
gray.Set(x, y, img.At(x, y))
} else {
gray.Set(x, y, color.White)
}
}
}
return gray
}
// PSA this code is repetitive and ugly
func CropBorders(img image.Image) image.Image {
bounds := img.Bounds()
ix, iy, w, h := bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y
background := img.At(ix, iy)
for {
done := false
for y := iy; y < h; y++ {
if !IsTransparent(img.At(ix, y)) && img.At(ix, y) != background {
done = true
break
}
}
if done {
break
}
ix++
if ix == w {
break
}
}
for {
done := false
for x := ix; x < w; x++ {
if !IsTransparent(img.At(x, iy)) && img.At(x, iy) != background {
done = true
break
}
}
if done {
break
}
iy++
if iy == h {
break
}
}
for {
done := false
for y := iy; y < h; y++ {
if !IsTransparent(img.At(w-1, y)) && img.At(w-1, y) != background {
done = true
break
}
}
if done {
break
}
w--
if w == 0 {
break
}
}
for {
done := false
for x := ix; x < w; x++ {
if !IsTransparent(img.At(x, h-1)) && img.At(x, h-1) != background {
done = true
break
}
}
if done {
break
}
h--
if h == 0 {
break
}
}
if ix == bounds.Min.X && iy == bounds.Min.Y && w == bounds.Max.X && h == bounds.Max.Y {
return img
}
result := image.NewRGBA(image.Rect(0, 0, w-ix, h-iy))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
result.Set(x, y, img.At(ix+x, iy+y))
}
}
return result
}
//
// Misc utility functions
//
// Extracts Pixels from an image.Image
func GetPixels(img image.Image) [][]Pixel {
bounds := img.Bounds()
img_size := bounds.Size()
w, h := img_size.X, img_size.Y
pixels := make([][]Pixel, h)
for i := 0; i < h; i++ {
pixels[i] = make([]Pixel, w)
}
di := 0
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
dj := 0
for x := bounds.Min.X; x < bounds.Max.X; x++ {
orig_color := img.At(x, y)
color, alpha_flag := colorful.MakeColor(orig_color)
if !alpha_flag {
color = colorful.Color{R: 1.0, G: 1.0, B: 1.0}
}
_, _, _, alpha := orig_color.RGBA()
pixels[di][dj] = Pixel{
color: color,
alpha: alpha,
}
dj++
}
di++
}
return pixels
}
func ColorDistance(mode RenderMode, c1 colorful.Color, c2 colorful.Color) float64 {
if mode == term16 { // heuristic; I think it looks nicer
return c1.DistanceLuv(c2)
} else {
return c1.DistanceLab(c2)
}
}
func IsTransparent(c color.Color) bool {
_, _, _, alpha := c.RGBA()
return alpha < TRANSPARENCY_THRESHOLD
}
func RenderBraille(colors [][]Pixel) string {
const braille_threshold = 0.5
// NOTE: the input image is always grayscale here
canvas := drawille.NewCanvas()
for y := 0; y < len(colors); y++ {
for x := 0; x < len(colors[0]); x++ {
oldpx := colors[y][x].color.R
quant_error := oldpx
if oldpx >= braille_threshold {
canvas.Set(x, y)
quant_error -= 1.0
}
if x+1 < len(colors[0]) {
colors[y][x+1].color.R = colors[y][x+1].color.R + quant_error*(7.0/16.0)
}
if y+1 < len(colors) {
if x > 0 {
colors[y+1][x-1].color.R = colors[y+1][x-1].color.R + quant_error*(3.0/16.0)
}
colors[y+1][x].color.R = colors[y+1][x].color.R + quant_error*(5.0/16.0)
if x+1 < len(colors[0]) {
colors[y+1][x+1].color.R = colors[y+1][x+1].color.R + quant_error*(1.0/16.0)
}
}
}
}
return strings.Replace(canvas.String(), string('⠀'), string(' '), -1)
}
// Returns the palette index closest to the color in the current mode
// converted to the proper string format
func ColorString(mode RenderMode, px Pixel) string {
if px.alpha < TRANSPARENCY_THRESHOLD {
return "" // sentinel for transparent colors
}
if mode == term24bit {
r, g, b := px.color.RGB255()
return fmt.Sprintf("%d;%d;%d", r, g, b)
}
result := 0
last := len(colors[mode]) - 1
dist := ColorDistance(mode, px.color, colors[mode][last])
// start from the end so higher color indices are favored in the irc palette
for i := last - 1; i >= 0; i-- {
d := ColorDistance(mode, px.color, colors[mode][i])
if d < dist {
dist = d
result = i
}
}
return strconv.Itoa(result)
}
//
// Escape squences
//
func StartFGColor(mode RenderMode) string {
if mode == motd || mode == motd16 {
return "$c"
} else {
if mode == irc || mode == irc16 {
return "\x03"
}
// Foreground color selector is 38;
if mode == term24bit {
return "\x1B[38;2;"
} else {
return "\x1B[38;5;"
}
}
}
func StartBGColor(mode RenderMode) string {
if mode == irc || mode == irc16 || mode == motd || mode == motd16 {
return ","
} else {
// Background color selector is 48;
if mode == term24bit {
return "\x1B[48;2;"
} else { // 256
return "\x1B[48;5;"
}
}
}
func EndColor(mode RenderMode) string {
if mode != irc && mode != irc16 && mode != motd && mode != motd16 {
// ANSI escape sequences are terminated by 'm'
return "m"
}
return ""
}
func Clear(mode RenderMode) string {
if mode != irc && mode != irc16 && mode != motd && mode != motd16 {
return "\x1B[0m"
}
return ""
}
func Render(mode RenderMode, use_spaces bool, colors [][]Pixel) string {
var buffer bytes.Buffer
ch := ""
step := 2
if use_spaces {
// Two spaces to keep aspect ratio
ch = " "
step = 1
}
for y := 0; y < len(colors); y += step {
var prev_fg_col string
var prev_bg_col string
for x := 0; x < len(colors[0]); x++ {
next_fg_col := ColorString(mode, colors[y][x])
next_bg_col := ""
// Process two vertical pixels per column unless we're printing spaces
if !use_spaces {
ch = "▀"
if y+1 < len(colors) {
next_bg_col = ColorString(mode, colors[y+1][x])
}
if next_fg_col == "" {
if next_bg_col == "" {
ch = " "
} else {
ch = "▄"
}
next_fg_col = next_bg_col
next_bg_col = ""
}
} else {
next_bg_col = next_fg_col
next_fg_col = ""
}
if (next_bg_col == "" && prev_bg_col != "") ||
(next_fg_col == "" && prev_fg_col != "") {
buffer.WriteString(Clear(mode))
prev_bg_col = ""
prev_fg_col = ""
}
if next_fg_col == "" && next_bg_col == "" {
if prev_fg_col != "" || prev_bg_col != "" {
buffer.WriteString(Clear(mode))
}
prev_fg_col = ""
prev_bg_col = ""
} else if prev_fg_col != next_fg_col || prev_bg_col != next_bg_col {
if (mode == irc || mode == irc16 || mode == motd || mode == motd16) || prev_fg_col != next_fg_col {
if (mode == irc || mode == irc16 || mode == motd || mode == motd16) && next_fg_col == "" {
next_fg_col = "0"
}
buffer.WriteString(StartFGColor(mode))
buffer.WriteString(next_fg_col)
buffer.WriteString(EndColor(mode))
prev_fg_col = next_fg_col
}
if next_bg_col != "" && prev_bg_col != next_bg_col {
buffer.WriteString(StartBGColor(mode))
buffer.WriteString(next_bg_col)
buffer.WriteString(EndColor(mode))
prev_bg_col = next_bg_col
}
}
buffer.WriteString(ch)
}
buffer.WriteString(Clear(mode))
buffer.WriteString("\n")
}
return buffer.String()
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment