Skip to content

Instantly share code, notes, and snippets.

@carbocation
Forked from bemasher/Subtitle.go
Created July 13, 2018 14:35
Show Gist options
  • Save carbocation/0fe15369bf0f9d0040b49535eb9155a8 to your computer and use it in GitHub Desktop.
Save carbocation/0fe15369bf0f9d0040b49535eb9155a8 to your computer and use it in GitHub Desktop.
Subtitle decoder basic functionality written in Golang.
package main
import (
"io"
"os"
"fmt"
"log"
"bytes"
"image"
"image/png"
"encoding/binary"
)
const (
SUBFILE = "Subtitles/Blade Runner.sub"
// SUBFILE = "Subtitles/Red.sub"
)
// Defines the fields necessary for reading
// a subtitle packet
type Packet struct {
PSHeader [14]uint8
PESHeader [4]uint8
PacketSize uint16
Extension uint16
HeaderSize uint8
SubStream uint8
DataSize uint16
ControlPtr uint16
}
// Header for each control sequence
type ControlHeader struct {
Date uint16
Next uint16
}
// Buffers for holding image data and control sequences
type Payload struct {
Data bytes.Buffer
Control bytes.Buffer
}
// Used for reading nibble-aligned data
// from the image data buffer
type Nibbler struct {
r *bytes.Buffer
Current uint8
Aligned uint8
}
// Returns a new Nibbler using the given byte buffer
func NewNibbler(r *bytes.Buffer) Nibbler {
return Nibbler{r, 0, 0}
}
// Returns the next nibble and reads a new byte if necessary
func (n *Nibbler) GetNibble() (b uint8, err os.Error) {
if n.Aligned == 0 {
err = ReadInto(n.r, &n.Current)
if err != nil {
b = 0
return
}
}
n.Aligned ^= 4
b = (n.Current >> n.Aligned) & 0x0F
return b, err
}
// Holds dimensions for the image
type Rect struct {
w uint16
h uint16
}
// Read the packet header from the given file stream
func (p *Packet) Read(r io.Reader) {
ReadInto(r, &p.PSHeader)
ReadInto(r, &p.PESHeader)
ReadInto(r, &p.PacketSize)
ReadInto(r, &p.Extension)
ReadInto(r, &p.HeaderSize)
r.(io.ReadSeeker).Seek(int64(p.HeaderSize), os.SEEK_CUR)
ReadInto(r, &p.SubStream)
ReadInto(r, &p.DataSize)
ReadInto(r, &p.ControlPtr)
p.PacketSize -= uint16(p.HeaderSize) + 4
// Backup since DataSize and ControlPtr are part of the payload
r.(io.ReadSeeker).Seek(-4, os.SEEK_CUR)
}
// Reads data into the Data and Control buffers of a payload
func (p *Payload) Read(r *bytes.Buffer, h Packet) {
ReadFromBuffer(r, &p.Data, int(h.ControlPtr))
ReadFromBuffer(r, &p.Control, int(h.DataSize - h.ControlPtr))
}
// Returns the current offset of the cursor in a reader
func LogOffset(r io.Reader) int64 {
offset, _ := r.(io.ReadSeeker).Seek(0, os.SEEK_CUR)
return offset
}
// Wrapper to remove the necessity to include the endianness
// of the data being read for every single field
func ReadInto(r io.Reader, d interface{}) os.Error {
return binary.Read(r, binary.BigEndian, d)
}
// Reads a particular number of bytes n from the reader into a buffer
func ReadFrom(r io.Reader, b *bytes.Buffer, n int64) {
newSection := io.NewSectionReader(r.(io.ReaderAt), LogOffset(r), n)
b.ReadFrom(newSection)
r.(io.ReadSeeker).Seek(n, os.SEEK_CUR)
}
// Copies number of bytes n into buffer w from buffer r
func ReadFromBuffer(r *bytes.Buffer, w *bytes.Buffer, n int) {
w.Write(r.Next(n))
}
// Draws number of pixels n of color c starting at pixel x,y along the +x-axis
func DrawPixels(s *image.Gray, x uint16, y uint16, n uint16, c uint8) {
for i := 0; i < int(n); i++ {
s.SetGray(int(x) + i, int(y), image.GrayColor{(c + 1) << 6})
}
}
// Determines bounds of the subtitle and crops
func TrimImage(s *image.Gray, reserve uint8) image.Image {
bound := s.Bounds()
newBound := image.Rect(bound.Max.X, bound.Max.Y, bound.Min.X, bound.Min.Y)
rr, rg, rb, ra := image.GrayColor{reserve}.RGBA()
for x := bound.Min.X; x < bound.Max.X; x++ {
for y := bound.Min.Y; y < bound.Max.Y; y++ {
r,g,b,a := s.At(x,y).RGBA()
if rr == r && rg == g && rb == b && ra == a {
if newBound.Max.Y > y {
newBound.Max.Y = y
}
if newBound.Min.Y < y {
newBound.Min.Y = y
}
if newBound.Max.X > x {
newBound.Max.X = x
}
if newBound.Min.X < x {
newBound.Min.X = x
}
s.SetGray(x, y, image.GrayColor{uint8(255)})
} else {
s.SetGray(x, y, image.GrayColor{uint8(0)})
}
}
}
fmt.Println("Cropped:",newBound)
slice := s.SubImage(newBound.Canon())
return slice
}
// Reads packets until an entire subtitle has been read
func ReadSubtitle(s *os.File) (head Packet, data bytes.Buffer) {
for i := 0; ; i++ {
var pack Packet
pack.Read(s)
if i == 0 {
head = pack
}
ReadFrom(s, &data, int64(pack.PacketSize))
if data.Len() == int(head.DataSize) {
break
}
}
return
}
// Reads the control sequencies and returns important information
func ReadControlSequences(head Packet, data *bytes.Buffer) (rect Rect, payload Payload, even, odd uint16) {
payload.Read(data, head)
for {
var header ControlHeader
ReadInto(&payload.Control, &header)
fmt.Printf("%+v\n", header)
end := false
for !end {
cmd, err := payload.Control.ReadByte()
if err != nil {
if err == os.EOF {
return
}
log.Fatal(err)
}
switch cmd {
case 0x00: fmt.Println("\tForced")
case 0x01: fmt.Printf("\tStart:\t\t%dms\n", 1024 * header.Date / 90)
case 0x02: fmt.Printf("\tStop:\t\t%dms\n", 1024 * header.Date / 90)
case 0x03:
fmt.Printf("\tPalette:\t%04X\n", payload.Control.Next(2))
case 0x04:
fmt.Printf("\tAlpha:\t\t%X\n", payload.Control.Next(2))
case 0x05:
buf := payload.Control.Next(6)
rect = Rect{((uint16(buf[1]) & 0xF) << 8) | uint16(buf[2]) - (uint16(buf[0]) << 4) | (uint16(buf[1]) >> 4) + 1, ((uint16(buf[4]) & 0xF) << 8) | uint16(buf[5]) - (uint16(buf[3]) << 4) | (uint16(buf[4]) >> 4) + 1}
fmt.Printf("\tDimensions:\t%+v\n", rect)
case 0x06:
buf := payload.Control.Next(4)
even = uint16(buf[0]) << 8 | uint16(buf[1])
odd = uint16(buf[2]) << 8 | uint16(buf[3])
fmt.Printf("\tOffsets:\t%d, %d\n", even, odd)
fmt.Printf("\tField Len:\t%d, %d\n", odd - even, uint16(payload.Data.Len()) - odd)
case 0xFF:
end = true
}
}
}
return
}
// Decodes the RLE image
func ReadRLEImage(rect Rect, payload *Payload, even, odd uint16) (*image.Gray) {
subImg := image.NewGray(int(rect.w), int(rect.h))
bData := payload.Data.Bytes()
evenNibbler := NewNibbler(bytes.NewBuffer(bData[even:odd]))
oddNibbler := NewNibbler(bytes.NewBuffer(bData[odd:]))
var x, y uint16
done := false
field := true
for !done {
var b uint16
var t uint8
var currentNibbler *Nibbler
// Switch fields if we've just done a carriage return
if field {
currentNibbler = &evenNibbler
} else {
currentNibbler = &oddNibbler
}
// Get a nibble
t, _ = currentNibbler.GetNibble()
b = (b << 4) | uint16(t)
// if it's a valid character then draw the pixels and
// incremnet x the number of pixels the character specifies
if b >= 0x4 {
run := b >> 2
DrawPixels(subImg, x, y, run, uint8(b & 0x3))
x += run
} else {
// keep reading nibbles until we get a valid character
t, _ := currentNibbler.GetNibble()
b = (b << 4) | uint16(t)
if b >= 0x10 {
run := b >> 2
DrawPixels(subImg, x, y, run, uint8(b & 0x3))
x += run
} else {
t, _ := currentNibbler.GetNibble()
b = (b << 4) | uint16(t)
if b >= 0x40 {
run := b >> 2
DrawPixels(subImg, x, y, run, uint8(b & 0x3))
x += run
} else {
t, _ := currentNibbler.GetNibble()
b = (b << 4) | uint16(t)
if b >= 0x100 {
run := b >> 2
DrawPixels(subImg, x, y, run, uint8(b & 0x3))
x += run
} else {
// Must be a carriage return, fill the rest of the row
// with the specified color
DrawPixels(subImg, x, y, rect.w - x, uint8(b & 0x3))
// Reset x coordinate, increment y and switch fields
x = 0
y += 1
field = !field
// If we've drawn all the rows given by the dimensions
// then we're done
if y >= rect.h {
done = true
}
// Make sure that we're byte-aligned after each carriage return
if currentNibbler.Aligned != 0 {
currentNibbler.GetNibble()
}
}
}
}
}
}
return subImg
}
// Wrapper for cropping the image
func CropImage(s *image.Gray) *image.Gray {
return TrimImage(s, 128).(*image.Gray)
}
func init() {
log.SetFlags(log.Lshortfile | log.Ltime)
}
func main() {
subFile, err := os.Open(SUBFILE)
if err != nil {
log.Fatal("Error openning file: ", err)
}
defer subFile.Close()
// Read a single subtitle and decode it
head, data := ReadSubtitle(subFile)
rect, payload, even, odd := ReadControlSequences(head, &data)
subImg := ReadRLEImage(rect, &payload, even, odd)
subImg = CropImage(subImg)
// Create the output file
pngFile, err := os.Create("Output/sub.png")
if err != nil {
log.Fatal("Error creating file: ", err)
}
defer pngFile.Close()
// Commit the image to disk
png.Encode(pngFile, subImg)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment