commit:0d77ac0596d5da46ee67a9f48c9ee9e34f996dab
author:Chip Black
committer:Chip Black
date:Sat Nov 21 23:41:53 2015 -0600
parents:db77dfae0be27796d02c1d5406bfc43a5eab3a93
Refactor bitsmash into its own package
diff --git a/bitsmash.go b/bitsmash.go
line changes: +0/-426
index 29b5f62..0000000
--- a/bitsmash.go
+++ /dev/null
@@ -1,426 +0,0 @@
-package main
-
-import (
-    "errors"
-    "fmt"
-    "image"
-    "image/draw"
-    "io"
-    "math"
-    "log"
-    "os"
-    "sort"
-
-    _ "image/png"
-
-    "bytex64.net/code/bitsmash/packet"
-    "bytex64.net/code/bitsmash/palette"
-    "bytex64.net/code/bitsmash/ac"
-)
-
-type scanFunc func(pixels []uint8) packet.Packet
-
-var scanFuncs []scanFunc = []scanFunc {
-    packet.ScanRLE,
-    packet.ScanBinaryPattern,
-    packet.ScanDirect,
-}
-
-var sizes []int = []int{
-    8,
-    16,
-    24,
-    32,
-    48,
-    64,
-    96,
-    128,
-}
-
-func validSize(s int) bool {
-    for _, n := range(sizes) {
-        if n == s {
-            return true
-        }
-    }
-    return false
-}
-
-func sizeIndex(s int) int {
-    for i, n := range(sizes) {
-        if n == s {
-            return i
-        }
-    }
-    return -1
-}
-
-type EncodeOpts struct {
-    optimize int
-    palette int
-}
-
-type BitSmash struct {
-    size int
-    palette int
-    transparent bool
-    packetList []packet.Packet
-}
-
-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)
-    }
-    defer reader.Close()
-
-    img, _, err := image.Decode(reader)
-    if err != nil {
-        log.Fatal(err)
-    }
-
-    size := img.Bounds().Size()
-    if size.X > 128 {
-        log.Fatal("Image width must be less than 128 pixels")
-    }
-
-    // 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 no palette has been specified, automatically select one
-    if (opts.palette == -1) {
-        opts.palette = findPalette(img)
-    }
-    log.Printf("Using palette %d\n", opts.palette);
-
-    // 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 {
-    results := make([]packet.PacketRun, 0)
-    if len(pixels) == 0 {
-        return results
-    }
-
-    for _, scan := range(scanFuncs) {
-        pkt := scan(pixels)
-        if pkt == nil {
-            continue
-        }
-        pkt_length := pkt.Length()
-        pkt_bytes := pkt.ByteLength()
-        if steps > 1 && len(pixels) > pkt_length {
-            rec_results := findAllEncodeLengths(pixels[pkt_length:], steps - 1)
-            for _, r := range(rec_results) {
-                results = append(results, packet.PacketRun{r.Length + pkt_length, r.Bytes + pkt_bytes, r.Depth + 1, pkt})
-            }
-        } else {
-            results = append(results, packet.PacketRun{pkt_length, pkt_bytes, 1, pkt})
-        }
-    }
-
-    return results
-}
-
-func findOptimalEncoding(pixels []uint8, optimize int) packet.Packet {
-    results := findAllEncodeLengths(pixels, optimize)
-    sort.Sort(packet.ByPacketEfficiency(results))
-    /*
-    fmt.Printf("optimal encodings (%d bytes remain):\n", len(pixels))
-    for _, v := range(results) {
-        fmt.Println(v)
-    }
-    */
-    return results[0].Head
-}
-
-func findRepeats(packetlist []packet.Packet) []packet.Packet {
-    outlist := make([]packet.Packet, 1, len(packetlist))
-    outlist[0] = packetlist[0]
-
-    for i := 1; i < len(packetlist); {
-        pkt := packet.ScanRepeat(packetlist, i)
-        if pkt != nil {
-            outlist = append(outlist, pkt)
-            rpkt := pkt.(*packet.RepeatPacket)
-            i += rpkt.Count()
-        } else {
-            outlist = append(outlist, packetlist[i])
-            i++
-            continue
-        }
-    }
-
-    return outlist
-}
-
-func (self *BitSmash) unpackRepeat() []packet.Packet {
-    outlist := make([]packet.Packet, 1)
-    outlist[0] = self.packetList[0]
-    
-    c := 1
-    for _, p := range(self.packetList) {
-        switch p.(type) {
-        case *packet.RepeatPacket:
-            rp := p.(*packet.RepeatPacket)
-            outlist = rp.UnRepeat(outlist, c)
-            c += rp.Count()
-        default:
-            outlist = append(outlist, p)
-            c++
-        }
-    }
-
-    return outlist
-}
-
-func getPixelList(img image.PalettedImage) []uint8 {
-    p := img.(*image.Paletted)
-
-    return p.Pix
-}
-
-func NewFromPixels(pixels []uint8, size int, opts *EncodeOpts) BitSmash {
-    bs := BitSmash{
-        size,
-        opts.palette,
-        false,
-        make([]packet.Packet, 0, 1),
-    }
-
-    i := 0
-    for i < len(pixels) {
-        packet := findOptimalEncoding(pixels[i:], opts.optimize)
-        i += packet.Length()
-        bs.packetList = append(bs.packetList, packet)
-    }
-
-    bs.packetList = findRepeats(bs.packetList)
-
-    return bs
-}
-
-func NewFromImage(filename string, opts *EncodeOpts) BitSmash {
-    image := loadImage(filename, opts)
-    size := image.Bounds().Size().X
-    return NewFromPixels(getPixelList(image), size, opts)
-}
-
-func (self *BitSmash) Size() image.Point {
-    return image.Point{self.size, self.Length() / self.size}
-}
-
-func (self *BitSmash) Length() int {
-    n := 0
-    for _, p := range(self.packetList) {
-        n += p.Length()
-    }
-    return n
-}
-
-func (self *BitSmash) ByteLength() int {
-    n_pd := int(math.Ceil(float64(len(self.packetList)) / 4.0))
-    total_len := n_pd + 3
-    for _, p := range(self.packetList) {
-        total_len += p.ByteLength()
-    }
-    return total_len
-}
-
-func (self *BitSmash) packetType(n int) int {
-    switch self.packetList[n].(type) {
-    case *packet.RLEPacket:
-        return 0
-    case *packet.BinaryPatternPacket:
-        return 1
-    case *packet.DirectPacket:
-        return 2
-    case *packet.RepeatPacket:
-        return 3
-    }
-    return -1
-}
-
-func NewFromBSFile(filename string) BitSmash {
-    bs := BitSmash{
-        0,
-        0,
-        false,
-        nil,
-    }
-
-    file, err := os.Open(filename)
-    defer file.Close()
-    if err != nil {
-        log.Fatalf("Failed to open: %v", err)
-    }
-    err = bs.ReadFrom(file)
-    if err != nil {
-        log.Fatalf("Reading file: %v", err)
-    }
-
-    return bs
-}
-
-func (self *BitSmash) codecManagerInit(c ac.CodecManager) {
-    c.AddModel(packet.CONTEXT_PACKET_TYPE,     ac.NewModelOrder0(4))
-    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))
-    c.AddModel(packet.CONTEXT_REPEAT_DISTANCE, ac.NewModelOrder0PreBias(32, packet.ContextBias(packet.CONTEXT_REPEAT_DISTANCE)))
-    c.AddModel(packet.CONTEXT_REPEAT_LENGTH,   ac.NewModelOrder0PreBias(8,  packet.ContextBias(packet.CONTEXT_REPEAT_LENGTH)))
-}
-
-func (self *BitSmash) ReadFrom(file io.Reader) error {
-    var header [3]byte
-    n, err := file.Read(header[:])
-    if n < 3 {
-        log.Fatal("Short read on header")
-    } else if err != nil {
-        return err
-    }
-    self.size = sizes[int(header[0]) & 0x7]
-    self.palette = (int(header[0]) >> 3) & 0xF
-    self.transparent = ((header[0] >> 7) & 0x1) == 1
-
-    n_packets := int(header[1]) + (int(header[2]) & 0x3) + 1
-    self.packetList = make([]packet.Packet, n_packets)
-
-    decoder, err := ac.NewDecoder(file)
-    if err != nil {
-        return err
-    }
-    self.codecManagerInit(decoder)
-
-    for i := 0; i < n_packets; i++ {
-        var pkt packet.Packet // Packet! PACKET! PACKET!!!
-        packet_type := decoder.Decode(packet.CONTEXT_PACKET_TYPE)
-        switch packet_type {
-        case 0:
-            pkt = packet.DecodeRLE(decoder)
-        case 1:
-            pkt = packet.DecodeBinaryPattern(decoder)
-        case 2:
-            pkt = packet.DecodeDirect(decoder)
-        case 3:
-            pkt = packet.DecodeRepeat(decoder)
-        }
-        self.packetList[i] = pkt
-    }
-    return nil
-}
-
-func (self *BitSmash) WriteTo(file io.Writer) error {
-    n_packets := len(self.packetList)
-    if n_packets > 1024 {
-        fmt.Println("Too many packets:", n_packets)
-        return errors.New("Too many packets")
-    }
-
-    var header [3]byte
-    transparent := 0
-    if (self.transparent) {
-        transparent = 1
-    }
-    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)
-
-    file.Write(header[:])
-
-    encoder, err := ac.NewEncoder(file)
-    if err != nil {
-        return err
-    }
-    self.codecManagerInit(encoder)
-
-    for i, p := range(self.packetList) {
-        encoder.Encode(packet.CONTEXT_PACKET_TYPE, uint32(self.packetType(i)))
-        p.Encode(encoder)
-    }
-    encoder.Finish()
-    
-    return nil
-}
-
-func (self *BitSmash) Dump() {
-    self.unpackRepeat()
-    size := self.Size()
-    fmt.Printf("%d pixels, %dx%d\n", self.Length(), size.X, size.Y)
-    fmt.Printf("%d packets:\n", len(self.packetList))
-
-    c := 0
-
-    for _, p := range(self.packetList) {
-        switch p.(type) {
-        case *packet.RepeatPacket:
-            fmt.Printf("      %s\n", p)
-            rp := p.(*packet.RepeatPacket)
-            //d := c - rp.Distance()
-            //tpl = rp.UnRepeat(tpl, c)
-            for i := 0; i < rp.Count(); i++ {
-                fmt.Printf("%-4d    %s\n", c + i, rp.Slice()[i])
-            }
-            c += rp.Count()
-        default:
-            fmt.Printf("%-4d  %s\n", c, p)
-            c++
-        }
-    }
-
-    fmt.Printf("%d total bytes\n", self.ByteLength())
-}
-
-func (self *BitSmash) RawDump() {
-    for i, p := range(self.packetList) {
-        fmt.Printf("%-4d  %s\n", i, p)
-    }
-}

