File Coverage

blib/lib/Subtitles.pm
Criterion Covered Total %
statement 336 461 72.8
branch 88 176 50.0
condition 16 46 34.7
subroutine 38 50 76.0
pod 20 20 100.0
total 498 753 66.1


line stmt bran cond sub pod time code
1             # $Id: Subtitles.pm,v 1.22 2012/02/14 13:21:48 dk Exp $
2             package Subtitles;
3 1     1   71563 use strict;
  1         3  
  1         44  
4             require Exporter;
5 1     1   5 use vars qw(@ISA @EXPORT @EXPORT_OK @codecs $VERSION);
  1         2  
  1         114  
6             @ISA = qw(Exporter);
7             @EXPORT = qw(codecs time2str);
8             @EXPORT_OK = qw(codecs time2hms time2shms hms2time time2str);
9             $VERSION = '1.04';
10            
11 1     1   1121 use Encode;
  1         12604  
  1         2217  
12              
13              
14             push @codecs, map { "Subtitles::Codec::$_" } qw( srt mdvd sub2 smi idx);
15              
16             #
17             # package-oriented API
18             #
19              
20             sub time2hms
21             {
22 8 50   8 1 30 shift if $#_ == 1; # package and object
23 8         10 my $time = $_[0];
24 8 50       23 $time = 0 if $time < 0;
25 8         8 $time += .0005;
26 8         49 return int($time/3600),int(($time%3600)/60),int($time%60),int(($time-int($time))*1000),
27             }
28              
29             sub time2shms
30             {
31 0 0   0 1 0 shift if $#_ == 1; # package and object
32 0         0 my $time = $_[0];
33 0         0 my $sign;
34 0 0       0 if ( $time < 0) {
35 0         0 $sign = -1;
36 0         0 $time = -$time;
37             } else {
38 0         0 $sign = 1;
39             }
40 0         0 $time += .0005;
41 0         0 return $sign,int($time/3600),int(($time%3600)/60),int($time%60),int(($time-int($time))*1000),
42             }
43              
44             sub hms2time
45             {
46 8 50   8 1 26 shift if $#_ == 4; # package and object
47 8         28 my ( $h, $m, $s, $ms) = @_;
48 8         37 return $h * 3600 + $m * 60 + $s + $ms / 1000;
49             }
50              
51              
52             sub time2str
53             {
54 0 0   0 1 0 shift if $#_ == 1; # package and object
55 0         0 my $time = $_[0];
56 0         0 my $is_minus = '';
57 0 0       0 $time = -$time, $is_minus = "-" if $time < 0;
58 0         0 return sprintf ( "$is_minus%02d:%02d:%02d.%03d", time2hms($time));
59             }
60              
61 1     1 1 12 sub codecs { @codecs }
62              
63             #
64             # object-oriented API
65             #
66              
67             sub new
68             {
69 5     5 1 752 my $class = shift;
70 5         40 return bless {
71             codec => undef,
72             @_,
73             text => [],
74             from => [],
75             to => [],
76             class => $class,
77             }, $class;
78             }
79              
80             sub load
81             {
82 4     4 1 23 my ( $self, $fh, $codec) = @_;
83 4         10 $self-> clear;
84 4         13 local $/;
85 4         87 my $content = <$fh>;
86 4 50       28 if ( $content =~ s/^(\xff\xfe|\xfe\xff)//) {
    50          
87             # found a 16-bit bom
88 0 0       0 my $le = ( $1 eq "\xff\xfe" ) ? 'v*' : 'n*';
89 0         0 $content = join('', map { chr } unpack($le, $content));
  0         0  
90             } elsif ( $content =~ s/^\xef\xbb\xbf//) {
91             # found a utf-8 bom
92 0         0 Encode::_utf8_on($content);
93             }
94 4         7 my @content;
95 4         28 for ( split "\n", $content) {
96 55         126 s/[\s\n\r]+$//;
97 55         103 push @content, $_;
98             }
99 4 50       15 unless ( defined $codec) {
100 4         8 for ( @content) {
101 55         79 my $line = $_;
102 55         74 for ( @codecs) {
103 275 100       546 next unless $_-> match( $line);
104 8         16 $codec = $_;
105             }
106             }
107             }
108 4 50       14 unless ( defined $codec) {
109 0         0 $@ = "No suitable codec is found";
110 0         0 return undef;
111             }
112 4         4 my $ret;
113 4         7 eval {
114 4         18 $ret = $codec-> read( $self, \@content);
115             };
116 4 50 33     26 return undef if $@ or !defined $ret;
117             # validate
118 4 50       6 if ( @{$self->{from}} == 0) {
  4         22  
119 0         0 $@ = "Empty subtitle";
120 0         0 return undef;
121             }
122 4 50       6 if ( @{$self->{from}} != @{$self->{to}}) {
  4         7  
  4         12  
123 0 0       0 if ( @{$self->{from}} == @{$self->{to}} + 1) {
  0         0  
  0         0  
124 0         0 push @{$self->{to}}, $self->{from}->[-1] + 2; # fix a dangling tail
  0         0  
125             } else {
126 0         0 my $a = @{$self->{from}};
  0         0  
127 0         0 my $b = @{$self->{to}};
  0         0  
128 0         0 $@ = "Number of 'from' ($a) and 'to' ($b) timeframe positions is different";
129 0         0 return undef;
130             }
131             }
132 4 50       6 if ( @{$self->{from}} != @{$self->{text}}) {
  4         8  
  4         13  
133 0 0       0 if ( @{$self->{from}} == @{$self->{text}} + 1) {
  0         0  
  0         0  
134 0         0 push @{$self->{text}}, ''; # fix a dangling tail
  0         0  
135             } else {
136 0         0 my $a = @{$self->{from}};
  0         0  
137 0         0 my $b = @{$self->{text}};
  0         0  
138 0         0 $@ = "Number of timeframes ($a) is different from the number of text lines ($b)";
139 0         0 return undef;
140             }
141             }
142 4         9 $self->{codec} = $codec;
143 4         28 return 1;
144             }
145              
146             sub codec
147             {
148 4 50   4 1 1883 return $_[0]-> {codec} unless $#_;
149 4         9 my ( $self, $codec) = @_;
150 4         9 my %c = map { $_ => 1 } @codecs;
  20         48  
151 4 50       15 return unless exists $c{$codec};
152 4 50 66     25 return if defined $self->{codec} && $self->{codec} eq $codec;
153 4 100       32 $self->{codec}-> downgrade($self, $codec) if defined $self->{codec};
154 4         15 $self->{codec} = $codec;
155             }
156              
157             sub rate
158             {
159 0 0   0 1 0 return $_[0]-> {rate} unless $#_;
160 0 0 0     0 return if defined $_[1] && $_[1] <= 0;
161 0         0 $_[0]->{rate} = $_[1];
162             }
163              
164             # parses
165             # SS
166             # MM:SS
167             # HH:MM:SS
168             # HH:MM:SS,msec
169             # MM:SS,msec
170             # into time
171             sub parse_time
172             {
173 0     0 1 0 my ( $self, $time) = @_;
174 0         0 my $sign = 1;
175 0 0       0 $sign = -1 if $time =~ s/^-//;
176 0 0 0     0 if ( $time =~ m/^(?:(\d{1,2}):)?(?:(\d{1,2}):)?(\d{1,2})(?:[\,\.\:](\d{1,3}))?$/) {
    0          
177 0         0 my ( $h, $m, $s, $ms) = ( $1, $2, $3, $4);
178 0 0 0     0 ( $h, $m) = ( $m, $h) if defined $h && ! defined $m;
179 0 0       0 $h = 0 unless defined $h;
180 0 0       0 $m = 0 unless defined $m;
181 0 0       0 $ms = '0' unless defined $ms;
182 0         0 $ms .= '0' while length($ms) < 3;
183 0         0 return $sign * ( $h * 3600 + $m * 60 + $s + $ms / 1000);
184             } elsif ( $self && $self-> {codec}) {
185 0         0 my $t = $self->{codec}->time($time);
186 0 0       0 return $sign * $t if defined $t;
187             }
188 0         0 undef;
189             }
190              
191 1     1 1 4 sub shift { $_[0]-> transform( 1, $_[1]) }
192 0     0 1 0 sub scale { $_[0]-> transform( $_[1], 0) }
193              
194 2     2 1 4 sub lines { scalar @{$_[0]->{text}} }
  2         7  
