File Coverage

blib/lib/ChordPro/Output/PDF/Writer.pm
Criterion Covered Total %
statement 272 471 57.7
branch 62 198 31.3
condition 40 136 29.4
subroutine 38 50 76.0
pod 0 33 0.0
total 412 888 46.4


line stmt bran cond sub pod time code
1             #! perl
2              
3             package main;
4              
5             our $config;
6              
7             package ChordPro::Output::PDF::Writer;
8              
9 10     10   77 use strict;
  10         31  
  10         444  
10 10     10   69 use warnings;
  10         48  
  10         963  
11 10     10   173 use Encode;
  10         20  
  10         1029  
12 10     10   5395 use Text::Layout;
  10         112874  
  10         363  
13 10     10   4790 use IO::String;
  10         30087  
  10         347  
14 10     10   87 use Carp;
  10         24  
  10         718  
15 10     10   79 use utf8;
  10         32  
  10         79  
16              
17 10     10   312 use ChordPro::Utils qw( expand_tilde demarkup );
  10         28  
  10         628  
18 10     10   78 use ChordPro::Output::Common qw( fmt_subst prep_outlines );
  10         37  
  10         2644  
19              
20             # For regression testing, run perl with PERL_HASH_SEED set to zero.
21             # This eliminates the arbitrary order of font definitions and triggers
22             # us to pinpoint some other data that would otherwise be varying.
23             my $regtest = defined($ENV{PERL_HASH_SEED}) && $ENV{PERL_HASH_SEED} == 0;
24             my $faketime = 1465041600;
25              
26             my %fontcache; # speeds up 2 seconds per song
27              
28             sub new {
29 8     8 0 42 my ( $pkg, $ps, $pdfapi ) = @_;
30 8         38 my $self = bless { ps => $ps }, $pkg;
31 8         65 $self->{pdfapi} = $pdfapi;
32 8         77 $self->{pdf} = $pdfapi->new;
33 8 50       23260 $self->{pdf}->{forcecompress} = 0 if $regtest;
34             $self->{pdf}->mediabox( $ps->{papersize}->[0],
35 8         75 $ps->{papersize}->[1] );
36 8         1694 $self->{layout} = Text::Layout->new( $self->{pdf} );
37 8         48641 $self->{tmplayout} = undef;
38              
39 8         31 %fontcache = ();
40              
41 8         42 $self;
42             }
43              
44             sub info {
45 8     8 0 53 my ( $self, %info ) = @_;
46              
47 8   33     80 $info{CreationDate} //= pdf_date();
48              
49             # PDF::API2 2.42+ does not accept the final apostrophe.
50 10     10   87 no warnings 'redefine';
  10         46  
  10         2345  
51 8     8   91 local *PDF::API2::_is_date = sub { 1 };
  8         241  
52              
53 8 50       99 if ( $self->{pdf}->can("info_metadata") ) {
54 8         44 for ( keys(%info) ) {
55 24         727 $self->{pdf}->info_metadata( $_, $info{$_} );
56             }
57             }
58             else {
59 0         0 $self->{pdf}->info(%info);
60             }
61             }
62              
63             # Return a PDF compliant date/time string.
64             sub pdf_date {
65 8     8 0 33 my ( $t ) = @_;
66 8 50 33     65 $t ||= $regtest ? $faketime : time;
67              
68 10     10   96 use POSIX qw( strftime );
  10         25  
  10         109  
69 8         420 my $r = strftime( "%Y%m%d%H%M%S%z", localtime($t) );
70             # Don't use s///r to keep PERL_MIN_VERSION low.
71 8         139 $r =~ s/(..)$/'$1'/; # +0100 -> +01'00'
72 8         49 $r;
73             }
74              
75             sub wrap {
76 144     144 0 368 my ( $self, $text, $m ) = @_;
77              
78 144         266 my $ex = "";
79 144         224 my $sp = "";
80             #warn("TEXT: |$text| ($m)\n");
81 144         398 while ( $self->strwidth($text) > $m ) {
82 0         0 my ( $l, $s, $r ) = $text =~ /^(.+)([-_,.:;\s])(.+)$/;
83 0 0       0 return ( $text, $ex ) unless defined $s;
84             #warn("WRAP: |$text| -> |$l|$s|$r$sp$ex|\n");
85 0 0       0 if ( $s =~ /\S/ ) {
86 0         0 $l .= $s;
87 0         0 $s = "";
88             }
89 0         0 $text = $l;
90 0         0 $ex = $r . $sp . $ex;
91 0         0 $sp = $s;
92             }
93              
94 144         39849 return ( $text, $ex );
95             }
96              
97             sub _fgcolor {
98 1428     1428   3072 my ( $self, $col ) = @_;
99 1428 100 66     8171 if ( !defined($col) || $col =~ /^foreground(?:-medium|-light)?$/ ) {
    50          
    50          
100 684   50     2567 $col = $self->{ps}->{theme}->{$col//"foreground"};
101             }
102             elsif ( $col eq "background" ) {
103 0         0 $col = $self->{ps}->{theme}->{background};
104             }
105             elsif ( !$col ) {
106 0         0 Carp::confess("Undefined fgcolor: $col");
107             }
108 1428         4454 $col;
109             }
110              
111             sub _bgcolor {
112 732     732   1696 my ( $self, $col ) = @_;
113 732 50 66     3236 if ( !defined($col) || $col eq "background" ) {
    0          
    0          
114 732         1795 $col = $self->{ps}->{theme}->{background};
115             }
116             elsif ( $col =~ /^foreground(?:-medium|-light)?$/ ) {
117 0         0 $col = $self->{ps}->{theme}->{$col};
118             }
119             elsif ( !$col ) {
120 0         0 Carp::confess("Undefined bgcolor: $col");
121             }
122 732         1612 $col;
123             }
124              
125             sub _yflip {
126             #warn("Text::Layout = $Text::Layout::VERSION\n" );
127 8     8   60 $Text::Layout::VERSION gt "0.027";
128             }
129              
130             my $yflip;
131              
132             sub fix_musicsyms {
133 1243     1243 0 2493 my ( $text, $font ) = @_;
134              
135 1243         2585 for ( $text ) {
136 1243 50       3849 if ( /♯/ ) {
137 0 0 0     0 unless ( $font->{has_sharp} //=
138             $font->{fd}->{font}->glyphByUni(ord("♯")) ne ".notdef" ) {
139 0         0 s;♯;#;g;
140             }
141             }
142 1243 50       3293 if ( /♭/ ) {
143 0 0 0     0 unless ( $font->{has_flat} //=
144             $font->{fd}->{font}->glyphByUni(ord("♭")) ne ".notdef" ) {
145 0         0 s;♭;!;g;
146             }
147             }
148             }
149 1243         2706 return $text;
150             }
151              
152             sub text {
153 684     684 0 27620 my ( $self, $text, $x, $y, $font, $size, $nomarkup ) = @_;
154             # print STDERR ("T: @_\n");
155 684   66     2204 $font ||= $self->{font};
156 684         1463 $text = fix_musicsyms( $text, $font );
157 684   66     2735 $size ||= $font->{size};
158              
159 684         2796 $self->{layout}->set_font_description($font->{fd});
160 684         10827 $self->{layout}->set_font_size($size);
161             # We don't have set_color in the API.
162 684         4605 $self->{layout}->{_currentcolor} = $self->_fgcolor($font->{color});
163             # Watch out for regression... May have to do this in the nomarkup case only.
164 684 50       1833 if ( $nomarkup ) {
165 0         0 $text =~ s/'/\x{2019}/g; # friendly quote
166 0         0 $self->{layout}->set_text($text);
167             }
168             else {
169 684         2183 $self->{layout}->set_markup($text);
170 684         34836 for ( @{ $self->{layout}->{_content} } ) {
  684         2320  
171 684         1865 $_->{text} =~ s/\'/\x{2019}/g; # friendly quote
172             }
173             }
174 684         2020 $y -= $self->{layout}->get_baseline;
175 684         168737 $self->{layout}->show( $x, $y, $self->{pdftext} );
176              
177 684         1134357 my $e = $self->{layout}->get_pixel_extents;
178 684 50 66     19407 if ( ref($e) eq 'ARRAY' ) { # Text::Layout <= 0.026
    50          
179 0         0 $e = $e->[1];
180             }
181             elsif ( $yflip //= _yflip() ) {
182 684         1460 $e->{y} += $e->{height};
183             }
184              
185             # Handle decorations (background, box).
186 684         2193 my $bgcol = $self->_bgcolor($font->{background});
187 684 50 33     4992 undef $bgcol if $bgcol && $bgcol =~ /^no(?:ne)?$/i;
188 684 50       2028 my $debug = $ENV{CHORDPRO_DEBUG_TEXT} ? "magenta" : undef;
189 684   33     2264 my $frame = $font->{frame} || $debug;
190 684 50 33     1704 undef $frame if $frame && $frame =~ /^no(?:ne)?$/i;
191 684 50 33     2126 if ( $bgcol || $frame ) {
192 0 0       0 printf("BB: %.2f %.2f %.2f %.2f\n", @{$e}{qw( x y width height ) } )
  0         0  
193             if $debug;
194             # Draw background and.or frame.
195 0 0       0 my $d = $debug ? 0 : 1;
196 0 0 0     0 $frame = $debug || $font->{color} || $self->{ps}->{theme}->{foreground} if $frame;
197             # $self->crosshair( $x, $y, 20, 0.2, "magenta" );
198             $self->rectxy( $x + $e->{x} - $d,
199             $y + $e->{y} + $d,
200             $x + $e->{x} + $e->{width} + $d,
201 0         0 $y + $e->{y} - $e->{height} - $d,
202             0.5, $bgcol, $frame);
203             }
204              
205 684         1255 $x += $e->{width};
206             # print STDERR ("TX: $x\n");
207 684         3103 return $x;
208             }
209              
210             sub setfont {
211 682     682 0 1529 my ( $self, $font, $size ) = @_;
212 682         1293 $self->{font} = $font;
213             warn("PDF: Font ", $font->{_ff}, " should have a size!\n")
214 682 50 66     2540 unless $size ||= $font->{size};
215 682   0     1663 $self->{fontsize} = $size ||= $font->{size} || $font->{fd}->{size};
      33        
216 682         2756 $self->{pdftext}->font( $font->{fd}->{font}, $size );
217             }
218              
219             sub strwidth {
220 559     559 0 1415 my ( $self, $text, $font, $size ) = @_;
221 559   33     2943 $font ||= $self->{font};
222 559         1291 $text = fix_musicsyms( $text, $font );
223 559   33     2812 $size ||= $self->{fontsize} || $font->{size};
      33        
224 559   66     1462 $self->{tmplayout} //= Text::Layout->new( $self->{pdf} );
225 559         2973 $self->{tmplayout}->set_font_description($font->{fd});
226 559         8860 $self->{tmplayout}->set_font_size($size);
227 559         3944 $self->{tmplayout}->set_markup($text);
228 559         29134 $self->{tmplayout}->get_pixel_size->{width};
229             }
230              
231             sub strheight {
232 0     0 0 0 my ( $self, $text, $font, $size ) = @_;
233 0   0     0 $font ||= $self->{font};
234 0         0 $text = fix_musicsyms( $text, $font );
235 0   0     0 $size ||= $self->{fontsize} || $font->{size};
      0        
236 0   0     0 $self->{tmplayout} //= Text::Layout->new( $self->{pdf} );
237 0         0 $self->{tmplayout}->set_font_description($font->{fd});
238 0         0 $self->{tmplayout}->set_font_size($size);
239 0         0 $self->{tmplayout}->set_markup($text);
240 0         0 $self->{tmplayout}->get_pixel_size->{height};
241             }
242              
243             sub line {
244 0     0 0 0 my ( $self, $x0, $y0, $x1, $y1, $lw, $color ) = @_;
245 0         0 my $gfx = $self->{pdfgfx};
246 0         0 $gfx->save;
247 0         0 $gfx->strokecolor( $self->_fgcolor($color) );
248 0         0 $gfx->linecap(1);
249 0   0     0 $gfx->linewidth($lw||1);
250 0         0 $gfx->move( $x0, $y0 );
251 0         0 $gfx->line( $x1, $y1 );
252 0         0 $gfx->stroke;
253 0         0 $gfx->restore;
254             }
255              
256             sub hline {
257 120     120 0 4717 my ( $self, $x, $y, $w, $lw, $color, $cap ) = @_;
258 120   50     563 $cap //= 2;
259 120         237 my $gfx = $self->{pdfgfx};
260 120         421 $gfx->save;
261 120         5873 $gfx->strokecolor( $self->_fgcolor($color) );
262 120         17666 $gfx->linecap($cap);
263 120   50     7019 $gfx->linewidth($lw||1);
264 120         6192 $gfx->move( $x, $y );
265 120         11168 $gfx->hline( $x + $w );
266 120         10178 $gfx->stroke;
267 120         5140 $gfx->restore;
268             }
269              
270             sub vline {
271 144     144 0 5940 my ( $self, $x, $y, $h, $lw, $color, $cap ) = @_;
272 144   50     798 $cap //= 2;
273 144         311 my $gfx = $self->{pdfgfx};
274 144         435 $gfx->save;
275 144         6677 $gfx->strokecolor( $self->_fgcolor($color) );
276 144         20326 $gfx->linecap($cap);
277 144   50     7793 $gfx->linewidth($lw||1);
278 144         6956 $gfx->move( $x, $y );
279 144         13270 $gfx->vline( $y - $h );
280 144         11753 $gfx->stroke;
281 144         5928 $gfx->restore;
282             }
283              
284             sub rectxy {
285 0     0 0 0 my ( $self, $x, $y, $x1, $y1, $lw, $fillcolor, $strokecolor ) = @_;
286 0         0 my $gfx = $self->{pdfgfx};
287 0         0 $gfx->save;
288 0 0       0 $gfx->strokecolor($self->_fgcolor($strokecolor)) if $strokecolor;
289 0 0       0 $gfx->fillcolor($self->_fgcolor($fillcolor)) if $fillcolor;
290 0         0 $gfx->linecap(2);
291 0   0     0 $gfx->linewidth($lw||1);
292 0         0 $gfx->rectxy( $x, $y, $x1, $y1 );
293 0 0 0     0 $gfx->fill if $fillcolor && !$strokecolor;
294 0 0 0     0 $gfx->fillstroke if $fillcolor && $strokecolor;
295 0 0 0     0 $gfx->stroke if $strokecolor && !$fillcolor;
296 0         0 $gfx->restore;
297             }
298              
299             sub poly {
300 0     0 0 0 my ( $self, $points, $lw, $fillcolor, $strokecolor ) = @_;
301 0 0       0 undef $strokecolor unless $lw;
302 0         0 my $gfx = $self->{pdfgfx};
303 0         0 $gfx->save;
304 0 0       0 $gfx->strokecolor($self->_fgcolor($strokecolor)) if $strokecolor;
305 0 0       0 $gfx->fillcolor($self->_fgcolor($fillcolor)) if $fillcolor;
306 0         0 $gfx->linecap(2);
307 0         0 $gfx->linewidth($lw);
308 0         0 $gfx->poly( @$points );
309 0         0 $gfx->close;
310 0 0 0     0 $gfx->fill if $fillcolor && !$strokecolor;
311 0 0 0     0 $gfx->fillstroke if $fillcolor && $strokecolor;
312 0 0 0     0 $gfx->stroke if $strokecolor && !$fillcolor;
313 0         0 $gfx->restore;
314             }
315              
316             sub circle {
317 240     240 0 1109 my ( $self, $x, $y, $r, $lw, $fillcolor, $strokecolor ) = @_;
318 240         483 my $gfx = $self->{pdfgfx};
319 240         837 $gfx->save;
320 240 50       13239 $gfx->strokecolor($self->_fgcolor($strokecolor)) if $strokecolor;
321 240 100       36946 $gfx->fillcolor($self->_fgcolor($fillcolor)) if $fillcolor;
322 240   50     28110 $gfx->linewidth($lw||1);
323 240         12134 $gfx->circle( $x, $y, $r );
324 240 100       910724 $gfx->fill if $fillcolor;
325 240 50       9478 $gfx->stroke if $strokecolor;
326 240         10143 $gfx->restore;
327             }
328              
329             sub cross {
330 48     48 0 183 my ( $self, $x, $y, $r, $lw, $strokecolor ) = @_;
331 48         112 my $gfx = $self->{pdfgfx};
332 48         192 $gfx->save;
333 48 50       3358 $gfx->strokecolor($self->_fgcolor($strokecolor)) if $strokecolor;
334 48   50     7419 $gfx->linewidth($lw||1);
335 48         2525 $r = 0.9 * $r;
336 48         253 $gfx->move( $x-$r, $y-$r );
337 48         5876 $gfx->line( $x+$r, $y+$r );
338 48 50       5862 $gfx->stroke if $strokecolor;
339 48         2218 $gfx->move( $x-$r, $y+$r );
340 48         5476 $gfx->line( $x+$r, $y-$r );
341 48 50       5384 $gfx->stroke if $strokecolor;
342 48         2152 $gfx->restore;
343             }
344              
345             sub crosshair { # for debugging
346 0     0 0 0 my ( $self, $x, $y, $r, $lw, $strokecolor ) = @_;
347 0         0 my $gfx = $self->{pdfgfx};
348 0         0 $gfx->save;
349 0 0       0 $gfx->strokecolor($self->_fgcolor($strokecolor)) if $strokecolor;
350 0   0     0 $gfx->linewidth($lw||1);
351 0         0 $gfx->move( $x, $y - $r );
352 0         0 $gfx->line( $x, $y + $r );
353 0 0       0 $gfx->stroke if $strokecolor;
354 0         0 $gfx->move( $x - $r, $y );
355 0         0 $gfx->line( $x + $r, $y );
356 0 0       0 $gfx->stroke if $strokecolor;
357 0         0 $gfx->restore;
358             }
359              
360             sub get_image {
361 0     0 0 0 my ( $self, $elt ) = @_;
362              
363 0         0 my $img;
364 0         0 my $uri = $elt->{uri};
365 0 0       0 warn("get_image($uri)\n") if $config->{debug}->{images};
366 0 0       0 if ( $uri =~ /^id=(.+)/ ) {
367 0         0 my $a = $ChordPro::Output::PDF::assets->{$1};
368              
369 0 0       0 if ( $a->{type} eq "abc" ) {
    0          
    0          
    0          
370 0         0 my $res = ChordPro::Output::PDF::abc2image( undef, $self, $a );
371 0         0 return $self->get_image( { %$elt, uri => $res->{src} } );
372             }
373             elsif ( $a->{type} eq "jpg" ) {
374 0         0 $img = $self->{pdf}->image_jpeg(IO::String->new($a->{data}));
375             }
376             elsif ( $a->{type} eq "png" ) {
377 0         0 $img = $self->{pdf}->image_png(IO::String->new($a->{data}));
378             }
379             elsif ( $a->{type} eq "gif" ) {
380 0         0 $img = $self->{pdf}->image_gif(IO::String->new($a->{data}));
381             }
382 0         0 return $img;
383             }
384 0         0 for ( $uri ) {
385 0 0       0 $img = $self->{pdf}->image_png($_) if /\.png$/i;
386 0 0       0 $img = $self->{pdf}->image_jpeg($_) if /\.jpe?g$/i;
387 0 0       0 $img = $self->{pdf}->image_gif($_) if /\.gif$/i;
388             }
389 0         0 return $img;
390             }
391              
392             sub add_image {
393 0     0 0 0 my ( $self, $img, $x, $y, $w, $h, $border ) = @_;
394              
395 0         0 my $gfx = $self->{pdfgfx};
396              
397 0         0 $gfx->save;
398 0         0 $gfx->image( $img, $x, $y-$h, $w, $h );
399 0 0       0 if ( $border ) {
400 0         0 $gfx->rect( $x, $y-$h, $w, $h )
401             ->linewidth($border)
402             ->stroke;
403             }
404 0         0 $gfx->restore;
405             }
406              
407             sub newpage {
408 67     67 0 239 my ( $self, $ps, $page ) = @_;
409             #$self->{pdftext}->textend if $self->{pdftext};
410 67   100     267 $page ||= 0;
411              
412             # PDF::API2 says $page must refer to an existing page.
413             # Set to 0 to append.
414 67 100       365 $page = 0 if $page == $self->{pdf}->pages + 1;
415              
416 67         920 $self->{pdfpage} = $self->{pdf}->page($page);
417             $self->{pdfpage}->mediabox( $ps->{papersize}->[0],
418 67         60413 $ps->{papersize}->[1] );
419              
420 67         11402 $self->{pdfgfx} = $self->{pdfpage}->gfx;
421 67         15698 $self->{pdftext} = $self->{pdfpage}->text;
422 67 50       20572 unless ($ps->{theme}->{background} =~ /^white|none|#ffffff$/i ) {
423 0         0 for ( $self->{pdfgfx} ) {
424 0         0 $_->save;
425 0         0 $_->fillcolor( $ps->{theme}->{background} );
426 0         0 $_->linewidth(0);
427             $_->rectxy( 0, 0, $ps->{papersize}->[0],
428 0         0 $ps->{papersize}->[1] );
429 0         0 $_->fill;
430 0         0 $_->restore;
431             }
432             }
433             }
434              
435             sub openpage {
436 42     42 0 127 my ( $self, $ps, $page ) = @_;
437 42         229 $self->{pdfpage} = $self->{pdf}->openpage($page);
438 42         2204 $self->{pdfgfx} = $self->{pdfpage}->gfx;
439 42         10922 $self->{pdftext} = $self->{pdfpage}->text;
440             }
441              
442             sub importpage {
443 0     0 0 0 my ( $self, $fn, $pg ) = @_;
444 0         0 my $bg = $self->{pdfapi}->open($fn);
445 0 0       0 return unless $bg; # should have been checked
446 0 0       0 $pg = $bg->pages if $pg > $bg->pages;
447 0         0 $self->{pdf}->import_page( $bg, $pg, $self->{pdfpage} );
448             # Make sure the contents get on top of it.
449 0         0 $self->{pdfgfx} = $self->{pdfpage}->gfx;
450 0         0 $self->{pdftext} = $self->{pdfpage}->text;
451             }
452              
453             sub importfile {
454 0     0 0 0 my ( $self, $filename ) = @_;
455 0         0 my $pdf = $self->{pdfapi}->open($filename);
456 0 0       0 return unless $pdf; # should have been checked
457 0         0 for ( my $page = 1; $page <= $pdf->pages; $page++ ) {
458 0         0 $self->{pdf}->import_page( $pdf, $page );
459             }
460 0         0 return { pages => $pdf->pages, $pdf->info_metadata };
461             }
462              
463             sub pagelabel {
464 22     22 0 76 my ( $self, $page, $style, $prefix ) = @_;
465 22   50     65 $style //= 'arabic';
466              
467             # PDF::API2 2.042 has some incompatible changes...
468 22         98 my $c = $self->{pdf}->can("page_labels");
469 22 50       73 if ( $c ) { # 2.042+
470 22 50       188 my $opts = { style => $style eq 'Roman' ? 'R' :
    50          
    100          
    50          
    100          
471             $style eq 'roman' ? 'r' :
472             $style eq 'Alpha' ? 'A' :
473             $style eq 'alpha' ? 'a' : 'D',
474             defined $prefix ? ( prefix => $prefix ) : (),
475             start => 1 };
476 22         121 $c->( $self->{pdf}, $page+1, %$opts );
477             }
478             else {
479 0 0       0 my $opts = { -style => $style,
480             defined $prefix ? ( -prefix => $prefix ) : (),
481             -start => 1 };
482 0         0 $self->{pdf}->pageLabel( $page, $opts );
483             }
484             }
485              
486             sub make_outlines {
487 8     8 0 39 my ( $self, $book, $start ) = @_;
488 8 50 33     75 return unless $book && @$book; # unlikely
489              
490 8         39 my $pdf = $self->{pdf};
491 8         19 $start--; # 1-relative
492 8         20 my $ol_root;
493              
494             # Process outline defs from config.
495 8         24 foreach my $ctl ( @{ $self->{ps}->{outlines} } ) {
  8         38  
496 16         937 my $book = prep_outlines( $book, $ctl );
497 16 50       73 next unless @$book;
498              
499             # Seems not to matter whether we re-use the root or create new.
500 16   66     119 $ol_root //= $pdf->outlines;
501              
502 16         35300 my $outline;
503              
504             # Skip level for a single outline.
505 16 50       38 if ( @{ $self->{ps}->{outlines} } == 1 ) {
  16         78  
506 0         0 $outline = $ol_root;
507 0 0       0 $outline->closed if $ctl->{collapse}; # TODO?
508             }
509             else {
510 16         100 $outline = $ol_root->outline;
511 16         1840 $outline->title( $ctl->{label} );
512 16 50       465 $outline->closed if $ctl->{collapse};
513             }
514              
515 16         186 my %lh; # letter hierarchy
516 16         39 my $needlh = 0;
517 16 50       61 if ( $ctl->{letter} > 0 ) {
518 16         53 for ( @$book ) {
519             # Group on first letter.
520             # That's why we left the sort fields in...
521 48         152 my $cur = uc(substr( $_->[0], 0, 1 ));
522 48   100     171 $lh{$cur} //= [];
523             # Last item is the song.
524 48         92 push( @{$lh{$cur}}, $_->[-1] );
  48         124  
525             }
526             # Need letter hierarchy?
527 16         104 $needlh = keys(%lh) >= $ctl->{letter};
528             }
529              
530 16 50       58 if ( $needlh ) {
531 0         0 my $cur_ol;
532 0         0 my $cur_let = "";
533 0         0 foreach my $let ( sort keys %lh ) {
534 0         0 foreach my $song ( @{$lh{$let}} ) {
  0         0  
535 0 0 0     0 unless ( defined $cur_ol && $cur_let eq $let ) {
536             # Intermediate level autoline.
537 0         0 $cur_ol = $outline->outline;
538 0         0 $cur_ol->title($let);
539 0         0 $cur_let = $let;
540             }
541             # Leaf outline.
542 0         0 my $ol = $cur_ol->outline;
543             # Display info.
544 0         0 $ol->title( demarkup( fmt_subst( $song, $ctl->{line} ) ) );
545 0 0       0 if ( my $c = $ol->can("destination") ) {
546 0         0 $c->( $ol, $pdf->openpage( $song->{meta}->{tocpage} + $start ) );
547             }
548             else {
549 0         0 $ol->dest($pdf->openpage( $song->{meta}->{tocpage} + $start ));
550             }
551             }
552             }
553             }
554             else {
555 16         50 foreach my $b ( @$book ) {
556 48         3306 my $song = $b->[-1];
557             # Leaf outline.
558 48         143 my $ol = $outline->outline;
559             # Display info.
560 48         5170 $ol->title( demarkup( fmt_subst( $song, $ctl->{line} ) ) );
561 48 50       1780 if ( my $c = $ol->can("destination") ) {
562 48         253 $c->( $ol, $pdf->openpage( $song->{meta}->{tocpage} + $start ) );
563             }
564             else {
565 0         0 $ol->dest($pdf->openpage( $song->{meta}->{tocpage} + $start ));
566             }
567             }
568             }
569             }
570             }
571              
572             sub finish {
573 8     8 0 44 my ( $self, $file ) = @_;
574              
575 8 50 33     67 if ( $file && $file ne "-" ) {
576 8         71 $self->{pdf}->saveas($file);
577             }
578             else {
579 0         0 binmode(STDOUT);
580 0         0 print STDOUT ( $self->{pdf}->stringify );
581 0         0 close(STDOUT);
582             }
583             }
584              
585             sub init_fonts {
586 40     40 0 145 my ( $self ) = @_;
587 40         113 my $ps = $self->{ps};
588 40         85 my $fail;
589              
590 40         429 my $fc = Text::Layout::FontConfig->new( debug => $config->{debug}->{fonts} > 1 );
591              
592             # Add font dirs.
593 40         806 my @d = ( @{$ps->{fontdir}}, ::rsc_or_file("fonts/"), $ENV{FONTDIR} );
  40         334  
594             # Avoid rsc result if dummy.
595 40 50       223 splice( @d, -2, 1 ) if $d[-2] eq "fonts/";
596 40         159 for my $fontdir ( @d ) {
597 80 100       1660 next unless $fontdir;
598 40         186 $fontdir = expand_tilde($fontdir);
599 40 50       701 if ( -d $fontdir ) {
600 40         676 $self->{pdfapi}->can("addFontDirs")->($fontdir);
601 40         762 $fc->add_fontdirs($fontdir);
602             }
603             else {
604 0         0 warn("PDF: Ignoring fontdir $fontdir [$!]\n");
605 0         0 undef $fontdir;
606             }
607             }
608              
609             # Make sure we have this one.
610 40         343 $fc->register_font( "ChordProSymbols.ttf", "chordprosymbols", "", {} );
611              
612             # Process the fontconfig.
613 40         4911 foreach my $ff ( keys( %{ $ps->{fontconfig} } ) ) {
  40         225  
614 160         449 my @fam = split( /\s*,\s*/, $ff );
615 160         270 foreach my $s ( keys( %{ $ps->{fontconfig}->{$ff} } ) ) {
  160         497  
616 520         17347 my $v = $ps->{fontconfig}->{$ff}->{$s};
617 520 50       2224 if ( UNIVERSAL::isa( $v, 'HASH' ) ) {
618 0         0 my $file = delete( $v->{file} );
619 0         0 $fc->register_font( $file, $fam[0], $s, $v );
620             }
621             else {
622 520         1242 $fc->register_font( $v, $fam[0], $s );
623             }
624             }
625 160 50       7765 $fc->register_aliases(@fam) if @fam > 1;
626             }
627              
628 40         144 foreach my $ff ( keys( %{ $ps->{fonts} } ) ) {
  40         257  
629 680 50       13556 $self->init_font($ff) or $fail++;
630             }
631              
632 40 50       1122 die("Unhandled fonts detected -- aborted\n") if $fail;
633             }
634              
635             sub init_font {
636 680     680 0 1406 my ( $self, $ff ) = @_;
637 680         1166 my $ps = $self->{ps};
638 680         1035 my $fd;
639 680 100       2917 if ( $ps->{fonts}->{$ff}->{file} ) {
    50          
    50          
640 40         347 $fd = $self->init_filefont($ff);
641             }
642             elsif ( $ps->{fonts}->{$ff}->{description} ) {
643 0         0 $fd = $self->init_pangofont($ff);
644             }
645             elsif ( $ps->{fonts}->{$ff}->{name} ) {
646 640         1399 $fd = $self->init_corefont($ff);
647             }
648 680 50       2102 warn("No font found for \"$ff\"\n") unless $fd;
649 680         16024 $fd;
650             }
651              
652             sub init_pangofont {
653 0     0 0 0 my ( $self, $ff ) = @_;
654              
655 0         0 my $ps = $self->{ps};
656 0         0 my $font = $ps->{fonts}->{$ff};
657              
658 0         0 my $fc = Text::Layout::FontConfig->new( debug => $config->{debug}->{fonts} > 1 );
659 0         0 eval {
660 0         0 $font->{fd} = $fc->from_string($font->{description});
661 0         0 $font->{fd}->get_font($self->{layout}); # force load
662 0 0       0 $font->{fd}->{font}->{Name}->{val} =~ s/~.*/~$faketime/ if $regtest;
663 0         0 $font->{_ff} = $ff;
664 0   0     0 $font->{fd}->set_shaping( $font->{fd}->get_shaping || $font->{shaping}//0);
      0        
665 0 0       0 $font->{size} = $font->{fd}->get_size if $font->{fd}->get_size;
666             };
667 0         0 $font->{fd};
668             }
669              
670             sub init_filefont {
671 40     40 0 149 my ( $self, $ff ) = @_;
672              
673 40         141 my $ps = $self->{ps};
674 40         109 my $font = $ps->{fonts}->{$ff};
675              
676 40         236 my $fc = Text::Layout::FontConfig->new( debug => $config->{debug}->{fonts} > 1 );
677 40         632 eval {
678 40         243 my $t = $fc->from_filename(expand_tilde($font->{file}));
679 40         3166 $t->get_font($self->{layout}); # force load
680 40 50       928455 $t->{font}->{Name}->{val} =~ s/~.*/~$faketime/ if $regtest;
681 40         129 $t->{_ff} = $ff;
682 40         119 $font->{fd} = $t;
683             };
684 40         165 $font->{fd};
685             }
686              
687             sub init_corefont {
688 640     640 0 1191 my ( $self, $ff ) = @_;
689              
690 640         1114 my $ps = $self->{ps};
691 640         981 my $font = $ps->{fonts}->{$ff};
692 640         1858 my $cf = ChordPro::Output::PDF::is_corefont($font->{name});
693 640 50       1411 die("Config error: \"$font->{name}\" is not a built-in font\n")
694             unless $cf;
695 640         2426 my $fc = Text::Layout::FontConfig->new( debug => $config->{debug}->{fonts} > 1 );
696 640         8392 eval {
697 640         1593 $font->{fd} = $fc->from_filename($cf);
698 640         42257 $font->{fd}->get_font($self->{layout}); # force load
699 640         1282812 $font->{_ff} = $ff;
700             };
701 640         2041 $font->{fd};
702             }
703              
704             sub show_vpos {
705 0     0 0   my ( $self, $y, $w ) = @_;
706 0           $self->{pdfgfx}->move(100*$w,$y)->linewidth(0.25)->hline(100*(1+$w))->stroke;
707             }
708              
709 10     10   62527 use File::Temp;
  10         31  
  10         4747  
710              
711             my $cname;
712             my $rname;
713             sub embed {
714 0     0 0   my ( $self, $file ) = @_;
715 0 0         return unless -f $file;
716 0           my $a = $self->{pdfpage}->annotation();
717              
718             # The only reliable way currently is pretend it's a movie :) .
719 0           $a->movie($file, "ChordPro" );
720 0           $a->open(1);
721              
722             # Create/reuse temp file for (final) config and run time info.
723 0           my $cf;
724 0 0         if ( $cname ) {
725 0           open( $cf, '>', $cname );
726             }
727             else {
728 0           ( $cf, $cname ) = File::Temp::tempfile( UNLINK => 0);
729             }
730 0           binmode( $cf, ':utf8' );
731 0           print $cf ChordPro::Config::config_final();
732 0           close($cf);
733              
734 0           $a = $self->{pdfpage}->annotation();
735 0           $a->movie($cname, "ChordProConfig" );
736 0           $a->open(0);
737              
738 0           my $rf;
739 0 0         if ( $rname ) {
740 0           open( $rf, '>', $rname );
741             }
742             else {
743 0           ( $rf, $rname ) = File::Temp::tempfile( UNLINK => 0);
744             }
745 0           binmode( $rf, ':utf8' );
746 0           open( $rf, '>', $rname );
747 0           binmode( $rf, ':utf8' );
748 0           print $rf (::runtimeinfo());
749 0           close($rf);
750              
751 0           $a = $self->{pdfpage}->annotation();
752 0           $a->movie($rname, "ChordProRunTime" );
753 0           $a->open(0);
754             }
755              
756             END {
757 10 50   10   10887 return unless $cname;
758 0         0 unlink($cname);
759 0         0 undef $cname;
760 0         0 unlink($rname);
761 0         0 undef $rname;
762             }
763              
764             1;