diff --git a/bs/codec_manager.go b/bs/codec_manager.go
line changes: +16/-0
index 0000000..72284cf
--- /dev/null
+++ b/bs/codec_manager.go
@@ -0,0 +1,16 @@
+package bs
+
+import (
+    "bytex64.net/code/bitsmash/packet"
+    "bytex64.net/code/bitsmash/ac"
+)
+
+func (self *BitSmash) codecManagerInit(c ac.CodecManager) {
+    c.AddModel(packet.CONTEXT_PACKET_TYPE,     ac.NewModelOrder0(4))
+    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))
+    c.AddModel(packet.CONTEXT_REPEAT_DISTANCE, ac.NewModelOrder0PreBias(32, packet.ContextBias(packet.CONTEXT_REPEAT_DISTANCE)))
+    c.AddModel(packet.CONTEXT_REPEAT_LENGTH,   ac.NewModelOrder0PreBias(8,  packet.ContextBias(packet.CONTEXT_REPEAT_LENGTH)))
+}

diff --git a/bs/encode.go b/bs/encode.go
line changes: +73/-0
index 0000000..4610135
--- /dev/null
+++ b/bs/encode.go
@@ -0,0 +1,73 @@
+package bs
+
+import (
+    "sort"
+
+    "bytex64.net/code/bitsmash/packet"
+)
+
+type scanFunc func(pixels []uint8) packet.Packet
+
+var scanFuncs []scanFunc = []scanFunc {
+    packet.ScanRLE,
+    packet.ScanBinaryPattern,
+    packet.ScanDirect,
+}
+
+func findAllEncodeLengths(pixels []uint8, steps int) []packet.PacketRun {
+    results := make([]packet.PacketRun, 0)
+    if len(pixels) == 0 {
+        return results
+    }
+
+    for _, scan := range(scanFuncs) {
+        pkt := scan(pixels)
+        if pkt == nil {
+            continue
+        }
+        pkt_length := pkt.Length()
+        pkt_bytes := pkt.ByteLength()
+        if steps > 1 && len(pixels) > pkt_length {
+            rec_results := findAllEncodeLengths(pixels[pkt_length:], steps - 1)
+            for _, r := range(rec_results) {
+                results = append(results, packet.PacketRun{r.Length + pkt_length, r.Bytes + pkt_bytes, r.Depth + 1, pkt})
+            }
+        } else {
+            results = append(results, packet.PacketRun{pkt_length, pkt_bytes, 1, pkt})
+        }
+    }
+
+    return results
+}
+
+func findOptimalEncoding(pixels []uint8, optimize int) packet.Packet {
+    results := findAllEncodeLengths(pixels, optimize)
+    sort.Sort(packet.ByPacketEfficiency(results))
+    /*
+    fmt.Printf("optimal encodings (%d bytes remain):\n", len(pixels))
+    for _, v := range(results) {
+        fmt.Println(v)
+    }
+    */
+    return results[0].Head
+}
+
+func findRepeats(packetlist []packet.Packet) []packet.Packet {
+    outlist := make([]packet.Packet, 1, len(packetlist))
+    outlist[0] = packetlist[0]
+
+    for i := 1; i < len(packetlist); {
+        pkt := packet.ScanRepeat(packetlist, i)
+        if pkt != nil {
+            outlist = append(outlist, pkt)
+            rpkt := pkt.(*packet.RepeatPacket)
+            i += rpkt.Count()
+        } else {
+            outlist = append(outlist, packetlist[i])
+            i++
+            continue
+        }
+    }
+
+    return outlist
+}

