You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
140 lines
3.8 KiB
140 lines
3.8 KiB
package Subtitle::Format::SRT; |
|
|
|
use strict; |
|
use warnings; |
|
use utf8; |
|
|
|
use Subtitle::Event; |
|
use Subtitle::Utils qw(:string :timing); |
|
|
|
use base 'Subtitle::Format'; |
|
|
|
sub new { |
|
my ($class, %args) = @_; |
|
my $self = { |
|
debug => $args{debug} || 0, |
|
eol => $args{eol} || "\n", |
|
events => [], |
|
log => [], |
|
timing_fmt => "%02d:%02d:%02d,%03d", |
|
}; |
|
|
|
return bless($self, $class); |
|
} |
|
|
|
sub new_event { return Subtitle::Event->new; } |
|
|
|
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->t_start or $event->t_end)) { |
|
$self->log(debug => "Expecting timing line at $linenum"); |
|
my @t; |
|
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; |
|
} |
|
$t[0] = parse_timing($start); |
|
unless ($t[0] >= 0) { |
|
$self->log(warn => "Can't parse timing at line $linenum: $start"); |
|
next; |
|
} |
|
$t[1] = parse_timing($end); |
|
unless ($t[1] >= 0) { |
|
$self->log(warn => "Can't parse timing at line $linenum: $end"); |
|
next; |
|
} |
|
$event->t_start($t[0]); |
|
$event->t_end ($t[1]); |
|
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->t_start or $event->t_end)) { |
|
trim($line); |
|
$self->log(debug => "Text line at $linenum -> append"); |
|
my $text = $event->text; |
|
$text .= $self->{eol} if $text; |
|
$text .= $line; |
|
$event->text($text); |
|
next; |
|
} |
|
} # foreach @lines |
|
# finalize last event |
|
if ($event and $event->text and ($event->t_start or $event->t_end)) { |
|
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->t_start); |
|
my $end = sprintf $self->{timing_fmt}, make_timing($e->t_end); |
|
push @lines, sprintf "%s --> %s", $start, $end; |
|
push @lines, $e->text; |
|
push @lines, ""; |
|
} |
|
|
|
return join($self->{eol} => @lines); |
|
} |
|
|
|
1;
|
|
|