861 lines
32 KiB
Go
861 lines
32 KiB
Go
package astisub
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/binary"
|
||
"fmt"
|
||
"io"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/asticode/go-astitools/byte"
|
||
"github.com/asticode/go-astitools/map"
|
||
"github.com/asticode/go-astitools/ptr"
|
||
"github.com/pkg/errors"
|
||
"golang.org/x/text/unicode/norm"
|
||
)
|
||
|
||
// https://tech.ebu.ch/docs/tech/tech3264.pdf
|
||
// https://github.com/yanncoupin/stl2srt/blob/master/to_srt.py
|
||
|
||
// STL block sizes
|
||
const (
|
||
stlBlockSizeGSI = 1024
|
||
stlBlockSizeTTI = 128
|
||
)
|
||
|
||
// STL character code table number
|
||
const (
|
||
stlCharacterCodeTableNumberLatin uint16 = 12336
|
||
stlCharacterCodeTableNumberLatinCyrillic = 12337
|
||
stlCharacterCodeTableNumberLatinArabic = 12338
|
||
stlCharacterCodeTableNumberLatinGreek = 12339
|
||
stlCharacterCodeTableNumberLatinHebrew = 12340
|
||
)
|
||
|
||
// STL character code tables
|
||
// TODO Add missing tables
|
||
var (
|
||
stlCharacterCodeTables = map[uint16]*astimap.Map{
|
||
stlCharacterCodeTableNumberLatin: astimap.NewMap(0x0, "").
|
||
Set(0x20, " ").Set(0x21, "!").Set(0x22, "\"").Set(0x23, "#").
|
||
Set(0x24, "¤").Set(0x25, "%").Set(0x26, "&").Set(0x27, "'").
|
||
Set(0x28, "(").Set(0x29, ")").Set(0x2a, "*").Set(0x2b, "+").
|
||
Set(0x2c, ",").Set(0x2d, "-").Set(0x2e, ".").Set(0x2f, "/").
|
||
Set(0x30, "0").Set(0x31, "1").Set(0x32, "2").Set(0x33, "3").
|
||
Set(0x34, "4").Set(0x35, "5").Set(0x36, "6").Set(0x37, "7").
|
||
Set(0x38, "8").Set(0x39, "9").Set(0x3a, ":").Set(0x3b, ";").
|
||
Set(0x3c, "<").Set(0x3d, "=").Set(0x3e, ">").Set(0x3f, "?").
|
||
Set(0x40, "@").Set(0x41, "A").Set(0x42, "B").Set(0x43, "C").
|
||
Set(0x44, "D").Set(0x45, "E").Set(0x46, "F").Set(0x47, "G").
|
||
Set(0x48, "H").Set(0x49, "I").Set(0x4a, "J").Set(0x4b, "K").
|
||
Set(0x4c, "L").Set(0x4d, "M").Set(0x4e, "N").Set(0x4f, "O").
|
||
Set(0x50, "P").Set(0x51, "Q").Set(0x52, "R").Set(0x53, "S").
|
||
Set(0x54, "T").Set(0x55, "U").Set(0x56, "V").Set(0x57, "W").
|
||
Set(0x58, "X").Set(0x59, "Y").Set(0x5a, "Z").Set(0x5b, "[").
|
||
Set(0x5c, "\\").Set(0x5d, "]").Set(0x5e, "^").Set(0x5f, "_").
|
||
Set(0x60, "`").Set(0x61, "a").Set(0x62, "b").Set(0x63, "c").
|
||
Set(0x64, "d").Set(0x65, "e").Set(0x66, "f").Set(0x67, "g").
|
||
Set(0x68, "h").Set(0x69, "i").Set(0x6a, "j").Set(0x6b, "k").
|
||
Set(0x6c, "l").Set(0x6d, "m").Set(0x6e, "n").Set(0x6f, "o").
|
||
Set(0x70, "p").Set(0x71, "q").Set(0x72, "r").Set(0x73, "s").
|
||
Set(0x74, "t").Set(0x75, "u").Set(0x76, "v").Set(0x77, "w").
|
||
Set(0x78, "x").Set(0x79, "y").Set(0x7a, "z").Set(0x7b, "{").
|
||
Set(0x7c, "|").Set(0x7d, "}").Set(0x7e, "~").
|
||
Set(0xa0, string([]byte{0xC2, 0xA0})).Set(0xa1, "¡").Set(0xa2, "¢").
|
||
Set(0xa3, "£").Set(0xa4, "$").Set(0xa5, "¥").Set(0xa7, "§").
|
||
Set(0xa9, "‘").Set(0xaa, "“").Set(0xab, "«").Set(0xac, "←").
|
||
Set(0xad, "↑").Set(0xae, "→").Set(0xaf, "↓").
|
||
Set(0xb0, "°").Set(0xb1, "±").Set(0xb2, "²").Set(0xb3, "³").
|
||
Set(0xb4, "×").Set(0xb5, "µ").Set(0xb6, "¶").Set(0xb7, "·").
|
||
Set(0xb8, "÷").Set(0xb9, "’").Set(0xba, "”").Set(0xbb, "»").
|
||
Set(0xbc, "¼").Set(0xbd, "½").Set(0xbe, "¾").Set(0xbf, "¿").
|
||
Set(0xc1, string([]byte{0xCC, 0x80})).Set(0xc2, string([]byte{0xCC, 0x81})).
|
||
Set(0xc3, string([]byte{0xCC, 0x82})).Set(0xc4, string([]byte{0xCC, 0x83})).
|
||
Set(0xc5, string([]byte{0xCC, 0x84})).Set(0xc6, string([]byte{0xCC, 0x86})).
|
||
Set(0xc7, string([]byte{0xCC, 0x87})).Set(0xc8, string([]byte{0xCC, 0x88})).
|
||
Set(0xca, string([]byte{0xCC, 0x8A})).Set(0xcb, string([]byte{0xCC, 0xA7})).
|
||
Set(0xcd, string([]byte{0xCC, 0x8B})).Set(0xce, string([]byte{0xCC, 0xA8})).
|
||
Set(0xcf, string([]byte{0xCC, 0x8C})).
|
||
Set(0xd0, "―").Set(0xd1, "¹").Set(0xd2, "®").Set(0xd3, "©").
|
||
Set(0xd4, "™").Set(0xd5, "♪").Set(0xd6, "¬").Set(0xd7, "¦").
|
||
Set(0xdc, "⅛").Set(0xdd, "⅜").Set(0xde, "⅝").Set(0xdf, "⅞").
|
||
Set(0xe0, "Ω").Set(0xe1, "Æ").Set(0xe2, "Đ").Set(0xe3, "ª").
|
||
Set(0xe4, "Ħ").Set(0xe6, "IJ").Set(0xe7, "Ŀ").Set(0xe8, "Ł").
|
||
Set(0xe9, "Ø").Set(0xea, "Œ").Set(0xeb, "º").Set(0xec, "Þ").
|
||
Set(0xed, "Ŧ").Set(0xee, "Ŋ").Set(0xef, "ʼn").
|
||
Set(0xf0, "ĸ").Set(0xf1, "æ").Set(0xf2, "đ").Set(0xf3, "ð").
|
||
Set(0xf4, "ħ").Set(0xf5, "ı").Set(0xf6, "ij").Set(0xf7, "ŀ").
|
||
Set(0xf8, "ł").Set(0xf9, "ø").Set(0xfa, "œ").Set(0xfb, "ß").
|
||
Set(0xfc, "þ").Set(0xfd, "ŧ").Set(0xfe, "ŋ").Set(0xff, string([]byte{0xC2, 0xAD})),
|
||
}
|
||
)
|
||
|
||
// STL code page numbers
|
||
const (
|
||
stlCodePageNumberCanadaFrench uint32 = 3683891
|
||
stlCodePageNumberMultilingual = 3683632
|
||
stlCodePageNumberNordic = 3683893
|
||
stlCodePageNumberPortugal = 3683888
|
||
stlCodePageNumberUnitedStates = 3420983
|
||
)
|
||
|
||
// STL comment flag
|
||
const (
|
||
stlCommentFlagTextContainsSubtitleData = '\x00'
|
||
stlCommentFlagTextContainsCommentsNotIntendedForTransmission = '\x01'
|
||
)
|
||
|
||
// STL country codes
|
||
const (
|
||
stlCountryCodeFrance = "FRA"
|
||
)
|
||
|
||
// STL cumulative status
|
||
const (
|
||
stlCumulativeStatusFirstSubtitleOfACumulativeSet = '\x01'
|
||
stlCumulativeStatusIntermediateSubtitleOfACumulativeSet = '\x02'
|
||
stlCumulativeStatusLastSubtitleOfACumulativeSet = '\x03'
|
||
stlCumulativeStatusSubtitleNotPartOfACumulativeSet = '\x00'
|
||
)
|
||
|
||
// STL display standard code
|
||
const (
|
||
stlDisplayStandardCodeOpenSubtitling = "0"
|
||
stlDisplayStandardCodeLevel1Teletext = "1"
|
||
stlDisplayStandardCodeLevel2Teletext = "2"
|
||
)
|
||
|
||
// STL framerate mapping
|
||
var stlFramerateMapping = astimap.NewMap("STL25.01", 25).
|
||
Set("STL25.01", 25).
|
||
Set("STL30.01", 30)
|
||
|
||
// STL justification code
|
||
const (
|
||
stlJustificationCodeCentredText = '\x02'
|
||
stlJustificationCodeLeftJustifiedText = '\x01'
|
||
stlJustificationCodeRightJustifiedText = '\x03'
|
||
stlJustificationCodeUnchangedPresentation = '\x00'
|
||
)
|
||
|
||
// STL language codes
|
||
const (
|
||
stlLanguageCodeEnglish = "09"
|
||
stlLanguageCodeFrench = "0F"
|
||
)
|
||
|
||
// STL language mapping
|
||
var stlLanguageMapping = astimap.NewMap(stlLanguageCodeEnglish, LanguageEnglish).
|
||
Set(stlLanguageCodeFrench, LanguageFrench)
|
||
|
||
// STL timecode status
|
||
const (
|
||
stlTimecodeStatusNotIntendedForUse = "0"
|
||
stlTimecodeStatusIntendedForUse = "1"
|
||
)
|
||
|
||
// TTI Special Extension Block Number
|
||
const extensionBlockNumberReservedUserData = 0xfe
|
||
|
||
// ReadFromSTL parses an .stl content
|
||
func ReadFromSTL(i io.Reader) (o *Subtitles, err error) {
|
||
// Init
|
||
o = NewSubtitles()
|
||
|
||
// Read GSI block
|
||
var b []byte
|
||
if b, err = readNBytes(i, stlBlockSizeGSI); err != nil {
|
||
return
|
||
}
|
||
|
||
// Parse GSI block
|
||
var g *gsiBlock
|
||
if g, err = parseGSIBlock(b); err != nil {
|
||
err = errors.Wrap(err, "astisub: building gsi block failed")
|
||
return
|
||
}
|
||
|
||
// Create character handler
|
||
var ch *stlCharacterHandler
|
||
if ch, err = newSTLCharacterHandler(g.characterCodeTableNumber); err != nil {
|
||
err = errors.Wrap(err, "astisub: creating stl character handler failed")
|
||
return
|
||
}
|
||
|
||
// Update metadata
|
||
// TODO Add more STL fields to metadata
|
||
o.Metadata = &Metadata{
|
||
Framerate: g.framerate,
|
||
Language: stlLanguageMapping.B(g.languageCode).(string),
|
||
STLPublisher: g.publisher,
|
||
Title: g.originalProgramTitle,
|
||
}
|
||
|
||
// Parse Text and Timing Information (TTI) blocks.
|
||
for {
|
||
// Read TTI block
|
||
if b, err = readNBytes(i, stlBlockSizeTTI); err != nil {
|
||
if err == io.EOF {
|
||
err = nil
|
||
break
|
||
}
|
||
return
|
||
}
|
||
|
||
// Parse TTI block
|
||
var t = parseTTIBlock(b, g.framerate)
|
||
|
||
if t.extensionBlockNumber != extensionBlockNumberReservedUserData {
|
||
|
||
// Create item
|
||
var i = &Item{
|
||
EndAt: t.timecodeOut - g.timecodeStartOfProgramme,
|
||
StartAt: t.timecodeIn - g.timecodeStartOfProgramme,
|
||
}
|
||
|
||
// Loop through rows
|
||
for _, text := range bytes.Split(t.text, []byte{0x8a}) {
|
||
parseTeletextRow(i, ch, func() styler { return newSTLStyler() }, text)
|
||
}
|
||
|
||
// Append item
|
||
o.Items = append(o.Items, i)
|
||
}
|
||
|
||
}
|
||
return
|
||
}
|
||
|
||
// readNBytes reads n bytes
|
||
func readNBytes(i io.Reader, c int) (o []byte, err error) {
|
||
o = make([]byte, c)
|
||
var n int
|
||
if n, err = i.Read(o); err != nil || n != len(o) {
|
||
if err != nil {
|
||
if err == io.EOF {
|
||
return
|
||
}
|
||
err = errors.Wrapf(err, "astisub: reading %d bytes failed", c)
|
||
return
|
||
}
|
||
err = fmt.Errorf("astisub: Read %d bytes, should have read %d", n, c)
|
||
return
|
||
}
|
||
return
|
||
}
|
||
|
||
// gsiBlock represents a GSI block
|
||
type gsiBlock struct {
|
||
characterCodeTableNumber uint16
|
||
codePageNumber uint32
|
||
countryOfOrigin string
|
||
creationDate time.Time
|
||
diskSequenceNumber int
|
||
displayStandardCode string
|
||
editorContactDetails string
|
||
editorName string
|
||
framerate int
|
||
languageCode string
|
||
maximumNumberOfDisplayableCharactersInAnyTextRow int
|
||
maximumNumberOfDisplayableRows int
|
||
originalEpisodeTitle string
|
||
originalProgramTitle string
|
||
publisher string
|
||
revisionDate time.Time
|
||
revisionNumber int
|
||
subtitleListReferenceCode string
|
||
timecodeFirstInCue time.Duration
|
||
timecodeStartOfProgramme time.Duration
|
||
timecodeStatus string
|
||
totalNumberOfDisks int
|
||
totalNumberOfSubtitleGroups int
|
||
totalNumberOfSubtitles int
|
||
totalNumberOfTTIBlocks int
|
||
translatedEpisodeTitle string
|
||
translatedProgramTitle string
|
||
translatorContactDetails string
|
||
translatorName string
|
||
userDefinedArea string
|
||
}
|
||
|
||
// newGSIBlock builds the subtitles GSI block
|
||
func newGSIBlock(s Subtitles) (g *gsiBlock) {
|
||
// Init
|
||
g = &gsiBlock{
|
||
characterCodeTableNumber: stlCharacterCodeTableNumberLatin,
|
||
codePageNumber: stlCodePageNumberMultilingual,
|
||
countryOfOrigin: stlCountryCodeFrance,
|
||
creationDate: Now(),
|
||
diskSequenceNumber: 1,
|
||
displayStandardCode: stlDisplayStandardCodeLevel1Teletext,
|
||
framerate: 25,
|
||
languageCode: stlLanguageCodeFrench,
|
||
maximumNumberOfDisplayableCharactersInAnyTextRow: 40,
|
||
maximumNumberOfDisplayableRows: 23,
|
||
subtitleListReferenceCode: "12345678",
|
||
timecodeStatus: stlTimecodeStatusIntendedForUse,
|
||
totalNumberOfDisks: 1,
|
||
totalNumberOfSubtitleGroups: 1,
|
||
totalNumberOfSubtitles: len(s.Items),
|
||
totalNumberOfTTIBlocks: len(s.Items),
|
||
}
|
||
|
||
// Add metadata
|
||
if s.Metadata != nil {
|
||
g.framerate = s.Metadata.Framerate
|
||
g.languageCode = stlLanguageMapping.A(s.Metadata.Language).(string)
|
||
g.originalProgramTitle = s.Metadata.Title
|
||
g.publisher = s.Metadata.STLPublisher
|
||
}
|
||
|
||
// Timecode first in cue
|
||
if len(s.Items) > 0 {
|
||
g.timecodeFirstInCue = s.Items[0].StartAt
|
||
}
|
||
return
|
||
}
|
||
|
||
// parseGSIBlock parses a GSI block
|
||
func parseGSIBlock(b []byte) (g *gsiBlock, err error) {
|
||
// Init
|
||
g = &gsiBlock{
|
||
characterCodeTableNumber: binary.BigEndian.Uint16(b[12:14]),
|
||
countryOfOrigin: string(bytes.TrimSpace(b[274:277])),
|
||
codePageNumber: binary.BigEndian.Uint32(append([]byte{0x0}, b[0:3]...)),
|
||
displayStandardCode: string(bytes.TrimSpace([]byte{b[11]})),
|
||
editorName: string(bytes.TrimSpace(b[309:341])),
|
||
editorContactDetails: string(bytes.TrimSpace(b[341:373])),
|
||
framerate: stlFramerateMapping.B(string(b[3:11])).(int),
|
||
languageCode: string(bytes.TrimSpace(b[14:16])),
|
||
originalEpisodeTitle: string(bytes.TrimSpace(b[48:80])),
|
||
originalProgramTitle: string(bytes.TrimSpace(b[16:48])),
|
||
publisher: string(bytes.TrimSpace(b[277:309])),
|
||
subtitleListReferenceCode: string(bytes.TrimSpace(b[208:224])),
|
||
timecodeStatus: string(bytes.TrimSpace([]byte{b[255]})),
|
||
translatedEpisodeTitle: string(bytes.TrimSpace(b[80:112])),
|
||
translatedProgramTitle: string(bytes.TrimSpace(b[112:144])),
|
||
translatorContactDetails: string(bytes.TrimSpace(b[176:208])),
|
||
translatorName: string(bytes.TrimSpace(b[144:176])),
|
||
userDefinedArea: string(bytes.TrimSpace(b[448:])),
|
||
}
|
||
|
||
// Creation date
|
||
if v := strings.TrimSpace(string(b[224:230])); len(v) > 0 {
|
||
if g.creationDate, err = time.Parse("060102", v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: parsing date %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Revision date
|
||
if v := strings.TrimSpace(string(b[230:236])); len(v) > 0 {
|
||
if g.revisionDate, err = time.Parse("060102", v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: parsing date %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Revision number
|
||
if v := strings.TrimSpace(string(b[236:238])); len(v) > 0 {
|
||
if g.revisionNumber, err = strconv.Atoi(v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Total number of TTI blocks
|
||
if v := strings.TrimSpace(string(b[238:243])); len(v) > 0 {
|
||
if g.totalNumberOfTTIBlocks, err = strconv.Atoi(v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Total number of subtitles
|
||
if v := strings.TrimSpace(string(b[243:248])); len(v) > 0 {
|
||
if g.totalNumberOfSubtitles, err = strconv.Atoi(v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Total number of subtitle groups
|
||
if v := strings.TrimSpace(string(b[248:251])); len(v) > 0 {
|
||
if g.totalNumberOfSubtitleGroups, err = strconv.Atoi(v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Maximum number of displayable characters in any text row
|
||
if v := strings.TrimSpace(string(b[251:253])); len(v) > 0 {
|
||
if g.maximumNumberOfDisplayableCharactersInAnyTextRow, err = strconv.Atoi(v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Maximum number of displayable rows
|
||
if v := strings.TrimSpace(string(b[253:255])); len(v) > 0 {
|
||
if g.maximumNumberOfDisplayableRows, err = strconv.Atoi(v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Timecode start of programme
|
||
if v := strings.TrimSpace(string(b[256:264])); len(v) > 0 {
|
||
if g.timecodeStartOfProgramme, err = parseDurationSTL(v, g.framerate); err != nil {
|
||
err = errors.Wrapf(err, "astisub: parsing of stl duration %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Timecode first in cue
|
||
if v := strings.TrimSpace(string(b[264:272])); len(v) > 0 {
|
||
if g.timecodeFirstInCue, err = parseDurationSTL(v, g.framerate); err != nil {
|
||
err = errors.Wrapf(err, "astisub: parsing of stl duration %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Total number of disks
|
||
if v := strings.TrimSpace(string(b[272])); len(v) > 0 {
|
||
if g.totalNumberOfDisks, err = strconv.Atoi(v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
|
||
// Disk sequence number
|
||
if v := strings.TrimSpace(string(b[273])); len(v) > 0 {
|
||
if g.diskSequenceNumber, err = strconv.Atoi(v); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", v)
|
||
return
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// bytes transforms the GSI block into []byte
|
||
func (b gsiBlock) bytes() (o []byte) {
|
||
bs := make([]byte, 4)
|
||
binary.BigEndian.PutUint32(bs, b.codePageNumber)
|
||
o = append(o, astibyte.ToLength(bs[1:], ' ', 3)...) // Code page number
|
||
o = append(o, astibyte.ToLength([]byte(stlFramerateMapping.A(b.framerate).(string)), ' ', 8)...) // Disk format code
|
||
o = append(o, astibyte.ToLength([]byte(b.displayStandardCode), ' ', 1)...) // Display standard code
|
||
binary.BigEndian.PutUint16(bs, b.characterCodeTableNumber)
|
||
o = append(o, astibyte.ToLength(bs[:2], ' ', 2)...) // Character code table number
|
||
o = append(o, astibyte.ToLength([]byte(b.languageCode), ' ', 2)...) // Language code
|
||
o = append(o, astibyte.ToLength([]byte(b.originalProgramTitle), ' ', 32)...) // Original program title
|
||
o = append(o, astibyte.ToLength([]byte(b.originalEpisodeTitle), ' ', 32)...) // Original episode title
|
||
o = append(o, astibyte.ToLength([]byte(b.translatedProgramTitle), ' ', 32)...) // Translated program title
|
||
o = append(o, astibyte.ToLength([]byte(b.translatedEpisodeTitle), ' ', 32)...) // Translated episode title
|
||
o = append(o, astibyte.ToLength([]byte(b.translatorName), ' ', 32)...) // Translator's name
|
||
o = append(o, astibyte.ToLength([]byte(b.translatorContactDetails), ' ', 32)...) // Translator's contact details
|
||
o = append(o, astibyte.ToLength([]byte(b.subtitleListReferenceCode), ' ', 16)...) // Subtitle list reference code
|
||
o = append(o, astibyte.ToLength([]byte(b.creationDate.Format("060102")), ' ', 6)...) // Creation date
|
||
o = append(o, astibyte.ToLength([]byte(b.revisionDate.Format("060102")), ' ', 6)...) // Revision date
|
||
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.revisionNumber)), '0', 2), '0', 2)...) // Revision number
|
||
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.totalNumberOfTTIBlocks)), '0', 5), '0', 5)...) // Total number of TTI blocks
|
||
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.totalNumberOfSubtitles)), '0', 5), '0', 5)...) // Total number of subtitles
|
||
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.totalNumberOfSubtitleGroups)), '0', 3), '0', 3)...) // Total number of subtitle groups
|
||
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.maximumNumberOfDisplayableCharactersInAnyTextRow)), '0', 2), '0', 2)...) // Maximum number of displayable characters in any text row
|
||
o = append(o, astibyte.ToLength(astibyte.PadLeft([]byte(strconv.Itoa(b.maximumNumberOfDisplayableRows)), '0', 2), '0', 2)...) // Maximum number of displayable rows
|
||
o = append(o, astibyte.ToLength([]byte(b.timecodeStatus), ' ', 1)...) // Timecode status
|
||
o = append(o, astibyte.ToLength([]byte(formatDurationSTL(b.timecodeStartOfProgramme, b.framerate)), ' ', 8)...) // Timecode start of a programme
|
||
o = append(o, astibyte.ToLength([]byte(formatDurationSTL(b.timecodeFirstInCue, b.framerate)), ' ', 8)...) // Timecode first in cue
|
||
o = append(o, astibyte.ToLength([]byte(strconv.Itoa(b.totalNumberOfDisks)), ' ', 1)...) // Total number of disks
|
||
o = append(o, astibyte.ToLength([]byte(strconv.Itoa(b.diskSequenceNumber)), ' ', 1)...) // Disk sequence number
|
||
o = append(o, astibyte.ToLength([]byte(b.countryOfOrigin), ' ', 3)...) // Country of origin
|
||
o = append(o, astibyte.ToLength([]byte(b.publisher), ' ', 32)...) // Publisher
|
||
o = append(o, astibyte.ToLength([]byte(b.editorName), ' ', 32)...) // Editor's name
|
||
o = append(o, astibyte.ToLength([]byte(b.editorContactDetails), ' ', 32)...) // Editor's contact details
|
||
o = append(o, astibyte.ToLength([]byte{}, ' ', 75+576)...) // Spare bytes + user defined area // // Editor's contact details
|
||
return
|
||
}
|
||
|
||
// parseDurationSTL parses a STL duration
|
||
func parseDurationSTL(i string, framerate int) (d time.Duration, err error) {
|
||
// Parse hours
|
||
var hours, hoursString = 0, i[0:2]
|
||
if hours, err = strconv.Atoi(hoursString); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", hoursString)
|
||
return
|
||
}
|
||
|
||
// Parse minutes
|
||
var minutes, minutesString = 0, i[2:4]
|
||
if minutes, err = strconv.Atoi(minutesString); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", minutesString)
|
||
return
|
||
}
|
||
|
||
// Parse seconds
|
||
var seconds, secondsString = 0, i[4:6]
|
||
if seconds, err = strconv.Atoi(secondsString); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", secondsString)
|
||
return
|
||
}
|
||
|
||
// Parse frames
|
||
var frames, framesString = 0, i[6:8]
|
||
if frames, err = strconv.Atoi(framesString); err != nil {
|
||
err = errors.Wrapf(err, "astisub: atoi of %s failed", framesString)
|
||
return
|
||
}
|
||
|
||
// Set duration
|
||
d = time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second + time.Duration(1e9*frames/framerate)*time.Nanosecond
|
||
return
|
||
}
|
||
|
||
// formatDurationSTL formats a STL duration
|
||
func formatDurationSTL(d time.Duration, framerate int) (o string) {
|
||
// Add hours
|
||
if d.Hours() < 10 {
|
||
o += "0"
|
||
}
|
||
var delta = int(math.Floor(d.Hours()))
|
||
o += strconv.Itoa(delta)
|
||
d -= time.Duration(delta) * time.Hour
|
||
|
||
// Add minutes
|
||
if d.Minutes() < 10 {
|
||
o += "0"
|
||
}
|
||
delta = int(math.Floor(d.Minutes()))
|
||
o += strconv.Itoa(delta)
|
||
d -= time.Duration(delta) * time.Minute
|
||
|
||
// Add seconds
|
||
if d.Seconds() < 10 {
|
||
o += "0"
|
||
}
|
||
delta = int(math.Floor(d.Seconds()))
|
||
o += strconv.Itoa(delta)
|
||
d -= time.Duration(delta) * time.Second
|
||
|
||
// Add frames
|
||
var frames = int(int(d.Nanoseconds()) * framerate / 1e9)
|
||
if frames < 10 {
|
||
o += "0"
|
||
}
|
||
o += strconv.Itoa(frames)
|
||
return
|
||
}
|
||
|
||
// ttiBlock represents a TTI block
|
||
type ttiBlock struct {
|
||
commentFlag byte
|
||
cumulativeStatus byte
|
||
extensionBlockNumber int
|
||
justificationCode byte
|
||
subtitleGroupNumber int
|
||
subtitleNumber int
|
||
text []byte
|
||
timecodeIn time.Duration
|
||
timecodeOut time.Duration
|
||
verticalPosition int
|
||
}
|
||
|
||
// newTTIBlock builds an item TTI block
|
||
func newTTIBlock(i *Item, idx int) (t *ttiBlock) {
|
||
// Init
|
||
t = &ttiBlock{
|
||
commentFlag: stlCommentFlagTextContainsSubtitleData,
|
||
cumulativeStatus: stlCumulativeStatusSubtitleNotPartOfACumulativeSet,
|
||
extensionBlockNumber: 255,
|
||
justificationCode: stlJustificationCodeLeftJustifiedText,
|
||
subtitleGroupNumber: 0,
|
||
subtitleNumber: idx,
|
||
timecodeIn: i.StartAt,
|
||
timecodeOut: i.EndAt,
|
||
verticalPosition: 20,
|
||
}
|
||
|
||
// Add text
|
||
var lines []string
|
||
for _, l := range i.Lines {
|
||
lines = append(lines, l.String())
|
||
}
|
||
t.text = []byte(strings.Join(lines, "\n"))
|
||
return
|
||
}
|
||
|
||
// parseTTIBlock parses a TTI block
|
||
func parseTTIBlock(p []byte, framerate int) *ttiBlock {
|
||
return &ttiBlock{
|
||
commentFlag: p[15],
|
||
cumulativeStatus: p[4],
|
||
extensionBlockNumber: int(uint8(p[3])),
|
||
justificationCode: p[14],
|
||
subtitleGroupNumber: int(uint8(p[0])),
|
||
subtitleNumber: int(binary.LittleEndian.Uint16(p[1:3])),
|
||
text: p[16:128],
|
||
timecodeIn: parseDurationSTLBytes(p[5:9], framerate),
|
||
timecodeOut: parseDurationSTLBytes(p[9:13], framerate),
|
||
verticalPosition: int(uint8(p[13])),
|
||
}
|
||
}
|
||
|
||
// bytes transforms the TTI block into []byte
|
||
func (t *ttiBlock) bytes(g *gsiBlock) (o []byte) {
|
||
o = append(o, byte(uint8(t.subtitleGroupNumber))) // Subtitle group number
|
||
var b = make([]byte, 2)
|
||
binary.LittleEndian.PutUint16(b, uint16(t.subtitleNumber))
|
||
o = append(o, b...) // Subtitle number
|
||
o = append(o, byte(uint8(t.extensionBlockNumber))) // Extension block number
|
||
o = append(o, t.cumulativeStatus) // Cumulative status
|
||
o = append(o, formatDurationSTLBytes(t.timecodeIn, g.framerate)...) // Timecode in
|
||
o = append(o, formatDurationSTLBytes(t.timecodeOut, g.framerate)...) // Timecode out
|
||
o = append(o, byte(uint8(t.verticalPosition))) // Vertical position
|
||
o = append(o, t.justificationCode) // Justification code
|
||
o = append(o, t.commentFlag) // Comment flag
|
||
o = append(o, astibyte.ToLength(encodeTextSTL(string(t.text)), '\x8f', 112)...) // Text field
|
||
return
|
||
}
|
||
|
||
// formatDurationSTLBytes formats a STL duration in bytes
|
||
func formatDurationSTLBytes(d time.Duration, framerate int) (o []byte) {
|
||
// Add hours
|
||
var hours = int(math.Floor(d.Hours()))
|
||
o = append(o, byte(uint8(hours)))
|
||
d -= time.Duration(hours) * time.Hour
|
||
|
||
// Add minutes
|
||
var minutes = int(math.Floor(d.Minutes()))
|
||
o = append(o, byte(uint8(minutes)))
|
||
d -= time.Duration(minutes) * time.Minute
|
||
|
||
// Add seconds
|
||
var seconds = int(math.Floor(d.Seconds()))
|
||
o = append(o, byte(uint8(seconds)))
|
||
d -= time.Duration(seconds) * time.Second
|
||
|
||
// Add frames
|
||
var frames = int(int(d.Nanoseconds()) * framerate / 1e9)
|
||
o = append(o, byte(uint8(frames)))
|
||
return
|
||
}
|
||
|
||
// parseDurationSTLBytes parses a STL duration in bytes
|
||
func parseDurationSTLBytes(b []byte, framerate int) time.Duration {
|
||
return time.Duration(uint8(b[0]))*time.Hour + time.Duration(uint8(b[1]))*time.Minute + time.Duration(uint8(b[2]))*time.Second + time.Duration(1e9*int(uint8(b[3]))/framerate)*time.Nanosecond
|
||
}
|
||
|
||
type stlCharacterHandler struct {
|
||
accent string
|
||
c uint16
|
||
m *astimap.Map
|
||
}
|
||
|
||
func newSTLCharacterHandler(characterCodeTable uint16) (*stlCharacterHandler, error) {
|
||
if v, ok := stlCharacterCodeTables[characterCodeTable]; ok {
|
||
return &stlCharacterHandler{
|
||
c: characterCodeTable,
|
||
m: v,
|
||
}, nil
|
||
}
|
||
return nil, fmt.Errorf("astisub: table doesn't exist for character code table %d", characterCodeTable)
|
||
}
|
||
|
||
// TODO Use this instead of encodeTextSTL => use in teletext process like for decode
|
||
// TODO Test
|
||
func (h *stlCharacterHandler) encode(i []byte) byte {
|
||
return ' '
|
||
}
|
||
|
||
func (h *stlCharacterHandler) decode(i byte) (o []byte) {
|
||
k := int(i)
|
||
if !h.m.InA(k) {
|
||
return
|
||
}
|
||
v := h.m.B(k).(string)
|
||
if len(h.accent) > 0 {
|
||
o = norm.NFC.Bytes([]byte(v + h.accent))
|
||
h.accent = ""
|
||
return
|
||
} else if h.c == stlCharacterCodeTableNumberLatin && k >= 0xc0 && k <= 0xcf {
|
||
h.accent = v
|
||
return
|
||
}
|
||
return []byte(v)
|
||
}
|
||
|
||
type stlStyler struct {
|
||
boxing *bool
|
||
italics *bool
|
||
underline *bool
|
||
}
|
||
|
||
func newSTLStyler() *stlStyler {
|
||
return &stlStyler{}
|
||
}
|
||
|
||
func (s *stlStyler) parseSpacingAttribute(i byte) {
|
||
switch i {
|
||
case 0x80:
|
||
s.italics = astiptr.Bool(true)
|
||
case 0x81:
|
||
s.italics = astiptr.Bool(false)
|
||
case 0x82:
|
||
s.underline = astiptr.Bool(true)
|
||
case 0x83:
|
||
s.underline = astiptr.Bool(false)
|
||
case 0x84:
|
||
s.boxing = astiptr.Bool(true)
|
||
case 0x85:
|
||
s.boxing = astiptr.Bool(false)
|
||
}
|
||
}
|
||
|
||
func (s *stlStyler) hasBeenSet() bool {
|
||
return s.italics != nil || s.boxing != nil || s.underline != nil
|
||
}
|
||
|
||
func (s *stlStyler) hasChanged(sa *StyleAttributes) bool {
|
||
return s.boxing != sa.STLBoxing || s.italics != sa.STLItalics || s.underline != sa.STLUnderline
|
||
}
|
||
|
||
func (s *stlStyler) propagateStyleAttributes(sa *StyleAttributes) {
|
||
sa.propagateSTLAttributes()
|
||
}
|
||
|
||
func (s *stlStyler) update(sa *StyleAttributes) {
|
||
if s.boxing != nil && s.boxing != sa.STLBoxing {
|
||
sa.STLBoxing = s.boxing
|
||
}
|
||
if s.italics != nil && s.italics != sa.STLItalics {
|
||
sa.STLItalics = s.italics
|
||
}
|
||
if s.underline != nil && s.underline != sa.STLUnderline {
|
||
sa.STLUnderline = s.underline
|
||
}
|
||
}
|
||
|
||
// WriteToSTL writes subtitles in .stl format
|
||
func (s Subtitles) WriteToSTL(o io.Writer) (err error) {
|
||
// Do not write anything if no subtitles
|
||
if len(s.Items) == 0 {
|
||
err = ErrNoSubtitlesToWrite
|
||
return
|
||
}
|
||
|
||
// Write GSI block
|
||
var g = newGSIBlock(s)
|
||
if _, err = o.Write(g.bytes()); err != nil {
|
||
err = errors.Wrap(err, "astisub: writing gsi block failed")
|
||
return
|
||
}
|
||
|
||
// Loop through items
|
||
for idx, item := range s.Items {
|
||
// Write tti block
|
||
if _, err = o.Write(newTTIBlock(item, idx+1).bytes(g)); err != nil {
|
||
err = errors.Wrapf(err, "astisub: writing tti block #%d failed", idx+1)
|
||
return
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// TODO Remove below
|
||
|
||
// STL unicode diacritic
|
||
var stlUnicodeDiacritic = astimap.NewMap(byte('\x00'), "\x00").
|
||
Set(byte('\xc1'), "\u0300"). // Grave accent
|
||
Set(byte('\xc2'), "\u0301"). // Acute accent
|
||
Set(byte('\xc3'), "\u0302"). // Circumflex
|
||
Set(byte('\xc4'), "\u0303"). // Tilde
|
||
Set(byte('\xc5'), "\u0304"). // Macron
|
||
Set(byte('\xc6'), "\u0306"). // Breve
|
||
Set(byte('\xc7'), "\u0307"). // Dot
|
||
Set(byte('\xc8'), "\u0308"). // Umlaut
|
||
Set(byte('\xca'), "\u030a"). // Ring
|
||
Set(byte('\xcb'), "\u0327"). // Cedilla
|
||
Set(byte('\xcd'), "\u030B"). // Double acute accent
|
||
Set(byte('\xce'), "\u0328"). // Ogonek
|
||
Set(byte('\xcf'), "\u030c") // Caron
|
||
|
||
// STL unicode mapping
|
||
var stlUnicodeMapping = astimap.NewMap(byte('\x00'), "\x00").
|
||
Set(byte('\x8a'), "\u000a"). // Line break
|
||
Set(byte('\xa8'), "\u00a4"). // ¤
|
||
Set(byte('\xa9'), "\u2018"). // ‘
|
||
Set(byte('\xaa'), "\u201C"). // “
|
||
Set(byte('\xab'), "\u00AB"). // «
|
||
Set(byte('\xac'), "\u2190"). // ←
|
||
Set(byte('\xad'), "\u2191"). // ↑
|
||
Set(byte('\xae'), "\u2192"). // →
|
||
Set(byte('\xaf'), "\u2193"). // ↓
|
||
Set(byte('\xb4'), "\u00D7"). // ×
|
||
Set(byte('\xb8'), "\u00F7"). // ÷
|
||
Set(byte('\xb9'), "\u2019"). // ’
|
||
Set(byte('\xba'), "\u201D"). // ”
|
||
Set(byte('\xbc'), "\u00BC"). // ¼
|
||
Set(byte('\xbd'), "\u00BD"). // ½
|
||
Set(byte('\xbe'), "\u00BE"). // ¾
|
||
Set(byte('\xbf'), "\u00BF"). // ¿
|
||
Set(byte('\xd0'), "\u2015"). // ―
|
||
Set(byte('\xd1'), "\u00B9"). // ¹
|
||
Set(byte('\xd2'), "\u00AE"). // ®
|
||
Set(byte('\xd3'), "\u00A9"). // ©
|
||
Set(byte('\xd4'), "\u2122"). // ™
|
||
Set(byte('\xd5'), "\u266A"). // ♪
|
||
Set(byte('\xd6'), "\u00AC"). // ¬
|
||
Set(byte('\xd7'), "\u00A6"). // ¦
|
||
Set(byte('\xdc'), "\u215B"). // ⅛
|
||
Set(byte('\xdd'), "\u215C"). // ⅜
|
||
Set(byte('\xde'), "\u215D"). // ⅝
|
||
Set(byte('\xdf'), "\u215E"). // ⅞
|
||
Set(byte('\xe0'), "\u2126"). // Ohm Ω
|
||
Set(byte('\xe1'), "\u00C6"). // Æ
|
||
Set(byte('\xe2'), "\u0110"). // Đ
|
||
Set(byte('\xe3'), "\u00AA"). // ª
|
||
Set(byte('\xe4'), "\u0126"). // Ħ
|
||
Set(byte('\xe6'), "\u0132"). // IJ
|
||
Set(byte('\xe7'), "\u013F"). // Ŀ
|
||
Set(byte('\xe8'), "\u0141"). // Ł
|
||
Set(byte('\xe9'), "\u00D8"). // Ø
|
||
Set(byte('\xea'), "\u0152"). // Œ
|
||
Set(byte('\xeb'), "\u00BA"). // º
|
||
Set(byte('\xec'), "\u00DE"). // Þ
|
||
Set(byte('\xed'), "\u0166"). // Ŧ
|
||
Set(byte('\xee'), "\u014A"). // Ŋ
|
||
Set(byte('\xef'), "\u0149"). // ʼn
|
||
Set(byte('\xf0'), "\u0138"). // ĸ
|
||
Set(byte('\xf1'), "\u00E6"). // æ
|
||
Set(byte('\xf2'), "\u0111"). // đ
|
||
Set(byte('\xf3'), "\u00F0"). // ð
|
||
Set(byte('\xf4'), "\u0127"). // ħ
|
||
Set(byte('\xf5'), "\u0131"). // ı
|
||
Set(byte('\xf6'), "\u0133"). // ij
|
||
Set(byte('\xf7'), "\u0140"). // ŀ
|
||
Set(byte('\xf8'), "\u0142"). // ł
|
||
Set(byte('\xf9'), "\u00F8"). // ø
|
||
Set(byte('\xfa'), "\u0153"). // œ
|
||
Set(byte('\xfb'), "\u00DF"). // ß
|
||
Set(byte('\xfc'), "\u00FE"). // þ
|
||
Set(byte('\xfd'), "\u0167"). // ŧ
|
||
Set(byte('\xfe'), "\u014B"). // ŋ
|
||
Set(byte('\xff'), "\u00AD") // Soft hyphen
|
||
|
||
// encodeTextSTL encodes the STL text
|
||
func encodeTextSTL(i string) (o []byte) {
|
||
i = string(norm.NFD.Bytes([]byte(i)))
|
||
for _, c := range i {
|
||
if stlUnicodeMapping.InB(string(c)) {
|
||
o = append(o, stlUnicodeMapping.A(string(c)).(byte))
|
||
} else if stlUnicodeDiacritic.InB(string(c)) {
|
||
o = append(o[:len(o)-1], stlUnicodeDiacritic.A(string(c)).(byte), o[len(o)-1])
|
||
} else {
|
||
o = append(o, byte(c))
|
||
}
|
||
}
|
||
return
|
||
}
|