diff --git a/bs/image.go b/bs/image.go
line changes: +63/-0
index 0000000..f519f70
--- /dev/null
+++ b/bs/image.go
@@ -0,0 +1,63 @@
+package bs
+
+import (
+    "image"
+    "image/draw"
+    "log"
+    "os"
+
+    "bytex64.net/code/bitsmash/palette"
+)
+
+func loadImage(filename string, opts *EncodeOpts) image.PalettedImage {
+    reader, err := os.Open(filename)
+    if err != nil {
+        log.Fatal(err)
+    }
+    defer reader.Close()
+
+    img, _, err := image.Decode(reader)
+    if err != nil {
+        log.Fatal(err)
+    }
+
+    size := img.Bounds().Size()
+    if size.X > 128 {
+        log.Fatal("Image width must be less than 128 pixels")
+    }
+
+    // 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 no palette has been specified, automatically select one
+    if (opts.Palette == -1) {
+        opts.Palette = findPalette(img)
+        log.Printf("Automatically selected palette %d\n", opts.Palette);
+    }
+
+    // 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 getPixelList(img image.PalettedImage) []uint8 {
+    p := img.(*image.Paletted)
+
+    return p.Pix
+}

diff --git a/bs/io.go b/bs/io.go
line changes: +98/-0
index 0000000..46de10a
--- /dev/null
+++ b/bs/io.go
@@ -0,0 +1,98 @@
+package bs
+
+import (
+    "io"
+    "log"
+    "fmt"
+    "errors"
+
+    "bytex64.net/code/bitsmash/packet"
+    "bytex64.net/code/bitsmash/ac"
+)
+
+func (self *BitSmash) packetType(n int) int {
+    switch self.packetList[n].(type) {
+    case *packet.RLEPacket:
+        return 0
+    case *packet.BinaryPatternPacket:
+        return 1
+    case *packet.DirectPacket:
+        return 2
+    case *packet.RepeatPacket:
+        return 3
+    }
+    return -1
+}
+
+func (self *BitSmash) ReadFrom(file io.Reader) error {
+    var header [3]byte
+    n, err := file.Read(header[:])
+    if n < 3 {
+        log.Fatal("Short read on header")
+    } else if err != nil {
+        return err
+    }
+    self.size = sizes[int(header[0]) & 0x7]
+    self.palette = (int(header[0]) >> 3) & 0xF
+    self.transparent = ((header[0] >> 7) & 0x1) == 1
+
+    n_packets := int(header[1]) + (int(header[2]) & 0x3) + 1
+    self.packetList = make([]packet.Packet, n_packets)
+
+    decoder, err := ac.NewDecoder(file)
+    if err != nil {
+        return err
+    }
+    self.codecManagerInit(decoder)
+
+    for i := 0; i < n_packets; i++ {
+        var pkt packet.Packet // Packet! PACKET! PACKET!!!
+        packet_type := decoder.Decode(packet.CONTEXT_PACKET_TYPE)
+        switch packet_type {
+        case 0:
+            pkt = packet.DecodeRLE(decoder)
+        case 1:
+            pkt = packet.DecodeBinaryPattern(decoder)
+        case 2:
+            pkt = packet.DecodeDirect(decoder)
+        case 3:
+            pkt = packet.DecodeRepeat(decoder)
+        }
+        self.packetList[i] = pkt
+    }
+    return nil
+}
+
+func (self *BitSmash) WriteTo(file io.Writer) error {
+    n_packets := len(self.packetList)
+    if n_packets > 1024 {
+        fmt.Println("Too many packets:", n_packets)
+        return errors.New("Too many packets")
+    }
+
+    var header [3]byte
+    transparent := 0
+    if (self.transparent) {
+        transparent = 1
+    }
+    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)
+
+    file.Write(header[:])
+
+    encoder, err := ac.NewEncoder(file)
+    if err != nil {
+        return err
+    }
+    self.codecManagerInit(encoder)
+
+    for i, p := range(self.packetList) {
+        encoder.Encode(packet.CONTEXT_PACKET_TYPE, uint32(self.packetType(i)))
+        p.Encode(encoder)
+    }
+    encoder.Finish()
+    
+    return nil
+}
+

