commit:db77dfae0be27796d02c1d5406bfc43a5eab3a93
author:Chip Black
committer:Chip Black
date:Mon Oct 26 02:42:26 2015 -0500
parents:3ec508b7c46a608066b608f0b39052927c6971e8
Add automatic palette/size fixing

In particular:

- Automatically pad the width to one of our allowed sizes
- Convert the given image to a predefined palette
- Allow the user to manually select a palette
diff --git a/bitsmash.go b/bitsmash.go
line changes: +75/-33
index b5d62d8..29b5f62
--- a/bitsmash.go
+++ b/bitsmash.go
@@ -4,10 +4,9 @@ import (
     "errors"
     "fmt"
     "image"
-    "image/color"
+    "image/draw"
     "io"
     "math"
-    "reflect"
     "log"
     "os"
     "sort"
@@ -15,6 +14,7 @@ import (
     _ "image/png"
 
     "bytex64.net/code/bitsmash/packet"
+    "bytex64.net/code/bitsmash/palette"
     "bytex64.net/code/bitsmash/ac"
 )
 
@@ -55,15 +55,48 @@ func sizeIndex(s int) int {
     return -1
 }
 
+type EncodeOpts struct {
+    optimize int
+    palette int
+}
+
 type BitSmash struct {
     size int
     palette int
     transparent bool
-    n_colors uint
     packetList []packet.Packet
 }
 
-func loadImage(filename string) image.PalettedImage {
+func findPalette(img image.Image) int {
+    selected := 0
+    minimum_error := float64(1e99)
+    size := img.Bounds().Size()
+
+    for i, p := range(palette.Palettes) {
+        error := float64(0)
+        for y := 0; y < size.Y; y++ {
+            for x := 0; x < size.X; x++ {
+                pixel_color := img.At(x, y)
+                _, _, _, a := pixel_color.RGBA()
+                if a < 0x7FFF {
+                    // Don't calculate transparent pixels
+                    continue
+                }
+                palette_color := p.Convert(pixel_color)
+                error += palette_color.(palette.RGB8).Distance(pixel_color)
+            }
+        }
+        log.Printf("Palette %d error %f\n", i, error);
+        if error < minimum_error {
+            selected = i
+            minimum_error = error
+        }
+    }
+
+    return selected
+}
+
+func loadImage(filename string, opts *EncodeOpts) image.PalettedImage {
     reader, err := os.Open(filename)
     if err != nil {
         log.Fatal(err)
@@ -76,19 +109,38 @@ func loadImage(filename string) image.PalettedImage {
     }
 
     size := img.Bounds().Size()
-    if !validSize(size.X) {
-        log.Fatal(fmt.Sprintf("Image dimensions must be one of %v", sizes))
+    if size.X > 128 {
+        log.Fatal("Image width must be less than 128 pixels")
     }
 
-    cm := img.ColorModel()
-    if reflect.TypeOf(cm).String() != "color.Palette" {
-        log.Fatal("Image must be paletted")
+    // Pad the width to one of our valid sizes
+    if !validSize(size.X) {
+        var newWidth int
+        for _, s := range(sizes) {
+            if size.X < s {
+                newWidth = s
+                break
+            }
+        }
+        log.Printf("Padding image width %d to %d pixels", size.X, newWidth)
+        r := image.Rect(0, 0, newWidth, size.Y)
+        newImage := image.NewRGBA(r)
+        draw.Draw(newImage, r, image.Transparent, image.ZP, draw.Src)
+        draw.Draw(newImage, img.Bounds(), img, image.ZP, draw.Src)
+        img = newImage
     }
-    if len(cm.(color.Palette)) > 16 {
-        log.Fatal("Palette must be 16-color or less")
+
+    // If no palette has been specified, automatically select one
+    if (opts.palette == -1) {
+        opts.palette = findPalette(img)
     }
+    log.Printf("Using palette %d\n", opts.palette);
 
-    return img.(image.PalettedImage)
+    // Convert to the selected palette
+    pImage := image.NewPaletted(img.Bounds(), palette.Palettes[opts.palette])
+    draw.Draw(pImage, img.Bounds(), img, image.ZP, draw.Src)
+
+    return pImage
 }
 
 func findAllEncodeLengths(pixels []uint8, steps int) []packet.PacketRun {
@@ -175,25 +227,17 @@ func getPixelList(img image.PalettedImage) []uint8 {
     return p.Pix
 }
 
-func NewFromPixels(pixels []uint8, size int, optimize int) BitSmash {
-    highest_color := byte(0)
-    for _, p := range(pixels) {
-        if p > highest_color {
-            highest_color = p
-        }
-    }
-        
+func NewFromPixels(pixels []uint8, size int, opts *EncodeOpts) BitSmash {
     bs := BitSmash{
         size,
-        0,
+        opts.palette,
         false,
-        uint(highest_color + 1),
         make([]packet.Packet, 0, 1),
     }
 
     i := 0
     for i < len(pixels) {
-        packet := findOptimalEncoding(pixels[i:], optimize)
+        packet := findOptimalEncoding(pixels[i:], opts.optimize)
         i += packet.Length()
         bs.packetList = append(bs.packetList, packet)
     }
@@ -203,10 +247,10 @@ func NewFromPixels(pixels []uint8, size int, optimize int) BitSmash {
     return bs
 }
 
-func NewFromImage(filename string, optimize int) BitSmash {
-    image := loadImage(filename)
+func NewFromImage(filename string, opts *EncodeOpts) BitSmash {
+    image := loadImage(filename, opts)
     size := image.Bounds().Size().X
-    return NewFromPixels(getPixelList(image), size, optimize)
+    return NewFromPixels(getPixelList(image), size, opts)
 }
 
 func (self *BitSmash) Size() image.Point {
@@ -249,18 +293,17 @@ func NewFromBSFile(filename string) BitSmash {
         0,
         0,
         false,
-        0,
         nil,
     }
 
     file, err := os.Open(filename)
     defer file.Close()
     if err != nil {
-        log.Fatal(fmt.Sprintf("Failed to open: %v", err))
+        log.Fatalf("Failed to open: %v", err)
     }
     err = bs.ReadFrom(file)
     if err != nil {
-        log.Fatal(fmt.Sprintf("Reading file: %v", err))
+        log.Fatalf("Reading file: %v", err)
     }
 
     return bs
@@ -268,7 +311,7 @@ func NewFromBSFile(filename string) BitSmash {
 
 func (self *BitSmash) codecManagerInit(c ac.CodecManager) {
     c.AddModel(packet.CONTEXT_PACKET_TYPE,     ac.NewModelOrder0(4))
-    c.AddModel(packet.CONTEXT_COLOR,           ac.NewModelOrder0(self.n_colors))
+    c.AddModel(packet.CONTEXT_COLOR,           ac.NewModelOrder0(16))
     c.AddModel(packet.CONTEXT_PATTERN_NUMBER,  ac.NewModelOrder0PreBias(16, packet.ContextBias(packet.CONTEXT_PATTERN_NUMBER)))
     c.AddModel(packet.CONTEXT_PATTERN_REPEAT,  ac.NewModelOrder0PreBias(8,  packet.ContextBias(packet.CONTEXT_PATTERN_REPEAT)))
     c.AddModel(packet.CONTEXT_RLE_REPEAT,      ac.NewModelOrder0(16))
@@ -289,7 +332,6 @@ func (self *BitSmash) ReadFrom(file io.Reader) error {
     self.transparent = ((header[0] >> 7) & 0x1) == 1
 
     n_packets := int(header[1]) + (int(header[2]) & 0x3) + 1
-    self.n_colors = (uint(header[2]) >> 2) & 0xF
     self.packetList = make([]packet.Packet, n_packets)
 
     decoder, err := ac.NewDecoder(file)
@@ -330,7 +372,7 @@ func (self *BitSmash) WriteTo(file io.Writer) error {
     }
     header[0] = byte(sizeIndex(self.size) + (self.palette << 3) + (transparent << 7))
     header[1] = byte((n_packets - 1) & 0xFF)
-    header[2] = byte(((n_packets - 1) >> 8) & 0x3 + (int(self.n_colors) << 2))
+    header[2] = byte(((n_packets - 1) >> 8) & 0x3)
 
     file.Write(header[:])
 
@@ -352,7 +394,7 @@ func (self *BitSmash) WriteTo(file io.Writer) error {
 func (self *BitSmash) Dump() {
     self.unpackRepeat()
     size := self.Size()
-    fmt.Printf("%d pixels, %d colors, %dx%d\n", self.Length(), self.n_colors, size.X, size.Y)
+    fmt.Printf("%d pixels, %dx%d\n", self.Length(), size.X, size.Y)
     fmt.Printf("%d packets:\n", len(self.packetList))
 
     c := 0

diff --git a/main.go b/main.go
line changes: +5/-3
index 07d716a..593fff8
--- a/main.go
+++ b/main.go
@@ -13,15 +13,17 @@ type BitsmashOpts struct {
     dump bool
     base64 bool
     files []string
-    opt int
+    encodeOpts EncodeOpts
 }
 
 func parseArgs() BitsmashOpts {
     bo := BitsmashOpts{}
+    bo.encodeOpts = EncodeOpts{}
     flag.BoolVar(&bo.decode, "decode", false, "Decode from bitsmash input rather than encode image")
     flag.BoolVar(&bo.dump,   "dump",   false, "dump detailed information about encoding")
     flag.BoolVar(&bo.base64, "base64", false, "Encode the result with base64")
-    flag.IntVar(&bo.opt,     "o",      3,     "Optimization level")
+    flag.IntVar(&bo.encodeOpts.optimize,    "o",      3,     "Optimization level")
+    flag.IntVar(&bo.encodeOpts.palette,     "p",      -1,    "Force palette N")
     flag.Parse()
     bo.files = flag.Args()
     return bo
@@ -38,7 +40,7 @@ func main() {
     if (args.decode) {
         smash = NewFromBSFile(args.files[0])
     } else {
-        smash = NewFromImage(args.files[0], args.opt)
+        smash = NewFromImage(args.files[0], &args.encodeOpts)
     }
 
     if args.dump {