|
|
|
package Subtitle::Format::SSA;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
use warnings;
|
|
|
|
use utf8;
|
|
|
|
|
|
|
|
use Subtitle::Format::SSA::Header;
|
|
|
|
use Subtitle::Format::SSA::Style;
|
|
|
|
use Subtitle::Format::SSA::Event;
|
|
|
|
use Subtitle::Format::SSA::File;
|
|
|
|
|
|
|
|
use Subtitle::Utils qw(:string);
|
|
|
|
|
|
|
|
use base 'Subtitle::Format';
|
|
|
|
|
|
|
|
sub new {
|
|
|
|
my ($class, %args) = @_;
|
|
|
|
my $self = {
|
|
|
|
debug => 0,
|
|
|
|
eol => "\n",
|
|
|
|
type => undef,
|
|
|
|
%args,
|
|
|
|
h_order => [], # headers order
|
|
|
|
headers => {},
|
|
|
|
styles => [],
|
|
|
|
events => [],
|
|
|
|
fonts => [],
|
|
|
|
log => [],
|
|
|
|
};
|
|
|
|
|
|
|
|
return bless($self, $class);
|
|
|
|
}
|
|
|
|
|
|
|
|
sub new_event { ... }
|
|
|
|
|
|
|
|
sub parse {
|
|
|
|
my ($self, $lines) = @_;
|
|
|
|
my $linenum = 0;
|
|
|
|
my ($section, $font);
|
|
|
|
|
|
|
|
foreach my $line (@$lines) {
|
|
|
|
$linenum++;
|
|
|
|
chomp_all($line);
|
|
|
|
trim($line);
|
|
|
|
next unless $line;
|
|
|
|
study $line;
|
|
|
|
|
|
|
|
# check section switch
|
|
|
|
if (index($line, '[') >= 0 and $line =~ m/^\[([a-zA-Z0-9+ ]+)\]\s*$/o) {
|
|
|
|
my $name = lc($1);
|
|
|
|
if ($name =~ m{script \s+ info}oix) {
|
|
|
|
$section = 'header';
|
|
|
|
} elsif ($name eq 'events') {
|
|
|
|
$section = 'events';
|
|
|
|
} elsif ($name eq 'fonts') {
|
|
|
|
$section = 'fonts';
|
|
|
|
} elsif ($name =~ m{v4(\+)? \s+ styles}oix) {
|
|
|
|
$section = 'styles';
|
|
|
|
unless ($self->{type}) {
|
|
|
|
$self->{type} = ($1) ? 'ass' : 'ssa';
|
|
|
|
$self->log(warn => "Set type to $self->{type} because of line $linenum (fallback mode)");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$section = 'unknown';
|
|
|
|
$self->log(warn => "Unknown section at line $linenum: $line");
|
|
|
|
}
|
|
|
|
$self->log(debug => "section changed to [$section] at line $linenum");
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
# parse fonts data
|
|
|
|
if ($section eq 'fonts') {
|
|
|
|
if (index($line, 'fontname:') == 0) {
|
|
|
|
$line =~ m/^fontname:\s*(.*)/o;
|
|
|
|
$font = Subtitle::Format::SSA::File->new;
|
|
|
|
$font->type('font');
|
|
|
|
$font->name($1);
|
|
|
|
push @{ $self->{fonts} }, $font;
|
|
|
|
next;
|
|
|
|
} elsif ($font) {
|
|
|
|
$font->add_uue_line($line);
|
|
|
|
} # else: missing 'fontname' line, skip
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
# TODO: graphics section
|
|
|
|
unless ($section) {
|
|
|
|
$self->log(warn => "Line $linenum outside any section, skip");
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
# skip comments
|
|
|
|
next if $line =~ m{^ \s* ;}xo;
|
|
|
|
if ($section eq 'header') {
|
|
|
|
my $header = Subtitle::Format::SSA::Header->new;
|
|
|
|
unless ($header->parse($line)) {
|
|
|
|
$self->log(error => "Can't parse header at line $linenum: $line");
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
my $key = $header->hash_key();
|
|
|
|
if (exists $self->{headers}->{$key}) {
|
|
|
|
$self->log(warn => "Duplicate header at line $linenum: $header->{name}, replacing");
|
|
|
|
} else {
|
|
|
|
push @{ $self->{h_order} }, $key;
|
|
|
|
}
|
|
|
|
$self->{headers}->{$key} = $header;
|
|
|
|
if ($key eq 'scripttype') {
|
|
|
|
my $type = index($header->{value}, "+") >= 0 ? 'ass' : 'ssa';
|
|
|
|
$self->log(debug => "File recognized as '$type' type from header at line $linenum");
|
|
|
|
$self->{type} = $type;
|
|
|
|
}
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
if ($section eq 'styles') {
|
|
|
|
if ($line =~ m/^\s*Format:/oi) {
|
|
|
|
$self->log(debug => "Set style format from line $linenum");
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
my $style = Subtitle::Format::SSA::Style->new(type => $self->{type});
|
|
|
|
if ($style->parse($line)) {
|
|
|
|
push @{ $self->{styles} }, $style;
|
|
|
|
} else {
|
|
|
|
my $error = $style->error();
|
|
|
|
$self->log(error => "Can't parse style at line $linenum: $error");
|
|
|
|
}
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
if ($section eq 'events') {
|
|
|
|
if ($line =~ m/^\s*Format:/oi) {
|
|
|
|
$self->log(debug => "Set event format from line $linenum");
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
my $event = Subtitle::Format::SSA::Event->new(type => $self->{type});
|
|
|
|
if ($event->parse($line)) {
|
|
|
|
push @{ $self->{events} }, $event;
|
|
|
|
} else {
|
|
|
|
my $error = $event->error();
|
|
|
|
$self->log(error => "Can't parse event at line $linenum: $error");
|
|
|
|
}
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
$self->log(warn => "unrecognized line at line $linenum: $line");
|
|
|
|
}
|
|
|
|
|
|
|
|
return scalar @{ $self->{events} };
|
|
|
|
}
|
|
|
|
|
|
|
|
sub build {
|
|
|
|
my ($self) = @_;
|
|
|
|
my (@lines, $out);
|
|
|
|
|
|
|
|
# headers
|
|
|
|
push @lines, "[Script Info]";
|
|
|
|
push @lines, "; generated with Subtitle::Format::SSA";
|
|
|
|
foreach my $key (@{ $self->{h_order} }) {
|
|
|
|
my $h = $self->{headers}->{$key};
|
|
|
|
push @lines, $h->to_string();
|
|
|
|
}
|
|
|
|
push @lines, "";
|
|
|
|
|
|
|
|
# styles
|
|
|
|
if ($self->{type} eq 'ass') {
|
|
|
|
push @lines, "[V4+ Styles]";
|
|
|
|
} else {
|
|
|
|
push @lines, "[V4 Styles]";
|
|
|
|
}
|
|
|
|
push @lines, Subtitle::Format::SSA::Style->new(type => $self->{type})->get_format_line();
|
|
|
|
foreach my $s (@{ $self->{styles} }) {
|
|
|
|
push @lines, $s->to_string();
|
|
|
|
}
|
|
|
|
push @lines, "";
|
|
|
|
|
|
|
|
# events
|
|
|
|
push @lines, "[Events]";
|
|
|
|
push @lines, Subtitle::Format::SSA::Event->new(type => $self->{type})->get_format_line();
|
|
|
|
foreach my $e (@{ $self->{events} }) {
|
|
|
|
push @lines, $e->to_string();
|
|
|
|
}
|
|
|
|
push @lines, "";
|
|
|
|
|
|
|
|
if (scalar @{ $self->{fonts} } > 0) {
|
|
|
|
push @lines, "[Fonts]";
|
|
|
|
foreach my $font (@{ $self->{fonts} }) {
|
|
|
|
push @lines, $font->uue_block;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# TODO: graphics
|
|
|
|
|
|
|
|
return join($self->{eol}, @lines);
|
|
|
|
}
|
|
|
|
|
|
|
|
sub style_usage {
|
|
|
|
my ($self) = @_;
|
|
|
|
|
|
|
|
my $styles = {};
|
|
|
|
foreach my $s (@{ $self->{styles} }) {
|
|
|
|
$styles->{ $s->{name} } = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
my $stats = {};
|
|
|
|
foreach my $e (@{ $self->{events} }) {
|
|
|
|
my $sn = $e->{style} || 'Default';
|
|
|
|
if ($sn eq '*Default') {
|
|
|
|
# even libass author not knows what really '*' means
|
|
|
|
$sn = 'Default';
|
|
|
|
}
|
|
|
|
$stats->{$sn} //= 0;
|
|
|
|
$stats->{$sn}++;
|
|
|
|
next if exists $styles->{$sn};
|
|
|
|
my $msg = sprintf "Event uses style named '%s' but it's not defined", $sn;
|
|
|
|
$self->log(warn => $msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
my @unused = grep { $stats->{$_} == 0 } keys %{ $stats };
|
|
|
|
foreach my $u (@unused) {
|
|
|
|
$self->log(warn => "Style '$u' not used by any event");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $stats;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub fonts_usage {
|
|
|
|
my ($self) = @_;
|
|
|
|
my $stats = {};
|
|
|
|
foreach my $s (@{ $self->{styles} }) {
|
|
|
|
my $fn = $s->{fontname};
|
|
|
|
$stats->{$fn} //= 0;
|
|
|
|
$stats->{$fn}++;
|
|
|
|
}
|
|
|
|
foreach my $e (@{ $self->{events} }) {
|
|
|
|
next unless index($e->{text}, '\fn') > 0;
|
|
|
|
my ($fn) = ($e->{text} =~ m<\\fn([^\\}]+)>);
|
|
|
|
next unless $fn;
|
|
|
|
$stats->{$fn} //= 0;
|
|
|
|
$stats->{$fn}++;
|
|
|
|
}
|
|
|
|
return $stats;
|
|
|
|
}
|
|
|
|
|
|
|
|
1;
|