322 lines
7.2 KiB
Go
322 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"tildegit.org/sloum/pbmview/termios"
|
|
)
|
|
|
|
const (
|
|
grays float64 = 23.0
|
|
colors float64 = 255.0
|
|
grayOffset int = 232
|
|
)
|
|
|
|
var maxFactor float64 = 23.0
|
|
|
|
type rgb struct {
|
|
r int
|
|
g int
|
|
b int
|
|
}
|
|
|
|
type colorCell struct {
|
|
top rgb
|
|
bottom rgb
|
|
}
|
|
|
|
func (c *colorCell) String() string {
|
|
format := "\033[38;2;%d;%d;%dm\033[48;2;%d;%d;%dm▀\033[0m"
|
|
topR := int(float64(c.top.r) * maxFactor)
|
|
topG := int(float64(c.top.g) * maxFactor)
|
|
topB := int(float64(c.top.b) * maxFactor)
|
|
bottomR := int(float64(c.bottom.r) * maxFactor)
|
|
bottomG := int(float64(c.bottom.g) * maxFactor)
|
|
bottomB := int(float64(c.bottom.b) * maxFactor)
|
|
return fmt.Sprintf(format, topR, topG, topB, bottomR, bottomG, bottomB)
|
|
}
|
|
|
|
type cell struct {
|
|
top int
|
|
bottom int
|
|
}
|
|
|
|
func (c *cell) String() string {
|
|
format := "\033[38;5;%dm\033[48;5;%dm▀\033[0m"
|
|
top := int(float64(c.top) * maxFactor) + grayOffset
|
|
bottom := int(float64(c.bottom) * maxFactor) + grayOffset
|
|
return fmt.Sprintf(format, top, bottom)
|
|
}
|
|
|
|
|
|
type image struct {
|
|
width int
|
|
height int
|
|
pbmType string
|
|
maxVal int
|
|
maxFactor float64
|
|
|
|
colorImage [][]colorCell
|
|
image [][]cell
|
|
// screenCols int
|
|
// screenRows int
|
|
// colOff int
|
|
// rowOff int
|
|
}
|
|
|
|
func (i *image) String() string {
|
|
var out strings.Builder
|
|
if i.pbmType != "P3" {
|
|
for r := range i.image {
|
|
for _, c := range i.image[r] {
|
|
out.WriteString(c.String())
|
|
}
|
|
out.WriteRune('\n')
|
|
}
|
|
} else {
|
|
for r := range i.colorImage {
|
|
for _, c := range i.colorImage[r] {
|
|
out.WriteString(c.String())
|
|
}
|
|
out.WriteRune('\n')
|
|
}
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
func (i *image) SetType(t string) error {
|
|
if i.pbmType != "" {
|
|
return fmt.Errorf("PBM type already set")
|
|
}
|
|
|
|
t = strings.TrimSpace(strings.ToUpper(t))
|
|
switch t {
|
|
case "P1", "P2", "P3":
|
|
i.pbmType = t
|
|
i.maxVal = 1
|
|
return nil
|
|
case "P4", "P5", "P6", "P7":
|
|
return fmt.Errorf("%s is not yet supported", t)
|
|
default:
|
|
return fmt.Errorf("Invalid pbm type: %q", t)
|
|
}
|
|
}
|
|
|
|
func (i *image) SetSize(s string, height bool) error {
|
|
v, err := strconv.Atoi(s)
|
|
if err != nil || v < 1 {
|
|
return fmt.Errorf("Invalid dimension value: %s", s)
|
|
}
|
|
if height {
|
|
i.height = v
|
|
} else {
|
|
i.width = v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (im *image) BuildColorCells(bm []int) {
|
|
colorSlice := make([]rgb, 0, len(bm)/3)
|
|
for i := 0; i < len(bm); i+=3 {
|
|
colorSlice = append(colorSlice, rgb{bm[i], bm[i+1], bm[i+2]})
|
|
}
|
|
im.colorImage = make([][]colorCell, im.height/2)
|
|
for row := 0; row < len(im.colorImage); row++ {
|
|
for col := 0; col < im.width; col++ {
|
|
im.colorImage[row] = append(im.colorImage[row], colorCell{colorSlice[row*2*im.width+col], rgb{}})
|
|
bottomIndex := (row*2+1)*im.width+col
|
|
if bottomIndex < len(colorSlice) {
|
|
im.colorImage[row][col].bottom = colorSlice[bottomIndex]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (i *image) BuildCells(bm []int) {
|
|
i.image = make([][]cell, i.height/2)
|
|
for row := 0; row < len(i.image); row++ {
|
|
for col := 0; col < i.width; col++ {
|
|
i.image[row] = append(i.image[row], cell{bm[row*2*i.width+col], 0})
|
|
bottomIndex := (row*2+1)*i.width+col
|
|
if bottomIndex < len(bm) {
|
|
i.image[row][col].bottom = bm[bottomIndex]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO this works for shrinking mostly, but blowing up is broken
|
|
// particularly for small images. This is due to rows being combined
|
|
// in pairs. Maybe this processing can happen elsewhere?
|
|
func (i *image) ScaleColor(newWidth int) {
|
|
newHeight := int(float64(newWidth) * (float64(i.height) / float64(i.width))) / 2
|
|
|
|
out := make([][]colorCell, newHeight)
|
|
for i := range out {
|
|
out[i] = make([]colorCell, newWidth)
|
|
}
|
|
|
|
xRatio := float64(i.width) / float64(newWidth)
|
|
yRatio := float64(i.height/2) / float64(newHeight)
|
|
var px, py int
|
|
for x := 0; x < newHeight; x++ {
|
|
for y := 0; y < newWidth; y++ {
|
|
px = int(float64(y) * xRatio)
|
|
py = int(float64(x) * yRatio)
|
|
out[x][y] = i.colorImage[py][px]
|
|
}
|
|
}
|
|
i.colorImage = out
|
|
}
|
|
|
|
// TODO this works for shrinking mostly, but blowing up is broken
|
|
// particularly for small images. This is due to rows being combined
|
|
// in pairs. Maybe this processing can happen elsewhere?
|
|
func (i *image) Scale(newWidth int) {
|
|
newHeight := int(float64(newWidth) * (float64(i.height) / float64(i.width))) / 2
|
|
|
|
out := make([][]cell, newHeight)
|
|
for i := range out {
|
|
out[i] = make([]cell, newWidth)
|
|
}
|
|
|
|
xRatio := float64(i.width) / float64(newWidth)
|
|
yRatio := float64(i.height/2) / float64(newHeight)
|
|
var px, py int
|
|
for x := 0; x < newHeight; x++ {
|
|
for y := 0; y < newWidth; y++ {
|
|
px = int(float64(y) * xRatio)
|
|
py = int(float64(x) * yRatio)
|
|
out[x][y] = i.image[py][px]
|
|
}
|
|
}
|
|
i.image = out
|
|
}
|
|
|
|
func NewImage(path string) (image, error) {
|
|
if path == "" {
|
|
HandleError(fmt.Errorf("No filepath provided"))
|
|
}
|
|
path = ExpandedAbsFilepath(path)
|
|
fbytes, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
fmt.Fprint(os.Stderr, "Unable to read file\n")
|
|
os.Exit(1)
|
|
}
|
|
img := image{}
|
|
img.maxVal = 1
|
|
fdata := strings.Split(string(fbytes), "\n")
|
|
var bitmap []int
|
|
var intval int
|
|
maxSet := false
|
|
for _, line := range fdata {
|
|
if len(line) < 1 || line[0] == '#' {
|
|
continue
|
|
}
|
|
if ind := strings.Index(line, "#"); ind != -1 {
|
|
line = line[:ind]
|
|
}
|
|
fields := strings.Fields(line)
|
|
for _, field := range fields {
|
|
if img.pbmType == "" {
|
|
HandleError(img.SetType(field))
|
|
} else if img.width == 0 {
|
|
HandleError(img.SetSize(field, false))
|
|
} else if img.height == 0 {
|
|
HandleError(img.SetSize(field, true))
|
|
bitmap = make([]int, 0, img.width*img.height)
|
|
} else if img.pbmType != "P1" && !maxSet {
|
|
maxSet = true
|
|
mv, err := strconv.Atoi(field)
|
|
if err != nil {
|
|
HandleError(fmt.Errorf("Invalud max value: %s", field))
|
|
}
|
|
img.maxVal = mv
|
|
if img.pbmType == "P3" {
|
|
maxFactor = colors / float64(img.maxVal)
|
|
} else {
|
|
maxFactor = grays / float64(img.maxVal)
|
|
}
|
|
} else {
|
|
intval, err = strconv.Atoi(field)
|
|
if err != nil {
|
|
HandleError(err)
|
|
}
|
|
bitmap = append(bitmap, intval)
|
|
}
|
|
}
|
|
}
|
|
multiplier := 1
|
|
if img.pbmType == "P3" {
|
|
multiplier = 3
|
|
}
|
|
if len(bitmap) != img.width * img.height * multiplier {
|
|
// TODO remove this Printf once debugging is complete
|
|
fmt.Printf("W: %d H: %d Vals: %d\n%v", img.width, img.height, len(bitmap), bitmap)
|
|
HandleError(fmt.Errorf("Corrupt bitmap data"))
|
|
}
|
|
if img.pbmType == "P3" {
|
|
img.BuildColorCells(bitmap)
|
|
} else {
|
|
img.BuildCells(bitmap)
|
|
}
|
|
|
|
return img, nil
|
|
}
|
|
|
|
func ExpandedAbsFilepath(p string) string {
|
|
if strings.HasPrefix(p, "~/") {
|
|
usr, _ := user.Current()
|
|
homedir := usr.HomeDir
|
|
p = filepath.Join(homedir, p[2:])
|
|
}
|
|
|
|
path, _ := filepath.Abs(p)
|
|
return path
|
|
}
|
|
|
|
func HandleError(err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
fmt.Fprintf(os.Stderr, err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
|
|
func main() {
|
|
fitToWidth := flag.Bool("fit", false, "Scale the image to fit the terminal width")
|
|
scaleToWidth := flag.Int("scale", -1, "Scale the image to the given width")
|
|
flag.Parse()
|
|
img, err := NewImage(flag.Arg(0))
|
|
if err != nil {
|
|
HandleError(err)
|
|
}
|
|
cols := -1
|
|
if *fitToWidth {
|
|
cols, _ = termios.GetWindowSize()
|
|
if img.width < cols {
|
|
cols = -1
|
|
}
|
|
} else if *scaleToWidth != -1 {
|
|
cols = *scaleToWidth
|
|
}
|
|
|
|
if cols != -1 {
|
|
if img.pbmType == "P3" {
|
|
img.ScaleColor(cols)
|
|
} else {
|
|
img.Scale(cols)
|
|
}
|
|
}
|
|
|
|
fmt.Print(img.String())
|
|
}
|