diff --git a/bs/new.go b/bs/new.go
line changes: +69/-0
index 0000000..4c9eb20
--- /dev/null
+++ b/bs/new.go
@@ -0,0 +1,69 @@
+package bs
+
+import (
+    "log"
+    "os"
+
+    _ "image/png"
+
+    "bytex64.net/code/bitsmash/packet"
+)
+
+type EncodeOpts struct {
+    Optimize int
+    Palette int
+}
+
+type BitSmash struct {
+    size int
+    palette int
+    transparent bool
+    packetList []packet.Packet
+}
+
+func NewFromPixels(pixels []uint8, size int, opts *EncodeOpts) BitSmash {
+    bs := BitSmash{
+        size,
+        opts.Palette,
+        false,
+        make([]packet.Packet, 0, 1),
+    }
+
+    i := 0
+    for i < len(pixels) {
+        packet := findOptimalEncoding(pixels[i:], opts.Optimize)
+        i += packet.Length()
+        bs.packetList = append(bs.packetList, packet)
+    }
+
+    bs.packetList = findRepeats(bs.packetList)
+
+    return bs
+}
+
+func NewFromImage(filename string, opts *EncodeOpts) BitSmash {
+    image := loadImage(filename, opts)
+    size := image.Bounds().Size().X
+    return NewFromPixels(getPixelList(image), size, opts)
+}
+
+func NewFromFile(filename string) BitSmash {
+    bs := BitSmash{
+        0,
+        0,
+        false,
+        nil,
+    }
+
+    file, err := os.Open(filename)
+    defer file.Close()
+    if err != nil {
+        log.Fatalf("Failed to open: %v", err)
+    }
+    err = bs.ReadFrom(file)
+    if err != nil {
+        log.Fatalf("Reading file: %v", err)
+    }
+
+    return bs
+}