195              
196             # applies linear (y = ax+b) transformation within a scope
197             sub transform
198             {
199 1     1 1 3 my ( $self, $a, $b, $qfrom, $qto) = @_;
200 1 50 33     8 return if $a == 1 && $b == 0;
201 1 50       4 $qfrom = 0 unless defined $qfrom;
202 1 50       4 $qto = $self->{to}->[-1] unless defined $qto;
203 1         2 my $i;
204 1         3 my $n = $self-> lines;
205 1         4 my $from = $self->{from};
206 1         2 my $to = $self->{to};
207 1         5 for ( $i = 0; $i < $n; $i++) {
208 1 50 33     23 next if $$from[$i] > $qto || $$to[$i] < $qfrom;
209 1         3 $$from[$i] = $a * $$from[$i] + $b;
210 1         5 $$to[$i] = $a * $$to[$i] + $b;
211             }
212             }
213              
214             sub dup
215             {
216 2     2 1 6 my ( $self, $clear) = @_;
217 2 50       6 if ( $clear) {
218 2         25 return bless {
219             %$self,
220             text => [],
221             from => [],
222             to => [],
223             }, $self-> {class};
224             } else {
225 0         0 return bless {
226             %$self,
227 0         0 text => [ @{$self->{text}}],
228 0         0 from => [ @{$self->{from}}],
229 0         0 to => [ @{$self->{to}}],
230             }, $self-> {class};
231             }
232             }
233              
234             sub clear
235             {
236 4     4 1 7 my $self = $_[0];
237 4         9 $self-> {text} = [];
238 4         7 $self-> {from} = [];
239 4         9 $self-> {to} = [];
240             }
241              
242             sub join
243             {
244 1     1 1 3 my ( $self, $guest, $time_between) = @_;
245 1 50       4 $time_between = 2 unless defined $time_between;
246 1         4 my $delta = $time_between + $self-> length;
247 1         3 push @{$self->{text}}, @{$guest->{text}};
  1         2  
  1         3  
248 1         2 push @{$self->{from}}, map { $_ + $delta } @{$guest->{from}};
  1         5  
  1         2  
  1         3  
249 1         2 push @{$self->{to}}, map { $_ + $delta } @{$guest->{to}};
  1         2  
  1         3  
  1         2  
250             }
251              
252             sub split
253             {
254 1     1 1 470 my ( $self, $where) = @_;
255              
256 1         6 my ( $s1, $s2) = ( $self-> dup(1), $self-> dup(1));
257              
258 1         4 my $i;
259 1         5 my $n = $self->lines;
260 1         4 my $t = $self->{to};
261 1         2 my ( $end, $begin);
262              
263 1         2 $end = $n - 1;
264 1         5 for ( $i = 0; $i < $n; $i++) {
265 2 100       9 next if $$t[$i] <= $where;
266 1         2 $begin = $i;
267 1         2 $end = $i - 1;
268 1         2 last;
269             }
270              
271 1 50 33     21 if ( defined $end && $end >= 0) {
272 1         4 @{$s1->{text}} = @{$self->{text}}[0..$end];
  1         4  
  1         3  
273 1         3 @{$s1->{from}} = @{$self->{from}}[0..$end];
  1         3  
  1         2  
274 1         3 @{$s1->{to}} = @{$self->{to}}[0..$end];
  1         3  
  1         2  
275             }
276 1 50 33     30 if ( defined $begin && $begin < $n) {
277 1         4 @{$s2->{text}} = @{$self->{text}}[$begin..$n-1];
  1         3  
  1         3  
278 1         3 @{$s2->{from}} = @{$self->{from}}[$begin..$n-1];
  1         2  
  1         2  
279 1         3 @{$s2->{to}} = @{$self->{to}}[$begin..$n-1];
  1         2  
  1         2  
280 1         7 $s2-> shift( -$where);
281             }
282 1         5 ($s1,$s2);
283             }
284              
285             sub length
286             {
287 9     9 1 23 my $self = $_[0];
288 9 50       11 return @{$self->{to}} ? $self->{to}->[-1] : 0;
  9         51  
289             }
290              
291             sub save
292             {
293 4     4 1 20 my ( $self, $fh) = @_;
294 4         6 my $content;
295 4         5 eval {
296 4         22 $content = $self-> {codec}-> write( $self);
297 4 50 33     24 die "no content" unless defined $content and @$content;
298              
299 4         21 $content = CORE::join("\n", @$content);
300 4 50       30 if ( Encode::is_utf8($content)) {
301             # bomify
302 0 0       0 print $fh "\xef\xbb\xbf" or die "write error:$!";
303 0         0 binmode $fh, ':utf8';
304             }
305              
306 4 50       69 print $fh $content, "\n" or die "write error:$!";
307             };
308              
309 4 50       21 return $@ ? 0 : 1;
310             }
311              
312             package Subtitles::Codec;
313 1     1   13 use vars qw(@ISA);
  1         1  
  1         194  
