File Coverage

blib/lib/Text/Layout/PDFAPI2/ImageElement.pm
Criterion Covered Total %
statement 26 165 15.7
branch 0 76 0.0
condition 0 23 0.0
subroutine 9 14 64.2
pod 3 5 60.0
total 38 283 13.4


line stmt bran cond sub pod time code
1             #! perl
2              
3 3     3   43 use v5.26;
  3         12  
4 3     3   70 use warnings;
  3         7  
  3         192  
5 3     3   20 use utf8;
  3         5  
  3         24  
6 3     3   2296 use Object::Pad;
  3         33202  
  3         19  
7              
8 3     3   2408 class Text::Layout::PDFAPI2::ImageElement :does(Text::Layout::ElementRole);
  3         11  
  3         378  
9              
10             =head1 NAME
11              
12             Text::Layout::PDFAPI2::ImageElement - element for PDF images
13              
14             =head1 DESCRIPTION
15              
16             This class implements support for an C<< >> element. It can be
17             used to include inline images in marked-up texts.
18              
19             The class provides the three mandatory methods according to the
20             requirements of L.
21              
22             =over 4
23              
24             =item parse
25              
26             To parse the C<< >> tag in marked-up text.
27              
28             =item bbox
29              
30             To provide the augmented bounding box for the image.
31              
32             =item render
33              
34             To render the image using one of the PDF::API2 compatible libraries.
35              
36             =back
37              
38             An additional, overridable method getimage() is provided to actually
39             produce the desired image object. See L
40              
41             =head1 THE C<< >> ELEMENT
42              
43            
44              
45             Note that the C<< >> element must be self-closed, i.e., end with
46             C<< /> >>.
47              
48             The image is placed at the current location in the text and aligned on
49             the baseline of the text. The image dimensions contribute to the
50             resultant bounding box of the formatted text. See C and C
51             below.
52              
53             All attributes are key=value pairs. The value should (but need not) be
54             enclosed in quotes.
55              
56             Dimensional values may be a (fractional) number optionally
57             postfixed by C or C, or a percentage.
58             A number indicates points.
59             C values are multiplied by the current font size and
60             C values are multiplied by half the font size.
61              
62             =over 4
63              
64             =item CI
65              
66             Provides the source of the image. This can be the filename of a jpg,
67             png or gif image.
68              
69             =item CI
70              
71             The desired width for the image.
72             Dimensional.
73             The image is scaled if necessary.
74              
75             =item CI
76              
77             The desired height for the image.
78             Dimensional.
79             The image is scaled if necessary.
80              
81             =item CI
82              
83             A horizontal offset for the image, wrt. the current location in the text.
84             Dimensional.
85              
86             =item CI
87              
88             Same, but vertical. Positive amounts move up.
89              
90             Note the direction is opposite to the Pango C.
91              
92             =item CI
93              
94             A scaling factor, to be applied I width/height scaling.
95             The value may be expressed as a percentage.
96              
97             Independent horizontal and vertical scaling can be specified as two
98             comma-separated scale values.
99              
100             =item CI
101              
102             Align the image in the width given by CI.
103              
104             Possible alignments are C, C
, and C.
105              
106             =item CI
107              
108             If true, the actual bounding box of an object is used for placement.
109              
110             By default the bounding box is only used to obtain the width and height.
111              
112             This attribute has no effect on image objects.
113              
114             =item CI
115              
116             The advance width of the image.
117             Dimensional.
118             Default advance is the image width plus horizontal offset.
119             This overrides the advance and may be zero.
120              
121             =item CI
122              
123             The advance height of the image.
124             Dimensional.
125             Default advance is the image height plus vertical offset.
126             This overrides the advance and may be zero.
127              
128             =back
129              
130             =head1 CONSTRUCTOR
131              
132             This class is usually instantiated in a Text::Layout register_element call:
133              
134             $layout->register_element
135             ( Text::Layout::PDFAPI2::ImageElement->new( pdf => $pdf ) );
136              
137             =cut
138              
139 3     3   455 use constant TYPE => "img";
  3         8  
  3         336  
140 3     3   23 use Carp;
  3         5  
  3         395  
141              
142 3     3   25 use Text::Layout::Utils qw(parse_kv);
  3         7  
  3         505  
143              
144 0 0   0 0   field $pdf :param :accessor;
  0            
145              
146 3     3   21 use constant DEBUG => 0;
  3         6  
  3         23858  