diff --git a/bs/palette.go b/bs/palette.go
line changes: +35/-0
index 0000000..0c7fa2c
--- /dev/null
+++ b/bs/palette.go
@@ -0,0 +1,35 @@
+package bs
+
+import (
+    "image"
+
+    "bytex64.net/code/bitsmash/palette"
+)
+
+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)
+            }
+        }
+        if error < minimum_error {
+            selected = i
+            minimum_error = error
+        }
+    }
+
+    return selected
+}

diff --git a/bs/sizes.go b/bs/sizes.go
line changes: +30/-0
index 0000000..c4767fa
--- /dev/null
+++ b/bs/sizes.go
@@ -0,0 +1,30 @@
+package bs
+
+var sizes []int = []int{
+    8,
+    16,
+    24,
+    32,
+    48,
+    64,
+    96,
+    128,
+}
+
+func validSize(s int) bool {
+    for _, n := range(sizes) {
+        if n == s {
+            return true
+        }
+    }
+    return false
+}
+
+func sizeIndex(s int) int {
+    for i, n := range(sizes) {
+        if n == s {
+            return i
+        }
+    }
+    return -1
+}

diff --git a/bs/util.go b/bs/util.go
line changes: +84/-0
index 0000000..65b2b44
--- /dev/null
+++ b/bs/util.go
@@ -0,0 +1,84 @@
+package bs
+
+import (
+    "fmt"
+    "image"
+    "math"
+
+    "bytex64.net/code/bitsmash/packet"
+)
+
+func (self *BitSmash) Size() image.Point {
+    return image.Point{self.size, self.Length() / self.size}
+}
+
+func (self *BitSmash) Length() int {
+    n := 0
+    for _, p := range(self.packetList) {
+        n += p.Length()
+    }
+    return n
+}
+
+func (self *BitSmash) ByteLength() int {
+    n_pd := int(math.Ceil(float64(len(self.packetList)) / 4.0))
+    total_len := n_pd + 3
+    for _, p := range(self.packetList) {
+        total_len += p.ByteLength()
+    }
+    return total_len
+}
+
+func (self *BitSmash) unpackRepeat() []packet.Packet {
+    outlist := make([]packet.Packet, 1)
+    outlist[0] = self.packetList[0]
+    
+    c := 1
+    for _, p := range(self.packetList) {
+        switch p.(type) {
+        case *packet.RepeatPacket:
+            rp := p.(*packet.RepeatPacket)
+            outlist = rp.UnRepeat(outlist, c)
+            c += rp.Count()
+        default:
+            outlist = append(outlist, p)
+            c++
+        }
+    }
+
+    return outlist
+}
+
+func (self *BitSmash) Dump() {
+    self.unpackRepeat()
+    size := self.Size()
+    fmt.Printf("%d pixels, %dx%d\n", self.Length(), size.X, size.Y)
+    fmt.Printf("%d packets:\n", len(self.packetList))
+
+    c := 0
+
+    for _, p := range(self.packetList) {
+        switch p.(type) {
+        case *packet.RepeatPacket:
+            fmt.Printf("      %s\n", p)
+            rp := p.(*packet.RepeatPacket)
+            //d := c - rp.Distance()
+            //tpl = rp.UnRepeat(tpl, c)
+            for i := 0; i < rp.Count(); i++ {
+                fmt.Printf("%-4d    %s\n", c + i, rp.Slice()[i])
+            }
+            c += rp.Count()
+        default:
+            fmt.Printf("%-4d  %s\n", c, p)
+            c++
+        }
+    }
+
+    fmt.Printf("%d total bytes\n", self.ByteLength())
+}
+
+func (self *BitSmash) RawDump() {
+    for i, p := range(self.packetList) {
+        fmt.Printf("%-4d  %s\n", i, p)
+    }
+}