314              
315             sub match
316             {
317 0     0   0 my ( $self, $line) = @_;
318 0         0 undef;
319             }
320              
321             sub read
322             {
323 0     0   0 my ( $self, $sub, $content) = @_;
324 0         0 die "abstract method call";
325             }
326              
327             sub write
328             {
329 0     0   0 my ( $self, $sub) = @_;
330 0         0 die "abstract method call";
331             }
332              
333 0     0   0 sub time { undef }
334              
335 3     3   4 sub downgrade {}
336              
337             package Subtitles::Codec::srt;
338 1     1   6 use vars qw(@ISA);
  1         1  
  1         649  
339             @ISA=qw(Subtitles::Codec);
340              
341             sub match
342             {
343 55     55   202 $_[1] =~ m/^(\d\d):(\d\d):(\d\d)[.,](\d\d\d)\s*-->\s*(\d\d):(\d\d):(\d\d)[.,](\d\d\d)/;
344             }
345              
346             sub read
347             {
348 1     1   2 my ( $self, $sub, $content) = @_;
349              
350 1         2 my $stage = 0;
351 1         3 my $num = 1;
352 1         2 my $line = 0;
353             # 0:
354             # 1: 1
355             # 2: 00:00:04,073 --> 00:00:05,781
356             # 3: Subtitle
357              
358 1         10 for ( @$content) {
359 7         6 $line++;
360 7 100       22 if ( $stage == 0) {
    100          
    100          
361 2 50       7 next unless length;
362 2 50       10 die "Invalid line numbering at line $line\n" unless m/^\d+$/;
363 2         3 $num++;
364 2         3 $stage++;
365             } elsif ( $stage == 1) {
366 2 50       12 die "Invalid timing at line $line\n" unless
367             m/^(\d\d):(\d\d):(\d\d)[.,](\d\d\d)\s*-->\s*(\d\d):(\d\d):(\d\d)[.,](\d\d\d)/;
368 2         3 push @{$sub->{from}}, Subtitles::hms2time( $1, $2, $3, $4);
  2         8  
369 2         4 push @{$sub->{to}}, Subtitles::hms2time( $5, $6, $7, $8);
  2         6  
370 2         4 $stage++;
371             } elsif ( $stage == 2) {
372 2 50       35 if ( length) {
373 2         4 push @{$sub->{text}}, $_;
  2         5  
374 2         4 $stage++;
375             } else {
376 0         0 push @{$sub->{text}}, '';
  0         0  
377 0         0 $stage = 0;
378             }
379             } else {
380 1 50       3 if ( length) {
381 0         0 $sub->{text}->[-1] .= "\n$_";
382             } else {
383 1         2 $stage = 0;
384             }
385             }
386             }
387 1         3 1;
388             }
389              
390             sub write
391             {
392 1     1   2 my ( $self, $sub) = @_;
393              
394 1         2 my $n = @{$sub->{text}};
  1         2  
395 1         2 my $i;
396             my @ret;
397 1         3 my $from = $sub->{from};
398 1         3 my $to = $sub->{to};
399 1         2 my $text = $sub->{text};
400 1         5 for ( $i = 0; $i < $n; $i++) {
401 2         7 push @ret,
402             $i + 1,
403             sprintf ( "%02d:%02d:%02d,%03d --> %02d:%02d:%02d,%03d",
404             Subtitles::time2hms($from->[$i]),
405             Subtitles::time2hms($to->[$i]),
406             ),
407             split ("\n", $text->[$i]),
408             ''
409             ;
410             }
411 1         3 \@ret;
412             }
413              
414             package Subtitles::Codec::mdvd;
415 1     1   5 use vars qw(@ISA);
  1         1  
  1         468  
