File Coverage

lib/ChordPro/Output/PDF/StringDiagram.pm
Criterion Covered Total %
statement 134 225 59.5
branch 24 90 26.6
condition 8 39 20.5
subroutine 12 13 92.3
pod 0 8 0.0
total 178 375 47.4


line stmt bran cond sub pod time code
1             #! perl
2              
3 8     8   156 use v5.26;
  8         36  
4 8     8   64 use Object::Pad;
  8         22  
  8         163  
5 8     8   1704 use utf8;
  8         23  
  8         141  
6              
7             my $dcache; # cache core grids
8             my $pdf = ""; # for cache flush
9              
10             class ChordPro::Output::PDF::StringDiagram;
11              
12             field $pr :param;
13              
14             field $config;
15             field $ps;
16              
17             field $gw; # width of a cell, pt
18             field $gh; # height of a cell, pt
19             field $lw; # fraction of cell width
20             field $nutwidth; # width (in linewidth) of the top nut
21             field $nw; # extra width for the top nut, pt
22             field $vc; # cells, vertical
23             field $strings; # number of strings
24             field $hc; # cells, horizontal (= strings)
25             field $dot; # dot size, fraction of cell width
26             field $bsz; # barre size, fraction of dot
27             field $bstyle; # barre style ("line", "arc")
28             field $fsh; # show fingers (0, 1, "below")
29             field $fg; # foreground color
30             field $bg; # background color
31             field $fbp; # fret base position ("left", "right")
32             field $fbt; # fret base text ("%s" is default)
33              
34             ADJUST {
35             $config = $::config;
36             $ps = $pr->{ps};
37             $strings = $config->diagram_strings;
38             my $ctl = $ps->{diagrams};
39             $gw = $ctl->{width} || 6;
40             $gh = $ctl->{height} || 6;
41             $lw = ($ctl->{linewidth} || 0.10) * $gw;
42             $nutwidth = $ctl->{nutwidth} || 1;
43             $nw = ($nutwidth-1) * $lw;
44             $vc = $ctl->{vcells} || 4;
45             $hc = $strings;
46             $dot = $ctl->{dotsize} * ( $gh < $gw ? $gh : $gw );
47             $bsz = $ctl->{barwidth} * $dot;
48             $bstyle = $ctl->{barstyle} || "line";
49             $fsh = $ctl->{fingers} || 0;
50             $fbp = $ctl->{fretbaseposition} || "left";
51             $fbt = $ctl->{fretbasetext} || "%s";
52             $dcache = {} if $pr->{pdf} ne $pdf;
53             $pdf = $pr->{pdf};
54             }
55              
56 8     8   10541 use constant DIAG_DEBUG => 0;
  8         21  
  8         66880  