diff --git a/main.go b/main.go
line changes: +9/-7
index 593fff8..a63a6bf
--- a/main.go
+++ b/main.go
@@ -6,6 +6,8 @@ import (
 	"fmt"
     "flag"
     "encoding/base64"
+
+    "bytex64.net/code/bitsmash/bs"
 )
 
 type BitsmashOpts struct {
@@ -13,17 +15,17 @@ type BitsmashOpts struct {
     dump bool
     base64 bool
     files []string
-    encodeOpts EncodeOpts
+    encodeOpts bs.EncodeOpts
 }
 
 func parseArgs() BitsmashOpts {
     bo := BitsmashOpts{}
-    bo.encodeOpts = EncodeOpts{}
+    bo.encodeOpts = bs.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.encodeOpts.optimize,    "o",      3,     "Optimization level")
-    flag.IntVar(&bo.encodeOpts.palette,     "p",      -1,    "Force palette N")
+    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
@@ -36,11 +38,11 @@ func main() {
         os.Exit(1)
     }
 
-    var smash BitSmash
+    var smash bs.BitSmash
     if (args.decode) {
-        smash = NewFromBSFile(args.files[0])
+        smash = bs.NewFromFile(args.files[0])
     } else {
-        smash = NewFromImage(args.files[0], &args.encodeOpts)
+        smash = bs.NewFromImage(args.files[0], &args.encodeOpts)
     }
 
     if args.dump {