416             @ISA=qw(Subtitles::Codec);
417              
418             sub match
419             {
420 55     55   188 $_[1] =~ m/^[{\[]\d+[}\]][{\[]\d*[}\]]/;
421             }
422              
423             sub read
424             {
425 1     1   3 my ( $self, $sub, $content) = @_;
426              
427 1         2 my $line = 0;
428             # {3724}{3774}Text
429              
430 1 50       4 my $fps = $sub->{rate} ? $sub->{rate} : 23.976;
431 1         2 my $from = $sub->{from};
432 1         2 my $to = $sub->{to};
433 1         3 my $text = $sub->{text};
434              
435 1         3 for ( @$content) {
436 2         2 $line++;
437 2 50       11 unless ( m/^[{\[](\d+)[}\]][{\[](\d*)[}\]](.*)$/) {
438 0         0 warn "Invalid input at line $line\n";
439 0         0 next;
440             }
441 2         8 push @$from, $1/$fps;
442 2 50       9 push @$to, length($2) ? ($2/$fps) : ($1+1)/$fps;
443 2         4 my $t = $3;
444 2         3 $t=~ s/\|\s*/\n/g;
445 2         6 push @$text, $t;
446             }
447 1         3 1;
448             }
449              
450             sub write
451             {
452 1     1   2 my ( $self, $sub) = @_;
453            
454 1 50       4 my $fps = $sub->{rate} ? $sub->{rate} : 23.976;
455              
456 1         2 my $n = @{$sub->{text}};
  1         2  
457 1         2 my $i;
458             my @ret;
459 1         2 my $from = $sub->{from};
460 1         4 my $to = $sub->{to};
461 1         3 my $text = $sub->{text};
462 1         4 for ( $i = 0; $i < $n; $i++) {
463 2         3 my $t = $text->[$i];
464 2         5 $t =~ s/\n/\|/g;
465 2         14 push @ret,
466             sprintf ( "{%d}{%d}%s",
467             int( $from->[$i] * $fps + .5),
468             int( $to->[$i] * $fps + .5),
469             $t
470             );
471             }
472 1         3 \@ret;
473             }
474              
475             package Subtitles::Codec::sub2;
476 1     1   5 use vars qw(@ISA);
  1         2  
  1         678  
