Adds support for image rendering in the terminal #137
14
client.go
14
client.go
|
@ -929,7 +929,7 @@ func (c *client) Visit(url string) {
|
|||
// +++ Begin Protocol Handlers +++
|
||||
|
||||
func (c *client) handleGopher(u Url) {
|
||||
if u.DownloadOnly {
|
||||
if u.DownloadOnly || (c.Options["showimages"] == "false" && (u.Mime == "I" || u.Mime == "g")) {
|
||||
nameSplit := strings.Split(u.Resource, "/")
|
||||
filename := nameSplit[len(nameSplit)-1]
|
||||
filename = strings.Trim(filename, " \t\r\n\v\f\a")
|
||||
|
@ -947,6 +947,11 @@ func (c *client) handleGopher(u Url) {
|
|||
return
|
||||
}
|
||||
pg := MakePage(u, content, links)
|
||||
if u.Mime == "I" || u.Mime == "g" {
|
||||
pg.FileType = "image"
|
||||
} else {
|
||||
pg.FileType = "text"
|
||||
}
|
||||
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
|
||||
c.PageState.Add(pg)
|
||||
c.SetPercentRead()
|
||||
|
@ -968,8 +973,9 @@ func (c *client) handleGemini(u Url) {
|
|||
case 1:
|
||||
c.search("", u.Full, capsule.Content)
|
||||
case 2:
|
||||
if capsule.MimeMaj == "text" {
|
||||
if capsule.MimeMaj == "text" || (c.Options["showimages"] == "true" && capsule.MimeMaj == "image") {
|
||||
pg := MakePage(u, capsule.Content, capsule.Links)
|
||||
pg.FileType = capsule.MimeMaj
|
||||
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
|
||||
c.PageState.Add(pg)
|
||||
c.SetPercentRead()
|
||||
|
@ -1018,6 +1024,10 @@ func (c *client) handleLocal(u Url) {
|
|||
return
|
||||
}
|
||||
pg := MakePage(u, content, links)
|
||||
ext := strings.ToLower(filepath.Ext(u.Full))
|
||||
if ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".png" {
|
||||
pg.FileType = "image"
|
||||
}
|
||||
pg.WrapContent(c.Width-1, (c.Options["theme"] == "color"))
|
||||
c.PageState.Add(pg)
|
||||
c.SetPercentRead()
|
||||
|
|
|
@ -45,12 +45,13 @@ var defaultOptions = map[string]string{
|
|||
// the "configlocation" as follows:
|
||||
// "configlocation": xdgConfigPath()
|
||||
|
||||
"configlocation": xdgConfigPath(),
|
||||
"defaultscheme": "gopher", // "gopher", "gemini", "http", "https"
|
||||
"homeurl": "gopher://bombadillo.colorfield.space:70/1/user-guide.map",
|
||||
"savelocation": homePath(),
|
||||
"searchengine": "gopher://gopher.floodgap.com:70/7/v2/vs",
|
||||
"showimages": "true",
|
||||
"telnetcommand": "telnet",
|
||||
"configlocation": xdgConfigPath(),
|
||||
"defaultscheme": "gopher", // "gopher", "gemini", "http", "https"
|
||||
"theme": "normal", // "normal", "inverted", "color"
|
||||
"tlscertificate": "",
|
||||
"tlskey": "",
|
||||
|
|
|
@ -131,9 +131,11 @@ func parseMap(text string) (string, []string) {
|
|||
|
||||
if len(line[0]) > 1 {
|
||||
title = line[0][1:]
|
||||
} else if len(line[0]) == 1 {
|
||||
title = ""
|
||||
} else {
|
||||
title = ""
|
||||
line[0] = "i "
|
||||
line[0] = "i"
|
||||
}
|
||||
|
||||
if len(line) < 4 || strings.HasPrefix(line[0], "i") {
|
||||
|
|
3
main.go
3
main.go
|
@ -65,6 +65,7 @@ func validateOpt(opt, val string) bool {
|
|||
"webmode": []string{"none", "gui", "lynx", "w3m", "elinks"},
|
||||
"theme": []string{"normal", "inverse", "color"},
|
||||
"defaultscheme": []string{"gopher", "gemini", "http", "https"},
|
||||
"showimages": []string{"true", "false"},
|
||||
}
|
||||
|
||||
opt = strings.ToLower(opt)
|
||||
|
@ -83,7 +84,7 @@ func validateOpt(opt, val string) bool {
|
|||
|
||||
func lowerCaseOpt(opt, val string) string {
|
||||
switch opt {
|
||||
case "webmode", "theme", "defaultscheme":
|
||||
case "webmode", "theme", "defaultscheme", "showimages":
|
||||
return strings.ToLower(val)
|
||||
default:
|
||||
return val
|
||||
|
|
22
page.go
22
page.go
|
@ -3,6 +3,8 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tildegit.org/sloum/bombadillo/tdiv"
|
||||
)
|
||||
|
||||
//------------------------------------------------\\
|
||||
|
@ -21,6 +23,9 @@ type Page struct {
|
|||
FoundLinkLines []int
|
||||
SearchTerm string
|
||||
SearchIndex int
|
||||
FileType string
|
||||
WrapWidth int
|
||||
Color bool
|
||||
}
|
||||
|
||||
//------------------------------------------------\\
|
||||
|
@ -47,11 +52,24 @@ func (p *Page) ScrollPositionRange(termHeight int) (int, int) {
|
|||
return p.ScrollPosition, end
|
||||
}
|
||||
|
||||
func (p *Page) RenderImage(width int) {
|
||||
w := (width - 5) * 2
|
||||
if w > 300 {
|
||||
w = 300
|
||||
}
|
||||
p.WrappedContent = tdiv.Render([]byte(p.RawContent), w)
|
||||
p.WrapWidth = width
|
||||
}
|
||||
|
||||
// WrapContent performs a hard wrap to the requested
|
||||
// width and updates the WrappedContent
|
||||
// of the Page struct width a string slice
|
||||
// of the wrapped data
|
||||
func (p *Page) WrapContent(width int, color bool) {
|
||||
if p.FileType == "image" {
|
||||
p.RenderImage(width)
|
||||
return
|
||||
}
|
||||
counter := 0
|
||||
var content strings.Builder
|
||||
var esc strings.Builder
|
||||
|
@ -116,6 +134,8 @@ func (p *Page) WrapContent(width int, color bool) {
|
|||
}
|
||||
|
||||
p.WrappedContent = strings.Split(content.String(), "\n")
|
||||
p.WrapWidth = width
|
||||
p.Color = color
|
||||
p.HighlightFoundText()
|
||||
}
|
||||
|
||||
|
@ -165,6 +185,6 @@ func (p *Page) FindText() {
|
|||
|
||||
// MakePage returns a Page struct with default values
|
||||
func MakePage(url Url, content string, links []string) Page {
|
||||
p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0}
|
||||
p := Page{make([]string, 0), content, links, url, 0, make([]int, 0), "", 0, "", 40, false}
|
||||
return p
|
||||
}
|
||||
|
|
4
pages.go
4
pages.go
|
@ -66,7 +66,11 @@ func (p *Pages) Render(termHeight, termWidth int, color bool) []string {
|
|||
}
|
||||
pos := p.History[p.Position].ScrollPosition
|
||||
prev := len(p.History[p.Position].WrappedContent)
|
||||
|
||||
if termWidth != p.History[p.Position].WrapWidth || p.History[p.Position].Color != color {
|
||||
p.History[p.Position].WrapContent(termWidth, color)
|
||||
}
|
||||
|
||||
now := len(p.History[p.Position].WrappedContent)
|
||||
if prev > now {
|
||||
diff := prev - now
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
package tdiv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getBraille(pattern string) (rune, error) {
|
||||
switch pattern {
|
||||
case "000000":
|
||||
return ' ', nil
|
||||
case "100000":
|
||||
return '⠁', nil
|
||||
case "001000":
|
||||
return '⠂', nil
|
||||
case "101000":
|
||||
return '⠃', nil
|
||||
case "000010":
|
||||
return '⠄', nil
|
||||
case "100010":
|
||||
return '⠅', nil
|
||||
case "001010":
|
||||
return '⠆', nil
|
||||
case "101010":
|
||||
return '⠇', nil
|
||||
case "010000":
|
||||
return '⠈', nil
|
||||
case "110000":
|
||||
return '⠉', nil
|
||||
case "011000":
|
||||
return '⠊', nil
|
||||
case "111000":
|
||||
return '⠋', nil
|
||||
case "010010":
|
||||
return '⠌', nil
|
||||
case "110010":
|
||||
return '⠍', nil
|
||||
case "011010":
|
||||
return '⠎', nil
|
||||
case "111010":
|
||||
return '⠏', nil
|
||||
case "000100":
|
||||
return '⠐', nil
|
||||
case "100100":
|
||||
return '⠑', nil
|
||||
case "001100":
|
||||
return '⠒', nil
|
||||
case "101100":
|
||||
return '⠓', nil
|
||||
case "000110":
|
||||
return '⠔', nil
|
||||
case "100110":
|
||||
return '⠕', nil
|
||||
case "001110":
|
||||
return '⠖', nil
|
||||
case "101110":
|
||||
return '⠗', nil
|
||||
case "010100":
|
||||
return '⠘', nil
|
||||
case "110100":
|
||||
return '⠙', nil
|
||||
case "011100":
|
||||
return '⠚', nil
|
||||
case "111100":
|
||||
return '⠛', nil
|
||||
case "010110":
|
||||
return '⠜', nil
|
||||
case "110110":
|
||||
return '⠝', nil
|
||||
case "011110":
|
||||
return '⠞', nil
|
||||
case "111110":
|
||||
return '⠟', nil
|
||||
case "000001":
|
||||
return '⠠', nil
|
||||
case "100001":
|
||||
return '⠡', nil
|
||||
case "001001":
|
||||
return '⠢', nil
|
||||
case "101001":
|
||||
return '⠣', nil
|
||||
case "000011":
|
||||
return '⠤', nil
|
||||
case "100011":
|
||||
return '⠥', nil
|
||||
case "001011":
|
||||
return '⠦', nil
|
||||
case "101011":
|
||||
return '⠧', nil
|
||||
case "010001":
|
||||
return '⠨', nil
|
||||
case "110001":
|
||||
return '⠩', nil
|
||||
case "011001":
|
||||
return '⠪', nil
|
||||
case "111001":
|
||||
return '⠫', nil
|
||||
case "010011":
|
||||
return '⠬', nil
|
||||
case "110011":
|
||||
return '⠭', nil
|
||||
case "011011":
|
||||
return '⠮', nil
|
||||
case "111011":
|
||||
return '⠯', nil
|
||||
case "000101":
|
||||
return '⠰', nil
|
||||
case "100101":
|
||||
return '⠱', nil
|
||||
case "001101":
|
||||
return '⠲', nil
|
||||
case "101101":
|
||||
return '⠳', nil
|
||||
case "000111":
|
||||
return '⠴', nil
|
||||
case "100111":
|
||||
return '⠵', nil
|
||||
case "001111":
|
||||
return '⠶', nil
|
||||
case "101111":
|
||||
return '⠷', nil
|
||||
case "010101":
|
||||
return '⠸', nil
|
||||
case "110101":
|
||||
return '⠹', nil
|
||||
case "011101":
|
||||
return '⠺', nil
|
||||
case "111101":
|
||||
return '⠻', nil
|
||||
case "010111":
|
||||
return '⠼', nil
|
||||
case "110111":
|
||||
return '⠽', nil
|
||||
case "011111":
|
||||
return '⠾', nil
|
||||
case "111111":
|
||||
return '⠿', nil
|
||||
default:
|
||||
return '!', fmt.Errorf("Invalid character entry")
|
||||
}
|
||||
}
|
||||
|
||||
// scaleImage loads and scales an image and returns a 2d pixel-int slice
|
||||
//
|
||||
// Adapted from:
|
||||
// http://tech-algorithm.com/articles/nearest-neighbor-image-scaling/
|
||||
func scaleImage(file io.Reader, newWidth int) (int, int, [][]int, error) {
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
width, height := bounds.Max.X, bounds.Max.Y
|
||||
newHeight := int(float64(newWidth) * (float64(height) / float64(width)))
|
||||
|
||||
out := make([][]int, newHeight)
|
||||
for i := range out {
|
||||
out[i] = make([]int, newWidth)
|
||||
}
|
||||
|
||||
xRatio := float64(width) / float64(newWidth)
|
||||
yRatio := float64(height) / float64(newHeight)
|
||||
var px, py int
|
||||
for i := 0; i < newHeight; i++ {
|
||||
for j := 0; j < newWidth; j++ {
|
||||
px = int(float64(j) * xRatio)
|
||||
py = int(float64(i) * yRatio)
|
||||
out[i][j] = rgbaToGray(img.At(px, py).RGBA())
|
||||
}
|
||||
}
|
||||
return newWidth, newHeight, out, nil
|
||||
}
|
||||
|
||||
// Get the bi-dimensional pixel array
|
||||
func getPixels(file io.Reader) (int, int, [][]int, error) {
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return 0, 0, nil, err
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
width, height := bounds.Max.X, bounds.Max.Y
|
||||
|
||||
var pixels [][]int
|
||||
for y := 0; y < height; y++ {
|
||||
var row []int
|
||||
for x := 0; x < width; x++ {
|
||||
row = append(row, rgbaToGray(img.At(x, y).RGBA()))
|
||||
}
|
||||
pixels = append(pixels, row)
|
||||
}
|
||||
|
||||
return width, height, pixels, nil
|
||||
}
|
||||
|
||||
func errorDither(w, h int, p [][]int) [][]int {
|
||||
mv := [4][2]int{
|
||||
[2]int{0, 1},
|
||||
[2]int{1, 1},
|
||||
[2]int{1, 0},
|
||||
[2]int{1, -1},
|
||||
}
|
||||
per := [4]float64{0.4375, 0.0625, 0.3125, 0.1875}
|
||||
var res, diff int
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
cur := p[y][x]
|
||||
if cur > 128 {
|
||||
res = 1
|
||||
diff = -(255 - cur)
|
||||
} else {
|
||||
res = 0
|
||||
diff = cur // TODO see why this was abs() in the py version
|
||||
}
|
||||
for i, v := range mv {
|
||||
if y+v[0] >= h || x+v[1] >= w || x+v[1] <= 0 {
|
||||
continue
|
||||
}
|
||||
px := p[y+v[0]][x+v[1]]
|
||||
px = int(float64(diff)*per[i] + float64(px))
|
||||
if px < 0 {
|
||||
px = 0
|
||||
} else if px > 255 {
|
||||
px = 255
|
||||
}
|
||||
p[y+v[0]][x+v[1]] = px
|
||||
p[y][x] = res
|
||||
}
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func toBraille(p [][]int) []rune {
|
||||
w := len(p[0]) // TODO this is unsafe
|
||||
h := len(p)
|
||||
rows := h / 3
|
||||
cols := w / 2
|
||||
out := make([]rune, rows*(cols+1))
|
||||
counter := 0
|
||||
for y := 0; y < h-3; y += 4 {
|
||||
for x := 0; x < w-1; x += 2 {
|
||||
str := fmt.Sprintf(
|
||||
"%d%d%d%d%d%d",
|
||||
p[y][x], p[y][x+1],
|
||||
p[y+1][x], p[y+1][x+1],
|
||||
p[y+2][x], p[y+2][x+1])
|
||||
b, err := getBraille(str)
|
||||
if err != nil {
|
||||
out[counter] = ' '
|
||||
} else {
|
||||
out[counter] = b
|
||||
}
|
||||
counter++
|
||||
}
|
||||
out[counter] = '\n'
|
||||
counter++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func rgbaToGray(r uint32, g uint32, b uint32, a uint32) int {
|
||||
rf := float64(r/257) * 0.92126
|
||||
gf := float64(g/257) * 0.97152
|
||||
bf := float64(b/257) * 0.90722
|
||||
grey := int((rf + gf + bf) / 3)
|
||||
return grey
|
||||
}
|
||||
|
||||
func Render(in []byte, width int) []string {
|
||||
image.RegisterFormat("jpeg", "jpeg", jpeg.Decode, jpeg.DecodeConfig)
|
||||
image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig)
|
||||
image.RegisterFormat("gif", "gif", gif.Decode, gif.DecodeConfig)
|
||||
w, h, p, err := scaleImage(bytes.NewReader(in), width)
|
||||
|
||||
if err != nil {
|
||||
return []string{"Unable to render image.", "Please download using:", "", " :w ."}
|
||||
}
|
||||
px := errorDither(w, h, p)
|
||||
b := toBraille(px)
|
||||
out := strings.SplitN(string(b), "\n", -1)
|
||||
return out
|
||||
}
|
Loading…
Reference in New Issue