-
-
Save carbocation/0fe15369bf0f9d0040b49535eb9155a8 to your computer and use it in GitHub Desktop.
Subtitle decoder basic functionality written in Golang.
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 ( | |
"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