File Coverage

lib/ChordPro/Output/PDF.pm
Criterion Covered Total %
statement 399 689 57.9
branch 141 362 38.9
condition 35 135 25.9
subroutine 34 44 77.2
pod 0 9 0.0
total 609 1239 49.1


line stmt bran cond sub pod time code
1             #! perl
2              
3             package main;
4              
5 10     10   4157 use utf8;
  10         42  
  10         87  
6             our $config;
7             our $options;
8              
9             our $ps;
10             our $pr;
11             our $dw;
12              
13             package ChordPro::Output::PDF;
14              
15 10     10   1293 use strict;
  10         22  
  10         316  
16 10     10   48 use warnings;
  10         18  
  10         799  
17 10     10   12414 use File::Temp ();
  10         316808  
  10         637  
18 10     10   98 use Ref::Util qw(is_hashref is_arrayref is_coderef);
  10         24  
  10         782  
19 10     10   58 use Carp;
  10         20  
  10         720  
20 10     10   60 use ChordPro::Output::Common qw( prep_outlines fmt_subst );
  10         23  
  10         811  
21 10     10   87 use feature 'signatures';
  10         20  
  10         1436  
22              
23 10     10   4813 use ChordPro::Output::PDF::Song;
  10         53  
  10         7224  
24 10     10   7010 use ChordPro::Output::PDF::Writer;
  10         54  
  10         4217  
25 10     10   1006 use ChordPro::Files;
  10         55  
  10         2528  
26 10     10   107 use ChordPro::Paths;
  10         23  
  10         821  
27 10     10   73 use ChordPro::Utils;
  10         23  
  10         1879  
28              
29             my $pdfapi;
30              
31 10     10   76 use Text::Layout;
  10         21  
  10         388  
32 10     10   59 use List::Util qw(any);
  10         23  
  10         763  
33 10     10   69 use Unicode::Collate;
  10         21  
  10         124713  
34              
35             my $verbose = 0;
36              
37             # For regression testing, run perl with PERL_HASH_SEED set to zero.
38             # This eliminates the arbitrary order of font definitions and triggers
39             # us to pinpoint some other data that would otherwise be varying.
40             my $regtest = defined($ENV{PERL_HASH_SEED}) && $ENV{PERL_HASH_SEED} == 0;
41              
42             # Convenience.
43             *generate_song = \&ChordPro::Output::PDF::Song::generate_song;
44              
45             sub generate_songbook {
46 8     8 0 36 my ( $self, $sb ) = @_;
47              
48             return [] unless $sb->{songs}->[0]->{body}
49 8 0 33     88 || $sb->{songs}->[0]->{source}->{embedding};
50 8   33     81 $verbose ||= $options->{verbose};
51              
52              
53 8         80 $config->unlock;
54 8         21431 $ps = $config->{pdf};
55             # use DDP; p $ps->{songbook}, as => "in PDF";
56 8         1081 my $pagectrl = $self->pagectrl;
57 8         68 $config->lock;
58              
59 8         26885 my $extra_matter = 0;
60 8 50 33     143 if ( $options->{toc} // (@{$sb->{songs}} > 1) ) {
  8         101  
61 8         23 for ( @{ $::config->{contents} } ) {
  8         48  
62             # Treat ToCs as one.
63 8 50       70 $extra_matter++, last unless $_->{omit};
64             }
65 8 50       50 $extra_matter++ if $options->{title};
66             }
67 8 50 33     57 $extra_matter++ if $pagectrl->{cover} && !$options->{title};
68 8 100       42 $extra_matter++ if $pagectrl->{front_matter};
69 8 100       684 $extra_matter++ if $pagectrl->{back_matter};
70 8 50       70 $extra_matter++ if $options->{csv};
71              
72             # $prefill indicates that in 2page mode, a filler page is needed to
73             # get the songs properly aligned.
74 8         24 my $prefill = 0;
75 8 100       48 if ( $pagectrl->{align_songs_spread} ) {
76 1         3 $prefill = 1;
77             }
78 8 50       33 if ( $pagectrl->{sort_songs} ) {
79 0         0 sort_songbook( $sb, $pagectrl );
80             }
81 8 50       33 if ( $pagectrl->{compact_songs} ) {
82 0         0 $prefill = compact_songbook( $sb, $pagectrl );
83 0 0       0 return unless defined $prefill; # cancelled
84             }
85              
86             progress( phase => "PDF",
87             index => 0,
88 8         24 total => scalar(@{$sb->{songs}}) );
  8         70  
89              
90 8         151 $pr = (__PACKAGE__."::Writer")->new( $ps, $pdfapi );
91             warn("Generating PDF ", $options->{output} || "__new__.pdf", "...\n")
92 8 50 0     75 if $options->{verbose};
93              
94 8         86 my $name = ::runtimeinfo("short");
95 8 50       42 $name =~ s/version.*/regression testing/ if $regtest;
96 8         115 my %info = ( Title => $sb->{songs}->[0]->{meta}->{title}->[0],
97             Creator => $name );
98 8         21 while ( my ( $k, $v ) = each %{ $ps->{info} } ) {
  40         2874  
99 32 100 66     285 next unless defined($v) && $v ne "";
100 8         88 $info{ucfirst($k)} = fmt_subst( $sb->{songs}->[0], $v );
101             }
102              
103 8         65 $info{PageCtrl} = pagectrl_msg($pagectrl);
104 8         116 $pr->info(%info);
105              
106             # The resultant songbook consists of 5 parts:
107             # 1, The cover. PDF doc or cho template.
108             # 2. The front matter. PDF doc or cho template.
109             # 3. The table of contents. May be templated.
110             # 4. The songs.
111             # 5. The back matter. PDF doc.
112             # All parts except the songs are optional.
113 8         838 my ( %start_of, %pages_of );
114 8         33 for ( qw( cover front toc songbook back ) ) {
115 40         122 $start_of{$_} = 1;
116 40         77 $pages_of{$_} = 0;
117             }
118              
119             # The songbook...
120 8         23 my @book;
121              
122             # Page number in the PDF (for now, later we'll prepend tocs etc.).
123             # Note that PDF page numbers start at 1.
124 8         22 my $page = 1;
125             # Logical page number offset.
126 8   50     87 my $page_offset = ( $options->{'start-page-number'} || 1 ) - 1;
127 8 100 66     52 $page_offset++ if $prefill && is_even($page_offset);
128              
129             # if ( $pagectrl->{dual_pages} && is_odd($page_offset) ) {
130             # warn("Warning: Specifying an even start page when ".
131             # "pdf.odd-even-pages is in effect may yield surprising results.\n");
132             # }
133              
134             # If there is back matter, and it has even pages, force
135             # alignment of the final song as well.
136 8         28 my $back_matter;
137             my $force_align;
138 8 100       51 if ( $pagectrl->{back_matter} ) {
139 3         25 $back_matter = $pdfapi->open( expand_tilde($pagectrl->{back_matter}) );
140 3 50       34231 die("Missing back matter: ", $pagectrl->{back_matter}, "\n")
141             unless $back_matter;
142             $force_align =
143             !( is_even($page_offset) xor is_even($back_matter->pages))
144 3 50 0     32 if $pagectrl->{align_songs_extend};
145             }
146              
147 8         23 for my $songindex ( 1 .. @{$sb->{songs}} ) {
  8         57  
148 24         163 my $song = $sb->{songs}->[$songindex-1];
149 24         115 local $pagectrl->{align_songs_spread} = $pagectrl->{align_songs_spread};
150 24 100       182 $pagectrl->{align_songs_spread} = 1 if is_odd($page_offset);
151              
152             # Align.
153 24 50       215 if ( $song->{meta}->{pages} ) { # 2nd pass
154 0 0 0     0 if ( ( ($page+$page_offset) % 2)
      0        
155             && $song->{meta}->{pages}
156             && $song->{meta}->{pages} == 2 ) {
157 0         0 $pr->newpage($page+1);
158 0         0 $page++;
159             }
160              
161             }
162             else {
163 24         304 $page += $pr->page_align( $pagectrl, "song$songindex", $page );
164             }
165              
166 24         187 $song->{meta}->{tocpage} = $page; # physical
167 24         162 push( @book, [ $song->{meta}->{title}->[0], $song ] );
168              
169             # Copy persistent assets into each of the songs.
170 24 50 33     185 if ( $sb->{assets} && %{$sb->{assets}} ) {
  0         0  
171 0   0     0 $song->{assets} //= {};
172 0         0 while ( my ($k,$v) = each %{$sb->{assets}} ) {
  0         0  
173 0         0 $song->{assets}->{$k} = $v;
174             }
175             }
176              
177 24 50       203 return unless progress( msg => $song->{meta}->{title}->[0] );
178              
179 24   33     309 $song->{meta}->{"chordpro.songsource"} //= $song->{source}->{file};
180 24         119 $pr->{bookmark} = "song_$songindex";
181             my $pages =
182             generate_song( $song,
183             { pr => $pr,
184             page_idx => $page,
185             page_num => $page+$page_offset,
186             songindex => $songindex,
187 24         83 numsongs => scalar(@{$sb->{songs}}),
  24         451  
188             forcealign => $force_align,
189             pagectrl => $pagectrl,
190             } );
191              
192             # Easy access to toc page.
193 24         266 $song->{meta}->{page} = $page+$page_offset;
194 24 50       154 if ( $song->{meta}->{bookmark} ) {
195             $pr->named_dest( $song->{meta}->{bookmark},
196 24 50       345 $pr->{pdf}->openpage($page)) if $pages;
197             }
198             else {
199             # Embedded PDF -> no toc.
200 0         0 $song->{meta}->{_TOC} = [ "no" ];
201             }
202 24         236 $page += $song->{meta}->{pages} = $pages;
203             }
204 8         91 $pages_of{songbook} = $page - 1;
205 8         37 $start_of{back} = $page;
206              
207             $::config->{contents} //=
208             [ { $::config->{toc}->{order} eq "alpha"
209             ? ( fields => [ "title" ] )
210             : ( fields => [ "songindex" ] ),
211             label => $::config->{toc}->{title},
212 8 0 50     61 line => $::config->{toc}->{line} } ];
213              
214 8         25 my @tocs = @{ $::config->{contents} };
  8         46  
215              
216 8 50       43 if ( $extra_matter ) {
217 8         76 progress( phase => "PDF(extra)",
218             index => 0,
219             total => $extra_matter );
220             }
221              
222 8         34 my $tocix;
223             my $frontmatter_songbook;
224 8         39 while ( @tocs ) {
225 24         85 my $ctl = shift(@tocs);
226 24 50 33     269 next unless $options->{toc} // @book > 1;
227              
228 24         81 for ( qw( fields label line pageno ) ) {
229 96 50       372 next if exists $ctl->{$_};
230 0         0 die("Config error: \"contents\" is missing \"$_\"\n");
231             }
232 24 100       175 next if $ctl->{omit};
233 16         54 $tocix++;
234              
235 16         54 my $book = prep_outlines( [ map { $_->[1] } @book ], $ctl );
  48         245  
236              
237             # Create a pseudo-song for the table of contents.
238 16         214 my $toctitle = fmt_subst( $book[0][-1], $ctl->{label} );
239 16         2160 my $start = $start_of{songbook} - $page_offset;
240             # Templates for toc line and page.
241 16         86 my $tltpl = $ctl->{line};
242 16         64 my $pgtpl = $ctl->{pageno};
243              
244 16         58 my $song;
245             my $tmplfile;
246 16 50       94 if ( $ctl->{template} ) {
247 16         65 my $tpl = $ctl->{template};
248 16 50       91 if ( $tpl =~ /\.\w+/ ) { # file
249             $tmplfile = CP->siblingres( $book[0][-1]->{source}->{file},
250 0         0 $tpl, class => "templates" );
251 0 0       0 warn("ToC template not found: $tpl\n") unless $tmplfile;
252             }
253             else {
254 16         128 $tmplfile = CP->findres( $tpl.".cho", class => "templates" );
255 16 50       92 if ( $verbose ) {
256 0 0       0 warn("ToC template",
257             $tmplfile ? " found: $tmplfile" : " not found: $tpl.cho\n")
258             }
259             }
260             }
261              
262             # Construct front matter songbook.
263 16         76 my $fmsb;
264             my $lines;
265 16         0 my $opts;
266 16 50       79 if ( $tmplfile ) {
267             # Songbook from template file.
268 0         0 $opts = { fail => 'hard' };
269 0         0 $lines = fs_load( $tmplfile, $opts );
270             }
271             else {
272 16         73 $lines = [ "{title: $toctitle}" ];
273 16         93 $opts = { _filesource => "" };
274             }
275 16         242 $fmsb = ChordPro::Songbook->new;
276 16         265 $fmsb->parse_file( $lines, { %$opts,
277             bookmark => "toc_$tocix",
278             generate => 'PDF' } );
279 16         117 for ( $fmsb->{songs}->[-1] ) {
280             $_->{title} = $_->{title}
281             ? fmt_subst( $book[0][-1], $_->{title} )
282 16 50       175 : $toctitle;
283 16   50     1911 $_->{meta}->{title} //= [ $_->{title} ];
284             }
285              
286 16         48 my @songs = @{$fmsb->{songs}};
  16         87  
287              
288             # The first (of multiple) gets the global title/subtitle.
289 16 50       73 if ( @songs > 1 ) {
290 0         0 for ( $songs[0] ) {
291             $_->{meta}->{title} =
292             [ fmt_subst( $_, $options->{title} ) ]
293 0 0       0 if defined $options->{title};
294             $_->{meta}->{subtitle} =
295             [ fmt_subst( $_, $options->{subtitle} ) ]
296 0 0       0 if defined $options->{subtitle};
297 0         0 $_->{title} = $_->{meta}->{title}->[0];
298             }
299             }
300              
301             # The last song gets the ToC appended.
302 16         51 $song = pop(@songs);
303              
304 16 50       89 if ( $ctl->{break} ) {
305 0         0 my $prevbreak = "";
306 0   0     0 $song->{body} //= [];
307 0         0 for ( @$book ) {
308 0         0 my $break = fmt_subst( $_->[-1], $ctl->{break} );
309 0         0 my $nl = 0;
310 0         0 $nl++ while $break =~ s/^(\n|\\n)//;
311              
312 0         0 my $p = $pr->{pdf}->openpage($_->[-1]->{meta}->{tocpage});
313 0 0 0     0 if ( $nl && $break ne $prevbreak ) {
314 0         0 push( @{ $song->{body} },
315             { type => "empty",
316             context => "toc",
317 0         0 } ) for 1..$nl;
318             }
319 0 0       0 push( @{ $song->{body} },
  0         0  
320             { type => "tocline",
321             context => "toc",
322             title => fmt_subst( $_->[-1], $tltpl ),
323             page => $p,
324             pageno => fmt_subst( $_->[-1], $pgtpl ),
325             maybe break => ($break ne $prevbreak ? $break : undef),
326             } );
327 0         0 $prevbreak = $break;
328             }
329             }
330             else {
331 16   50     173 push( @{ $song->{body} //= [] },
332 16         44 map { my $p = $pr->{pdf}->openpage($_->[-1]->{meta}->{tocpage});
  48         9272  
333 48         2821 +{ type => "tocline",
334             context => "toc",
335             title => fmt_subst( $_->[-1], $tltpl ),
336             page => $p,
337             pageno => fmt_subst( $_->[-1], $pgtpl ),
338             } } @$book );
339             }
340              
341 16   66     4361 $frontmatter_songbook //= ChordPro::Songbook->new;
342 16         57 $frontmatter_songbook->add($_) for @songs;
343 16         121 $frontmatter_songbook->add($song);
344             }
345              
346             # Prepend the front matter songs.
347              
348 8         47 $force_align = $pagectrl->{align_songs_extend};
349 8 50 33     74 if ( $frontmatter_songbook && @{$frontmatter_songbook->{songs}} ) {
  8         47  
350 8 50       63 return unless progress( msg => "ToC" );
351 8         28 $page = 1;
352              
353 8         25 my $toc = 0;
354 8         20 for ( @{$frontmatter_songbook->{songs}} ) {
  8         32  
355             # Localize song alignment settings.
356             local $pagectrl->{align_songs} =
357 16         81 $pagectrl->{align_tocs};
358             local $pagectrl->{align_songs_spread} =
359             $pagectrl->{align_tocs} eq "songs"
360 16 50       131 ? $pagectrl->{align_songs_spread} : 0;
361             local $pagectrl->{align_songs_extend} =
362             $pagectrl->{align_tocs} eq "songs"
363 16 50       64 ? $pagectrl->{align_songs_extend} : 0;
364              
365 16         43 $toc++;
366 16         79 $pr->{bookmark} = "toc_$toc";
367             # warn("TOC $toc $page\n");
368             # use DDP; p $pagectrl, as => "for toc";
369 16         160 $page += $pr->page_align( $pagectrl, "toc$toc", $page );
370             # warn("TOC $toc $page\n");
371             my $pages =
372             generate_song( $_,
373             { pr => $pr,
374             prepend => 1,
375             roman => 1,
376             page_idx => $page,
377             page_num => $page,
378             songindex => $toc,
379 16         342 numsongs => 0+@{$frontmatter_songbook->{songs}},
380             bookmark => $pr->{bookmark},
381             # forcealign => $force_align,
382 16         67 pagectrl => $pagectrl,
383             } );
384             $pr->named_dest( $_->{meta}->{bookmark},
385 16 50       403 $pr->{pdf}->openpage($page)) if $pages;
386 16         144 $page += $pages;
387             # warn("TOC $toc $page\n");
388             }
389 8         47 $pages_of{toc} = $page - 1;
390 8         77 $start_of{$_} += $page - 1 for qw( songbook back );
391             }
392              
393 8 100       56 if ( $pagectrl->{front_matter} ) {
394 3         11 $page = 1;
395 3         28 my $matter = $pdfapi->open( expand_tilde($pagectrl->{front_matter}) );
396 3 50       32747 die("Missing front matter: ", $pagectrl->{front_matter}, "\n") unless $matter;
397 3 50       28 return unless progress( msg => "Front matter" );
398 3         25 for ( 1 .. $matter->pages ) {
399 3         67 $pr->{pdf}->import_page( $matter, $_, $_ );
400 3         31764 $page++;
401             }
402 3         32 $pages_of{front} = $matter->pages;
403 3         447 $start_of{$_} += $page - 1 for qw( toc songbook back );
404             }
405              
406             # If we have a template, process it as a song and prepend.
407 8         32 my $covertpl;
408 8 50 33     85 if ( defined($options->{title}) && !@tocs ) {
409 0         0 my $tpl = "cover";
410 0         0 $covertpl = CP->findres( "$tpl.cho", class => "templates" );
411 0 0       0 if ( $verbose ) {
412 0 0       0 warn("Cover template",
413             $covertpl ? " found: $covertpl" : " not found: $tpl.cho\n")
414             }
415             }
416 8 50       79 if ( $covertpl ) {
    50          
417 0         0 my $page = 1;
418 0         0 my $opts = { fail => 'hard' };
419 0         0 my $lines = fs_load( $covertpl, $opts );
420 0         0 my $csb = ChordPro::Songbook->new;
421 0         0 $csb->parse_file( $lines, { %$opts,
422             generate => 'PDF' } );
423 0         0 for ( $csb->{songs}->[0] ) {
424 0         0 @{$_->{meta}}{ keys( %{$config->{meta}} ) } =
  0         0  
425 0         0 values ( %{$config->{meta}} );
  0         0  
426             $_->{meta}->{title} =
427             $options->{title} ?
428 0 0       0 [ $options->{title} ] : [ $_->{meta}->{title}->[0] ];
429             $_->{meta}->{subtitle} =
430             $options->{subtitle} ?
431 0 0       0 [ $options->{subtitle} ] : $_->{meta}->{subtitle};
432             }
433 0         0 for ( @{$csb->{songs}} ) {
  0         0  
434 0         0 my $p =
435             generate_song( $_,
436             { pr => $pr,
437             prepend => 1,
438             roman => 1,
439             page_idx => $page,
440             page_num => $page,
441             songindex => 0,
442             numsongs => 1,
443             pagectrl => $pagectrl,
444             } );
445 0         0 $page += $p;
446 0         0 $start_of{$_} += $p for qw( songbook front toc back );
447             }
448 0         0 $pages_of{cover} = $page - 1;
449             }
450             elsif ( defined( $pagectrl->{cover} ) ) {
451 0         0 my $cover = $pdfapi->open( expand_tilde($pagectrl->{cover}) );
452 0 0       0 die("Missing cover: ", $pagectrl->{cover}, "\n") unless $cover;
453 0         0 $page = 0;
454 0 0       0 return unless progress( msg => "Cover" );
455 0         0 for ( 1 .. $cover->pages ) {
456 0         0 $page++;
457 0         0 $pr->{pdf}->import_page( $cover, $_, $page );
458             }
459 0         0 $pages_of{cover} = $page;
460 0         0 $start_of{$_} += $page for qw( songbook front toc back );
461             }
462              
463             # Back matter (if any) has already been opened.
464 8 100       40 if ( $back_matter ) {
465 3         11 $page = $start_of{back};
466 3 50       20 return unless progress( msg => "Back matter" );
467             warn( "ASSERT: pages=", $pr->{pdf}->pages,
468             " back=", $start_of{back}, "\n" )
469 3 50       24 unless 1+$pr->{pdf}->pages == $start_of{back};
470 3         42 for ( 1 .. $back_matter->pages ) {
471 3         43 $pr->{pdf}->import_page( $back_matter, $_, $page );
472 3         24520 $page++;
473             }
474 3         17 $pages_of{back} = $back_matter->pages;
475             }
476              
477 8         46 if ( 0 and $::config->{debug}->{pages} & 0x01 ) {
478             warn("-- pre alignment\n");
479             for ( qw( cover front toc songbook back ) ) {
480             warn( sprintf("%4d %-10s %s\n",
481             $start_of{$_}, $_,
482             plural( sprintf("%4d",$pages_of{$_})," page") ));
483             }
484             warn("-- final\n");
485             }
486              
487             # Alignment. Only if odd/even pages.
488 8 100       87 if ( $pagectrl->{dual_pages} ) {
489 6         35 my @parts = qw( front toc songbook back );
490 6         30 while ( @parts ) {
491 24         67 my $part = shift(@parts);
492 24 100       91 next unless $pages_of{$part};
493              
494             # Always align parts, regardless of pagealign-songs.
495 18         53 local $pagectrl->{align_songs} = 1;
496              
497 18 100       47 if ( @parts ) {
498 15 100       128 if ( $pr->page_align( $pagectrl,
    100          
    100          
499             $part,
500             $start_of{$part},
501             $part eq "songbook"
502             ? $prefill
503             ? 1
504             : is_odd($page_offset)
505             : 0 ) ) {
506 8         66 $start_of{$_}++ for $part, @parts;
507             }
508             }
509             else {
510             $start_of{$part} +=
511 3         24 $pr->page_align( $pagectrl, $part, $start_of{$part},
512             is_odd($back_matter->pages) );
513             }
514             }
515             }
516              
517 8 50       87 if ( $::config->{debug}->{pages} & 0x01 ) {
518 0         0 for ( qw( cover front toc songbook back ) ) {
519             warn( sprintf("%4d %-10s %s\n",
520             $start_of{$_}, $_,
521 0         0 plural( sprintf("%4d",$pages_of{$_})," page") ));
522             }
523             }
524              
525             # Note that the page indices run from zero.
526 8         67 $pr->pagelabel( 0, 'arabic', 'cover-' );
527             $pr->pagelabel( $start_of{front}-1, 'arabic', 'front-' )
528 8 100       1480 if $pages_of{front};
529             $pr->pagelabel( $start_of{toc}-1, 'roman' )
530 8 50       408 if $pages_of{toc};
531             # Label song pages according to the user visible number.
532             $pr->pagelabel( $start_of{songbook}-1, 'arabic', '', ,
533             $options->{'start-page-number'} || 1 )
534 8 50 50     917 if $pages_of{songbook};
535             $pr->pagelabel( $start_of{back}-1, 'arabic', 'back-' )
536 8 100       989 if $pages_of{back};
537              
538             # Add the bookmarks.
539 8         332 for ( qw( cover front toc back ) ) {
540 32 100       138 next unless $pages_of{$_};
541 14         82 my $p = $pr->{pdf}->openpage( $start_of{$_} );
542 14         666 $pr->named_dest( $_, $p );
543             }
544              
545             # Add the outlines.
546 8         34 $pr->make_outlines( [ map { $_->[1] } @book ], $start_of{songbook} );
  24         136  
547              
548 8   50     155 $pr->finish( $options->{output} || "__new__.pdf" );
549 8 50       111 warn("Generated PDF...\n") if $options->{verbose};
550              
551 8 50       77 if ( $options->{csv} ) {
552 8 50       131 return unless progress( msg => "CSV" );
553 8         124 generate_csv( \@book, $page, \%pages_of, \%start_of )
554             }
555              
556 8         901 []
557             }
558              
559             sub generate_csv {
560 8     8 0 70 my ( $book, $page, $pages_of, $start_of ) = @_;
561              
562             # Create an MSPro compatible CSV for this PDF.
563 8         137 push( @$book, [ "CSV", { meta => { tocpage => $page } } ] );
564 8         104 my $csv = CP->sibling( $options->{output}, ext => ".csv" );
565 8 50       56 my $fd = fs_open( $csv, '>:utf8' )
566             or die( $csv, ": $!\n" );
567              
568             warn("Generating CSV $csv...\n")
569 8 50 33     129 if $config->{debug}->{csv} || $options->{verbose};
570              
571 8         61 $ps = $config->{pdf};
572 8         42 my $ctl = $ps->{csv};
573 8   50     61 my $sep = $ctl->{separator} // ";";
574 8   50     65 my $vsep = $ctl->{vseparator} // "|";
575              
576             my $rfc4180 = sub {
577 328     328   61229 my ( $v ) = @_;
578 328 50       1527 $v = [$v] unless is_arrayref($v);
579 328 50 33     1966 return "" unless defined($v) && defined($v->[0]);
580 328         1018 $v = join( $sep, @$v );
581 328 50       9747 return $v unless $v =~ m/[$sep"\n\r]/s;
582 0         0 $v =~ s/"/""/g;
583 0         0 return '"' . $v . '"';
584 8         145 };
585              
586             my $pagerange = sub {
587 33     33   170 my ( $pages, $page ) = @_;
588 33 100       152 if ( @_ == 1 ) {
589 9         32 $pages = $pages_of->{$_[0]};
590 9         31 $page = $start_of->{$_[0]};
591             }
592 33 100       589 $pages > 1
593             ? ( $page ."-". ($page+$pages-1) )
594             : $page,
595 8         122 };
596              
597             my $csvline = sub {
598 33     33   88 my ( $m ) = @_;
599 33         89 my @cols = ();
600 33         71 for ( @{ $ctl->{fields} } ) {
  33         141  
601 297 100       1286 next if $_->{omit};
602 264   33     1755 my $v = $_->{value} // '%{'.$_->{meta}.'}';
603 264         1106 local( $config->{metadata}->{separator} ) = $vsep;
604 264         1535 push( @cols, $rfc4180->( fmt_subst( { meta => $m }, $v ) ) );
605             }
606 33         334 print $fd ( join( $sep, @cols ), "\n" );
607 33         442 scalar(@cols);
608 8         97 };
609              
610 8         34 my @cols;
611             my $ncols;
612 8         19 for ( @{ $ctl->{fields} } ) {
  8         51  
613 72 100       332 next if $_->{omit};
614 64         163 push( @cols, $rfc4180->($_->{name}) );
615             }
616 8         34 $ncols = @cols;
617             #warn( "CSV: $ncols fields\n" );
618 8         98 print $fd ( join( $sep, @cols ), "\n" );
619              
620             # Extra meta info from command line, for non-song CSV.
621 8   50     90 my $xm = $options->{meta} // {};
622 8 100       53 unless ( $ctl->{songsonly} ) {
623             $csvline->( { %$xm,
624             title => 'Cover',
625             pagerange => $pagerange->("cover"),
626             sorttitle => 'Cover',
627             artist => 'ChordPro' } )
628 3 50       21 if $pages_of->{cover};
629             $csvline->( { %$xm,
630             title => 'Front Matter',
631             pagerange => $pagerange->("front"),
632             sorttitle => 'Front Matter',
633             artist => 'ChordPro' } )
634 3 50       29 if $pages_of->{front};
635             $csvline->( { %$xm,
636             title => "Table of Contents",
637             pagerange => $pagerange->("toc"),
638             sorttitle => "Table of Contents",
639             artist => 'ChordPro' } )
640 3 50       69 if $pages_of->{toc};
641             }
642              
643             warn( "CSV: ", scalar(@$book), " songs in book\n")
644 8 50       145 if $config->{debug}->{csv};
645 8         69 for ( my $p = 0; $p < @$book-1; $p++ ) {
646 24         56 my ( $title, $song ) = @{$book->[$p]};
  24         164  
647 24         177 my $page = $start_of->{songbook} + $song->{meta}->{tocpage} - 1;
648 24         88 my $pp = $song->{meta}->{pages};
649 24         58 my $m = { %{$song->{meta}},
  24         340  
650             pagerange => [ $pagerange->($pp, $page) ] };
651 24         163 $csvline->($m);
652             }
653              
654 8 100       138 unless ( $ctl->{songsonly} ) {
655             $csvline->( { %$xm,
656             title => 'Back Matter',
657             pagerange => $pagerange->("back"),
658             sorttitle => 'Back Matter',
659             artist => 'ChordPro'} )
660 3 50       31 if $pages_of->{back};
661             }
662 8         708 close($fd);
663             warn("Generated CSV...\n")
664 8 50 33     518 if $config->{debug}->{csv} || $options->{verbose};
665             }
666              
667             ################ ################
668              
669             sub _dump {
670 0 0   0   0 return unless $config->{debug}->{fonts};
671 0         0 my ( $ps ) = @_;
672 0         0 print STDERR ("== Font family map\n");
673 0 0       0 Text::Layout::FontConfig->new->_dump if $verbose;
674 0         0 print STDERR ("== Font associations\n");
675 0         0 foreach my $f ( sort keys( %{$ps->{fonts}} ) ) {
  0         0  
676             printf STDERR ("%-15s %s\n", $f,
677             eval { $ps->{fonts}->{$f}->{description} } ||
678             eval { $ps->{fonts}->{$f}->{file} } ||
679 0   0     0 eval { "[".$ps->{fonts}->{$f}->{name}."]" } ||
680             "[]"
681             );
682             }
683             }
684              
685             # Derive new style page controls from old style.
686             sub pagectrl {
687 8     8 0 705 my ( $self ) = @_;
688              
689             # If at this point we still have old style page controls,
690             # they were passed via command line and thus override.
691             # $config->migrate_songbook_pagectrl;
692              
693 8         642 my $sb = $config->{pdf}->{songbook};
694             my $pagectrl = { dual_pages => $sb->{'dual-pages'},
695             align_tocs => $sb->{'align-tocs'},
696             align_songs => $sb->{'align-songs'},
697             align_songs_spread => $sb->{'align-songs-spread'},
698             align_songs_extend => $sb->{'align-songs-extend'},
699             sort_songs => $sb->{'sort-songs'},
700             compact_songs => $sb->{'compact-songs'},
701             cover => $sb->{cover},
702             front_matter => $sb->{'front-matter'},
703 8         1383 back_matter => $sb->{'back-matter'},
704             };
705              
706 8 100       645 unless ( $pagectrl->{dual_pages} ) {
707 2         6 $pagectrl->{align_songs} = 0;
708 2         7 $pagectrl->{align_tocs} = 0;
709             }
710 8 100       41 unless ( $pagectrl->{align_songs} ) {
711             $pagectrl->{$_} = 0
712 3         22 for qw( align_songs_spread align_songs_extend compact_songs);
713             }
714 8         30 for ( qw( cover front_matter back_matter ) ) {
715 24 100       120 $pagectrl->{$_} = undef unless is_true($pagectrl->{$_});
716             }
717 8 50       104 if ( $config->{debug}->{pagectrl} ) {
718 10     10   7133 use DDP; p $pagectrl, as => "pagectrl";
  10         4806  
  10         116  
  0         0  
719             }
720 8         31 return $pagectrl;
721             }
722              
723             sub pagectrl_msg {
724 8     8 0 26 my ( $pagectrl ) = @_;
725 8 100       56 my $msg = $pagectrl->{dual_pages} ? "dual" : "single";
726 8 100       41 if ( $pagectrl->{align_tocs} ) {
727 6         20 $msg .= ", align_tocs";
728 6 50       27 $msg .= "_song" if $pagectrl->{align_tocs} eq "song";
729             }
730 8 100       40 if ( $pagectrl->{align_songs} ) {
731 5         18 $msg .= ", align_songs";
732 5 50       19 $msg .= ", extend" if $pagectrl->{align_songs_extend};
733 5 100       20 $msg .= ", spread" if $pagectrl->{align_songs_spread};
734             }
735 8 50       36 $msg .= ", " . $pagectrl->{sort_songs} if $pagectrl->{sort_songs};
736              
737 8         37 return $msg;
738             }
739              
740             sub sort_songbook {
741 0     0 0 0 my ( $sb, $pagectrl ) = @_;
742 0 0       0 return unless my $sorting = $pagectrl->{sort_songs};
743              
744 0         0 foreach my $song ( @{$sb->{songs}} ) {
  0         0  
745 0 0       0 if (!defined($song->{meta}->{sorttitle})) {
746 0         0 $song->{meta}->{sorttitle} = $song->{meta}->{title};
747             }
748             }
749              
750 0         0 my @songlist = @{$sb->{songs}};
  0         0  
751              
752 0         0 my @tbs; # to be sorted
753 0         0 my $desc = 0; # descending
754 0 0       0 if ( $sorting =~ /^([-+]?)title$/i ) {
    0          
755 0         0 $desc = $1 eq "-";
756 0         0 @tbs = map { [ $_->{meta}->{sorttitle}->[0], $_ ] } @songlist;
  0         0  
757             }
758             elsif ( $sorting =~ /^([-+]?)subtitle$/i ) {
759 0         0 $desc = $1 eq "-";
760 0         0 @tbs = map { [ $_->{meta}->{subtitle}->[0], $_ ] } @songlist;
  0         0  
761             }
762 0 0       0 return unless @tbs;
763              
764 0         0 if ( 1 ) {
765 0         0 my $collator = Unicode::Collate->new;
766 0 0       0 my ( $aa, $bb ) = $desc ? qw( b a ) : qw( a b );
767 0         0 my $l = "\$$aa"."->[0]";
768 0         0 my $r = "\$$bb"."->[0]";
769 0         0 my $proc = 'sub { my $tbs = shift; ';
770 0         0 $proc .= '[ map { $_->[1] } sort { ';
771 0         0 $proc .= "\$collator->cmp( $l, $r )";
772 0         0 $proc .= ' } @$tbs ] }';
773 0         0 my $sorter = eval $proc;
774 0 0       0 die("OOPS $proc\n$@") if $@;
775 0         0 $sb->{songs} = $sorter->(\@tbs);
776             }
777             else {
778             my $proc = 'sub { my $tbs = shift; use locale; ';
779             $proc .= '[ map { $_->[1] } sort { ';
780             $proc .= $desc ? '$b->[0] cmp $a->[0]' : '$a->[0] cmp $b->[0]';
781             $proc .= ' } @$tbs ] }';
782             my $sorter = eval $proc;
783             die("OOPS $proc\n$@") if $@;
784             $sb->{songs} = $sorter->(\@tbs);
785             }
786             }
787              
788             sub compact_songbook {
789 0     0 0 0 my ( $sb, $pagectrl ) = @_;
790 0 0       0 return 0 unless $pagectrl->{compact_songs};
791              
792 0         0 my $ps = $config->{pdf};
793 0         0 my $pri = ( __PACKAGE__."::Writer" )->new( $ps, $pdfapi );
794              
795             # Count pages to properly align multi-page songs without
796             # needing to turn page.
797 0   0     0 my $page = $options->{"start-page-number"} ||= 1;
798              
799 0         0 foreach my $song ( @{$sb->{songs}} ) {
  0         0  
800 0 0       0 if (!defined($song->{meta}->{sorttitle})) {
801 0         0 $song->{meta}->{sorttitle} = $song->{meta}->{title};
802             }
803             }
804              
805 0         0 my @songlist = @{$sb->{songs}};
  0         0  
806 0         0 my $filler = 0; # filler for 2page
807              
808             # Progress indicator
809             progress( phase => "Counting",
810             index => 0,
811 0         0 total => scalar(@{$sb->{songs}}) );
  0         0  
812              
813 0         0 my $i = 1;
814 0         0 foreach my $song ( @songlist ) {
815 0 0       0 return unless progress( msg => $song->{title} );
816 0         0 $i++;
817              
818             #### HACK ATTACK.
819             # Assets will be rendered, but then they are part of the temp
820             # PDF, not the final one.
821             # We copy the unprocessed assets and restore after the 1st pass.
822 10     10   19882 use Storable qw(dclone);
  10         29  
  10         33190  
823 0         0 my $assets;
824 0 0       0 $assets = dclone( $song->{assets} ) if $song->{assets};
825             ####
826              
827             $song->{meta}->{pages} =
828 0         0 generate_song( $song,
829             { pr => $pri,
830             startpage => 1,
831             pagectrl => $pagectrl,
832             } );
833             ####
834 0 0       0 $song->{assets} = $assets if $assets;
835             ####
836             }
837              
838 0         0 my @new;
839 0         0 my $used = "";
840             # First an arbitrary odd-pages song.
841 0         0 for ( my $i=0; $i < @songlist; $i++ ) {
842 0 0 0     0 next unless is_odd( $options->{'start-page-number'}||1 );
843 0 0       0 next unless is_odd($songlist[$i]->{meta}->{pages});
844 0         0 push( @new, $songlist[$i] );
845 0         0 vec( $used, $i, 1 ) = 1;
846 0         0 last;
847             }
848             ##### TODO: If still empty, need filler.
849 0 0       0 $filler++ unless @new;
850              
851             # Then all even-pages songs.
852 0         0 for ( my $i=0; $i < @songlist; $i++ ) {
853 0 0       0 next if vec( $used, $i, 1 );
854 0 0       0 next unless is_even($songlist[$i]->{meta}->{pages});
855 0         0 push( @new, $songlist[$i] );
856 0         0 vec( $used, $i, 1 ) = 1;
857             }
858              
859             # Finally, all other odd-pages songs.
860 0         0 for ( my $i=0; $i < @songlist; $i++ ) {
861 0 0       0 next if vec( $used, $i, 1 );
862 0 0       0 next unless is_odd($songlist[$i]->{meta}->{pages});
863 0         0 push( @new, $songlist[$i] );
864 0         0 vec( $used, $i, 1 ) = 1;
865             }
866              
867 0 0       0 die("compact ", scalar(@new), " <> ", scalar(@songlist), "!\n")
868             unless scalar(@new) == scalar(@songlist);
869              
870 0         0 @songlist = @new;
871              
872 0         0 $sb->{songs} = [@songlist];
873              
874 0         0 return $filler;
875             }
876              
877             sub config_pdfapi {
878 11     11 0 885263 my ( $lib, $verbose ) = @_;
879 11         30 my $pdfapi;
880              
881 11         34 my $t = "config";
882             # Get PDF library.
883 11 50       74 if ( $ENV{CHORDPRO_PDF_API} ) {
884 0         0 $t = "CHORDPRO_PDF_API";
885 0         0 $lib = $ENV{CHORDPRO_PDF_API};
886             }
887 11 50       50 if ( $lib ) {
888 0 0       0 unless ( eval( "require $lib" ) ) {
889 0         0 die("Missing PDF library $lib ($t)\n");
890             }
891 0         0 $pdfapi = $lib;
892 0 0       0 warn("Using PDF library $lib ($t)\n") if $verbose;
893             }
894             else {
895 11         39 for ( qw( PDF::API2 PDF::Builder ) ) {
896 11 50       1636 eval "require $_" or next;
897 11         3058037 $pdfapi = $_;
898 11 50       56 warn("Using PDF library $_ (detected)\n") if $verbose;
899 11         38 last;
900             }
901             }
902 11 50       47 die("Missing PDF library\n") unless $pdfapi;
903 11         87 return $pdfapi;
904             }
905              
906             sub configurator {
907 8     8 0 28 my ( $cfg ) = @_;
908              
909             # From here, we're mainly dealing with the PDF settings.
910 8         28 my $pdf = $cfg->{pdf};
911              
912             # Get PDF library.
913 8   33     134 $pdfapi //= config_pdfapi( $pdf->{library} );
914              
915 8         67 my $fonts = $pdf->{fonts};
916              
917             # Apply Chordii command line compatibility.
918              
919             # Command line only takes text and chord fonts.
920 8         34 for my $type ( qw( text chord ) ) {
921 16         83 for ( $options->{"$type-font"} ) {
922 16 50       774 next unless $_;
923 0 0       0 if ( m;/; ) {
924 0         0 $fonts->{$type}->{file} = $_;
925             }
926             else {
927 0 0       0 if ( is_corefont($_) ) {
    0          
928 0         0 $fonts->{$type}->{name} = is_corefont($_);
929             }
930             elsif ( defined $pdf->{fontconfig}->{s/\s+\d+$//r} ) {
931 0         0 $fonts->{$type}->{description} = $_;
932             }
933             else {
934 0         0 die("Config error: \"$_\" is not a built-in font\n")
935             }
936             }
937             }
938 16         61 for ( $options->{"$type-size"} ) {
939 16 50       59 $fonts->{$type}->{size} = $_ if $_;
940             }
941             }
942              
943 8         58 for ( $options->{"page-size"} ) {
944 8 50       36 $pdf->{papersize} = $_ if $_;
945             }
946 8         33 for ( $options->{"vertical-space"} ) {
947 8 50       42 next unless $_;
948             $pdf->{spacing}->{lyrics} +=
949 0         0 $_ / $fonts->{text}->{size};
950             }
951 8         34 for ( $options->{"lyrics-only"} ) {
952 8 50       39 next unless defined $_;
953             # If set on the command line, it cannot be overridden
954             # by configs and {controls}.
955 0         0 $pdf->{"lyrics-only"} = 2 * $_;
956             }
957 8         32 for ( $options->{"single-space"} ) {
958 8 50       38 next unless defined $_;
959 0         0 $pdf->{"suppress-empty-chords"} = $_;
960             }
961              
962             # Chord grid width.
963 8 50       46 if ( $options->{'chord-grid-size'} ) {
964             # Note that this is legacy, so for the chord diagrams only,
965             $pdf->{diagrams}->{width} =
966             $pdf->{diagrams}->{height} =
967             $options->{'chord-grid-size'} /
968 0         0 @{ $config->{notes}->{sharps} };
  0         0  
969             }
970              
971             # Map papersize name to [ width, height ].
972 8 50       23 unless ( eval { $pdf->{papersize}->[0] } ) {
  8         119  
973 8         3436 eval "require ${pdfapi}::Resource::PaperSizes";
974 8         121 my %ps = "${pdfapi}::Resource::PaperSizes"->get_paper_sizes;
975             die("Unhandled paper size: ", $pdf->{papersize}, "\n")
976 8 50       731 unless exists $ps{lc $pdf->{papersize}};
977             $pdf->{papersize} = $ps{lc $pdf->{papersize}}
978 8         105 }
979              
980             # Merge properties for derived fonts.
981             my $fm = sub {
982 120     120   292 my ( $font, $def ) = @_;
983 120         336 for ( keys %{ $fonts->{$def} } ) {
  120         1084  
984 464 100       1503 next if /^(?:background|frame)$/;
985 360 100 100     891 next if $font eq "chordfingers" && $_ eq "size";
986 352   66     1642 $fonts->{$font}->{$_} //= $fonts->{$def}->{$_};
987             }
988 8         76 };
989 8         40 $fm->( qw( subtitle text ) );
990 8         47 $fm->( qw( chorus text ) );
991 8         39 $fm->( qw( comment_italic text ) );
992 8         55 $fm->( qw( comment_box text ) );
993 8         36 $fm->( qw( comment text ) );
994 8         35 $fm->( qw( annotation chord ) );
995 8         38 $fm->( qw( label text ) );
996 8         37 $fm->( qw( toc text ) );
997 8         68 $fm->( qw( empty text ) );
998 8         34 $fm->( qw( grid chord ) );
999 8         36 $fm->( qw( grid_margin comment ) );
1000 8         37 $fm->( qw( diagram comment ) );
1001 8         32 $fm->( qw( diagram_base comment ) );
1002 8         959 $fm->( qw( chordfingers diagram ) );
1003              
1004             # Default footer is small subtitle.
1005 8   33     170 $fonts->{footer}->{size} //= 0.6 * $fonts->{subtitle}->{size};
1006 8         32 $fm->( qw( footer subtitle ) );
1007              
1008             # This one is fixed.
1009 8         116 $fonts->{chordprosymbols}->{file} = "ChordProSymbols.ttf";
1010              
1011             }
1012              
1013             sub diagrammer {
1014 0     0 0   my ( $type ) = @_;
1015 0           my $p;
1016 0 0         if ( $type eq "keyboard" ) {
1017 0           require ChordPro::Output::PDF::KeyboardDiagram;
1018 0           $p = ChordPro::Output::PDF::KeyboardDiagram->new( pr => $pr );
1019             }
1020             else {
1021 0           require ChordPro::Output::PDF::StringDiagram;
1022 0           $p = ChordPro::Output::PDF::StringDiagram->new( pr => $pr );
1023             }
1024 0           return $p;
1025             }
1026              
1027 10     10   99 use Object::Pad;
  10         26  
  10         128  
1028              
1029 10     10   8968 class TextLayoutImageElement :isa(Text::Layout::PDFAPI2::ImageElement);
  10         81925  
  10         798  
1030              
1031 10     10   1287 use Carp;
  10         40  
  10         911  
1032              
1033 10     10   73 use Text::ParseWords qw( shellwords );
  10         21  
  10         36016  
1034              
1035 0     0     method parse( $ctx, $k, $v ) {
  0            
  0            
  0            
  0            
  0            
1036              
1037 0           my %ctl = ( type => "img", %$ctx );
1038 0           my $err;
1039              
1040             # Split the attributes.
1041 0           foreach my $kk ( shellwords($v) ) {
1042              
1043             # key=value
1044 0 0         if ( $kk =~ /^([-\w]+)=(.+)$/ ) {
1045 0           my ( $k, $v ) = ( $1, $2 );
1046              
1047             # Ignore case unless required.
1048 0 0         $v = lc $v unless $k =~ /^(id|chord)$/;
1049              
1050 0 0 0       if ( $k =~ /^(id|bbox|chord|src)$/ ) {
    0 0        
    0          
    0          
    0          
1051 0 0         if ( $v =~ /^(chord|builtin):/ ) {
1052 0           $k = $1;
1053 0           $v = $';
1054             }
1055 0           $ctl{$k} = $v;
1056             }
1057             elsif ( $k eq "align" && $v =~ /^(left|right|center)$/ ) {
1058 0           $ctl{$k} = $v;
1059             }
1060             elsif ( $k eq "type" && $v =~ /^(strings?|keyboard)$/ ) {
1061 0           $ctl{instrument} = $v;
1062             }
1063             elsif ( $k =~ /^(width|height|dx|dy|w|h)$/ ) {
1064 0 0         $v = $1 if $v =~ /^(-?[\d.]+)pt$/;
1065 0 0         $v = $1 * $ctx->{size} if $v =~ /^(-?[\d.]+)em$/;
1066 0 0         $v = $1 * $ctx->{size} / 2 if $v =~ /^(-?[\d.]+)ex$/;
1067             #$v = $1 * $ctx->{size} / 100 if $v =~ /^(-?[\d.]+)\%$/;
1068 0 0         if ( $v =~ /^(-?[\d.]+)\%$/ ) {
1069 0           warn("Invalid img attribute: \"$kk\" (percentage not allowed)\n");
1070 0           $err++;
1071             }
1072             else {
1073 0           $ctl{$k} = $v;
1074             }
1075             }
1076             elsif ( $k =~ /^(scale)$/ ) {
1077 0           my @s;
1078 0           for ( split( /,/, $v ) ) {
1079 0 0         $_ = $1 / 100 if /^([\d.]+)\%$/;
1080 0           push( @s, $_ );
1081             }
1082 0 0         push( @s, $s[0] ) unless @s > 1;
1083 0 0         unless ( @s == 2 ) {
1084 0           warn("Invalid img attribute: \"$kk\" (too many values)\n");
1085 0           $err++;
1086             }
1087 0           $ctl{$k} = \@s;
1088             }
1089             else {
1090 0           warn("Invalid img attribute: \"$k\" ($kk)\n");
1091 0           $err++;
1092             }
1093             }
1094              
1095             # Currently we do not have value-less attributes.
1096             else {
1097 0           warn("Invalid img attribute: \"$kk\"\n");
1098 0           $err++;
1099             }
1100             }
1101              
1102 0 0         if ( $err ) {
    0          
1103 0 0         if ( $ctl{id} ) {
1104 0           $ctl{id} = "__ERROR__";
1105             }
1106             }
1107             elsif ( $ctl{id} ) {
1108 0           my $a = ChordPro::Output::PDF::Song::assets($ctl{id});
1109 0 0 0       if ( $a && $a->{opts}->{base} ) {
1110 0           $ctl{base} = $a->{opts}->{base};
1111             }
1112             }
1113              
1114 0           return \%ctl;
1115             }
1116              
1117 0     0     method getimage ($fragment) {
  0            
  0            
  0            
1118 0   0       $fragment->{_img} //= do {
1119 0           my $xo;
1120 0 0         if ( $fragment->{id} ) {
    0          
    0          
1121 0           my $o = ChordPro::Output::PDF::Song::assets($fragment->{id});
1122 0 0         $xo = $o->{data} if $o;
1123 0 0 0       unless ( $o && $xo ) {
1124             warn("Unknown image ID in : $fragment->{id}\n")
1125 0 0         unless $fragment->{id} eq "__ERROR__";
1126 0           $xo = alert( $fragment->{size} );
1127             }
1128 0           $fragment->{design_scale} = $o->{opts}->{scale};
1129 0 0 0       if ( $o->{width} && $o->{vwidth} ) {
1130 0   0       $fragment->{design_scale} ||= 1;
1131 0           $fragment->{design_scale} *= $o->{vwidth}/$o->{width};
1132             }
1133             }
1134             elsif ( $fragment->{builtin} ) {
1135 0           my $i = $fragment->{builtin};
1136 0 0         if ( $i =~ /^alert(?:\(([\d.]+)\))?$/ ) {
1137 0   0       $xo = alert( $1 || $fragment->{size} );
1138             }
1139             else {
1140 0           warn("Unknown builtin image in : $i\n");
1141 0           $xo = alert( $fragment->{size} );
1142             }
1143             }
1144             elsif ( $fragment->{chord} ) {
1145 0           my $info = ChordPro::Chords::known_chord($fragment->{chord});
1146 0 0         unless ( $info ) {
1147 0           warn("Unknown chord in : $fragment->{chord}\n");
1148 0           $xo = alert( $fragment->{size} );
1149             }
1150             else {
1151 0   0       my $type = $fragment->{instrument} // $config->{instrument}->{type};
1152 0           my $p = ChordPro::Output::PDF::diagrammer($type);
1153 0           $xo = $p->diagram_xo($info);
1154             }
1155             }
1156 0   0       $xo // $self->SUPER::getimage($fragment) // alert( $fragment->{size} );
      0        
1157             };
1158             }
1159              
1160 0     0     sub alert ($size) {
  0            
  0            
1161 0           my $scale = $size/20;
1162 0           my $xo = $pr->{pdf}->xo_form;
1163 0           $xo->bbox( 0, -18*$scale, 20*$scale, 0 );
1164 0           $xo->matrix( $scale, 0, 0, -$scale, 0, 0 );
1165 0           $xo->line_width(2)->line_join(1);
1166 0           $xo->stroke_color("red");
1167 0           $xo->fill_color("red");
1168 0           $xo->move( 1, 17 )->polyline( 19, 17, 10, 1 )->close->stroke;
1169 0           $xo->rectangle( 9, 13, 11, 15 );
1170 0           $xo->move( 9, 12 )->polyline( 8.5, 7, 11.5, 7, 11, 12 )->close->fill;
1171 0           return $xo;
1172             }
1173              
1174             class TextLayoutSymbolElement :does(Text::Layout::ElementRole);
1175              
1176 10     10   918 use ChordPro::Utils qw(parse_kv);
  10         22  
  10         737  
1177 10     10   89 use ChordPro::Symbols;
  10         21  
  10         115  
1178              
1179             field $glyphs;
1180              
1181             BUILD {
1182             $glyphs = ChordPro::Symbols::symbols();
1183             };
1184              
1185 0     0     method parse( $ctx, $k, $v ) {
  0            
  0            
  0            
  0            
  0            
1186 0           my $kv = parse_kv($v);
1187 0           my $res =
1188             { %$ctx,
1189             type => "text",
1190             font => Text::Layout::FontConfig->from_string("ChordProSymbols"),
1191             };
1192              
1193 0           while ( ( $k,$v) = each(%$kv) ) {
1194 0 0         $res->{$k} = $v, next
1195             if $k =~ /^(size|color|bgcolor|href)$/;
1196 0 0         $res->{text} = $glyphs->{$k}, next if defined $glyphs->{$k};
1197 0           warn("Unknown attribute in : $k (ignored)\n");
1198             }
1199              
1200 0           return $res;
1201             }
1202              
1203             # These methods must be defined for the role, but will not be used.
1204 0     0     method render( $hash, $gfx, $x, $y ) {}
  0            
  0            
  0            
  0            
  0            
1205 0     0     method bbox( $hash ) {}
  0            
  0            
1206              
1207             1;