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()) }