package Subtitle::Format::SRT; use strict; use warnings; use utf8; use Subtitle::Utils qw(:string :timing); use base 'Subtitle::BASE'; sub new { my ($class, %args) = @_; my $self = { debug => 0, eol => "\n", %args, events => [], log => [], timing_fmt => "%02d:%02d:%02d,%03d", }; return bless($self, $class); } sub new_event { return +{ id => undef, timing => undef, text => undef }; } sub parse { my ($self, $lines) = @_; my $linenum = 0; my $event; foreach my $line (@{ $lines }) { $linenum++; chomp_all($line); # expected: event id if ($line and not $event) { $event = $self->new_event; trim($line); $event->{id} = $line; $self->log(debug => "New event with id: $event->{id}"); next; } # expected: timing if ($line and $event and not $event->{timing}) { $self->log(debug => "Expecting timing line at $linenum"); my $timing = []; my ($start, $end, $rest) = ($line =~ m/(\S+)\s*-->\s*(\S+)(.*)/o); unless ($start and $end) { $self->log(warn => "Expected timing but found `$line` at $linenum, skipped"); next; } $timing->[0] = parse_timing($start); unless ($timing->[0] >= 0) { $self->log(warn => "Can't parse timing at line $linenum: $start"); next; } $timing->[1] = parse_timing($end); unless ($timing->[1] >= 0) { $self->log(warn => "Can't parse timing at line $linenum: $end"); next; } $event->{timing} = $timing; trim($rest); next unless $rest; # extension: timing # example: X1:050 X2:669 Y1:234 Y2:436 if ($rest =~ m/([XY][12]:(\d+))/io) { foreach my $token (split(/\s+/, $rest)) { $event->{coords} //= {}; if ($token =~ m/([XY][12]):(\d+)/io) { $event->{coords}->{lc($1)} = $2; next; } $self->log(warn => "Garbage detected instead event coord: $token"); } unless (scalar keys %{ $event->{coords} } == 4) { delete $event->{coords}; $self->log(warn => "Incomplete coord set for event at line $linenum"); } next; } # extension: ssa-like style # example: SSA: Dialogue, Layer: 0, Style: Reply, Name: NTP, MarginL: 0300, MarginR: 0000, MarginV: 0300, Effect: !Effect if ($rest =~ m/SSA:\s*\S+,\s*(.+)/o) { $rest = $1; foreach my $token (split(/\s*,\s*/, $rest)) { next unless $token =~ m/^(\S+):\s*(.+)/o; trim($2); $event->{style} //= {}; $event->{style}->{lc($1)} = $2; } next; } $self->log(warn => "garbage after timing at line $linenum: $rest"); next; } # expected: empty line if ($event and not $line) { $self->log(debug => "Empty line at line $linenum -> finalize"); push @{ $self->{events} }, $event; undef $event; next; } # expected: event text if ($line and $event and $event->{timing}) { trim($line); $self->log(debug => "Text line at $linenum -> append"); $event->{text} //= ''; $event->{text} .= $self->{eol} if $event->{text}; $event->{text} .= $line; next; } } # foreach @lines # finalize last event if ($event and $event->{timing} and $event->{text}) { push @{ $self->{events} }, $event; } return scalar @{ $self->{events} }; } sub build { my ($self) = @_; my (@lines, $out); foreach my $e (@{ $self->{events} }) { push @lines, $e->{id}; my $start = sprintf $self->{timing_fmt}, make_timing($e->{timing}->[0]); my $end = sprintf $self->{timing_fmt}, make_timing($e->{timing}->[1]); push @lines, sprintf("%s --> %s", $start, $end); push @lines, $e->{text}; push @lines, ""; } return join($self->{eol} => @lines); } 1;