477             @ISA=qw(Subtitles::Codec);
478              
479             sub match
480             {
481 55 100   55   321 $_[1] =~ m/^\[(SUBTITLE|COLF)\]/i or
482             $_[1] =~ m/^(\d\d):(\d\d):(\d\d)\.(\d\d),(\d\d):(\d\d):(\d\d)\.(\d\d)/;
483             }
484              
485             sub read
486             {
487 1     1   3 my ( $self, $sub, $content) = @_;
488              
489 1         2 my $line = 0;
490             # [INFORMATION]
491             # [AUTHOR]
492             # [SOURCE]
493             # [PRG]
494             # [FILEPATH]
495             # [DELAY]
496             # [CD TRACK]
497             # [COMMENT]
498             # [END INFORMATION]
499             #
500             # [SUBTITLE]
501             # [COLF]&HFFFFFF,[STYLE]no,[SIZE]18,[FONT]Arial
502             # 00:04:10.26,00:04:13.57
503             # Welcome to Gattaca.
504              
505 1         3 my $from = $sub->{from};
506 1         2 my $to = $sub->{to};
507 1         3 my $text = $sub->{text};
508 1         1 my @header;
509              
510 1         2 my $read_header = 1;
511 1         3 my $state = 0;
512              
513 1         3 for ( @$content) {
514 17         18 $line++;
515 17 100       31 if ( $read_header) {
516 13 100       31 if ( m/^(\d\d):(\d\d):(\d\d)\.(\d\d)\,(\d\d):(\d\d):(\d\d)\.(\d\d)/) {
517 1         2 $read_header = 0;
518 1         5 goto BODY;
519             }
520 12         21 push @header, $_;
521             } else {
522             BODY:
523 5 100       15 if ( $state == 0) {
524 3 100       16 next unless length;
525 2 50       12 die "Invalid timing at line $line\n" unless
526             m/^(\d\d):(\d\d):(\d\d)\.(\d\d)\,(\d\d):(\d\d):(\d\d)\.(\d\d)/;
527 2         14 push @$from, Subtitles::hms2time( $1, $2, $3, $4 * 10);
528 2         9 push @$to, Subtitles::hms2time( $5, $6, $7, $8 * 10);
529 2         5 $state = 1;
530             } else {
531 2         6 s/\[br\]\s*/\n/g;
532 2         4 push @$text, $_;
533 2         5 $state = 0;
534             }
535             }
536             }
537              
538 1         12 $sub->{sub2}->{header} = \@header;
539 1         4 1;
540             }
541              
542             sub write
543             {
544 1     1   3 my ( $self, $sub) = @_;
545              
546 1         2 my $n = @{$sub->{text}};
  1         3  
547 1         2 my $i;
548             my @ret;
549 1 50       5 if ( $sub->{sub2}->{header}) {
550 0         0 @ret = @{$sub->{sub2}->{header}};
  0         0  
551             } else {
552 1         9 @ret = split "\n", <
553             [INFORMATION]
554             [AUTHOR]
555             [SOURCE]
556             [PRG]
557             [FILEPATH]
558             [DELAY]
559             [CD TRACK]
560             [COMMENT]
561             [END INFORMATION]
562              
563             [SUBTITLE]
564             [STYLE]no,[SIZE]18
565             HEADER
566             }
567            
568 1         3 my $from = $sub->{from};
569 1         3 my $to = $sub->{to};
570 1         2 my $text = $sub->{text};
571 1         5 for ( $i = 0; $i < $n; $i++) {
572 2         7 my ($fh,$fm,$fs,$fms) = Subtitles::time2hms($from->[$i]);
573 2         5 my ($th,$tm,$ts,$tms) = Subtitles::time2hms($to->[$i]);
574 2         7 $fms = int ( $fms / 10);
575 2         3 $tms = int ( $tms / 10);
576 2         5 my $t = $text->[$i];
577 2         3 $t =~ s/\n/[br]/g;
578 2         14 push @ret,
579             sprintf ( "%02d:%02d:%02d.%02d,%02d:%02d:%02d.%02d",
580             $fh,$fm,$fs,$fms,
581             $th,$tm,$ts,$tms
582             ),
583             $t,
584             ''
585             ;
586             }
587 1         4 \@ret;
588             }
589              
590             package Subtitles::Codec::smi;
591 1     1   4 use vars qw(@ISA);
  1         1  
  1         1141  
