Created
January 28, 2023 01:10
-
-
Save cnelson/9fc1b31056e34cdb396e069764f8fd84 to your computer and use it in GitHub Desktop.
Golang packer / unpacker for the Roku BIF format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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