package mpeg // http://www.mpucoder.com/DVD/spu.html // http://sam.zoy.org/writings/dvd/subtitles/ // TODO: still some unknown data before and after pixel data import ( "fmt" "strings" "github.com/wader/fq/format" "github.com/wader/fq/format/registry" "github.com/wader/fq/pkg/decode" "github.com/wader/fq/pkg/scalar" ) func init() { registry.MustRegister(decode.Format{ Name: format.MPEG_SPU, Description: "Sub Picture Unit (DVD subtitle)", DecodeFn: spuDecode, }) } //nolint:revive const ( CMD_END = 0xff FSTA_DSP = 0x00 STA_DSP = 0x01 STP_DSP = 0x02 SET_COLOR = 0x03 SET_CONTR = 0x04 SET_DAREA = 0x05 SET_DSPXA = 0x06 CHG_COLCON = 0x07 ) var commandNames = scalar.UToSymStr{ CMD_END: "CMD_END", FSTA_DSP: "FSTA_DSP", STA_DSP: "STA_DSP", STP_DSP: "STP_DSP", SET_COLOR: "SET_COLOR", SET_CONTR: "SET_CONTR", SET_DAREA: "SET_DAREA", SET_DSPXA: "SET_DSPXA", CHG_COLCON: "CHG_COLCON", } func rleValue(d *decode.D) (uint64, uint64, int) { p := uint(d.PeekBits(8)) // match zero prefix switch { case p&0b1111_1100 == 0: // 000000nnnnnnnncc d.U6() return d.U8(), d.U2(), 16 case p&0b1111_0000 == 0: // 0000nnnnnncc d.U4() return d.U6(), d.U2(), 12 case p&0b1100_0000 == 0: // 00nnnncc d.U2() return d.U4(), d.U2(), 8 default: // nncc return d.U2(), d.U2(), 4 } } func decodeLines(d *decode.D, lines int, width int) []string { //nolint:unparam var ls []string for i := 0; i < lines; i++ { l := "" for x := 0; x < width; { n, c, b := rleValue(d) pixel := " " if c != 0 { pixel = fmt.Sprintf("%d", c) } if n == 0 && b == 16 { l += strings.Repeat(pixel, width-len(l)) break } x += int(n) } if d.ByteAlignBits() > 0 { d.U(d.ByteAlignBits()) } ls = append(ls, l) } return ls } func spuDecode(d *decode.D, in interface{}) interface{} { d.FieldU16("size") dcsqtOffset := d.FieldU16("dcsqt_offset") // to catch infinite loops offsetSeen := map[uint64]struct{}{} d.SeekAbs(int64(dcsqtOffset) * 8) d.FieldArray("dcsqt", func(d *decode.D) { lastDCSQ := false for !lastDCSQ { d.FieldStruct("dcsq", func(d *decode.D) { dcsqStart := uint64(d.Pos() / 8) d.FieldU16("delay") offset := d.FieldU16("offset") if offset == dcsqStart { lastDCSQ = true return } if _, ok := offsetSeen[offset]; ok { d.Fatalf("dcsqt loop detected for %d", offset) } offsetSeen[offset] = struct{}{} var pxdTFOffset int64 var pxdBFOffset int64 var width uint64 var height uint64 d.FieldArray("commands", func(d *decode.D) { seenEnd := false for !seenEnd { d.FieldStruct("command", func(d *decode.D) { cmd := d.FieldU8("type", commandNames) switch cmd { case CMD_END: seenEnd = true case FSTA_DSP: // no args case STA_DSP: // no args case STP_DSP: // no args case SET_COLOR: d.FieldU4("a0") d.FieldU4("a1") d.FieldU4("a2") d.FieldU4("a3") case SET_CONTR: d.FieldU4("a0") d.FieldU4("a1") d.FieldU4("a2") d.FieldU4("a3") case SET_DAREA: startX := d.FieldU12("start_x") endX := d.FieldU12("end_x") startY := d.FieldU12("start_y") endY := d.FieldU12("end_y") width = endX - startX + 1 height = endY - startY + 1 case SET_DSPXA: pxdTFOffset = int64(d.FieldU16("offset_top_field")) * 8 pxdBFOffset = int64(d.FieldU16("offset_bottom_field")) * 8 case CHG_COLCON: size := d.FieldU16("size") // TODO d.FieldRawLen("data", int64(size)*8) default: d.Fatalf("unknown command %d", cmd) } }) } }) halfHeight := int(height) / 2 // var tLines []string // var bLines []string if pxdTFOffset != 0 { d.SeekAbs(pxdTFOffset) /*tLines*/ _ = decodeLines(d, halfHeight, int(width)) d.RangeFn(pxdTFOffset, d.Pos()-pxdTFOffset, func(d *decode.D) { d.FieldRawLen("top_pixels", d.BitsLeft()) }) } if pxdBFOffset != 0 { d.SeekAbs(pxdBFOffset) /*bLines*/ _ = decodeLines(d, halfHeight, int(width)) d.RangeFn(pxdBFOffset, d.Pos()-pxdBFOffset, func(d *decode.D) { d.FieldRawLen("bottom_pixels", d.BitsLeft()) }) } // var lines []string // for i := 0; i < halfHeight; i++ { // lines = append(lines, tLines[i]) // lines = append(lines, bLines[i]) // } // for _, l := range lines { // log.Printf("l: '%s'\n", l) // } d.SeekAbs(int64(offset) * 8) }) } }) return nil }