Skip to content

Instantly share code, notes, and snippets.

@cnelson
Created January 28, 2023 01:10
Show Gist options
  • Save cnelson/9fc1b31056e34cdb396e069764f8fd84 to your computer and use it in GitHub Desktop.
Save cnelson/9fc1b31056e34cdb396e069764f8fd84 to your computer and use it in GitHub Desktop.
Golang packer / unpacker for the Roku BIF format
package main
import (
"encoding/binary"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
)
// BIF file format
// https://developer.roku.com/docs/developer-program/media-playback/trick-mode/bif-file-creation.md
//
// HEADER (64 bytes)
// magic (8 bytes)
// version (4 bytes)
// number of images (4 bytes)
// image interval (4 bytes)
// reserved (44 bytes)
//
// IMAGE INDEX (variable)
// image 0 (8 bytes)
// index (4 bytes)
// offset (4 bytes)
// ...
// image N (8 bytes)
// index (4 bytes)
// offset (4 bytes)
// end of index (8 bytes)
// magic (4 bytes)
// offset (4 bytes)
//
// IMAGE DATA (variable)
// image 0 data
// ...
// image N data
var BIF_MAGIC = [...]byte{0x89, 0x42, 0x49, 0x46, 0x0d, 0x0a, 0x1a, 0x0a}
var BIF_EOIDX = uint32(0xffffffff)
func getImages(inputDir string) ([]fs.FileInfo, error) {
entries, err := os.ReadDir(inputDir)
if err != nil {
return nil, err
}
images := make([]fs.FileInfo, 0, len(entries))
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".jpg") {
continue
}
if info, err := entry.Info(); err != nil {
return nil, err
} else {
images = append(images, info)
}
}
return images, nil
}
func packBIF(inputDir string, outputFilename string, interval int) error {
images, err := getImages(inputDir)
if err != nil {
return err
}
f, err := os.Create(outputFilename)
if err != nil {
return err
}
defer f.Close()
// write the header
// magic
if err := binary.Write(f, binary.LittleEndian, BIF_MAGIC); err != nil {
return err
}
//version
if err := binary.Write(f, binary.LittleEndian, uint32(0)); err != nil {
return err
}
// number of images
if err := binary.Write(f, binary.LittleEndian, uint32(len(images))); err != nil {
return err
}
// interval between images in ms
if err := binary.Write(f, binary.LittleEndian, uint32(interval)); err != nil {
return err
}
// all zeros, reserved for future
if err := binary.Write(f, binary.LittleEndian, make([]byte, 44)); err != nil {
return err
}
// calculate the offset from the start of the file where image data will start
// 64 byte header + 8 bytes per image + 8 bytes for the end-of-index marker
imageOffset := uint32(64 + (8 * len(images)) + 8)
// write the image index
for imageIdx, image := range images {
if err := binary.Write(f, binary.LittleEndian, uint32(imageIdx)); err != nil {
return err
}
if err := binary.Write(f, binary.LittleEndian, imageOffset); err != nil {
return err
}
imageOffset += uint32(image.Size())
}
// end-of-index record
if err := binary.Write(f, binary.LittleEndian, BIF_EOIDX); err != nil {
return err
}
if err := binary.Write(f, binary.LittleEndian, imageOffset); err != nil {
return err
}
// append all the images
for _, image := range images {
imgf, err := os.Open(filepath.Join(inputDir, image.Name()))
if err != nil {
return err
}
if _, err := io.Copy(f, imgf); err != nil {
imgf.Close()
return err
}
if err := imgf.Close(); err != nil {
return err
}
}
return f.Close()
}
func unpackBIF(inputFilename string, outputDir string) error {
f, err := os.Open(inputFilename)
if err != nil {
return err
}
// validate magic
magic := make([]byte, 8)
n, err := f.Read(magic)
if err != nil {
return err
}
if n != 8 || *(*[8]byte)(magic) != BIF_MAGIC {
return fmt.Errorf("Not a BIF file")
}
// validate version
var version uint32
if err := binary.Read(f, binary.LittleEndian, &version); err != nil {
return err
}
if version != 0 {
return fmt.Errorf("Unknown version: %d", version)
}
// number of images
var imageCount uint32
if err := binary.Read(f, binary.LittleEndian, &imageCount); err != nil {
return err
}
// interval
var imageInterval uint32
if err := binary.Read(f, binary.LittleEndian, &imageInterval); err != nil {
return err
}
// skip reserved header
if _, err := f.Seek(44, io.SeekCurrent); err != nil {
return err
}
// read image index
imageOffsets := make([]uint32, 0, imageCount)
for {
var index uint32
var offset uint32
if err := binary.Read(f, binary.LittleEndian, &index); err != nil {
return err
}
if err := binary.Read(f, binary.LittleEndian, &offset); err != nil {
return err
}
imageOffsets = append(imageOffsets, offset)
// end of index
if index == BIF_EOIDX {
break
}
}
if uint32(len(imageOffsets)) != imageCount+1 {
return fmt.Errorf("Unexpected number of images in index, %d != %d", len(imageOffsets), imageCount+1)
}
// write images
// seek to start of first image
if _, err := f.Seek(int64(imageOffsets[0]), io.SeekStart); err != nil {
return err
}
//loop through the rest of the images
for idx := 1; idx < len(imageOffsets); idx++ {
// we are writing the previous file, as we don't know where it ends
// until we read the next image's starting offset
imgf, err := os.Create(filepath.Join(outputDir, fmt.Sprintf("%08d.jpg", idx-1)))
if err != nil {
return err
}
// copy from the previous offset to the current one
_, err = io.CopyN(imgf, f, int64(imageOffsets[idx]-imageOffsets[idx-1]))
if err != nil {
imgf.Close()
return err
}
err = imgf.Close()
if err != nil {
return err
}
}
return f.Close()
}
func main() {
if err := packBIF("pack", "go.bif", 10000); err != nil {
panic(err)
}
if err := unpackBIF("go.bif", "unpack"); err != nil {
panic(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment