File Coverage

lib/ChordPro/Delegate/ABC.pm
Criterion Covered Total %
statement 72 297 24.2
branch 7 150 4.6
condition 8 79 10.1
subroutine 18 31 58.0
pod 0 7 0.0
total 105 564 18.6


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3 9     9   145 use v5.26;
  9         43  
4 9     9   63 use utf8;
  9         21  
  9         86  
5              
6             package main;
7              
8             our $config;
9             our $options;
10              
11             package ChordPro::Delegate::ABC;
12              
13 9     9   758 use strict;
  9         22  
  9         315  
14 9     9   49 use warnings;
  9         18  
  9         637  
15 9     9   61 use feature qw( signatures );
  9         18  
  9         1707  
16 9     9   64 no warnings "experimental::signatures";
  9         21  
  9         410  
17 9     9   48 use utf8;
  9         20  
  9         46  
18 9     9   325 use Carp;
  9         22  
  9         1056  
19 9     9   64 use File::Temp ();
  9         22  
  9         231  
20 9     9   48 use File::LoadLines;
  9         18  
  9         566  
21 9     9   51 use feature 'state';
  9         23  
  9         346  
22              
23 9     9   48 use ChordPro::Files;
  9         18  
  9         1509  
24 9     9   62 use ChordPro::Paths;
  9         19  
  9         482  
25 9     9   56 use ChordPro::Utils;
  9         17  
  9         1562  
26 9     9   66 use Text::ParseWords qw(shellwords);
  9         20  
  9         763  
27              
28 9         82957 use constant { QUICKJS => "QuickJS",
29 9     9   63 QUICKJSXS => "QuickJS_XS" };
  9         22  
30              
31 0     0 0 0 sub DEBUG() { $config->{debug}->{abc} }
  0         0  
  0         0  
