Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions srt.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ func parseTextSrt(i string, sa *StyleAttributes) (o Line) {
case "font":
if c := htmlTokenAttribute(&token, "color"); c != nil {
// Parse the color string into a Color struct
if color, err := newColorFromHTMLString(*c); err == nil {
sa.SRTColor = color
}
sa.SRTColor = newColorFromHTMLString(*c)
}
}
case html.TextToken:
Expand Down
99 changes: 70 additions & 29 deletions subtitles.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ func (i Item) String() string {
// Color represents a color
type Color struct {
Alpha, Blue, Green, Red uint8

// raw preserves the original TTML/SRT color expression when it cannot be
// decoded into an RGBA triple (e.g. #RRGGBBAA with alpha, rgb()/rgba()
// functional notation, or a named color outside the recognized set).
// HTMLString returns it verbatim so these values round-trip losslessly
// instead of being silently dropped.
raw string
}

// newColorFromSSAString builds a new color based on an SSA string
Expand All @@ -162,46 +169,75 @@ func newColorFromSSAString(s string, base int) (c *Color, err error) {
return
}

// newColorFromHTMLString builds a new color based on a TTML hex string (e.g., "#ffffff" or "white")
func newColorFromHTMLString(s string) (*Color, error) {
// newColorFromHTMLString builds a color from a TTML/SRT color expression, e.g.
// "#ffffff", "white", "#ffcc00ff", "orange" or "rgb(255,204,0)". Recognized
// 6-digit hex and named colors (TTML1 §8.3.2) are decoded into RGBA. Any other
// legal expression this parser does not decode — #RRGGBBAA, rgb()/rgba(),
// "transparent", or a name outside the set below — is preserved verbatim in
// Color.raw so it round-trips instead of being silently dropped (see
// HTMLString). An empty or blank expression yields a nil color.
func newColorFromHTMLString(s string) *Color {
if strings.TrimSpace(s) == "" {
return nil
}

// Keep the original expression for verbatim preservation of undecoded values.
original := s
// Remove leading # if present
s = strings.TrimPrefix(s, "#")

// Check for named colors
// Named colors. "transparent" is intentionally omitted: it has no opaque
// RGBA equivalent, so it is preserved verbatim rather than flattened to
// #000000.
switch strings.ToLower(s) {
case "black":
return ColorBlack, nil
return ColorBlack
case "silver":
return ColorSilver
case "gray":
return ColorGray
case "white":
return ColorWhite
case "maroon":
return ColorMaroon
case "red":
return ColorRed, nil
return ColorRed
case "purple":
return ColorPurple
case "fuchsia", "magenta":
return ColorMagenta
case "green":
return ColorGreen, nil
return ColorGreen
case "lime":
return ColorLime
case "olive":
return ColorOlive
case "yellow":
return ColorYellow, nil
return ColorYellow
case "navy":
return ColorNavy
case "blue":
return ColorBlue, nil
case "magenta":
return ColorMagenta, nil
case "cyan":
return ColorCyan, nil
case "white":
return ColorWhite, nil
}

// Parse hex color (RRGGBB format)
if len(s) != 6 {
return nil, fmt.Errorf("invalid TTML color format: %s", s)
}

i, err := strconv.ParseUint(s, 16, 32)
if err != nil {
return nil, fmt.Errorf("parsing TTML color %s failed: %w", s, err)
return ColorBlue
case "teal":
return ColorTeal
case "aqua", "cyan":
return ColorCyan
}

// Parse hex color (RRGGBB format).
if len(s) == 6 {
if i, err := strconv.ParseUint(s, 16, 32); err == nil {
return &Color{
Red: uint8(i >> 16 & 0xff),
Green: uint8(i >> 8 & 0xff),
Blue: uint8(i & 0xff),
}
}
}

return &Color{
Red: uint8(i >> 16 & 0xff),
Green: uint8(i >> 8 & 0xff),
Blue: uint8(i & 0xff),
}, nil
// Anything else (e.g. #RRGGBBAA, rgb()/rgba(), "transparent", or an
// unrecognized name) is legal and preserved verbatim.
return &Color{raw: original}
}

func newColorFromWebVTTString(color string) (*Color, error) {
Expand Down Expand Up @@ -253,6 +289,11 @@ func (c *Color) HTMLString() string {
if c == nil {
return ""
}
// A preserved original expression (alpha hex, rgb(), unrecognized name) is
// emitted verbatim to keep the value lossless.
if c.raw != "" {
return c.raw
}
// TODO Check named colors first
return fmt.Sprintf("#%.6x", uint32(c.Red)<<16|uint32(c.Green)<<8|uint32(c.Blue))
}
Expand Down
27 changes: 27 additions & 0 deletions subtitles_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,33 @@ func TestColor(t *testing.T) {
assert.Equal(t, "12345678", c.SSAString())
}

func TestColorHTMLRoundTrip(t *testing.T) {
// Recognized 6-digit hex and named colors decode to RGBA (and normalize to
// hex on write); every other legal expression must survive a read -> write
// round-trip verbatim rather than being dropped. #RRGGBBAA is legal TTML1
// (§8.3.2) and the form IMSC1 mandates; "transparent" and names outside the
// recognized set are legal TTML/TTML2.
for _, tc := range []struct {
name string
in string
want string
}{
{name: "rrggbb parses", in: "#00ff00", want: "#00ff00"},
{name: "named color normalizes to hex", in: "white", want: "#ffffff"},
{name: "extended named color parses", in: "silver", want: "#c0c0c0"},
{name: "rrggbbaa preserved", in: "#ffcc00ff", want: "#ffcc00ff"},
{name: "transparent preserved", in: "transparent", want: "transparent"},
{name: "unrecognized name preserved", in: "orange", want: "orange"},
{name: "functional notation preserved", in: "rgb(255,204,0)", want: "rgb(255,204,0)"},
{name: "empty yields nil color", in: "", want: ""},
{name: "blank yields nil color", in: " ", want: ""},
} {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, newColorFromHTMLString(tc.in).HTMLString())
})
}
}

func TestParseDuration(t *testing.T) {
_, err := parseDuration("12:34:56,1234", ",", 3)
assert.EqualError(t, err, "astisub: Invalid number of millisecond digits detected in 12:34:56,1234")
Expand Down
8 changes: 2 additions & 6 deletions ttml.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,10 @@ func (s TTMLInStyleAttributes) styleAttributes() (o *StyleAttributes) {
}
// Parse colors if present
if s.Color != nil {
if color, err := newColorFromHTMLString(*s.Color); err == nil {
o.TTMLColor = color
}
o.TTMLColor = newColorFromHTMLString(*s.Color)
}
if s.BackgroundColor != nil {
if color, err := newColorFromHTMLString(*s.BackgroundColor); err == nil {
o.TTMLBackgroundColor = color
}
o.TTMLBackgroundColor = newColorFromHTMLString(*s.BackgroundColor)
}
o.propagateTTMLAttributes()
return
Expand Down