File Coverage

blib/lib/PDF/FromHTML/Twig.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package PDF::FromHTML::Twig;
2              
3 1     1   6 use strict;
  1         2  
  1         34  
4 1     1   7 use warnings;
  1         2  
  1         24  
5 1     1   879 use XML::Twig;
  0            
  0            
6             use base 'XML::Twig';
7              
8             use charnames ':full';
9             use Graphics::ColorNames qw( hex2tuple );
10             use File::Spec;
11             use File::Basename;
12             use List::Util qw( sum first reduce );
13              
14             =head1 NAME
15              
16             PDF::FromHTML::Twig - PDF::FromHTML guts
17              
18             =head1 SYNOPSIS
19              
20             (internal use only)
21              
22             =head1 DESCRIPTION
23              
24             No user-serviceable parts inside.
25              
26             =cut
27              
28             sub new {
29             my $class = shift;
30             XML::Twig::new($class, $class->TwigArguments, @_);
31             }
32              
33             our $PageWidth = 640;
34             our $PageResolution = 540;
35             our $FontBold = 'HelveticaBold';
36             our $FontOblique = 'HelveticaOblique';
37             our $FontBoldOblique = 'HelveticaBoldOblique';
38             our $LineHeight = 12;
39             our $FontUnicode = 'Helvetica';
40             our $Font = $FontUnicode;
41              
42             # $Font = '/usr/local/share/fonts/TrueType/minguni.ttf';
43              
44             our $PageSize = 'A4';
45             our $Landscape = 0;
46             use constant SuperScript => [
47             "\N{SUPERSCRIPT ZERO}",
48             "\N{SUPERSCRIPT ONE}",
49             "\N{SUPERSCRIPT TWO}",
50             "\N{SUPERSCRIPT THREE}",
51             "\N{SUPERSCRIPT FOUR}",
52             "\N{SUPERSCRIPT FIVE}",
53             "\N{SUPERSCRIPT SIX}",
54             "\N{SUPERSCRIPT SEVEN}",
55             "\N{SUPERSCRIPT EIGHT}",
56             "\N{SUPERSCRIPT NINE}",
57             ];
58             use constant SubScript => [
59             "\N{SUBSCRIPT ZERO}",
60             "\N{SUBSCRIPT ONE}",
61             "\N{SUBSCRIPT TWO}",
62             "\N{SUBSCRIPT THREE}",
63             "\N{SUBSCRIPT FOUR}",
64             "\N{SUBSCRIPT FIVE}",
65             "\N{SUBSCRIPT SIX}",
66             "\N{SUBSCRIPT SEVEN}",
67             "\N{SUBSCRIPT EIGHT}",
68             "\N{SUBSCRIPT NINE}",
69             ];
70             use constant InlineTags => { map { $_ => 1 } '#PCDATA', 'font' };
71             use constant DeleteTags => {
72             map { $_ => 1 }
73             qw(
74             head style applet script
75             )
76             };
77             use constant IgnoreTags => {
78             map { $_ => 1 }
79             qw(
80             title a ul
81              
82             del address blockquote colgroup fieldset
83             input form frameset object noframes noscript
84             small optgroup isindex area textarea col
85             pre frame param menu acronym abbr bdo
86             label basefont big caption option cite
87             dd dfn dt base code map iframe ins kbd legend
88             samp span dir strike meta link tbody q tfoot
89             button thead tt select s
90              
91             var
92             )
93             };
94             use constant TwigArguments => (
95             twig_handlers => {
96             html => sub {
97             $_->del_atts;
98             $_->set_gi('pdftemplate');
99             },
100             map((
101             "h$_" => (
102             sub {
103             my $size = 4 + shift;
104             sub {
105             $_->insert_new_elt(before => 'textbox')
106             ->wrap_in('row')
107             ->wrap_in(font => { face => $FontBold });
108             $_->wrap_in(
109             font => { h => $LineHeight + 6 - $size });
110             $_->wrap_in(
111             row => { h => $LineHeight + 8 - $size });
112             $_->set_tag('textbox'), $_->set_att(w => '100%');
113             };
114             }
115             )->($_)
116             ),
117             1 .. 6),
118             center => sub {
119             foreach my $child ($_->children('p')) {
120              
121             # XXX - revert other blocklevel to left/original alignment
122             $child->set_att(align => 'center');
123             }
124             $_->erase;
125             },
126             sup => sub {
127             my $digits = $_->text;
128             my $text = '';
129             $text .= +SuperScript->[$1] while $digits =~ s/(\d)//;
130             $_->set_text($text);
131             $_->erase;
132             },
133             sub => sub {
134             my $digits = $_->text;
135             my $text = '';
136             $text .= +SubScript->[$1] while $digits =~ s/(\d)//;
137             $_->set_text($text);
138             $_->erase;
139             },
140             u => sub {
141             _set(underline => 1, $_);
142             $_->erase;
143             },
144             em => sub {
145             _set(font => $FontOblique, $_);
146             $_->erase;
147             },
148             i => sub {
149             _set(font => $FontOblique, $_);
150             $_->erase;
151             },
152             strong => sub {
153             _set(font => $FontBold, $_);
154             $_->erase;
155             },
156             b => sub {
157             _set(font => $FontBold, $_);
158             $_->erase;
159             },
160             div => sub {
161             if (my $tag = (_type(header => $_) || _type(footer => $_))) {
162             $_->set_tag($tag);
163             $_->set_att(
164             "${tag}_height" => int(
165             sum(
166             $LineHeight * 2,
167             grep defined,
168             map $_->att('h'),
169             $_->descendants
170             )
171             ),
172             );
173             }
174             else {
175             $_->erase;
176             }
177             },
178             hr => sub {
179             $_->insert_new_elt(first_child => (_type(pagebreak => $_) || 'hr'));
180             $_->erase;
181             },
182             img => sub {
183             my $src = $_->att('src');
184             my $file = File::Spec->rel2abs($src);
185             if ($src =~ m{^(\w+):/}) {
186             require LWP::Simple;
187             require File::Basename;
188             require File::Spec;
189             $file =
190             File::Spec->catfile(File::Spec->tmpdir,
191             File::Basename::basename($src));
192             LWP::Simple::mirror($src => $file);
193             }
194              
195             # CSA - check for real file first
196             #
197             if (-e $file) {
198             my $w = $_->att('width');
199             my $h = $_->att('height');
200             if (($w eq '') or ($h eq '')) {
201             require Image::Size;
202             my ($iw, $ih) = Image::Size::imgsize($file);
203              
204             # CSA - catch this now, before we crash
205             #
206             warn "unable to read image file '$file' ($w x $h)"
207             unless (defined $iw && defined $ih);
208             $iw ||= 1;
209             $ih ||= 1;
210              
211             if (!$w and !$h) {
212             ($w, $h) = ($iw, $ih);
213             }
214             elsif (!$w) {
215             $w = $iw * ($h / $ih);
216             }
217             else {
218             $h = $ih * ($w / $iw);
219             }
220             }
221              
222             my $image = $_->insert_new_elt(
223             first_child => image => {
224             filename => $file,
225             w => ($w / $PageWidth * $PageResolution),
226             h => ($h / $PageWidth * $PageResolution),
227             type => '',
228             }
229             );
230             $image->wrap_in('row');
231              
232             # CSA - File has gone missing
233             #
234             }
235             else {
236             warn "image file '$file' does not exist";
237             }
238              
239             $_->erase;
240             },
241             body => sub {
242             $_->wrap_in(
243             pagedef => {
244             pagesize => $PageSize,
245             landscape => $Landscape,
246             margins => $LineHeight - 2,
247             },
248             );
249             $_->wrap_in(
250             font => {
251             face => $Font,
252             h => $LineHeight - 2,
253             }
254             );
255             my $pagedef = $_->parent->parent;
256             my $head = ($pagedef->descendants('header'))[0]
257             || $pagedef->insert_new_elt(
258             first_child => header => { header_height => $LineHeight * 2 });
259             my $row = $head->insert_new_elt(first_child => 'row');
260             $row->insert_new_elt(
261             first_child => textbox => { w => '100%', text => '' });
262             foreach my $child ($_->children('#PCDATA')) {
263             $child->set_text(
264             join(' ', grep length, split(/\n+/, $child->text)));
265             if ($child->text =~ /[^\x00-\x7f]/) {
266             $child->wrap_in(font => { face => $FontUnicode });
267             }
268             $child->wrap_in('row');
269             $child->wrap_in(textbox => { w => '100%' });
270             $child->insert_new_elt(after => 'textbox')->wrap_in('row');
271             }
272              
273             $_->erase;
274             },
275             p => \&_p,
276             li => \&_p,
277             table => sub {
278             our @RowSpan = ();
279              
280             my $cols = $_->root->att('#total_cols') or do {
281             $_->erase for $_->children('tr');
282             $_->erase;
283             return;
284             };
285              
286             my $widths = $_->root->att('#widths');
287             if (!$widths) {
288             $widths = [];
289             $_->root->set_att('#widths', $widths);
290             }
291             my $table_width = $_->root->att('#total_width');
292             if (!$table_width) {
293             $table_width = _percentify($_->att('width'), $PageWidth);
294             $_->root->set_att('#total_width', $table_width);
295             }
296              
297             my $unallocated_sum = 100;
298             my $unallocated_cols = 0;
299             foreach my $idx (0..$cols-1) {
300             if (my $w = $widths->[$idx]) {
301             $unallocated_sum -= $w;
302             }
303             else {
304             $unallocated_cols++;
305             }
306             }
307              
308             if ($unallocated_cols and $unallocated_sum > 0) {
309             # warn "UNALLOC: $unallocated_cols, $unallocated_sum\n";
310             # Populate unallocated columns
311             my $w = int($unallocated_sum / $unallocated_cols);
312             $widths->[$_] ||= $w for (0..$cols-1);
313             }
314             elsif ($unallocated_cols) {
315             # Redistribute all columns.
316             my $w = int(100 / $cols);
317             $widths->[$_] = $w for (0..$cols-1);
318             }
319             elsif ($unallocated_sum < 0) {
320             # warn "WIDTHS: @$widths ($unallocated_sum)\n";
321             # Redistribute all columns, part 2. -- not sure we should do it actually.
322             my $overflow = (100-$unallocated_sum);
323             $widths->[$_] = int($widths->[$_] * 100 / $overflow) for (0..$cols-1);
324             }
325              
326             for ($_->children('tr')) {
327             return $_->erase if $_->descendants('row');
328              
329             my @children = $_->descendants('textbox');
330              
331             my @cells = @{ shift(@RowSpan) || [] };
332             foreach my $i (1 .. $#cells) {
333             my $cell = $cells[$i] or next;
334             my $child;
335              
336             if ($child = $children[ $i - 1 ]) {
337             $child->insert_new_elt(before => 'textbox', $cell);
338             }
339             elsif ($child = $children[ $i - 2 ]) {
340             $child->insert_new_elt(after => 'textbox', $cell);
341             }
342             else {
343             next;
344             }
345              
346             @children = $_->descendants('textbox');
347             }
348              
349             my $cols = sum(map { $_->att('colspan') || 1 } @children);
350              
351             # print STDERR "==> Total cols: $cols :".@children.$/;
352              
353             my $sum = 100;
354             my $last_child = pop(@children);
355             my $col_idx = 0;
356             foreach my $child (@children) {
357             my $colspan = $child->att('colspan') || 1;
358             my $w = 0;
359             foreach my $idx ($col_idx .. $col_idx+$colspan-1) {
360             $w += $widths->[$idx];
361             }
362             $col_idx += $colspan;
363             $child->set_att(w => "$w%");
364             $sum -= $w;
365             }
366              
367             $last_child->set_att(w => "$sum%") if $last_child;
368              
369             $_->set_tag('row');
370             $_->set_att(lmargin => '3');
371             $_->set_att(rmargin => '3');
372             $_->set_att(border => $_->parent('table')->att('border'));
373             $_->set_att(h => $LineHeight);
374             }
375              
376             $_->root->del_att('#widths');
377             $_->root->set_att('#total_width' => undef);
378             $_->root->set_att('#total_cols' => undef);
379             $_->insert_new_elt(last_child => row => { h => $LineHeight });
380             $_->erase;
381             },
382             ol => sub {
383             my $count = 1;
384             foreach my $child ($_->descendants('counter')) {
385             $child->set_tag('textbox');
386             $child->set_text("$count. ");
387             $count++;
388             }
389             $_->insert_new_elt(last_child => row => { h => $LineHeight });
390             $_->erase;
391             },
392             br => sub {
393             $_->insert_new_elt(last_child => row => { h => $LineHeight });
394             $_->erase;
395             },
396             ul => sub {
397             foreach my $child ($_->descendants('counter')) {
398             $child->set_tag('textbox');
399             $child->set_text("* ");
400             }
401             $_->insert_new_elt(last_child => row => { h => $LineHeight });
402             $_->erase;
403             },
404             dl => sub {
405             foreach my $child ($_->descendants('counter')) {
406             $child->delete;
407             }
408             $_->insert_new_elt(last_child => row => { h => $LineHeight });
409             $_->erase;
410             },
411             td => \&_td,
412             th => \&_td,
413             font => sub {
414             $_->del_att('face');
415              
416             if ($_->att_names) {
417             $_->set_att(face => $Font);
418             $_->erase; # XXX
419             }
420             else {
421             $_->erase;
422             }
423             },
424             var => sub {
425              
426             # XXX - Proper variable support
427             },
428             _default_ => sub {
429             $_->erase if +IgnoreTags->{ $_->tag };
430             $_->delete if +DeleteTags->{ $_->tag };
431             }
432             },
433             pretty_print => 'indented',
434             empty_tags => 'html',
435             start_tag_handlers => {
436             _all_ => sub {
437             if (my $h = $_->att('size')) {
438             $_->set_att(h => $LineHeight + (2 * ($h - 4)));
439             }
440             if (my $bgcolor = $_->att('bgcolor')) {
441             $_->set_att(bgcolor => _to_color($bgcolor));
442             }
443             $_->del_att(
444             qw(
445             color bordercolor bordercolordark bordercolorlight
446             cellpadding cellspacing size href
447             )
448             );
449             },
450             }
451             );
452              
453             sub _set {
454             my ($key, $value, $elt) = @_;
455             my $att = $elt->root->att("#$key") || {};
456             $att->{ $elt->parent } = $value;
457             $elt->root->set_att("#$key", $att);
458             }
459              
460             sub _get {
461             my ($key, $elt) = @_;
462             my $att = $elt->root->att("#$key") || {};
463             return $att->{$elt};
464             }
465              
466             sub _p {
467             my @children;
468             foreach my $child ($_->children) {
469             +InlineTags->{ $child->tag } or last;
470             push @children, $child->cut;
471             }
472              
473             if (@children) {
474             my $textbox = $_->insert_new_elt(
475             before => textbox => {
476             w => (($_->tag eq 'p') ? '100%' : '97%'),
477             align => $_->att('align')
478             },
479             );
480             $textbox->wrap_in('row');
481             if ($_->tag eq 'li') {
482             $textbox->insert_new_elt(
483             before => counter => { w => '3%', align => 'right' });
484             }
485             foreach my $child (@children) {
486             $child->paste(last_child => $textbox);
487             $child->set_text(
488             join(' ',
489             grep { length and $_ ne 1 } split(/\n+/, $child->text))
490             );
491             }
492              
493             my $font = _get(font => $_);
494              
495             if ($textbox->text =~ /[^\x00-\x7f]/) {
496             $font = $FontUnicode;
497             }
498             elsif ($_->parent('i') and $_->parent('b')) {
499             $font ||= $FontBoldOblique;
500             }
501             elsif ($_->parent('i')) {
502             $font ||= $FontOblique;
503             }
504             elsif ($_->parent('b')) {
505             $font ||= $FontBold;
506             }
507              
508             my %attr;
509             $attr{face} = $font if $font;
510             if (_get(underline => $_)) {
511             my $align = $textbox->att('align');
512             $align .= '_underline';
513             $textbox->del_att('align');
514              
515             require PDF::FromHTML::Template::Constants;
516             $PDF::FromHTML::Template::Constants::Verify{ALIGN}{$align} = 1
517             if %PDF::FromHTML::Template::Constants::Verify;
518             $attr{align} = $align;
519             }
520              
521             $textbox->wrap_in('font' => \%attr) if %attr;
522             }
523              
524             $_->insert_new_elt(first_child => 'textbox')->wrap_in('row')
525             if $_->tag eq 'p';
526             $_->erase;
527             }
528              
529             sub _td {
530             return $_->erase if $_->descendants('row');
531              
532             $_->set_tag('textbox');
533              
534             if (my $font = _get(font => $_)) {
535             $_->wrap_in(font => { face => $font });
536             }
537              
538             my $cols = $_->parent->att('_cols') || 0;
539              
540             no warnings 'uninitialized';
541             if ($_->att('colspan') <= 1 and my $width = $_->att('width')) {
542             my $table_width = $_->root->att('#total_width') || 100;
543             my $cell_width = _percentify($width, int($table_width * $PageWidth / 100));
544             # Register us in the width table
545             my $widths = $_->root->att('#widths');
546             if (!$widths) {
547             $widths = [];
548             $_->root->set_att('#widths', $widths);
549             }
550             # warn "[$cols] = $widths->[$cols] vs $cell_width\n";
551             $widths->[$cols] = $cell_width if $widths->[$cols] < $cell_width;
552             }
553              
554             $cols += ($_->att('colspan') || 1);
555             $_->parent->set_att(_cols => $cols);
556             $_->root->set_att('#total_cols', $cols)
557             if $_->root->att('#total_cols') < $cols;
558              
559             if (my $rowspan = $_->att('rowspan')) {
560              
561             # ok, we can't really do this.
562             # what we can do, though, is to add 'fake' cells in the next row.
563             our @RowSpan;
564             foreach my $i (1 .. ($rowspan - 1)) {
565             $RowSpan[$i][$cols] = $_->atts;
566             }
567             }
568             }
569            
570             sub _percentify {
571             my $num = shift or return '100';
572             my $total_width = shift or Carp::confess( '100') ;
573             return $1 if $num =~ /(\d+)%/;
574             return int($num / $total_width * 100);
575             }
576              
577             sub _type {
578             my ($val, $elt) = @_;
579             return first { $_ eq $val } grep defined, map $elt->att($_), qw(type class);
580             }
581              
582             sub _to_color {
583             my ($color) = @_;
584              
585             if ($color !~ s/^#//) {
586             $color = Graphics::ColorNames->new('Netscape')->hex($color);
587             }
588              
589             return join ',', hex2tuple($color);
590             }
591              
592             1;
593              
594             =head1 AUTHORS
595              
596             唐鳳 Ecpan@audreyt.orgE
597              
598             =head1 CC0 1.0 Universal
599              
600             To the extent possible under law, 唐鳳 has waived all copyright and related
601             or neighboring rights to PDF-FromHTML.
602              
603             This work is published from Taiwan.
604              
605             L
606              
607             =cut