pbmview/main.go

312 lines
6.8 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]
}
}
}
}
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
}
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)
}
if *fitToWidth || *scaleToWidth != -1 {
cols, _ := termios.GetWindowSize()
if img.width < cols {
cols = img.width
}
if *scaleToWidth != -1 {
cols = *scaleToWidth
}
if img.pbmType == "P3" {
img.ScaleColor(cols)
} else {
img.Scale(cols)
}
}
fmt.Print(img.String())
}