-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathblock.go
More file actions
182 lines (156 loc) · 4.37 KB
/
Copy pathblock.go
File metadata and controls
182 lines (156 loc) · 4.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package image
import (
"bytes"
"fmt"
"image"
"image/color"
"io"
"os"
"runtime"
"strings"
"golang.org/x/term"
)
const (
upperHalfBlock = "\u2580" // Unicode: upper half block
lowerHalfBlock = "\u2584" // Unicode: lower half block
)
// rgbColor holds 8-bit RGB values.
type rgbColor struct {
r, g, b int
}
// writeBlocks resizes the image and outputs it as terminal blocks.
func writeBlocks(img image.Image) error {
trueColor := supportsTrueColor()
termWidth, termHeight, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
return err
}
cols, rows := imageBlockOutputDimensions(img, termWidth, termHeight)
// Each terminal block represents 2 vertical pixels.
targetWidth := cols
targetHeight := rows * 2
dst := resizeImage(img, targetWidth, targetHeight)
// Process the image in blocks (each block = two vertical pixels).
var out bytes.Buffer
for row := range rows {
topY := row * 2
bottomY := topY + 1
for x := range cols {
top := pixelToColor(dst.At(x, topY))
var bottom *rgbColor
if bottomY < targetHeight {
bottom = pixelToColor(dst.At(x, bottomY))
}
writeBlock(&out, top, bottom, trueColor)
}
out.WriteString("\n")
}
// Reset ANSI formatting at the end.
out.WriteString("\x1b[0m")
out.WriteTo(os.Stdout)
return nil
}
// supportsTrueColor checks the current terminal emulator for true color support.
func supportsTrueColor() bool {
ct := os.Getenv("COLORTERM")
if strings.EqualFold(ct, "truecolor") || strings.EqualFold(ct, "24bit") {
return true
}
if runtime.GOOS == "windows" {
return os.Getenv("WT_SESSION") != "" || os.Getenv("ConEmuANSI") == "ON"
}
return false
}
// imageBlockOutputDimensions returns the desired number of block columns and rows.
// (Each block represents two vertical pixels.)
func imageBlockOutputDimensions(img image.Image, termWidth, termHeight int) (int, int) {
// Use only 4/5ths of the terminal height.
cols := termWidth
rows := 2 * termHeight * 4 / 5
bounds := img.Bounds()
width, height := bounds.Dx(), bounds.Dy()
// If image is smaller than bounds, return the scaled image dimensions.
if width <= cols && height <= rows {
return width, height/2 + height%2
}
// Otherwise calculate appropriate size.
if cols*height <= width*rows {
return cols, max((height*cols)/width/2, 1)
}
return (width * rows) / height, max(rows/2, 1)
}
// pixelToColor converts a color.Color into a *rgbColor (or nil if fully transparent).
func pixelToColor(c color.Color) *rgbColor {
r, g, b, a := c.RGBA()
if a == 0 {
return nil
}
// RGBA returns values in [0, 65535]; shift down to 8 bits.
return &rgbColor{int(r >> 8), int(g >> 8), int(b >> 8)}
}
// writeBlock writes the ANSI-coded string for one block given the top and
// bottom pixel colors.
func writeBlock(buf *bytes.Buffer, top, bottom *rgbColor, trueColor bool) {
// Both parts transparent.
if top == nil && bottom == nil {
buf.WriteString(" ")
return
}
// If there is no bottom pixel (or it's transparent), use the upper half block.
var ch string
if bottom == nil {
ansiFG(buf, top, trueColor)
ch = upperHalfBlock
} else if top == nil {
// Only bottom has color.
ansiFG(buf, bottom, trueColor)
ch = lowerHalfBlock
} else {
// Both have a color: use lower half block with top as background and bottom as foreground.
ansiBG(buf, top, trueColor)
ansiFG(buf, bottom, trueColor)
ch = lowerHalfBlock
}
// Reset after this block.
buf.WriteString(ch)
buf.WriteString("\x1b[0m")
}
// ansiFG returns the ANSI escape code for setting the foreground color.
func ansiFG(w io.Writer, c *rgbColor, trueColor bool) {
if c == nil {
return
}
if trueColor {
fmt.Fprintf(w, "\x1b[38;2;%d;%d;%dm", c.r, c.g, c.b)
} else {
fmt.Fprintf(w, "\x1b[38;5;%dm", ansi256FromRGB(c.r, c.g, c.b))
}
}
// ansiBG returns the ANSI escape code for setting the background color.
func ansiBG(w io.Writer, c *rgbColor, trueColor bool) {
if c == nil {
return
}
if trueColor {
fmt.Fprintf(w, "\x1b[48;2;%d;%d;%dm", c.r, c.g, c.b)
} else {
fmt.Fprintf(w, "\x1b[48;5;%dm", ansi256FromRGB(c.r, c.g, c.b))
}
}
// ansi256FromRGB converts an RGB triplet to an ANSI 256 color index.
func ansi256FromRGB(r, g, b int) int {
// Grayscale range.
if r == g && g == b {
if r < 8 {
return 16
}
if r > 248 {
return 231
}
return int((float64(r)-8)/10.0) + 232
}
red := r * 5 / 255
green := g * 5 / 255
blue := b * 5 / 255
return 16 + 36*red + 6*green + blue
}