592             @ISA=qw(Subtitles::Codec);
593              
594             sub match
595             {
596 55     55   175 $_[1] =~ m/^/i;
597             }
598              
599             sub read
600             {
601 1     1   2 my ( $self, $sub, $content) = @_;
602              
603             #
604             #
605             #
611             #
612             #
613             #
614             #

Juon - A curse born of a strong grudge held by someone
who died.

615             #
616             #

 

617             #
618             #
619             #
620              
621 1         3 my $from = $sub->{from};
622 1         2 my $to = $sub->{to};
623 1         2 my $text = $sub->{text};
624 1         3 my (@header,@footer);
625              
626 1         2 my $read_header = 1;
627 1         2 my $read_footer = 0;
628              
629 1         3 my $body = '';
630              
631             # extract body to inspect closer
632 1         3 for ( @$content) {
633 29 100       51 if ( $read_header) {
    100          
634 19 100       39 if ( m//i) {
635 1         2 $read_header = 0;
636             }
637 19         30 push @header, $_;
638             } elsif ( $read_footer) {
639 1         3 push @footer, $_;
640             } else {
641 9 100       22 if ( m/<\/BODY>/) {
642 1         2 push @footer, $_;
643 1         2 $read_footer = 1;
644 1         2 next;
645             }
646 8         11 $body .= $_;
647             }
648             }
649            
650             # parse body
651 1         3 my $sync = 0;
652 1         2 my $line = '';
653 1         8 while ( $body =~ m/\G(?:(?:(\s*)<\s*([^\>]*)\s*>)|([^<>]*))/gcs) {
654 13 100 66     84 if ( defined $2 and length $2) {
    100 66        
655 8         14 my $t = $1;
656 8         11 $_ = $2;
657 8 100       46 if ( m/^sync\s+start\s*=\s*(\d+)/i) {
    50          
    0          
658 4 100       16 $sub->{smi}->{s1gap} = length $t
659             unless defined $sub->{smi}->{s1gap};
660 4         6 my $s = $1;
661 4 50       16 die "Inconsistency near '$_' ( is less than previous sync $sync )\n"
662             if $s < $sync;
663 4 100       16 if ( $line !~ /^[\n\s]*$/s) {
664 3         7 $line =~ s/[\n\s]+$//s;
665 3         7 push @$from, $sync / 1000;
666 3         5 push @$to, $s / 1000;
667 3         6 push @$text, $line;
668             }
669 4         5 $sync = $s;
670 4         19 $line = '';
671             } elsif ( m/^p\s+class\s*\=\s*(\S+)/i) {
672 4 100       14 $sub->{smi}->{s2gap} = length $t
673             unless defined $sub->{smi}->{s2gap};
674 4 100       23 $sub-> {smi}-> {class} = $1
675             unless defined $sub->{smi}->{class};
676             } elsif ( m/^\s*br\s*/i) {
677 0         0 $line .= "\n";
678             }
679             } elsif ( defined $3 and length $3) {
680 4         8 $_ = $3;
681 4         5 s/&nsbp;/ /g;
682 4         59 $line .= $_;
683             }
684             }
685              
686 1         4 $sub->{smi}->{header} = \@header;
687 1         3 $sub->{smi}->{footer} = \@footer;
688 1         5 return 1;
689             }
690              
691             sub write
692             {
693 1     1   3 my ( $self, $sub) = @_;
694              
695 1         1 my $n = @{$sub->{text}};
  1         4  
696 1         2 my $i;
697             my @ret;
698 1         3 my $from = $sub->{from};
699 1         3 my $to = $sub->{to};
700 1         2 my $text = $sub->{text};
701              
702 1 50       6 my $smi_class = defined ($sub->{smi}->{class}) ? $sub->{smi}->{class} : 'SUBTTL';
703 1 50       4 if ( $sub->{smi}->{header}) {
704 0         0 @ret = @{$sub->{smi}->{header}};
  0         0  
705             } else {
706 1         13 @ret = split "\n", <
707            
708            
709            
724            
725            
726             HEADER
727             }
728              
729 1   50     9 my $s1 = ' ' x ( $sub->{smi}->{s1gap} || 0);
730 1   50     7 my $s2 = ' ' x ( $sub->{smi}->{s2gap} || 0);
731 1         6 for ( $i = 0; $i < $n; $i++) {
732 2         6 my $f = int($$from[$i] * 1000 + .5);
733 2         5 my $t = int($$to[$i] * 1000 + .5);
734 2         3 my $x = $$text[$i];
735 2         5 $x =~ s/\n/
/g;
736 2         12 push @ret,
737             "$s1",
738             "$s2

$x";

739 2 50 66     24 push @ret,
740             "$s1",
741             "$s2

 "

742             if $i == $n - 1 || int($$from[$i+1] * 1000 + .5) != $t;
743             ;
744             }
745 1 50       5 if ( $sub->{smi}->{footer}) {
746 0         0 push @ret, @{$sub->{smi}->{footer}};
  0         0  
747             } else {
748 1         3 push @ret, split "\n", <
749            
750            
751             FOOTER
752             }
753 1         4 \@ret;
754             }
755              
756             sub downgrade
757             {
758 0     0   0 for ( @{$_[1]->{text}}) {
  0         0  
759 0         0 s/<[^\>]*>//g;
760 0         0 s/{[^\}]*}//g;
761             }
762             }
763              
764             package Subtitles::Codec::idx;
765 1     1   8 use vars qw(@ISA);
  1         2  
  1         531  