57              
58             # The vertical space the diagram requires.
59 24     24 0 51 method vsp0( $elt, $dummy = 0 ) {
  24         72  
  24         51  
  24         52  
  24         45  
60             $ps->{fonts}->{diagram}->{size} * $ps->{spacing}->{diagramchords}
61             + $nutwidth * $lw + 0.40 * $gw
62             + $vc * $gh
63 24 50       445 + ( $fsh eq "below" ? $ps->{fonts}->{diagram}->{size} : 0 )
64             ;
65             }
66              
67             # The advance height.
68 48     48 0 93 method vsp1( $elt, $dummy = 0 ) {
  48         125  
  48         124  
  48         89  
  48         80  
69 48         266 $ps->{diagrams}->{vspace} * $gh;
70             }
71              
72             # The vertical space the diagram requires, including advance height.
73 24     24 0 55 method vsp( $elt, $dummy = 0 ) {
  24         64  
  24         317  
  24         60  
  24         45  
74 24         95 $self->vsp0($elt) + $self->vsp1($elt);
75             }
76              
77             # The horizontal space the diagram requires.
78 24     24 0 67 method hsp0( $elt, $dummy = 0 ) {
  24         126  
  24         59  
  24         54  
  24         78  
79 24         110 ($strings - 1) * $gw;
80             }
81              
82             # The advance width.
83 24     24 0 45 method hsp1( $elt, $dummy = 0 ) {
  24         71  
  24         67  
  24         55  
  24         83  
84 24         127 $ps->{diagrams}->{hspace} * $gw;
85             }
86              
87             # The horizontal space the diagram requires, including advance width.
88 0     0 0 0 method hsp( $elt, $dummy = 0 ) {
  0         0  
  0         0  
  0         0  
  0         0  
89 0         0 $self->hsp0($elt) + $self->hsp1($elt);
90             }
91              
92             # The actual draw method.
93 48     48 0 119 method draw( $info, $x, $y, $dummy=0 ) {
  48         269  
  48         7989  
  48         101  
  48         105  
  48         108  
  48         84  
94 48 50       169 return unless $info;
95              
96 48         206 my $font = $ps->{fonts}->{diagram};
97              
98 48         191 my $xo = $self->diagram_xo($info);
99 48         430 my @bb = $xo->bbox;
100 48         2343 warn("BB [ @bb ] $x $y\n") if DIAG_DEBUG;
101             $pr->{pdfgfx}->object( $xo, $x,
102              
103 48         764 $y - ($font->{size} * $ps->{spacing}->{diagramchords} + $dot + $lw) );
104              
105             # Draw name.
106 48         25607 my $w = $gw * ($strings - 1);
107 48         562 $pr->setfont($font);
108 48         12432 my $name = $info->chord_display;
109             $name = "$name"
110 48 50       229 if $info->{diagram};
111 48         520 $pr->text( $name, $x + ($w - $pr->strwidth($name))/2,
112             $y-$pr->font_bl($font));#+$font->{fd}->{ascender}/1000 );
113             }
114              
115             # Returns the complete diagram as an xo. This includes the core grid,
116             # finger/fret positions, open and muted string indicators.
117             # The bounding box includes space form the open and muted string indicators
118             # and dots on the first and last strings, even when absent.
119             # The bbox includes basefret and fingers (below) if present.
120             # Origin is top left of the grid.
121             # Note that the chord name is not part of the diagram.
122              
123 48     48 0 102 method diagram_xo( $info ) {
  48         232  
  48         102  
  48         90  
124 48 50       168 return unless $info;
125 48   33     568 $fg = $info->{diagram} // $config->{pdf}->{theme}->{foreground};
126 48         160 $bg = $config->{pdf}->{theme}->{background};
127              
128             # Set default options for safety if they have not already been set
129 48 50       167 $fg = "black" if $fg eq "none";
130 48 50       195 $bg = "white" if $bg eq "none";
131              
132 48         115 my $x = 0;
133 48         199 my $w = $gw * ($strings - 1);
134 48   50     296 my $baselabeloffset = $info->{baselabeloffset} || 0;
135 48         153 my $basefretno = $info->{base} + $baselabeloffset;
136 48         142 my $basefrettext=""; # for base label
137 48         122 my $basefont; # for base label
138             my $basesize; # for base label
139              
140             # Get the core grid.
141 48         308 my $xg = $self->grid_xo;
142 48         385 my @xgbb = $xg->bbox;
143              
144 48         2245 my $xo = $pdf->xo_form;
145 48         24072 my @bb = ( 0,
146             0.77 * $dot + 2*$lw,
147             $w + $dot/2,
148             $xgbb[3] );
149              
150 48 50       223 if ( $basefretno > 1 ) {
151 0         0 $basefont = $ps->{fonts}->{diagram_base}->{fd}->{font};
152 0         0 $basesize = $gh/0.85;
153 0         0 my $basefretformat = $fbt;
154 0 0       0 $basefretformat = '%s' unless $basefretformat =~ /^[^%]*\%s[^%]*$/;
155 0         0 $basefrettext = sprintf($basefretformat, $basefretno);
156              
157 0 0       0 if ( $fbp eq "left" ) {
158 0         0 $bb[0] -= $basefont->width("xx$basefrettext") * $basesize;
159             }
160             else {
161             #fret base position on "right" side
162 0         0 $bb[0] -= $dot/2;
163 0         0 $bb[2] += $basefont->width("xx$basefrettext") * $basesize;
164             }
165             }
166             else {
167 48         185 $bb[0] -= $dot/2;
168             }
169 48 0 33     372 if ( $fsh eq "below" && $info->{fingers} ) {
170 0         0 $bb[3] -= $gh + $lw;
171             }
172 48         271 $xo->bbox(@bb);
173 48         4979 $xo->line_width($lw);
174 48         3124 $xo->stroke_color($fg);
175 48         9063 $xo->fill_color($fg);
176              
177 48         7423 if ( DIAG_DEBUG ) {
178             # Draw the grid.
179             $xo->save;
180             $xo->fill_color('yellow');
181             $xo->rectangle($xo->bbox)->fill;
182             $xo->object( $xg, 0, 0, 1 );
183             $xo->fill_color('red');
184             my $lw = $lw/2;
185             $xo->rectangle( -$lw, -$lw, $lw, $lw )->fill;
186             $xo->restore;
187             }
188             else {
189 48         311 $xo->object( $xg, 0, 0, 1 );
190             }
191              
192             # Draw extended nut if base = 1.
193 48 50       18691 if ( $info->{base} <= 1 ) {
194 48 50       187 if ( $nutwidth > 1 ) {
195 48         269 for ( 0 .. $nutwidth-2 ) {
196 192         15259 $xo->move( -$lw/2, -$_*$lw );
197 192         21303 $xo->hline( $w + $lw/2 );
198             }
199 48         5020 $xo->stroke;
200             }
201             }
202              
203             # Draw first fret number, if > 1.
204 48 50       1784 if ( $basefretno > 1 ) {
205 0         0 $xo->textstart;
206 0         0 $xo->font( $basefont, $basesize );
207              
208 0 0       0 if ( $fbp eq "left" ) {
209 0         0 $xo->translate( -$basefont->width("x") * 0.85 * $basesize,
210             -$nw - ($baselabeloffset+0.85)*$gh );
211 0         0 $xo->text( $basefrettext, align => "right" );
212             }
213             else {
214             #fret base position on "right" side
215 0         0 $xo->translate( ($strings-1)*$gw + $basefont->width("x") * 0.85 * $basesize,
216             -$nw - ($baselabeloffset+0.85)*$gh );
217 0         0 $xo->text( $basefrettext, align => "left" );
218             }
219              
220 0         0 $xo->textend;
221             }
222              
223 48         148 my $fingers;
224 48 50       310 $fingers = $info->{fingers} if $fsh;
225              
226             # Bar detection.
227 48         162 my $bar = {};
228 48 50       170 if ( $fingers ) {
229 48         112 my %h;
230 48         113 my $str = 0;
231 48         117 my $got = 0;
232              
233 48         109 foreach ( @{ $fingers } ) {
  48         181  
234 0 0       0 $str++, next unless $info->{frets}->[$str] > 0;
235 0 0       0 if ( $bar->{$_} ) {
236             # Same finger on multiple strings -> bar.
237 0         0 $got++;
238 0         0 $bar->{$_}->[-1] = $str;
239             }
240             else {
241             # Register.
242 0         0 $bar->{$_} = [ $_, $info->{frets}->[$str], $str, $str ];
243             }
244 0         0 $str++;
245             }
246              
247 48 50       201 if ( $got ) {
248 0         0 $xo->save;
249            
250 0 0       0 if ( $bstyle eq "line" ) {
251 0         0 $xo->line_width($bsz)->line_cap(0);
252             }
253             else {
254             # bar in "arc" style.
255 0         0 $xo->line_width($lw+0.2);
256             }
257              
258 0         0 foreach ( sort keys %$bar ) {
259 0         0 my @bi = @{ $bar->{$_} };
  0         0  
260             # $bi array description = [finger, fret, first_string, last_string].
261              
262 0 0       0 if ( $bi[-2] == $bi[-1] ) { # not a bar
263 0         0 delete $bar->{$_};
264 0         0 next;
265             }
266              
267 0 0       0 if ( $bstyle eq "line" ) {
268             # Print the bar line.
269 0         0 $x = $bi[2]*$gw;
270 0         0 $xo->move( $x, -$nw -$bi[1]*$gh+$gh/2 );
271 0         0 $xo->hline( $x+($bi[3]-$bi[2])*$gw);
272             }
273             else {
274             # Print arcs for barre
275 0         0 my $arcw = (($bi[3]-$bi[2])*$gw + 0.7*$gw)/2;
276 0         0 my $arch = 0.4*$gw;
277 0         0 my $arcy = -$nw -$bi[1]*$gh +$gh+0.25*$gh;
278 0         0 my $arcx = $bi[2]*$gw - (0.7*$gw)/2;
279              
280 0 0       0 if ( $bi[1] == 1 ) {
281             # Bar is on the first fret so bar arcs
282             # must be drawn above the nut.
283 0         0 $arcy += $nw;
284             }
285              
286             # Draw first arc.
287 0         0 $xo->move( $arcx, $arcy );
288 0         0 $xo->arc( $arcx+$arcw, $arcy, $arcw, $arch, 180, 0 );
289              
290             # Draw second arc a little higher, this is
291             # a fast way to have narrower corners look at the arc edge.
292 0         0 $xo->move( $arcx, $arcy-0.8 );
293 0         0 $xo->arc( $arcx+$arcw, $arcy-0.8, $arcw, $arch, 180, 0 );
294             }
295              
296 0         0 $xo->stroke;
297 0         0 $xo->fill;
298             }
299 0         0 $xo->stroke->restore;
300             }
301             }
302              
303 48         112 my $oflo; # to detect out of range frets
304              
305             # Color of the dots and numbers.
306 48         133 my $fbg = ""; # numbers
307 48         150 my $ffg = $fg; # dots
308             # The numbercolor property of the chordfingers is used for the
309             # color of the dot numbers.
310 48         245 my $fcf = $ps->{fonts}->{chordfingers};
311 48         431 $fbg = $pr->_bgcolor($fcf->{numbercolor});
312 48         241 $ffg = $pr->_bgcolor($fcf->{color});
313              
314 48 50       259 if ( $fsh ne "below" ) {
315             # However, if none we should really use "background" color.
316 48 50       212 $fbg = $bg if $fbg eq "none";
317             }
318             else {
319             # However, for "below" case if none or numbercolor equals background color we should really use "foreground".
320 0 0 0     0 $fbg = $fg if ( $fbg eq "none") || ( $fbg eq $bg );
321             }
322              
323 48         158 $x = -$gw;
324 48         194 for my $sx ( 0 .. $strings-1 ) {
325 288         763608 $x += $gw;
326 288         1230 my $fret = $info->{frets}->[$sx];
327 288         583 my $fing = -1;
328 288 50 50     1761 $fing = $fingers->[$sx] // -1 if $fingers;
329              
330             # For bars in "line" style, only the first and last finger.
331 288 50 33     1965 if ( $fing && $bar->{$fing} && $bstyle eq "line" ) {
      33        
332 0 0 0     0 next unless $sx == $bar->{$fing}->[2] || $sx == $bar->{$fing}->[3];
333             }
334              
335 288 100       1092 if ( $fret > 0 ) {
    100          
    50          
336 192 50 33     634 if ( $fret > $vc && !$oflo++ ) {
337 0         0 warn("Diagram $info->{name}: ",
338             "Fret position $fret exceeds diagram size $vc\n");
339 0         0 next;
340             }
341 192         909 $xo->fill_color($ffg);
342 192         41213 $xo->circle( $x, -$nw - ($fret-0.5)*$gh, $dot/2 )->fill;
343              
344             }
345             elsif ( $fret < 0 ) {
346 48         379 $xo->move( $x - $dot/3, 0.77 * $dot + $lw );
347 48         6402 $xo->line( $x + $dot/3, 0.1 * $gh + $lw );
348 48         5599 $xo->move( $x + $dot/3, 0.77 * $dot + $lw );
349 48         5578 $xo->line( $x - $dot/3, 0.1 * $gh + $lw );
350 48         5053 $xo->stroke;
351             }
352             elsif ( $info->{base} > 0 ) {
353 48         409 $xo->circle( $x, 3.5*$gh/10 + $lw, $dot/3 )->stroke;
354             }
355             }
356              
357             # Show the fingers, if any.
358 48 50 33     189509 if ( $fingers && @$fingers ) {
359 0         0 my ( $font, $size );
360 0         0 $font = "chordfingers";
361 0         0 $size = $dot;
362 0 0       0 if ( $fsh eq "below" ) {
363 0   0     0 $size = $ps->{fonts}->{$font}->{size} // "00";
364 0 0       0 $size = $dot if $size <= 0;
365             }
366 0         0 $font = $ps->{fonts}->{$font}->{fd}->{font};
367 0         0 warn("XXX ", $font->{' data'}->{fontname}, " $size\n") if DIAG_DEBUG;
368              
369 0         0 $x = -$gw;
370 0         0 my $did = 0;
371 0         0 for my $sx ( 0 .. $strings-1 ) {
372             #when "below", chord fingers should be always drawn and not take into account the dot color
373 0 0 0     0 last if ( $fsh ne "below" ) && ( $fbg eq $ffg );
374 0         0 $x += $gw;
375 0         0 my $fret = $info->{frets}->[$sx];
376 0 0       0 next unless $fret > 0;
377 0         0 my $fing = uc $fingers->[$sx];
378 0 0       0 next unless $fing =~ /^[1-9A-Z]$/;
379              
380             # For barre, only the first and last finger.
381 0 0 0     0 if ( $bar->{$fing} && $fsh ne "below" ) {
382             next unless ( $sx == $bar->{$fing}->[2]
383 0 0 0     0 || $sx == $bar->{$fing}->[3] );
384             }
385              
386 0 0       0 unless ( $did++ ) {
387 0 0       0 if ( $fsh eq "below" ) {
388 0         0 $size *= 1.4;
389             }
390 0         0 $xo->fill_color($fbg);
391 0         0 $xo->textstart;
392 0         0 $xo->font( $font, $size );
393             }
394 0 0       0 if ( $fsh eq "below" ) {
395 0         0 $xo->translate( $x, -$nw - $lw - ($vc+1)*$gh );
396             }
397             else {
398 0         0 $xo->translate( $x, -$nw - ($fret-0.5)*$gh - $dot/3 );
399             }
400 0         0 $xo->text( $fing, align => "center" );
401             }
402 0 0       0 $xo->textend if $did;
403             }
404              
405 48         516 return $xo;
406             }
407              
408             # The core grid. Just the horizontal and vertical lines, with a fitting
409             # bounding box. Origin is (half linewidth from) top left.
410             # Grid objects are cached globally.
411              
412 8     8   117 use constant DIAG_GRID_XO => 0;
  8         24  
  8         13076  