32              
33             # ABC processing using abc2svg and custom SVG processor.
34             # See info() below how the method is determined.
35              
36             # Song and PDF module uses 'can' to get at this.
37 0     0 0 0 sub can( $class, $method ) {
  0         0  
  0         0  
  0         0  
38 0 0       0 if ( $method eq "options" ) {
39 0         0 return \&options;
40             }
41             # abc2svg handlers are sorted out by info().
42 0         0 return \&abc2svg;
43             }
44              
45             # Default entry point.
46              
47 0     0 0 0 sub abc2svg( $song, %args ) {
  0         0  
  0         0  
  0         0  
48              
49 0         0 my $abc2svg = info();
50              
51 0 0       0 if ( DEBUG() ) {
52 0         0 ::dump($abc2svg);
53             }
54              
55 0         0 state $cfg_checked;
56 0 0       0 unless ( $cfg_checked++ ) {
57 0 0 0     0 if ( ($config->{delegates}{abc}{config} // "default") ne "default" ) {
58 0         0 warn("ABC: delegates.abc.config is no longer used.\n");
59             warn("ABC: Config \"default.abc\" will be loaded instead.\n")
60 0 0 0     0 if !$abc2svg->{external} && fs_test( s => "default.abc" );
61             }
62             }
63              
64 0         0 my ( $elt, $pw ) = @args{qw(elt pagewidth)};
65              
66 0 0       0 return { type => "ignore" } unless @{ $elt->{data} };
  0         0  
67             # Bail out if we don't have a suitable program.
68 0 0       0 unless ( $abc2svg->{method} ) {
69 0         0 warn("Error in ABC embedding. Please install the JavaScript::QuickJS module.\n");
70 0         0 return;
71             }
72              
73 0         0 state $td = File::Temp::tempdir( CLEANUP => !$config->{debug}->{abc} );
74 0         0 my $cfg = { %{$config->{delegates}->{abc} } };
  0         0  
75              
76             # External tools usually process a default.abc.
77             warn("ABC: Using config \"default.abc\".\n")
78 0 0 0     0 if index( $abc2svg->{method}, QUICKJSXS ) < 0 && fs_test( s => "default.abc" );
79              
80 0         0 my $prep = make_preprocessor( $cfg->{preprocess} );
81              
82             # Prepare names for temporary files.
83 0         0 state $imgcnt = 0;
84 0         0 $imgcnt++;
85 0         0 my $src = fn_catfile( $td, "tmp${imgcnt}.abc" );
86 0         0 my $svg = fn_catfile( $td, "tmp${imgcnt}.svg" );
87 0         0 my $out = fn_catfile( $td, "tmp${imgcnt}.out" );
88 0         0 my $err = fn_catfile( $td, "tmp${imgcnt}.err" );
89              
90             # Get rid of as much space as possible.
91             # Jean-François Moine:
92             # If you have both "%%stretchstaff 1" and "%%trimsvg 1", and
93             # "%%stretchlast 0", only the final line is shorter.
94             # Otherwise, you can have "%%trimsvg 1" as the last line of the tune.
95             my @preamble =
96             ( "%%topspace 0",
97             "%%titlespace 0",
98             "%%musicspace 0",
99             "%%composerspace 0",
100             "%%infospace 0",
101             "%%textspace 0",
102             "%%leftmargin 0cm",
103             "%%rightmargin 0cm",
104             "%%stretchstaff 1",
105             "%%stretchlast 0",
106             "%%trimsvg 1",
107             "%%staffsep 0",
108 0   0     0 @{ $cfg->{preamble}//[] } );
  0         0  
109              
110 0         0 for ( keys(%{$elt->{opts}}) ) {
  0         0  
111              
112             # Suppress meaningless transpositions. ChordPro uses them to enforce
113             # certain chord renderings.
114 0 0       0 next if $_ ne "transpose";
115 0         0 my $x = $elt->{opts}->{$_} % @{ $config->{notes}->{sharp} };
  0         0  
116 0         0 unshift( @preamble, '%%transpose'." $x" );
117             }
118              
119             # Add mandatory field.
120 0         0 my @pre;
121 0         0 my @data = @{$elt->{data}};
  0         0  
122 0         0 while ( @data ) {
123 0         0 $_ = shift(@data);
124 0 0       0 unshift( @data, $_ ), last if /^X:/;
125 0         0 push( @pre, $_ );
126             }
127 0 0 0     0 if ( @pre && !@data ) { # no X: found
128 0 0       0 warn("X:1 (added)\n") if DEBUG;
129 0         0 @data = ( "X:1", @pre );
130 0         0 @pre = ();
131             }
132 0         0 my $kv = {};
133 0 0       0 $kv = parse_kvm( @pre ) if @pre;
134 0         0 $kv = { %$kv, %{$elt->{opts}} };
  0         0  
135 0   0     0 $kv->{split} //= 1; # less overhead. really.
136 0   0     0 $kv->{scale} ||= 1; # with id: design scale
137 0 0 0     0 $kv->{align} //= ($kv->{center}//0) ? "center" : "left";
      0        
138 0 0       0 if ( $kv->{width} ) {
139 0         0 $pw = $kv->{width};
140             }
141              
142             unshift( @preamble,
143 0 0       0 grep { /^%%/ } @pre,
  0         0  
144             $pw ? sprintf("%%%%pagewidth %dpx", $pw) : (),
145             );
146              
147             # Create the temp file for the ABC source.
148 0         0 my $fd;
149 0 0       0 unless ( $fd = fs_open( $src, '>:utf8' ) ) {
150 0         0 warn("Error in ABC embedding: $src: $!\n");
151 0         0 return;
152             }
153              
154             # Copy. We assume the user knows how to write ABC.
155 0         0 for ( @preamble ) {
156 0         0 print $fd $_, "\n";
157 0 0       0 warn($_, "\n") if DEBUG > 1;
158             }
159 0         0 for ( @data ) {
160 0 0       0 $prep->{abc}->($_) if $prep->{abc};
161 0         0 print $fd $_, "\n";
162 0 0       0 warn($_, "\n") if DEBUG > 1;
163             }
164 0   0     0 for ( @{ $cfg->{postamble}//[] } ) {
  0         0  
165 0         0 print $fd $_, "\n";
166 0 0       0 warn($_, "\n") if DEBUG > 1;
167             }
168              
169 0 0       0 unless ( close($fd) ) {
170 0         0 warn("Error in ABC embedding: $src: $!\n");
171 0         0 return;
172             }
173              
174 0         0 my @lines;
175             my $ret;
176              
177 0 0 0     0 if ( $abc2svg->{method} eq QUICKJSXS ) {
    0          
    0          
178              
179             # QuickJS with embedded interpreter.
180              
181 0         0 my $js = JavaScript::QuickJS->new;
182 0         0 my $base = $abc2svg->{abclib} . "/abc2svg";
183 0         0 $js->set_module_base($base);
184              
185             my $qjsdata =
186             {
187 0     0   0 print => sub { push( @lines, split(/\n/, $_) ) for @_ },
188 0     0   0 printErr => sub { print STDERR @_ },
189 0     0   0 quit => sub { exit 66 },
190 0     0   0 readFile => sub { slurp($_[0]) },
191             get_mtime => sub {
192 0     0   0 my @stat = stat($_[0]);
193 0 0       0 return @stat ? 1000*$stat[9] : undef;
194             },
195             loadjs => sub {
196 0     0   0 my ( $fn, $relay, $onerror ) = @_;
197 0 0       0 if ( fs_test( sr => "$base/$fn" ) ) {
    0          
198 0         0 $js->eval(slurp("$base/$_[0]"));
199 0 0       0 $relay->() if $relay;
200             }
201             elsif ( $onerror ) {
202 0         0 $onerror->();
203             }
204             else {
205 0         0 warn( qq{loadjs("$fn"): $!\n} );
206             }
207             },
208 0         0 };
209              
210             $js->set_globals
211             ( args => [ $src ],
212 0     0   0 load => sub { $js->eval(slurp("$base/$_[0]")) },
213 0         0 abc2svg => $qjsdata,
214             abc => {}, # for backends
215             );
216              
217 0 0       0 warn( "+ QuickJS_XS[", CP->display($base), "] $src\n") if DEBUG;
218 0         0 my $hooks = "$base/../hooks.js";
219 0 0       0 undef $hooks unless fs_test( s => $hooks );
220              
221 0         0 eval {
222 0         0 $js->eval( slurp("$base/abc2svg-1.js") );
223 0 0       0 $js->eval( slurp($hooks) ) if $hooks;
224 0 0       0 if ( -r "$base/../cmd.js" ) {
225 0 0       0 warn(" QuickJS_XS using ", CP->display("$base/../cmd.js"),
    0          
226             $hooks ? "+hooks" : "", "\n" )
227             if DEBUG;
228 0         0 $js->eval( slurp("$base/../cmd.js") );
229             }
230             else {
231 0 0       0 warn(" QuickJS_XS using ", CP->display("$base/cmdline.js"),
    0          
232             $hooks ? "+hooks" : "", "\n" )
233             if DEBUG;
234 0         0 $js->eval( slurp("$base/cmdline.js") );
235             }
236 0         0 $js->eval( slurp("$base/tohtml.js") );
237 0         0 $js->eval( qq{abc_cmd("ChordPro", args, "QuickJS_XS")} );
238             };
239 0 0       0 warn($@) if $@;
240 0         0 undef $js;
241              
242 0 0       0 if ( DEBUG ) {
243 0         0 my $fd = fs_open( $out, '>:utf8' );
244 0         0 print $fd join("\n", @lines), "\n";
245 0         0 close($fd);
246             }
247             }
248              
249             elsif ( $abc2svg->{method} eq QUICKJS ) {
250              
251             # QuickJS with external interpreter.
252              
253 0         0 my @cmd = @{ $abc2svg->{command} };
  0         0  
254              
255 0         0 push( @cmd, $out, $src );
256 0 0       0 if ( DEBUG ) {
257 0         0 warn( "+ @cmd\n" );
258 0         0 $ENV{CHORDPRO_ABC_DEBUG} = 1;
259             }
260              
261             # Run the command.
262 0         0 $ret = eval { sys( @cmd ) };
  0         0  
263              
264             # Load data.
265 0         0 @lines = loadlines($out)
266             }
267              
268             # Not packaged. Check for Wx on Windows since we cannot redirect STD***.
269             elsif ( !is_wx() && !is_msw() ) {
270              
271 0         0 my @cmd = @{ $abc2svg->{command} };
  0         0  
272              
273 0         0 push( @cmd, $src );
274 0 0       0 warn( "+ @cmd\n" ) if DEBUG;
275              
276             # Setup redirection for STDOUT/ERR.
277 0         0 my ( $oldout, $olderr );
278 0 0       0 open( $oldout, ">&STDOUT" )
279             or die "Can't dup STDOUT: $!";
280 0 0       0 open( $olderr, ">&", \*STDERR )
281             or die "Can't dup STDERR: $!";
282              
283 0 0       0 open(STDOUT, '>:utf8', $out)
284             or die "Can't redirect STDOUT: $!";
285 0 0       0 open(STDERR, ">:utf8", $err)
286             or die "Can't dup STDERR: $!";
287              
288 0         0 select STDERR; $| = 1; # make unbuffered
  0         0  
289 0         0 select STDOUT; $| = 1; # make unbuffered
  0         0  
290              
291             # Run the command.
292 0         0 $ret = eval { sys(@cmd) };
  0         0  
293              
294             # Reconnect STDOUT/ERR.
295 0 0       0 open(STDOUT, ">&", $oldout)
296             or die "Can't dup OLDOUT: $!";
297 0 0       0 open(STDERR, ">&", $olderr)
298             or die "Can't dup OLDERR: $!";
299 0         0 select STDERR; $| = 1; # make unbuffered
  0         0  
300              
301             # Load data.
302 0         0 @lines = loadlines($out);
303             }
304              
305             else {
306 0         0 my @cmd = @{ $abc2svg->{command} };
  0         0  
307 0         0 push( @cmd, $src );
308 0         0 if ( 0 ) {
309             # This seemed a good idea but unfortunately Wx has problems
310             # returning the UTF8 data correctly. Non-ASCII characters are
311             # crippled.
312             warn( "+ @cmd\n" ) if DEBUG;
313             ( $ret, $out, $err ) = Wx::ExecuteStdoutStderr( "@cmd", 32 );
314             warn("ABC: $_") for @$err;
315             @lines = @$out;
316             }
317             else {
318             # This will cause a console window flash, but at least we get
319             # the data right.
320 0 0       0 warn( "+ @cmd > $out\n" ) if DEBUG;
321 0         0 system( "@cmd > $out" );
322 0         0 @lines = loadlines($out);
323             }
324             }
325              
326 0 0       0 if ( $ret ) {
327 0         0 warn( sprintf( "Error in ABC embedding (ret = 0x%x)\n", $ret ) );
328 0         0 return;
329             }
330 0 0       0 if ( ! @lines ) {
331 0         0 warn("Error in ABC embedding (no output?)\n");
332 0         0 return;
333             }
334 0 0       0 warn("SVG: ", scalar(@lines), " lines (raw)\n") if DEBUG > 1;
335              
336             # Postprocess the SVG data.
337 0         0 my $staffbase;
338 0         0 my $copy = 0;
339 0         0 @data = ();
340 0         0 my $lines = 1;
341 0         0 while ( @lines ) {
342 0         0 $_ = shift(@lines);
343 0 0       0 if ( /\