766             @ISA=qw(Subtitles::Codec);
767              
768             sub match
769             {
770 55     55   220 $_[1] =~ m/^\s*\#\s*VobSub index file/
771             }
772              
773             sub read
774             {
775 0     0     my ( $self, $sub, $content) = @_;
776              
777 0           my $line = 0;
778             # # VobSub index file, v7 (do not modify this line!)
779             # #
780             # # To repair desyncronization, you can insert gaps this way:
781             # # (it usually happens after vob id changes)
782             # #
783             # # delay: [sign]hh:mm:ss:ms
784             # #
785             # # Where:
786             # # [sign]: +, - (optional)
787             # # hh: hours (0 <= hh)
788             # # mm/ss: minutes/seconds (0 <= mm/ss <= 59)
789             # # ms: milliseconds (0 <= ms <= 999)
790             # #
791             # # Note: You can't position a sub before the previous with a negative value.
792             # #
793             # # You can also modify timestamps or delete a few subs you don't like.
794             # # Just make sure they stay in increasing order.
795             #
796             #
797             # # Settings
798             #
799             # # Original frame size
800             # size: 720x576
801             #
802             # # Origin, relative to the upper-left corner, can be overloaded by aligment
803             # org: 0, 0
804             #
805             # # Image scaling (hor,ver), origin is at the upper-left corner or at the alignment coord (x, y)
806             # scale: 100%, 100%
807             #
808             # # Alpha blending
809             # alpha: 100%
810             #
811             # # Smoothing for very blocky images (use OLD for no filtering)
812             # smooth: OFF
813             #
814             # # In millisecs
815             # fadein/out: 50, 50
816             #
817             # # Force subtitle placement relative to (org.x, org.y)
818             # align: OFF at LEFT TOP
819             #
820             # # For correcting non-progressive desync. (in millisecs or hh:mm:ss:ms)
821             # # Note: Not effective in DirectVobSub, use "delay: ... " instead.
822             # time offset: 0
823             #
824             # # ON: displays only forced subtitles, OFF: shows everything
825             # forced subs: OFF
826             #
827             # # The original palette of the DVD
828             # palette: 0000e1, e83f07, 000000, fdfdfd, 033a03, ea12eb, faff1a, 095d76, 7c7c7c, e0e0e0, 701f03, 077307, 00006c, cc0ae9, d2ab0f, 730972
829             #
830             # # Custom colors (transp idxs and the four colors)
831             # custom colors: OFF, tridx: 1000, colors: fdfdfd, 000000, e0e0e0, faff1a
832             #
833             # # Language index in use
834             # langidx: 0
835             #
836             # # Dansk
837             # id: da, index: 0
838             # # Decomment next line to activate alternative name in DirectVobSub / Windows Media Player 6.x
839             # # alt: Dansk
840             # # Vob/Cell ID: 3, 1 (PTS: 0)
841             # timestamp: 00:00:44:280, filepos: 000000000
842             # timestamp: 00:00:50:520, filepos: 000003000
843              
844 0           my $from = $sub->{from};
845 0           my $to = $sub->{to};
846 0           my $text = $sub->{text};
847 0           my @header;
848              
849 0           my $read_header = 1;
850 0           my $state = 0;
851              
852 0           my @comments;
853              
854 0           for ( @$content) {
855 0 0         if ( m/^\s*timestamp\:\s*(\d\d)\:(\d\d)\:(\d\d)\:(\d+).*?filepos\:\s*(.*)$/) {
856 0           push @$from, Subtitles::hms2time( $1, $2, $3, $4);
857 0           push @$text, $5;
858             } else {
859 0           push @comments, [ scalar @$from, $_ ];
860             }
861 0           $line++;
862             }
863              
864 0           for ( $line = 0; $line < @$from - 1; $line++) {
865 0           $$to[$line] = $$from[$line + 1] - 0.002;
866             }
867 0 0         push @$to, $$from[-1] + 2.0 if @$from;
868              
869 0           $sub->{idx}->{comments} = \@comments;
870              
871 0           1;
872             }
873              
874             sub write
875             {
876 0     0     my ( $self, $sub) = @_;
877              
878 0 0         die "The idx format subtitles cannot be created from the other formats\n"
879             unless $sub->{idx}->{comments};
880              
881 0           my $from = $sub->{from};
882 0           my $to = $sub->{to};
883 0           my $text = $sub->{text};
884 0           my $c = $sub->{idx}->{comments};
885 0           my ( $i, $j);
886 0           my $n = @$text;
887 0           my @ret;
888 0           for ( $i = $j = 0; $i < $n; $i++) {
889 0   0       push @ret, $$c[$j++][1] while $j < @$c and $$c[$j][0] <= $i;
890 0           push @ret, sprintf( "timestamp: %02d:%02d:%02d:%03d, filepos: %s",
891             Subtitles::time2hms($from->[$i]), $text->[$i]);
892             }
893 0           \@ret;
894             }
895              
896             1;
897              
898             =pod
899              
900             =head1 NAME
901              
902             Subtitles - handle video subtitles in various text formats
903              
904             =head1 DESCRIPTION
905              
906             Video files (avi mpeg etc) are sometimes accompanied with subtitles, which are
907             currently very popular as text files. C provides means for simple
908             loading, re-timing, and storing these subtitle files. A command-line tool
909             F for the same purpose and using C interface is included in
910             the distribution.
911              
912             The module supports C, C, C, and C subtitle formats.
913              
914             Time values are floats, in seconds with millisecond precision.
915              
916             =head1 SYNOPSIS
917              
918             use Subtitles;
919            
920             my $sub = Subtitles->new();
921              
922             open F, 'Ichi The Killer.sub' or die "Cannot read:$!";
923             die "Cannot load:$@\n" unless $sub-> load(\*F);
924             close F;
925              
926             # back two minutes
927             $sub-> shift( $sub-> parse_time('-02:00'));
928              
929             # re-frame from 25 fps
930             $sub-> scale( 23.976 / 25 );
931              
932             # or both
933             $sub-> transform( -120, 0.96);
934             $sub-> transform( -120, 0.96, 0, $sub-> length - 60);
935              
936             # split in 2
937             my ( $part1, $part2) = $sub-> split( $self-> length / 2);
938              
939             # join back with 5-second gap
940             $part1-> join( $part2, 5);
941              
942             # save
943             open F, "> out.sub" or die "Cannot write:$!\n";
944             $part1-> save( \*F);
945             close F;
946              
947             # report
948             print "sub is ", time2str( $sub-> length);
949              
950             =head1 API
951              
952             =head2 Package methods
953              
954             =over
955              
956             =item codecs
957              
958             Returns array of installed codecs.
959              
960             =item hms2time HOURS, MINUTES, SECONDS, MILLISECONDS
961              
962             Combines four parameters into float time in seconds.
963              
964             =item time2hms TIME
965              
966             Splits time into four integers, - hours, minutes, seconds, and milliseconds.
967             If time is less than zero, zero times are returned.
968              
969             =item time2shms
970              
971             Splits time into five integers, - time sign, hours, minutes, seconds, and milliseconds.
972              
973             =item time2str TIME
974              
975             Converts time to a human-readable string.
976              
977             =back
978              
979             =head2 Object methods
980              
981             =over
982              
983             =item clear
984              
985             Removes all content
986              
987             =item codec [ STRING ]
988              
989             If STRING is not defined, returns currently associated codec.
990             Otherwise, sets the new codec in association. The STRING is
991             the codec's package name, such as C.
992              
993             =item dup [ CLEAR ]
994              
995             Duplicates object instance in deep-copy fashion. If CLEAR
996             flag is set, timeframes are not copied.
997              
998             =item join GUEST, GAP
999              
1000             Adds content of object GUEST at the end of the list of subtitles with GAP in seconds.
1001              
1002             =item length
1003              
1004             Returns length of subtitle span.
1005              
1006             =item load FH [ CODEC ]
1007              
1008             Reads subtitle content into object. If successful, returns 1;
1009             otherwise undef is returned and C<$@> contains the error.
1010              
1011             By default, tries to deduce which codec to use; to point the
1012             selection explicitly CODEC string is to be used.
1013              
1014             =item lines
1015              
1016             Returns number of subtitle cues.
1017              
1018             =item new
1019              
1020             Creates a new instance. To force a particular
1021             codec, supply C string here.
1022              
1023             =item parse_time STRING
1024              
1025             Parses STRING which is either a C<[[HH:]MM:]SS[,MSEC]> string
1026             or string in a format specific to a codec, for example, number
1027             of a frame.
1028              
1029             =item rate FPS
1030              
1031             Forces a particluar frame-per-second rate, if a codec
1032             can make use of it.
1033              
1034             =item save FH
1035              
1036             Writes content of instance into FH file handle,
1037             using the associated codec.
1038              
1039             =item scale A
1040              
1041             Changes time-scale. If A is 2, the subtitles
1042             go off 2 times slower, if 0.5 - two times faster, etc.
1043              
1044             =item shift B
1045              
1046             Shifts timings by B seconds. B can be negative.
1047              
1048             =item split TIME
1049              
1050             Splits the content of the instance between
1051             two newly created instances of the same class,
1052             by TIME, and returns these. The both resulting
1053             subtitles begin at time 0.
1054              
1055             =item transform A, B [FROM, TO]
1056              
1057             Applies linear transformation to the time-scale,
1058             such as C where C is the original
1059             time and C is the result. If FROM and TO
1060             brackets are set, the changes are applied only
1061             to the lines in the timeframe between these.
1062              
1063             =back
1064              
1065             =head1 BUGS
1066              
1067             This is alpha code, more a proof-of-concept rather
1068             that anything else, so most surely bugs are lurking.
1069              
1070             Anyway: not all subtitle types are recognized.
1071             The modules doesn't handle multi-language subtitles.
1072              
1073             =head1 SEE ALSO
1074              
1075             L - command-line wrapper for this module
1076              
1077             =head1 THANKS
1078              
1079             L, L.
1080              
1081             =head1 AUTHOR
1082              
1083             Dmitry Karasik, Edmitry@karasik.eu.orgE.
1084              
1085             =cut