413              
414             method grid_xo {
415              
416             return $dcache->{$gw,$gh,$lw,$nw,$fg,$vc,$hc} //= do {
417              
418             my $w = $gw * ($hc - 1);
419             my $h = $gh * $vc;
420              
421             my $xo = $pdf->xo_form;
422              
423             # Bounding box must take linewidth into account.
424             # Origin is top left, so y runs negative.
425             my $xp = DIAG_GRID_XO ? 2 : 0;
426             my @bb = ( -$lw/2 - $xp, $lw/2 + $xp,
427             $w + $lw/2 + $xp, -($vc * $gh + $nw) - $lw/2 - $xp );
428             $xo->bbox(@bb);
429             $xo->line_width($lw);
430              
431             if ( DIAG_GRID_XO ) {
432             # Draw the grid.
433             $xo->fill_color('yellow');
434             $xo->rectangle(@bb)->fill;
435              
436             # Draw additional nuts.
437             if ( 0&& $nutwidth > 1 ) {
438             for ( 0 .. $nutwidth-2 ) {
439             $xo->stroke_color( $_ % 2 ? "#c0c0c0" : "#e0e0e0" );
440             $xo->move( -$lw/2, -$_*$lw );
441             $xo->hline( $w + $lw/2 )->stroke;
442             }
443             }
444             }
445              
446             $xo->stroke_color($fg);
447             for ( 0 .. $vc ) {
448             $xo->move( -$lw/2, -$_*$gh - $nw);
449             $xo->hline( $w + $lw/2 );
450             }
451             for ( 0 .. $hc-1 ) {
452             $xo->move( $_*$gw, $lw/2 );
453             $xo->vline( -($gh*$vc + $nw + $lw/2 ) );
454             }
455             $xo->stroke;
456              
457             if ( DIAG_GRID_XO ) {
458             # Show origin.
459             $xo->fill_color('red');
460             my $lw = $lw/2;
461             $xo->rectangle( -$lw, -$lw, $lw, $lw )->fill;
462             }
463              
464             $xo;
465             };
466             }
467              
468             1;