File Coverage

blib/lib/Text/Layout/PDFAPI2.pm
Criterion Covered Total %
statement 279 590 47.2
branch 72 262 27.4
condition 53 259 20.4
subroutine 19 33 57.5
pod 2 14 14.2
total 425 1158 36.7


line stmt bran cond sub pod time code
1             #! perl
2              
3 5     5   718413 use strict;
  5         14  
  5         237  
4 5     5   29 use warnings;
  5         17  
  5         337  
5 5     5   28 use utf8;
  5         10  
  5         38  
6              
7             package Text::Layout::PDFAPI2;
8              
9 5     5   935 use parent 'Text::Layout';
  5         500  
  5         41  
10 5     5   379 use Carp;
  5         11  
  5         405  
11 5     5   41 use List::Util qw(max);
  5         9  
  5         31135  
12              
13             my $hb;
14             my $fc;
15              
16             #### API
17             sub new {
18 3     3 1 23 my ( $pkg, @data ) = @_;
19 3 50 33     48 unless ( @data == 1 && ref($data[0]) =~ /^PDF::(API2|Builder)\b/ ) {
20 0         0 croak("Usage: Text::Layout::PDFAPI2->new(\$pdf)");
21             }
22 3         66 my $self = $pkg->SUPER::new;
23 3         28 $self->{_context} = $data[0];
24 3 50 33     21 if ( !$fc || $fc->{__PDF__} ne $data[0] ) {
25             # Init cache.
26 3         10 $fc = { __PDF__ => $data[0] };
27 3         32 Text::Layout::FontConfig->reset;
28             }
29 3         1907 require Text::Layout::PDFAPI2::ImageElement;
30 3 50       53 $self->register_element
31             ( Text::Layout::PDFAPI2::ImageElement->new( pdf => $data[0] ), "img" )
32             unless $self->get_element_handler("img");
33              
34 3         19 $self;
35             }
36              
37             sub _pdf {
38 0     0   0 my ( $self ) = @_;
39 0         0 $self->{_context};
40             }
41              
42             # Creates a (singleton) HarfBuzz::Shaper object.
43             sub _hb_init {
44 0 0   0   0 return $hb if defined $hb;
45 0         0 $hb = 0;
46 0         0 eval {
47 0         0 require HarfBuzz::Shaper;
48 0         0 $hb = HarfBuzz::Shaper->new;
49             };
50 0         0 return $hb;
51             }
52              
53             # Verify if a font needs shaping, and we can do that.
54             sub _hb_font_check {
55 5     5   14 my ( $f ) = @_;
56 5 100       63 return $f->{_hb_checked} if defined $f->{_hb_checked};
57              
58 2 50       15 if ( $f->get_shaping ) {
59 0         0 my $fn = $f->to_string;
60 0 0       0 if ( $f->{font}->can("fontfilename") ) {
61 0 0       0 if ( _hb_init() ) {
62             # warn("Font $fn will use shaping.\n");
63 0         0 return $f->{_hb_checked} = 1;
64             }
65 0         0 carp("Font $fn: Requires shaping but HarfBuzz cannot be loaded.");
66             }
67             else {
68 0         0 carp("Font $fn: Shaping not supported");
69             }
70             }
71             else {
72             # warn("Font ", $f->to_string, " does not need shaping.\n");
73             }
74 2         14 return $f->{_hb_checked} = 0;
75             }
76              
77             #### API
78             sub render {
79 1     1 0 5 my ( $self, $x, $y, $text, $fp ) = @_;
80              
81 1         6 $self->{_lastx} = $x;
82 1         3 $self->{_lasty} = $y;
83              
84 1         5 my @bb = $self->get_pixel_bbox;
85 1         3 my $bl = $bb[0];
86 1   50     26 my $align = $self->{_alignment} // 0;
87 1 50       5 if ( $self->{_width} ) {
88 0         0 my $w = $bb[3];
89 0 0       0 if ( $w < $self->{_width} ) {
90 0 0       0 if ( $align eq "right" ) {
    0          
91 0         0 $x += $self->{_width} - $w;
92             }
93             elsif ( $align eq "center" ) {
94 0         0 $x += ( $self->{_width} - $w ) / 2;
95             }
96             else {
97 0         0 $x += $bb[1];
98             }
99             }
100             }
101 1         3 my $upem = 1000; # as per PDF::API2
102              
103             my $draw_bg = sub {
104 0     0   0 my ( $fx, $nfx, $x, $y, $w ) = @_;
105 0         0 my $h = $bb[2]; # "border"
106 0         0 my $d = abs($h)/25;
107              
108             # If first with background, extend a little to the left.
109 0 0 0     0 if ( $fx == 0 || !$self->{_content}->[$fx-1]->{bgcolor} ) {
110 0         0 $x -= $d;
111 0         0 $w += $d;
112             }
113             # If last with background, extend a little to the right.
114 0 0 0     0 if ( $fx == $nfx-1 || !$self->{_content}->[$fx+1]->{bgcolor} ) {
115 0         0 $w += 2*$d;
116             }
117              
118             # If next is a strut, followed by same bg color,
119             # have the background span the strut.
120             #### TODO: Span multiple struts.
121 0         0 my $delta;
122 0         0 for ( my $i = $fx+1; $i < $nfx; $i++ ) {
123 0 0 0     0 if ( $self->{_content}->[$i]->{type} eq "strut" ) {
    0 0        
124 0   0     0 $delta //= 0;
125 0         0 $delta += $self->{_content}->[$i]->{width};
126             }
127             elsif ( defined($delta)
128             && $self->{_content}->[$i]->{bgcolor}
129             && $self->{_content}->[$i]->{bgcolor}
130             eq $self->{_content}->[$fx]->{bgcolor} ) {
131 0         0 $w += $delta;
132 0         0 last;
133             }
134             }
135             # Draw the background.
136 0         0 $text->textend;
137 0         0 my $gfx = $text; # sanity
138 0         0 $gfx->save;
139 0         0 $gfx->fillcolor( $self->{_content}->[$fx]->{bgcolor} );
140 0         0 $gfx->linewidth(0);
141 0         0 $gfx->rectangle( $x, $y+$d, $x+$w, $y+$h-$d );
142 0         0 $text->fill;
143 0         0 $gfx->restore;
144 0         0 $text->textstart;
145 1         10 };
146              
147 1         2 my $nfx = @{ $self->{_content} };
  1         4  
148 1         17 for ( my $fx = 0; $fx < $nfx; $fx++ ) {
149 1         4 my $fragment = $self->{_content}->[$fx];
150              
151 1 50       11 if ( $fragment->{type} eq "strut" ) {
    50          
152 0         0 $x += $fragment->{width};
153 0 0       0 if ( length($fragment->{label}) ) {
154 0         0 my $pdf = $text->{' api'};
155 0         0 my $page = $text->{' apipage'};
156 0         0 my $target = $fragment->{label};
157              
158             # Augmented API for apps that keep track of bookmarks.
159 0         0 my $c = $pdf->can("named_dest_fiddle");
160 0 0       0 $target = $pdf->$c($target) if $c;
161 0         0 $c = $pdf->can("named_dest_register");
162 0         0 $pdf->$c( $target, $page );
163              
164 0         0 $c = ref($pdf) . '::NamedDestination';
165 0         0 my $dest = $c->new($pdf);
166             $dest->goto( $page,
167             xyz => ( $x - $fragment->{width},
168             $y + $self->{_currentsize},
169 0         0 undef ) );
170 0         0 $pdf->named_destination( 'Dests', $target, $dest );
171             }
172             }
173              
174             elsif ( my $hd = $self->get_element_handler($fragment->{type}) ) {
175 0         0 $text->textend;
176 0         0 my $ab = $hd->render($fragment, $text, $x, $y-$bl)->{abox};
177 0         0 $text->textstart;
178 0         0 $x += $ab->[2];
179             }
180 1 50 33     9 next unless $fragment->{type} eq "text" && length($fragment->{text});
181              
182 1         3 my $x0 = $x;
183 1         3 my $y0 = $y;
184 1         2 my $f = $fragment->{font};
185 1         7 my $font = $f->get_font($self);
186 1 50       5 unless ( $font ) {
187 0         0 carp("Can't happen?");
188 0         0 $f = $self->{_currentfont};
189 0         0 $font = $f->getfont($self);
190             }
191 1         23 $text->strokecolor( $fragment->{color} );
192 1         272 $text->fillcolor( $fragment->{color} );
193 1   33     188 $text->font( $font, $fragment->{size} || $self->{_currentsize} );
194              
195 1 50       373 if ( _hb_font_check($f) ) {
196 0         0 $hb->set_font( $font->fontfilename );
197 0   0     0 $hb->set_size( $fragment->{size} || $self->{_currentsize} );
198 0         0 $hb->set_text( $fragment->{text} );
199 0 0       0 $hb->set_direction( $f->{direction} ) if $f->{direction};
200 0 0       0 $hb->set_language( $f->{language} ) if $f->{language};
201 0         0 my $info = $hb->shaper($fp);
202 0         0 my $y = $y - $fragment->{base} - $bl;
203 0   0     0 my $sz = $fragment->{size} || $self->{_currentsize};
204 0         0 my $w = 0;
205 0         0 $w += $_->{ax} for @$info;
206              
207 0 0       0 if ( $fragment->{bgcolor} ) {
208 0         0 $draw_bg->( $fx, $nfx, $x0, $y0, $w );
209             }
210              
211 0         0 foreach my $g ( @$info ) {
212 0         0 $text->translate( $x + $g->{dx}, $y - $g->{dy} );
213 0         0 $text->glyph_by_CId( $g->{g} );
214 0         0 $x += $g->{ax};
215 0         0 $y += $g->{ay};
216             }
217             }
218             else {
219             printf("%.2f %.2f %.2f \"%s\" %s\n",
220             $x, $y-$fragment->{base}-$bl,
221             $font->width($fragment->{text}) * ($fragment->{size} || $self->{_currentsize}),
222             $fragment->{text},
223             join(" ", $fragment->{font}->{family},
224             $fragment->{font}->{style},
225             $fragment->{font}->{weight},
226             $fragment->{size} || $self->{_currentsize},
227             $fragment->{color},
228             $fragment->{underline}||'""', $fragment->{underline_color}||'""',
229 1         2 $fragment->{strikethrough}||'""', $fragment->{strikethrough_color}||'""',
230             ),
231             ) if 0;
232 1         22 my $t = $fragment->{text};
233 1 50       7 if ( $t ne "" ) {
234              
235             # See ChordPro issue 240.
236 1 50 33     7 if ( $font->issymbol && $font->is_standard ) {
237             # This enables byte access to these symbol fonts.
238 0         0 utf8::downgrade( $t, 1 );
239             }
240              
241 1         16 my $y = $y-$fragment->{base}-$bl;
242 1   33     5 my $sz = $fragment->{size} || $self->{_currentsize};
243 1         6 my $w = $font->width($t) * $sz;
244              
245 1 50       414 if ( $fragment->{bgcolor} ) {
246 0         0 $draw_bg->( $fx, $nfx, $x0, $y0, $w );
247             }
248              
249 1         8 $text->font( $f->get_font, $sz );
250 1         229 $text->translate( $x, $y );
251 1         744 $text->text($t);
252 1         734 $x += $w;
253             }
254             }
255              
256 1 50       6 next unless $x > $x0;
257             # While PDF::API2 delivers font metrics in 1/1000s,
258             # underlinethickness and position are unscaled UPEM.
259 1   50     5 my $dw = $font->data->{upem} // 1000;
260              
261 1         12 my @strikes;
262 1 50 33     6 if ( $fragment->{underline} && $fragment->{underline} ne 'none' ) {
263 0   0     0 my $sz = $fragment->{size} || $self->{_currentsize};
264             my $d = -( $f->{underline_position}
265 0   0     0 || $font->underlineposition ) * $sz/$dw;
266             my $h = ( $f->{underline_thickness}
267 0   0     0 || $font->underlinethickness ) * $sz/$dw;
268 0   0     0 my $col = $fragment->{underline_color} // $fragment->{color};
269 0 0       0 if ( $fragment->{underline} eq 'double' ) {
270 0         0 push( @strikes, [ $d-0.125*$h, $h * 0.75, $col ],
271             [ $d+1.125*$h, $h * 0.75, $col ] );
272             }
273             else {
274 0         0 push( @strikes, [ $d+$h/2, $h, $col ] );
275             }
276             }
277              
278 1 50       4 if ( $fragment->{strikethrough} ) {
279 0   0     0 my $sz = $fragment->{size} || $self->{_currentsize};
280 0         0 my $xh = $font->xheight / 1000;
281             my $d = -( $f->{strikeline_position}
282 0 0       0 ? $f->{strikeline_position} / $dw
283             : 0.6*$xh ) * $sz;
284             my $h = ( $f->{strikeline_thickness}
285             || $f->{underline_thickness}
286 0   0     0 || $font->underlinethickness ) * $sz/$dw;
287 0   0     0 my $col = $fragment->{strikethrough_color} // $fragment->{color};
288 0         0 push( @strikes, [ $d+$h/2, $h, $col ] );
289             }
290              
291 1 50 33     6 if ( $fragment->{overline} && $fragment->{overline} ne 'none' ) {
292 0   0     0 my $sz = $fragment->{size} || $self->{_currentsize};
293 0         0 my $xh = $font->xheight / 1000;
294             my $h = ( $f->{overline_thickness}
295             || $f->{underline_thickness}
296 0   0     0 || $font->underlinethickness ) * $sz/$dw;
297             my $d = -( $f->{overline_position}
298 0 0       0 ? $f->{overline_position} * $sz/$dw
299             : $xh*$sz + 2*$h );
300 0   0     0 my $col = $fragment->{overline_color} // $fragment->{color};
301 0 0       0 if ( $fragment->{overline} eq 'double' ) {
302 0         0 push( @strikes, [ $d-0.125*$h, $h * 0.75, $col ],
303             [ $d+1.125*$h, $h * 0.75, $col ] );
304             }
305             else {
306 0         0 push( @strikes, [ $d+$h/2, $h, $col ] );
307             }
308             }
309 1 50       4 $text->textend if @strikes;
310 1         4 for ( @strikes ) {
311 0         0 my $gfx = $text; # prevent mental insanity
312 0         0 $gfx->save;
313 0         0 $gfx->strokecolor($_->[2]);
314 0         0 $gfx->linewidth($_->[1]);
315 0         0 $gfx->move( $x0, $y0-$fragment->{base}-$bl-$_->[0] );
316 0         0 $gfx->line( $x, $y0-$fragment->{base}-$bl-$_->[0] );
317 0         0 $gfx->stroke;
318 0         0 $gfx->restore;
319             }
320 1 50       4 $text->textstart if @strikes;
321              
322 1 50       142 if ( $fragment->{href} ) {
323 0   0     0 my $sz = $fragment->{size} || $self->{_currentsize};
324 0         0 my $ann = $text->{' apipage'}->annotation;
325 0         0 my $target = $fragment->{href};
326              
327 0 0       0 if ( $target =~ /^#(.+)/ ) { # named destination
    0          
328             # Augmented API for apps that keep track of bookmarks.
329 0         0 my $pdf = $text->{' api'};
330 0 0       0 if ( my $c = $pdf->can("named_dest_fiddle") ) {
331 0         0 $target = $pdf->$c($1);
332             }
333              
334 0         0 $ann->link($target);
335             }
336             # Named destination in other PDF.
337             elsif ( $target =~ /^(?!\w{3,}:)(.*)\#(.+)$/ ) {
338 0         0 $ann->pdf( $1, "/$2" );
339             }
340             # Arbitrary document.
341             else {
342 0         0 $ann->uri($target);
343             }
344             # $ann->border( 0, 0, 1 );
345 0         0 $ann->rect( $x0, $y0, $x, $y0 + $bb[2] );
346             }
347             }
348             }
349              
350             #### API
351             sub bbox {
352 4     4 0 15 my ( $self, $all ) = @_;
353              
354 4         18 my ( $bl, $x, $y, $w, $h ) = (0) x 4;
355 4         12 my ( $d, $a ) = (0) x 2;
356 4         16 my ( $xMin, $xMax, $yMin, $yMax );
357 4         0 my $dir;
358 4         14 $self->{_struts} = [];
359              
360 4         11 foreach ( @{ $self->{_content} } ) {
  4         15  
361              
362             0&&
363             warn("IB: ",
364             join(", ",
365 6         12 map { defined($_) ? sprintf("%.2f", $_) : "" }
366             $xMin, $yMin, $xMax, $yMax ), "\n");
367              
368 6 100       37 if ( $_->{type} eq "strut" ) {
    50          
369             my @ab = ( 0, -($_->{desc}//0),
370 2   50     43 $_->{width}//0, $_->{asc}//0 );
      50        
      50        
371 2         14 my %s = %$_;
372 2         6 delete($s{type});
373 2         6 $s{_x} = $w;
374 2         6 $s{_strut} = $_;
375 2         3 push( @{ $self->{_struts} }, \%s );
  2         8  
376             # Add to bbox but not to inkbox.
377 2         4 $w += $ab[2];
378 2 50       7 $a = $ab[3] if $ab[3] > $a;
379 2 50       8 $d = $ab[1] if $ab[1] < $d;
380             }
381              
382             elsif ( my $hd = $self->get_element_handler($_->{type}) ) {
383 0         0 my @ab = @{$hd->bbox($_)->{abox}};
  0         0  
384 0 0 0     0 $xMin //= $w + $ab[0] if $all;
385 0         0 $xMax = $w + $ab[2];
386 0         0 $w += $ab[2];
387 0 0       0 $a = $ab[3] if $ab[3] > $a;
388 0 0       0 $d = $ab[1] if $ab[1] < $d;
389 0 0       0 if ( $all ) {
390 0 0 0     0 $yMin = $ab[1] if !defined($yMin) || $ab[1] < $yMin;
391 0 0 0     0 $yMax = $ab[3] if !defined($yMax) || $ab[3] > $yMax;
392             }
393             }
394              
395 6 100 66     39 next unless $_->{type} eq "text" && length($_->{text});
396              
397 4         11 my $f = $_->{font};
398 4         26 my $font = $f->get_font($self);
399 4 50       14 unless ( $font ) {
400 0         0 carp("Can't happen?");
401 0         0 $f = $self->{_currentfont};
402 0         0 $font = $f->getfont($self);
403             }
404 4         9 my $upem = 1000; # as delivered by PDF::API2
405 4         13 my $size = $_->{size};
406 4         10 my $base = $_->{base};
407 4   50     24 my $mydir = $f->{direction} || 'ltr';
408              
409             # Width and inkbox, if requested.
410 4 50 66     16 if ( _hb_font_check( $f ) ) {
    50          
411 0         0 $hb->set_font( $font->fontfilename );
412 0         0 $hb->set_size($size);
413 0 0       0 $hb->set_language( $f->{language} ) if $f->{language};
414 0 0       0 $hb->set_direction( $f->{direction} ) if $f->{direction};
415 0         0 $hb->set_text( $_->{text} );
416 0         0 my $info = $hb->shaper;
417 0         0 $mydir = $hb->get_direction;
418             # warn("mydir $mydir\n");
419              
420 0 0       0 if ( $all ) {
421 0         0 my $ext = $hb->get_extents;
422 0         0 foreach my $g ( @$info ) {
423 0         0 my $e = shift(@$ext);
424             printf STDERR ( "G %3d %6.2f %6.2f %6.2f %6.2f %6.2f\n",
425             $g->{g}, $g->{ax},
426 0         0 @$e{ qw( x_bearing y_bearing width height ) } ) if 0;
427             # It is easier to work with the baseline oriented box.
428 0         0 $e->{xMin} = $e->{x_bearing};
429 0         0 $e->{yMin} = $e->{y_bearing} + $e->{height} - $base;
430 0         0 $e->{xMax} = $e->{x_bearing} + $e->{width};
431 0         0 $e->{yMax} = $e->{y_bearing} - $base;
432              
433 0 0 0     0 $xMin //= $w + $e->{xMin} if $e->{width};
434             $yMin = $e->{yMin}
435 0 0 0     0 if !defined($yMin) || $e->{yMin} < $yMin;
436             $yMax = $e->{yMax}
437 0 0 0     0 if !defined($yMax) || $e->{yMax} > $yMax;
438 0         0 $xMax = $w + $e->{xMax};
439 0         0 $w += $g->{ax};
440             }
441             }
442             else {
443 0         0 foreach my $g ( @$info ) {
444 0         0 $w += $g->{ax};
445             }
446             }
447             }
448             elsif ( $all && $font->can("extents") ) {
449 0         0 my $e = $font->extents( $_->{text}, $size );
450 0 0 0     0 printf STDERR ("(%.2f,%.2f)(%.2f,%.2f) -> ",
      0        
      0        
      0        
      0        
451             $xMin//0, $yMin//0, $xMax//0, $yMax//0 ) if $all && 0;
452 0 0       0 $xMax = $w + $e->{xMax} if $all;
453 0         0 $w += $e->{wx};
454             # warn("W \"", $_->{text}, "\" $w, ", $e->{width}, "\n");
455 0 0       0 if ( $all ) {
456 0         0 $_ -= $base for $e->{yMin}, $e->{yMax};
457             # Baseline oriented box.
458 0   0     0 $xMin //= $w - $e->{wx} + $e->{xMin};
459             $yMin = $e->{yMin}
460 0 0 0     0 if !defined($yMin) || $e->{yMin} < $yMin;
461             $yMax = $e->{yMax}
462 0 0 0     0 if !defined($yMax) || $e->{yMax} > $yMax;
463 0         0 printf STDERR ("(%.2f,%.2f)(%.2f,%.2f)\n",
464             $xMin//0, $yMin//0, $xMax//0, $yMax//0 ) if 0;
465             }
466             }
467             else {
468 4         29 $w += $font->width( $_->{text} ) * $size;
469             }
470              
471             # We have width. Now the rest of the layoutbox.
472 4         1561 my ( $d0, $a0 );
473 4         26 $d0 = $f->get_descender * $size / $upem - $base;
474 4         16 $a0 = $f->get_ascender * $size / $upem - $base;
475             # Keep track of biggest decender/ascender.
476 4 50       30 $d = $d0 if $d0 < $d;
477 4 50       14 $a = $a0 if $a0 > $a;
478              
479             # Direction.
480 4   33     23 $dir //= $mydir;
481 4 50       19 $dir = 0 unless $dir eq $mydir; # mix
482             }
483 4         11 $bl = $a;
484 4         8 $h = $a - $d;
485              
486 4         12 my $align = $self->{_alignment};
487             # warn("ALIGN: ", $align//"","\n");
488 4 0 33     16 if ( $self->{_width} && $dir && $w < $self->{_width} ) {
      33        
489 0 0 0     0 if ( $dir eq 'rtl' && (!$align || $align eq "left") ) {
      0        
490 0         0 $align = "right";
491             # warn("ALIGN: set to $align\n");
492             }
493             }
494 4 0 33     27 if ( $self->{_width} && $align && $w < $self->{_width} ) {
      33        
495             # warn("ALIGNING...\n");
496 0 0       0 if ( $align eq "right" ) {
    0          
497             # warn("ALIGNING: to $align\n");
498 0         0 $x += my $d = $self->{_width} - $w;
499 0 0       0 $xMin += $d if defined $xMin;
500 0 0       0 $xMax += $d if defined $xMax;
501             }
502             elsif ( $align eq "center" ) {
503             # warn("ALIGNING: to $align\n");
504 0         0 $x += my $d = ( $self->{_width} - $w ) / 2;
505 0 0       0 $xMin += $d if defined $xMin;
506 0 0       0 $xMax += $d if defined $xMax;
507             }
508             }
509              
510 4 50       38 [ $bl, $x, $y-$h, $w, $h,
511             defined $xMin ? ( $xMin, $yMin-$bl, $xMax-$xMin, $yMax-$yMin ) : ()];
512             }
513              
514             #### API
515             sub load_font {
516 3     3 0 10 my ( $self, $font, $fd ) = @_;
517              
518 3 50       14 if ( my $f = $fc->{$font} ) {
519             # warn("Loaded font $font (cached)\n");
520 0   0     0 $fd->{ascender} //= $f->ascender;
521 0   0     0 $fd->{descender} //= $f->descender;
522 0         0 return $f;
523             }
524 3         5 my $ff;
525 3   33     17 my $actual = Text::Layout::FontConfig->remap($font) // $font;
526 3 50       21 if ( $actual =~ /\.[ot]tf$/ ) {
    50          
527 0         0 eval {
528             $ff = $self->{_context}->ttfont( $actual,
529             -dokern => 1,
530             $fd->{nosubset}
531 0 0       0 ? ( -nosubset => 1 )
532             : ( -nosubset => 0 ),
533             );
534             };
535             }
536             elsif ( $actual =~ /(.*\.ttc)(?::(.*))?$/ ) {
537             # This requires PDF::API2 augmentations. See below.
538 0         0 my $file = $1;
539 0   0     0 my $sel = $2 // "";
540 0         0 eval {
541 0         0 my $ttc = $self->{_context}->ttc($file);
542             $ff = $self->{_context}->ttcfont( $ttc,
543             font => $sel,
544             -dokern => 1,
545             $fd->{nosubset}
546 0 0       0 ? ( -nosubset => 1 )
547             : ( -nosubset => 0 ),
548             );
549             };
550             }
551             else {
552 3         7 eval {
553 3         28 $ff = $self->{_context}->corefont( $actual, -dokern => 1 );
554             };
555             }
556              
557 3 0       136781 croak( "Cannot load font: ", $actual,
    50          
558             $actual ne $font ? " (remapped from $font)" : "",
559             "\n", $@ ) unless $ff;
560             # warn("Loaded font: $font\n");
561 3         16 $self->{font} = $ff;
562 3   33     85 $fd->{ascender} //= $ff->ascender;
563 3   33     57 $fd->{descender} //= $ff->descender;
564 3         28 $fc->{$font} = $ff;
565 3         31 return $ff;
566             }
567              
568             sub xheight {
569 0     0 0 0 $_[0]->data->{xheight};
570             }
571              
572             sub bbextend {
573 0     0 0 0 my ( $cur, $bb, $dx, $dy ) = @_;
574 0   0     0 $dx //= 0;
575 0   0     0 $dy //= 0;
576 0 0       0 if ( defined $cur->[0] ) {
577 0         0 $dx += $cur->[2];
578 0         0 $dy += $cur->[3];
579 0 0       0 $cur->[0] = $bb->[0] + $dx if $cur->[0] > $bb->[0] + $dx;
580 0 0       0 $cur->[1] = $bb->[1] + $dy if $cur->[1] > $bb->[1] + $dy;
581 0 0       0 $cur->[2] = $bb->[2] + $dx if $cur->[2] < $bb->[2] + $dx;
582 0 0       0 $cur->[3] = $bb->[3] + $dy if $cur->[3] < $bb->[3] + $dy;
583             }
584             else {
585 0         0 $cur->[0] = $bb->[0] + $dx;
586 0         0 $cur->[1] = $bb->[1] + $dy;
587 0         0 $cur->[2] = $bb->[2] + $dx;
588 0         0 $cur->[3] = $bb->[3] + $dy;
589             }
590 0         0 return $cur; # for convenience
591             }
592              
593             ################ Extensions to PDF::API2 ################
594              
595             sub PDF::API2::Content::glyph_by_CId {
596 0     0 0 0 my ( $self, $cid ) = @_;
597 0         0 $self->add( sprintf("<%04x> Tj", $cid ) );
598 0         0 $self->{' font'}->fontfile->subsetByCId($cid);
599             }
600              
601             # HarfBuzz requires a TT/OT font. Define the fontfilename method only
602             # for classes that HarfBuzz can deal with.
603             sub PDF::API2::Resource::CIDFont::TrueType::fontfilename {
604 0     0 0 0 my ( $self ) = @_;
605 0         0 $self->fontfile->{' font'}->{' fname'};
606             }
607              
608             # Add extents calculation for CIDfonts.
609             # Note: Origin is x=0 at the baseline.
610             sub PDF::API2::Resource::CIDFont::extents {
611 1     1 0 22064 my ( $self, $text, $size ) = @_;
612 1   50     5 $size //= 1;
613 1         10 my $e = $self->extents_cid( $self->cidsByStr($text), $size );
614 1         8 return $e;
615             }
616              
617             sub PDF::API2::Resource::CIDFont::extents_cid {
618 1     1 0 262 my ( $self, $text, $size ) = @_;
619 1         3 my $width = 0;
620 1         2 my ( $xMin, $xMax, $yMin, $yMax, $bl );
621              
622 1         4 my $upem = $self->data->{upem};
623 1         9 my $glyphs = $self->fontobj->{loca}->read->{glyphs};
624 1         50 $bl = $self->ascender;
625 1         6 my $lastglyph = 0;
626 1         2 my $lastwidth;
627              
628             # Fun ahead! Widths are in 1000 and xMin and such in upem.
629             # Scale to 1000ths.
630 1         4 my $scale = 1000 / $upem;
631              
632 1         5 foreach my $n (unpack('n*', $text)) {
633 19         56 $width += $lastwidth = $self->wxByCId($n);
634 19 50 33     832 if ($self->{'-dokern'} and $self->haveKernPairs()) {
635 0 0       0 if ($self->kernPairCid($lastglyph, $n)) {
636 0         0 $width -= $self->kernPairCid($lastglyph, $n);
637             }
638             }
639 19         21 $lastglyph = $n;
640 19         36 my $ex = $glyphs->[$n];
641 19 50 33     71 unless ( defined $ex && %$ex ) {
642 0         0 warn("Missing glyph: $n\n");
643 0         0 next;
644             }
645 19         56 $ex->read;
646              
647 19         3858 my $e;
648             # Copy while scaling.
649 19         92 $e->{$_} = $ex->{$_} * $scale for qw( xMin yMin xMax yMax );
650              
651             printf STDERR ( "G %3d %6.2f %6.2f %6.2f %6.2f %6.2f\n",
652             $n, $lastwidth,
653 19         22 @$e{ qw( xMin yMin xMax yMax ) } ) if 0;
654              
655 19   66     43 $xMin //= ($width - $lastwidth) + $e->{xMin};
656 19 100 100     69 $yMin = $e->{yMin} if !defined($yMin) || $e->{yMin} < $yMin;
657 19 100 66     55 $yMax = $e->{yMax} if !defined($yMax) || $e->{yMax} > $yMax;
658 19         54 $xMax = ($width - $lastwidth) + $e->{xMax};
659             }
660              
661 1 50       8 if ( defined $lastwidth ) {
662             # $xMax += ($width - $lastwidth);
663             }
664             else {
665 0         0 $xMin = $yMin = $xMax = $yMax = 0;
666 0         0 $width = $self->missingwidth;
667             }
668 1   50     12 $_ = ($_//0)*$size/1000 for $xMin, $xMax, $yMin, $yMax, $bl;
669 1   50     7 $_ = ($_//0)*$size/1000 for $width;
670              
671 1         16 return { x => $xMin,
672             y => $yMin,
673             width => $xMax - $xMin,
674             height => $yMax - $yMin,
675             # These are for convenience
676             xMin => $xMin,
677             yMin => $yMin,
678             xMax => $xMax,
679             yMax => $yMax,
680             wx => $width,
681             bl => $bl,
682             };
683             }
684              
685             # Note: This is an augmented copy of the method from PDF::API2 2.047.
686 5     5   66 no warnings 'redefine';
  5         15  
  5         397  
687 5     5   4154 use PDF::API2::Resource::CIDFont::TrueType::FontFile;
  5         487797  
  5         488  
688             sub PDF::API2::Resource::CIDFont::TrueType::FontFile::new {
689             package PDF::API2::Resource::CIDFont::TrueType::FontFile;
690 1     1 1 285428 my ($class, $pdf, $file, %opts) = @_;
691 1         4 my $data = {};
692              
693             #### Start of changes ####
694 5     5   51 use Carp qw(confess);
  5         9  
  5         14140  
695 1         3 my $font;
696              
697             # If the file is already a suitable font object, use it.
698 1 50       13 if ( UNIVERSAL::isa( $file, 'Font::TTF::Font' ) ) {
699 0         0 $font = $file;
700             }
701             else {
702 1 50       41 confess "cannot find font '$file'" unless -f $file;
703 1         10 $font = Font::TTF::Font->open($file);
704             }
705             #### End of changes ####
706              
707 1         113019 $data->{'obj'} = $font;
708              
709 1 50       7 $class = ref($class) if ref($class);
710 1         44 my $self = $class->SUPER::new();
711              
712 1         40 $self->{'Filter'} = PDFArray(PDFName('FlateDecode'));
713 1         42 $self->{' font'} = $font;
714 1         4 $self->{' data'} = $data;
715              
716 1 50       6 $data->{'noembed'} = $opts{'embed'} ? 0 : 1;
717 1 50       5 $data->{'iscff'} = defined($font->{'CFF '}) ? 1 : 0;
718              
719 1 50       3 $self->{'Subtype'} = PDFName('CIDFontType0C') if $data->{'iscff'};
720              
721 1         14 $data->{'fontfamily'} = $font->{'name'}->read->find_name(1);
722 1         910 $data->{'fontname'} = $font->{'name'}->read->find_name(4);
723              
724 1         29 $font->{'OS/2'}->read();
725 1         3643 my @stretch = qw(
726             Normal
727             UltraCondensed
728             ExtraCondensed
729             Condensed
730             SemiCondensed
731             Normal
732             SemiExpanded
733             Expanded
734             ExtraExpanded
735             UltraExpanded
736             );
737 1   50     14 $data->{'fontstretch'} = $stretch[$font->{'OS/2'}->{'usWidthClass'}] || 'Normal';
738              
739 1         9 $data->{'fontweight'} = $font->{'OS/2'}->{'usWeightClass'};
740              
741 1         9 $data->{'panose'} = pack('n', $font->{'OS/2'}->{'sFamilyClass'});
742              
743 1         5 foreach my $p (qw[bFamilyType bSerifStyle bWeight bProportion bContrast bStrokeVariation bArmStyle bLetterform bMidline bXheight]) {
744 10         31 $data->{'panose'} .= pack('C', $font->{'OS/2'}->{$p});
745             }
746              
747 1         12 $data->{'apiname'} = join('', map { ucfirst(lc(substr($_, 0, 2))) } split m/[^A-Za-z0-9\s]+/, $data->{'fontname'});
  2         32  
748 1         13 $data->{'fontname'} =~ s/[\x00-\x1f\s]//g;
749              
750 1         13 $data->{'altname'} = $font->{'name'}->find_name(1);
751 1         96 $data->{'altname'} =~ s/[\x00-\x1f\s]//g;
752              
753 1         9 $data->{'subname'} = $font->{'name'}->find_name(2);
754 1         31 $data->{'subname'} =~ s/[\x00-\x1f\s]//g;
755              
756 1         12 $font->{'cmap'}->read->find_ms();
757 1 50       5017 if (defined $font->{'cmap'}->find_ms()) {
758 1   50     15 $data->{'issymbol'} = ($font->{'cmap'}->find_ms->{'Platform'} == 3 and $font->{'cmap'}->read->find_ms->{'Encoding'} == 0) || 0;
759             }
760             else {
761 0         0 $data->{'issymbol'} = 0;
762             }
763              
764 1         47 $data->{'upem'} = $font->{'head'}->read->{'unitsPerEm'};
765              
766             $data->{'fontbbox'} = [
767             int($font->{'head'}->{'xMin'} * 1000 / $data->{'upem'}),
768             int($font->{'head'}->{'yMin'} * 1000 / $data->{'upem'}),
769             int($font->{'head'}->{'xMax'} * 1000 / $data->{'upem'}),
770 1         38 int($font->{'head'}->{'yMax'} * 1000 / $data->{'upem'}),
771             ];
772              
773 1         5 $data->{'stemv'} = 0;
774 1         3 $data->{'stemh'} = 0;
775              
776 1   50     9 $data->{'missingwidth'} = int($font->{'hhea'}->read->{'advanceWidthMax'} * 1000 / $data->{'upem'}) || 1000;
777 1         1158 $data->{'maxwidth'} = int($font->{'hhea'}->{'advanceWidthMax'} * 1000 / $data->{'upem'});
778 1         6 $data->{'ascender'} = int($font->{'hhea'}->read->{'Ascender'} * 1000 / $data->{'upem'});
779 1         22 $data->{'descender'} = int($font->{'hhea'}{'Descender'} * 1000 / $data->{'upem'});
780              
781 1         4 $data->{'flags'} = 0;
782 1 50       10 $data->{'flags'} |= 1 if $font->{'OS/2'}->read->{'bProportion'} == 9;
783 1 50 33     25 $data->{'flags'} |= 2 unless $font->{'OS/2'}{'bSerifStyle'} > 10 and $font->{'OS/2'}{'bSerifStyle'} < 14;
784 1 50       5 $data->{'flags'} |= 8 if $font->{'OS/2'}{'bFamilyType'} == 2;
785 1         4 $data->{'flags'} |= 32; # if $font->{'OS/2'}{'bFamilyType'} > 3;
786 1 50       6 $data->{'flags'} |= 64 if $font->{'OS/2'}{'bLetterform'} > 8;
787              
788 1   33     16 $data->{'capheight'} = $font->{'OS/2'}->{'CapHeight'} || int($data->{'fontbbox'}->[3] * 0.8);
789 1   33     45 $data->{'xheight'} = $font->{'OS/2'}->{'xHeight'} || int($data->{'fontbbox'}->[3] * 0.4);
790              
791 1 50       6 if ($data->{'issymbol'}) {
792 0         0 $data->{'e2u'} = [0xf000 .. 0xf0ff];
793             }
794             else {
795 1         44 $data->{'e2u'} = [unpack('U*', decode('cp1252', pack('C*', 0 .. 255)))];
796             }
797              
798 1 50 33     137 if ($font->{'post'}->read->{'FormatType'} == 3 and defined $font->{'cmap'}->read->find_ms()) {
799 0         0 $data->{'g2n'} = [];
800 0         0 foreach my $u (sort { $a <=> $b } keys %{$font->{'cmap'}->read->find_ms->{'val'}}) {
  0         0  
  0         0  
801 0         0 my $n = nameByUni($u);
802 0         0 $data->{'g2n'}->[$font->{'cmap'}->read->find_ms->{'val'}->{$u}] = $n;
803             }
804             }
805             else {
806 1 50       4616 $data->{'g2n'} = [ map { $_ || '.notdef' } @{$font->{'post'}->read->{'VAL'}} ];
  262         713  
  1         10  
807             }
808              
809 1         22 $data->{'italicangle'} = $font->{'post'}->{'italicAngle'};
810 1         7 $data->{'isfixedpitch'} = $font->{'post'}->{'isFixedPitch'};
811 1         4 $data->{'underlineposition'} = $font->{'post'}->{'underlinePosition'};
812 1         7 $data->{'underlinethickness'} = $font->{'post'}->{'underlineThickness'};
813              
814 1 50       14 if ($self->iscff()) {
815 0         0 $data->{'cff'} = readcffstructs($font);
816             }
817              
818 1 50       23 if (defined $data->{'cff'}->{'ROS'}) {
819 0         0 my %cffcmap = (
820             'Adobe:Japan1' => 'japanese',
821             'Adobe:Korea1' => 'korean',
822             'Adobe:CNS1' => 'traditional',
823             'Adobe:GB1' => 'simplified',
824             );
825 0         0 my $key = $data->{'cff'}->{'ROS'}->[0] . ':' . $data->{'cff'}->{'ROS'}->[1];
826 0   0     0 my $ccmap = _look_for_cmap($cffcmap{$key} // $key);
827 0         0 $data->{'u2g'} = $ccmap->{'u2g'};
828 0         0 $data->{'g2u'} = $ccmap->{'g2u'};
829             }
830             else {
831 1         4 $data->{'u2g'} = {};
832              
833 1         13 my $gmap = $font->{'cmap'}->read->find_ms->{'val'};
834 1         175 foreach my $u (sort {$a <=> $b} keys %$gmap) {
  1656         2459  
835 245   50     480 my $uni = $u || 0;
836 245         734 $data->{'u2g'}->{$uni} = $gmap->{$uni};
837             }
838 1 100       28 $data->{'g2u'} = [ map { $_ || 0 } $font->{'cmap'}->read->reverse() ];
  262         1996  
839             }
840              
841 1 50       38 if ($data->{'issymbol'}) {
842 0   0     0 map { $data->{'u2g'}->{$_} ||= $font->{'cmap'}->read->ms_lookup($_) } (0xf000 .. 0xf0ff);
  0         0  
843 0   0     0 map { $data->{'u2g'}->{$_ & 0xff} ||= $font->{'cmap'}->read->ms_lookup($_) } (0xf000 .. 0xf0ff);
  0         0  
844             }
845              
846 1 50 100     3 $data->{'e2n'} = [ map { $data->{'g2n'}->[$data->{'u2g'}->{$_} || 0] || '.notdef' } @{$data->{'e2u'}} ];
  256         1033  
  1         7  
847              
848 1 100 100     15 $data->{'e2g'} = [ map { $data->{'u2g'}->{$_ || 0} || 0 } @{$data->{'e2u'}} ];
  256         799  
  1         6  
849 1         13 $data->{'u2e'} = {};
850 1         9 foreach my $n (reverse 0 .. 255) {
851 256   100     1123 $data->{'u2e'}->{$data->{'e2u'}->[$n]} //= $n;
852             }
853              
854 1         5 $data->{'u2n'} = { map { $data->{'g2u'}->[$_] => $data->{'g2n'}->[$_] } (0 .. (scalar @{$data->{'g2u'}} - 1)) };
  262         777  
  1         32  
855              
856 1         36 $data->{'wx'} = [];
857 1         3 foreach my $i (0 .. (scalar @{$data->{'g2u'}} - 1)) {
  1         7  
858 262         527 my $hmtx = $font->{'hmtx'}->read->{'advance'}->[$i];
859 262 100       9020 if ($hmtx) {
860 261         593 $data->{'wx'}->[$i] = int($hmtx * 1000 / $data->{'upem'});
861             }
862             else {
863 1         6 $data->{'wx'}->[$i] = $data->{'missingwidth'};
864             }
865             }
866              
867 1         15 $data->{'kern'} = read_kern_table($font, $data->{'upem'}, $self);
868 1 50       18 delete $data->{'kern'} unless defined $data->{'kern'};
869              
870 1         9 $data->{'fontname'} =~ s/\s+//g;
871 1         5 $data->{'fontfamily'} =~ s/\s+//g;
872 1         3 $data->{'apiname'} =~ s/\s+//g;
873 1         4 $data->{'altname'} =~ s/\s+//g;
874 1         4 $data->{'subname'} =~ s/\s+//g;
875              
876 1         8 $self->subsetByCId(0);
877              
878 1         6272 return ($self, $data);
879             }
880 5     5   51 use warnings 'redefine';
  5         10  
  5         1198  
881              
882             # These are additions. Methods of $pdf for convenience.
883              
884             # Open a TTC (TrueType font Collection) file.
885             #
886             # This is not really API related, but it is handy to use the API file
887             # lookups.
888             #
889             # Returns a Font::TTF::Ttc object.
890              
891             sub PDF::API2::ttc {
892 0     0 0   my ( $self, $name, %opts ) = @_;
893 0 0         my $file = $self->can("_find_font")->($name)
894             or croak "Unable to find ttc \"$name\"";
895 0           require Font::TTF::Font;
896 0           require Font::TTF::Ttc;
897 0           return Font::TTF::Ttc->open($file);
898             }
899              
900             # Create a API font from one of ttc fonts.
901             #
902             # The font is selected with a C option.
903             # Font selectors are
904             #
905             # * The PostScript name of the font (case insensitive)
906             # * The family name : style (case insensitive), where both may be omitted
907             # to select the first matching.
908             # Note that the style must match what is in the font.
909             # E.g. "bold italic" (with a space, in this order).
910             #
911             # If no font option is given, the first font found is used.
912              
913             our @CARP_NOT = qw( ChordPro::Logger );
914              
915             sub PDF::API2::ttcfont {
916 0     0 0   my ( $self, $ttc, %opts ) = @_;
917              
918 5     5   35 use Carp qw(confess);
  5         29  
  5         4690  
919              
920 0   0       my $sel = delete $opts{font} // "";
921              
922 0           my $font;
923 0           foreach my $d ( @{ $ttc->{directs} } ) {
  0            
924 0           $d->{name}->read;
925 0 0         if ( $sel =~ /^(.*):(.*)/ ) {
    0          
926 0 0 0       next if $1 && lc($d->{name}->find_name(1)) ne lc($1);
927 0 0 0       next if $2 && lc($d->{name}->find_name(2)) ne lc($2);
928             }
929             elsif ( $sel ) {
930 0 0         next unless lc($d->{name}->find_name(6)) eq lc($sel);
931             }
932             # else: use first found.
933              
934 0           $font = $d;
935 0           last;
936             }
937 0 0         confess "Missing font '$sel' in ttcfont" unless $font;
938 0 0         $opts{-unicodemap} = 1 unless exists $opts{-unicodemap};
939 0 0         $opts{embed} = 1 unless exists $opts{embed};
940              
941 0           require PDF::API2::Resource::CIDFont::TrueType;
942 0           my $obj = PDF::API2::Resource::CIDFont::TrueType->new($self->{'pdf'}, $font, %opts);
943 0           $self->{'pdf'}->out_obj($self->{'pages'});
944 0 0         $obj->tounicodemap() if $opts{-unicodemap};
945              
946 0           return $obj;
947             }
948              
949             ################ Extensions to PDF::Builder ################
950              
951             sub PDF::Builder::Content::glyph_by_CId {
952 0     0     my ( $self, $cid ) = @_;
953 0           $self->add( sprintf("<%04x> Tj", $cid ) );
954 0           $self->{' font'}->fontfile->subsetByCId($cid);
955             }
956              
957             # HarfBuzz requires a TT/OT font. Define the fontfilename method only
958             # for classes that HarfBuzz can deal with.
959             sub PDF::Builder::Resource::CIDFont::TrueType::fontfilename {
960 0     0     my ( $self ) = @_;
961 0           $self->fontfile->{' font'}->{' fname'};
962             }
963              
964             ################ For debugging/convenience ################
965              
966             # Shows the bounding box of the last piece of text that was rendered.
967             sub showbb {
968 0     0 0   my ( $self, $gfx, $x, $y, $col ) = @_;
969 0   0       $x //= $self->{_lastx};
970 0   0       $y //= $self->{_lasty};
971 0   0       $col ||= "magenta";
972              
973 0           my ( $ink, $bb ) = $self->get_pixel_extents;
974 0           my $bl = $bb->{bl};
975             # Bounding box, top-left coordinates.
976             printf( "Ink: %6.2f %6.2f %6.2f %6.2f\n",
977 0           @$ink{qw( x y width height )} );
978             printf( "Layout: %6.2f %6.2f %6.2f %6.2f BL %.2f\n",
979 0           @$bb{qw( x y width height )}, $bl );
980              
981             # NOTE: Some fonts include natural spacing in the bounding box.
982             # NOTE: Some fonts exclude accents on capitals from the bounding box.
983              
984 0           $gfx->save;
985 0           $gfx->translate( $x, $y );
986              
987             # Show origin.
988 0           _showloc($gfx);
989              
990             # Show baseline.
991 0           _line( $gfx, $bb->{x}, -$bl, $bb->{width}, 0, $col );
992 0           $gfx->restore;
993              
994             # Show layout box.
995 0           $gfx->save;
996 0           $gfx->linewidth( 0.25 );
997 0           $gfx->strokecolor($col);
998 0           $gfx->translate( $x, $y );
999 0           for my $e ( $bb ) {
1000 0           $gfx->rect( @$e{ qw( x y width height ) } );
1001 0           $gfx->stroke;
1002             }
1003 0           $gfx->restore;
1004              
1005             # Show ink box.
1006 0           $gfx->save;
1007 0           $gfx->linewidth( 0.25 );
1008 0           $gfx->strokecolor("cyan");
1009 0           $gfx->translate( $x, $y );
1010 0           for my $e ( $ink ) {
1011 0           $gfx->rect( @$e{ qw( x y width height ) } );
1012 0           $gfx->stroke;
1013             }
1014 0           $gfx->restore;
1015             }
1016              
1017             sub _showloc {
1018 0     0     my ( $gfx, $x, $y, $d, $col ) = @_;
1019 0   0       $x ||= 0; $y ||= 0; $d ||= 50; $col ||= "blue";
  0   0        
  0   0        
  0   0        
1020              
1021 0           _line( $gfx, $x-$d, $y, 2*$d, 0, $col );
1022 0           _line( $gfx, $x, $y-$d, 0, 2*$d, $col );
1023             }
1024              
1025             sub _line {
1026 0     0     my ( $gfx, $x, $y, $w, $h, $col, $lw ) = @_;
1027 0   0       $col ||= "black";
1028 0   0       $lw ||= 0.5;
1029              
1030 0           $gfx->save;
1031 0           $gfx->move( $x, $y );
1032 0           $gfx->line( $x+$w, $y+$h );
1033 0           $gfx->linewidth($lw);
1034 0           $gfx->strokecolor($col);
1035 0           $gfx->stroke;
1036 0           $gfx->restore;
1037             }
1038              
1039             1;