147              
148 0     0 1   method parse( $ctx, $k, $v ) {
  0            
  0            
  0            
  0            
  0            
149              
150 0           my %ctl = ( type => TYPE );
151              
152             # Split the attributes.
153 0           my $attr = parse_kv($v);
154 0           while ( my ( $k, $v ) = each( %$attr ) ) {
155              
156             # Ignore case unless required.
157 0 0         $v = lc $v unless $k =~ /^(src)$/;
158              
159 0 0 0       if ( $k =~ /^(src|bbox)$/ ) {
    0          
    0          
    0          
160 0           $ctl{$k} = $v;
161             }
162             elsif ( $k eq "align" && $v =~ /^(left|right|center)$/ ) {
163 0           $ctl{$k} = $v;
164             }
165             elsif ( $k =~ /^(width|height|dx|dy|w|h)$/ ) {
166 0 0         $v = $1 * $ctx->{size} if $v =~ /^(-?[\d.]+)em$/;
167 0 0         $v = $1 * $ctx->{size} / 2 if $v =~ /^(-?[\d.]+)ex$/;
168 0 0         $v = $1 * $ctx->{size} / 100 if $v =~ /^(-?[\d.]+)\%$/;
169 0           $ctl{$k} = $v;
170             }
171             elsif ( $k =~ /^(scale)$/ ) {
172 0           my @s;
173 0           for ( split( /,/, $v ) ) {
174 0 0         $_ = $1 / 100 if /^([\d.]+)\%$/;
175 0           push( @s, $_ );
176             }
177 0 0         push( @s, $s[0] ) unless @s > 1;
178 0 0         carp("Invalid " . TYPE . " attribute: \"$k\" (too many values)\n")
179             unless @s == 2;
180 0           $ctl{$k} = \@s;
181             }
182             else {
183 0           carp("Invalid " . TYPE . " attribute: \"$k\"\n");
184             }
185             }
186              
187 0           return \%ctl;
188             }
189              
190 0     0 1   method render( $fragment, $gfx, $x, $y ) {
  0            
  0            
  0            
  0            
  0            
  0            
191              
192 0           my $b = $self->bbox($fragment);
193 0           my @bbox = @{$b->{bbox}};
  0            
194 0           my @bb = @{$b->{bb}};
  0            
195 0           my @abox = @{$b->{abox}};
  0            
196              
197 0           my $width = $bb[2] - $bb[0];
198 0           my $height = $bb[3] - $bb[1];
199 0           my $img = $self->getimage($fragment);
200 0           my $is_image = ref($img) =~ /::Image::/;
201 0           my @a;
202              
203 0 0         if ( $is_image ) {
204 0           @a = ( $x + $bb[0], $y + $bb[1], $width, $height );
205 0           warn("IMG x=$a[0], y=$a[1], width=$a[2], height=$a[3]\n") if DEBUG;
206             }
207             else {
208 0           my ( $xscale, $yscale ) = @bb[4,5];
209              
210 0           @a = ( $x + $bb[0],
211             $y + $bb[1] - $yscale*($bbox[1]),
212             $xscale, $yscale );
213 0 0         unless ( $fragment->{bbox} ) {
214 0           $a[0] -= $xscale*($bbox[0]);
215             }
216 0           warn("OBJ x=${x}->$a[0], y=${y}->$a[1], width=$width, height=$height",
217             ( $xscale != 1 || $yscale != 1 )
218             ? ", scale=$xscale" : "",
219             ( $xscale != $yscale )
220             ? ",$yscale" : "", "\n") if DEBUG;
221             }
222              
223 0           $gfx->object( $img, @a );
224              
225 0 0         if ( $fragment->{href} ) {
226 0           my $ann = $gfx->{' apipage'}->annotation;
227 0           my $target = $fragment->{href};
228              
229 0 0         if ( $target =~ /^#(.+)/ ) { # named destination
    0          
230             # Augmented API for apps that keep track of bookmarks.
231 0           my $pdf = $gfx->{' api'};
232 0 0         if ( my $c = $pdf->can("named_dest_fiddle") ) {
233 0           $target = $pdf->$c($1);
234             }
235              
236 0           $ann->link($target);
237             }
238             # Named destination in other PDF.
239             elsif ( $target =~ /^(?!\w{3,}:)(.*)(\#.+)$/ ) {
240 0           $ann->pdf( $1, $2 );
241             }
242             # Arbitrary document.
243             else {
244 0           $ann->uri($target);
245             }
246             # $ann->border( 0, 0, 1 );
247 0           $ann->rect( $x + $bb[0], $y + $bb[1], $x + $bb[2], $y + $bb[3] );
248             }
249              
250 0           return { abox => \@abox };
251             }
252              
253 0     0 1   method bbox( $fragment ) {
  0            
  0            
  0            
254              
255 0 0         return $fragment->{_bb} if $fragment->{_bb};
256              
257 0           my @bbox; # bbox of image or object
258             my @bb; # bbox after scaling/displacement, plus scale factors
259 0           my @abox; # advance box
260              
261 0           my $img = $self->getimage($fragment);
262 0           my $is_image = ref($img) =~ /::Image::/;
263              
264 0           my $img_width;
265             my $img_height;
266 0 0         if ( $is_image ) {
267 0           $img_width = $img->width;
268 0           $img_height = $img->height;
269 0           @bbox = ( 0, 0, $img_width, $img_height );
270             }
271             else {
272 0           @bbox = $img->bbox;
273 0 0         @bbox[0,2] = @bbox[2,0] if $bbox[2] < $bbox[0];
274 0 0         @bbox[1,3] = @bbox[3,1] if $bbox[3] < $bbox[1];
275 0           $img_width = $bbox[2] - $bbox[0];
276 0           $img_height = $bbox[3] - $bbox[1];
277             }
278              
279             # Target width and height.
280 0   0       my $width = $fragment->{width} || $img_width;
281 0   0       my $height = $fragment->{height} || $img_height;
282              
283             # Calculate scale factors.
284 0           my $xscale = 1;
285 0 0         if ( $width != $img_width ) {
286 0           $xscale = $width / $img_width;
287             }
288 0           my $yscale = $xscale;
289 0 0         if ( $height != $img_height ) {
290 0           $yscale = $height / $img_height;
291             }
292              
293             # Apply design scale. This cannot be set via properties but it
294             # intended for 3rd party plugins.
295 0   0       my $ds = $fragment->{design_scale} || 1;
296 0 0         if ( $ds != 1 ) {
297 0           $_ *= $ds for $xscale, $yscale, $width, $height;
298             }
299              
300             # Apply custom scale.
301 0   0       my ( $sx, $sy ) = @{$fragment->{scale} // [1,1]};
  0            
302 0 0         if ( $sx != 1 ) {
303 0           $xscale *= $sx;
304 0           $width *= $sx;
305             }
306 0 0         if ( $sy != 1 ) {
307 0           $yscale *= $sy;
308 0           $height *= $sy;
309             }
310              
311             # Displacement wrt. the origin.
312 0   0       my $dx = $fragment->{dx} || 0;
313 0   0       my $dy = $fragment->{dy} || 0;
314              
315 0 0 0       if ( !$is_image && $fragment->{bbox} ) {
316 0           $dx += $bbox[0] * $xscale;
317 0           $dy += $bbox[1] * $yscale;
318             }
319              
320             # Use the image baseline, if any.
321 0 0         if ( $fragment->{base} ) {
322 0           $dy += ( $bbox[1] - $fragment->{base} ) * $yscale;
323             }
324            
325 0           @bb = ( $dx, $dy, $width + $dx, $height + $dy, $xscale, $yscale );
326 0           @abox = @bb;
327              
328             # Bounding box width/height.
329 0 0         if ( defined $fragment->{w} ) {
330 0           $abox[0] = 0;
331 0           $abox[2] = $fragment->{w};
332             }
333 0 0         if ( defined $fragment->{a} ) {
334 0           $abox[3] = $fragment->{a};
335             }
336 0 0         if ( defined $fragment->{d} ) {
337 0           $abox[1] = $fragment->{d};
338             }
339 0 0         if ( $fragment->{align} ) {
340 0 0         if ( $fragment->{align} eq "right" ) {
    0          
341 0           $bb[0] += $abox[2] - $width;
342             }
343             elsif ( $fragment->{align} eq "center" ) {
344 0           $bb[0] += ($abox[2]-$width)/2;
345             }
346             }
347              
348 0           warn( ref($img) =~ /::Image::/ ? "IMG" : "OBJ",
349             " bbox [@bbox]",
350             " bb [@bb]",
351             " abox [@abox]",
352             ( $xscale != 1 || $yscale != 1 )
353             ? " scale=$xscale" : "",
354             ( $xscale != $yscale )
355             ? ",$yscale" : "", "\n") if DEBUG;
356              
357 0           return $fragment->{_bb} = { bbox => \@bbox, bb => \@bb, abox => \@abox };
358             }
359              
360             =head1 IMAGE PRODUCER
361              
362             The image object is produced with a call to method getimage(), that
363             can be overridden in a subclass.
364             The method gets a hash ref as argument.
365             This hash contains all the attributes and may be used for cacheing purposes.
366              
367             For example,
368              
369             method getimage ($fragment) {
370             $fragment->{_img} //= $self->pdf->image($fragment->{src});
371             }
372              
373             An overridden getimage() may produce a PDF XObject instead of an image
374             object. An XObject is treated similar to an image object, but is
375             aligned according to its bounding box if attribute C is set to a
376             I value, i.e., not zero.
377              
378             =cut
379              
380 0     0 0   method getimage ($fragment) {
  0            
  0            
  0            
381 0 0         return $fragment->{_img} if $fragment->{_img};
382              
383 0           my $src = $fragment->{src};
384              
385             # API suports jpg, png, gif and tiff.
386             # If we have the SVGPDF module, we can do SVG images.
387 0 0         if ( $src =~ /\.svg$/i ) {
388 0           local $SIG{__WARN__} = '__IGNORE__';
389 0 0         if ( eval "require SVGPDF" ) {
390             $fragment->{_img} = SVGPDF->new( pdf => $pdf )
391 0           ->process( $src, combine => "stacked" )->[0]->{xo};
392             }
393             # API will complain.
394             }
395              
396             # Pass to API.
397 0   0       $fragment->{_img} //= $pdf->image($src);
398             }
399              
400             =head1 SEE ALSO
401              
402             L, L, L.
403              
404             =head1 AUTHOR
405              
406             Johan Vromans, C<< >>
407              
408             =head1 SUPPORT
409              
410             This class is part of .
411              
412             Development takes place on GitHub:
413             L.
414              
415             Please report any bugs or feature requests using the issue tracker for
416             Text::Layout on GitHub.
417              
418             =head1 LICENSE
419              
420             See L.
421              
422             =cut
423              
424             1;