File Coverage

blib/lib/Excel/Template/XLSX.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Excel::Template::XLSX;
2            
3 1     1   13552 use strict;
  1         2  
  1         35  
4 1     1   3 use warnings;
  1         1  
  1         24  
5 1     1   3 use base 'Excel::Writer::XLSX';
  1         8  
  1         1288  
6            
7 1     1   159320 use version; our $VERSION = version->declare("v1.0.3");
  1         1377  
  1         4  
8            
9 1     1   73 use Archive::Zip;
  1         2  
  1         47  
10 1     1   566 use Graphics::ColorUtils 'rgb2hls', 'hls2rgb';
  1         5370  
  1         89  
11 1     1   9 use Scalar::Util 'openhandle';
  1         2  
  1         38  
12 1     1   230 use XML::Twig;
  0            
  0            
13            
14             1; # Return True from module
15            
16             ###############################################################################
17             sub __podhead {
18            
19             =for pod
20            
21             =head1 NAME
22            
23             Excel-Template-XLSX - Create Excel .xlsx files starting from (one or more) template(s).
24            
25             =head1 SYNOPSIS
26            
27             use Excel::Template::XLSX;
28             my ($self, $workbook) = Excel::Template::XLSX->new('perl.xlsx', 'template1.xlsx', 'template2.xlsx', ...);
29             $self->parse_template();
30            
31             # Add a worksheet, ... and anything else you would do with Excel::Writer::XLSX
32             $worksheet = $workbook->add_worksheet();
33            
34             =head1 DESCRIPTION
35            
36             This module is a companion to
37             L(EWX), or
38             if you prefer, a wrapper to that module. It uses EWX as a base class. It opens
39             an existing spreadsheet file (.xlsx format), and also creates a new EWX object.
40             As it parses the template file(s), it calls EWX methods to re-create the template
41             contents in the EWX object.
42            
43             When parsing is complete, the workbook object is left open for the calling perl
44             script to add additional content.
45            
46             The purpose of this module is to separate the roles of content/presentation vs
47             programming in an Excel document, in much the same way that HTML templating
48             engines work. A user who is knowledgeable in Excel can create an Excel file for
49             use as a template, without requiring the skill set of Perl or
50             Excel::Writer::XLSX. Conversely, the Perl programmer who is creating dynamic
51             content does not need design skills to layout the presentation in the template.
52            
53             =head1 WHAT IT CAN DO
54            
55             Cell Values (strings, numbers, dates, ... )
56             Cell Formulas
57             Cell Hyperlinks
58            
59             Cell Formatting (borders, shading, fonts, font sizes, colors)
60            
61             Column Widths
62             Row Widths
63            
64             Headers and Footers
65            
66             Simple template variables (via callback) See L
67            
68             =head1 WHAT IT CAN NOT DO
69            
70             Excel::Template::Excel can not modify Excel files in place! It is not
71             intended to. Since the parser directly adds content to the EWX workbook object
72             as the contents are parsed, both the template, and the output file must
73             be open at the same time.
74            
75             It may be possible to open the output file to a file handle, and
76             after parsing of the template is complete, write the contents of file
77             over the template. The author has not tried this.
78            
79             It is not the design of this module to faithfully re-create the entire
80             contents of the template file in the EWX output. If you are using this
81             module to rewrite Excel files, you are on your own.
82            
83             These items are completely dropped from the output file:
84            
85             Images in the Sheet
86             Images in Headers/Footers
87             Charts
88             Shapes
89             Themes (gradients, fonts, fills, styles)
90             macros
91             modules (vba code)
92            
93             And probably other things. See the tests (t directory of the distribution)
94             for examples of what does work.
95            
96             =head1 SUBROUTINES AND METHODS
97            
98             =head2 __podhead
99            
100             Dummy subroutine to allow me to hide this pod documentation when using code
101             folding in the editor.
102            
103             =cut
104            
105             }
106             ###############################################################################
107             sub new {
108            
109             =head2 new
110            
111             Creates a new Excel::Template::XLSX object, and also creates a new
112             Excel::Writer::XLSX object. A workbook object is created for the output file.
113            
114             Returns the Template object, and the workbook object. Workbook object is also
115             available as $self->{EWX}; If the caller is only expecting a single
116             return value, then just the $self object is returned.
117            
118             =cut
119            
120             my ( $class, $output_file, @template_files ) = @_;
121             my $self = {
122             FORMATS => [],
123             HYPERLINKS => {},
124             NEED_PROPS => 1,
125             PRINT_AREA => {},
126             PRINT_TITLES => {},
127             SHARED_STRINGS => [],
128             THEMES => [],
129             ZIP => [],
130            
131             template_callback => undef,
132             };
133            
134             # Create a new Excel workbook
135             $self->{EWX} = Excel::Writer::XLSX->new($output_file);
136             if ( defined $self->{EWX} ) {
137             $self->{DEFAULT_FORMAT} = $self->{EWX}->add_format();
138             bless $self, $class;
139             }
140             else {
141             die
142             "Can't create new Excel::Writer::XLSX object using file ($output_file) $!";
143             }
144            
145             foreach my $template_file (@template_files) {
146             my $zip = Archive::Zip->new;
147             if ( openhandle($template_file) ) {
148             bless $template_file, 'IO::File'
149             if ref($template_file) eq 'GLOB'; # sigh
150             my $status = $zip->readFromFileHandle($template_file);
151             unless ( $status == Archive::Zip::AZ_OK ) {
152             warn "Can't open filehandle as a zip file, skipping";
153             $zip = undef;
154             }
155             }
156             elsif ( !ref($template_file) ) {
157             my $status = $zip->read($template_file);
158             unless ( $status == Archive::Zip::AZ_OK ) {
159             $template_file //= '(undef)';
160             warn "Can't open file '$template_file' as a zip file, skipping";
161             $zip = undef;
162             }
163             }
164             else {
165             warn
166             "Argument to 'new' must be a filename or open filehandle. skipping $template_file";
167             $zip = undef;
168             }
169            
170             # Create a list of template files to add to the workbook
171             push @{ $self->{ZIP} }, $zip;
172             }
173             if (wantarray) {
174             return ( $self, $self->{EWX} );
175             }
176             else {
177             return $self;
178             }
179             }
180             ###############################################################################
181             sub parse_template {
182            
183             =head2 parse_template
184            
185             Parses common elements of the Spreaadsheet, such as themes, styles, and strings.
186             These are stored in the main object ($self).
187            
188             Finds each sheet in the workbook, and initiates parsing of each sheet.
189            
190             Properties for the created workbook are set from the first template that has
191             properties. Properties in subsequent workbooks are ignored.
192            
193             =cut
194            
195             my $self = shift;
196            
197             my $remap = {
198             title => 'title',
199             subject => 'subject',
200             creator => 'author',
201             keywords => 'keywords',
202             description => 'comments',
203            
204             manager => 'manager',
205             company => 'company',
206             category => 'category',
207             status => 'status',
208             };
209            
210             # foreach my $zip ( @{ $self->{ZIP} } ) {
211             for my $z ( 0 .. $#{ $self->{ZIP} } ) {
212             my $zip = $self->{ZIP}[$z] // next;
213             $self->{PRINT_TITLES} = {};
214             $self->{SHARED_STRINGS} = [];
215             $self->{FORMATS} = [];
216            
217             my $files = $self->_extract_files($zip);
218            
219             my $callback = $self->{template_callback};
220             my $call = ref($callback) eq 'CODE';
221             if ( $self->{NEED_PROPS} ) {
222             if ( my @core_nodes
223             = $files->{core}->find_nodes('//cp:coreProperties') )
224             {
225             my $core = shift @core_nodes;
226             my %hash = map {
227             my $prop = $core->first_child( "dc:" . $_ )
228             // $core->first_child( "cp:" . $_ );
229             my %pair = ();
230             if ($prop) {
231             my $text = $prop->text();
232             $call and $self->$callback( \$text );
233             %pair = ( $remap->{$_}, $text );
234             }
235             %pair;
236             } keys %$remap;
237             $self->{EWX}->set_properties(%hash);
238             $self->{NEED_PROPS} = 0;
239             }
240             }
241            
242             $self->{THEMES}
243             = $self->_parse_themes( ( values %{ $files->{themes} } )[0] );
244            
245             $self->_parse_styles( $files->{styles} );
246             $self->_parse_shared_strings( $files->{strings} );
247            
248             # Defined Names (includes print area, print titles)
249             map {
250             my $name = $_->att('name') // '';
251             my $address = $_->text();
252            
253             # Print Titles (may contain none, one, or both. Delimited by comma if both supplied)
254             # e.g. Title_Page!$A:$A
255             if ( $name eq '_xlnm.Print_Titles' ) {
256             my @title = split( ',', $address );
257             foreach (@title) {
258             my ( $sheet_name, $range ) = split('!');
259             push @{ $self->{PRINT_TITLES}{$sheet_name} }, $range;
260             }
261            
262             # Print Area (Save it until sheets are processed)
263             }
264             elsif ( $name eq '_xlnm.Print_Area' ) {
265             my @title = split( ',', $address );
266             my ( $sheet_name, $range ) = split( '!', $address );
267             $self->{PRINT_AREA}{$sheet_name} = $range;
268             }
269             else {
270             $self->{EWX}->define_name( $name, $address );
271             }
272             } $files->{workbook}->find_nodes('//definedNames/definedName');
273            
274             # Sheets: Add a worksheet for each sheet in workbook
275             # Rename sheet if workbook already has a sheet by that name
276             my @sheet_names = @{ $self->{EWX}{_sheetnames} };
277             map {
278             my $name = $_->att('name');
279             my $test = $name;
280             for ( my $i = 1;; $i++ ) {
281             last unless grep( /^${test}$/, @sheet_names );
282             $test = $name . "($i)";
283             }
284             my $sheet = $self->{EWX}->add_worksheet($test);
285            
286             my $range = $self->{PRINT_AREA}{$name};
287             $sheet->print_area($range) if $range;
288            
289             foreach my $range ( @{ $self->{PRINT_TITLES}{$name} } ) {
290            
291             # Row Range like $1:$1
292             $sheet->repeat_rows($range) if $range =~ m/\d/;
293            
294             # Column Range like $A:$A
295             $sheet->repeat_columns($range) if $range =~ m/[A-Za-z]/;
296             }
297            
298             # Parse the contents of the sheet
299             my $idx = $_->att('r:id');
300             $self->_parse_sheet( $sheet, $files->{sheets}{$idx} );
301             } $files->{workbook}->find_nodes('//sheets/sheet');
302             $self->{ZIP}[$z] = undef;
303             }
304             }
305             ###############################################################################
306             sub template_callback {
307            
308             =head2 template_callback
309            
310             Place holder method for a callback routine to modify the content of the template
311             before being written to the output spreadsheet.
312            
313             This callback is activated for all shared string (both plain and rich text
314             strings), and also for header/footer text.
315            
316             The callback is supplied with the two parameters: The object name (since this is
317             a method), and the text to be processed. This is passed as a reference to single
318             scalar.
319            
320             This method is called numerous times during processing (e.g. once for each
321             unique string in the spreadsheet, so the user is advised to keep it efficient.
322            
323             This callback approach does not force any particular templating system on the
324             user. They are free to use whatever system they choose.
325            
326             Note that templating can only do simple scalars. Complex templating (if-then-
327             else, loops, etc) do not make sense in that the callback is supplied with the
328             contents of a single cell. Having said that, remember that the full power of
329             Excel::Writer::XLSX is available to the user to modify the template after it is
330             processed.
331            
332             # A snippet of code to replace [% template %] in the
333             # template spreadsheet with 'Output'
334            
335             my ($self, $wbk) = Excel::Template::XLSX->new($output_xlsx, $template_xlsx);
336            
337             use Template::Tiny;
338             my $template = Template::Tiny->new( TRIM => 1 );
339             $self->{template_callback} = sub {
340             my ($self, $textref) = @_;
341             $template->process($textref, { template => 'Output' }, $textref);
342             };
343            
344             $self->parse_template();
345            
346             =cut
347            
348             my $self = shift;
349             my ($text) = @_;
350             }
351             ###############################################################################
352             sub _apply_tint {
353            
354             =head2 _apply_tint
355            
356             Applies tinting to a color object, if the tint attribute is encountered in
357             parsing.
358            
359             =cut
360            
361             my $self = shift;
362             my ( $color, $tint ) = @_;
363            
364             my ( $r, $g, $b ) = map { oct("0x$_") } $color =~ /#(..)(..)(..)/;
365             my ( $h, $l, $s ) = rgb2hls( $r, $g, $b );
366            
367             if ( $tint < 0 ) {
368             $l = $l * ( 1.0 + $tint );
369             }
370             else {
371             $l = $l * ( 1.0 - $tint ) + ( 1.0 - 1.0 * ( 1.0 - $tint ) );
372             }
373            
374             return scalar hls2rgb( $h, $l, $s );
375             }
376             ###############################################################################
377             sub _base_path_for {
378            
379             =head2 _base_path_for
380            
381             Manipulates the path to a member in the zip file, to find the associated
382             rels file.
383            
384             =cut
385            
386             my $self = shift;
387             my ($file) = @_;
388            
389             my @path = split '/', $file;
390             pop @path;
391            
392             return join( '/', @path ) . '/';
393             }
394             ###############################################################################
395             sub _cell_to_row_col {
396            
397             =head2 _cell_to_row_col
398            
399             Converts an A1 style cell reference to a row and column index.
400            
401             =cut
402            
403             my $self = shift;
404             my $cell = shift;
405            
406             my ( $col, $row ) = $cell =~ /([A-Z]+)([0-9]+)/;
407            
408             my $ncol = 0;
409             for my $char ( split //, $col ) {
410             $ncol *= 26;
411             $ncol += ord($char) - ord('A') + 1;
412             }
413             $ncol = $ncol - 1;
414             my $nrow = $row - 1;
415             return ( $nrow, $ncol );
416             }
417             ###############################################################################
418             sub _color {
419            
420             =head2 _color
421            
422             Parses color element (rgb, index, theme, and tint)
423            
424             =cut
425            
426             my $self = shift;
427             my ( $color_node, $fill ) = @_;
428            
429             my $themes = $self->{THEMES};
430             my $color;
431             if ( $color_node && !$color_node->att('auto') ) {
432             my $rgb = $color_node->att('rgb');
433             my $theme = $color_node->att('theme');
434             my $index = $color_node->att('indexed');
435             my $tint = $color_node->att('tint');
436            
437             # see https://rt.cpan.org/Public/Bug/Display.html?id=93065 (still needed for XLSX??)
438             # defined $index and $color = ($fill && $index == 64) ? '#FFFFFF' : $index;
439             $rgb and $color = '#' . substr( $rgb, 2, 6 );
440             defined $theme and $color = '#' . $themes->{Color}[$theme];
441             $tint and $color = $self->_apply_tint( $color, $tint );
442             }
443             return $color;
444             }
445             ###############################################################################
446             sub _extract_files {
447            
448             =head2 _extract_files
449            
450             Called by parse_template to fetch the xml strings from the zip file. XML
451             strings are parsed, except for worksheets. Individual worksheets are
452             parsed separately.
453            
454             =cut
455            
456             my $self = shift;
457             my ($zip) = @_;
458            
459             my $type_base
460             = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
461            
462             my $rels = $self->_parse_xml( $zip, $self->_rels_for('') );
463            
464             my $node = qq;
465             my $wb_name = ( $rels->find_nodes($node) )[0]->att('Target');
466             my $wb_xml = $self->_parse_xml( $zip, $wb_name );
467            
468             my $path_base = $self->_base_path_for($wb_name);
469             my $wb_rels = $self->_parse_xml( $zip, $self->_rels_for($wb_name) );
470            
471             my $string_xpath = qq;
472             my ($strings_xml) = map {
473             $zip->memberNamed( $path_base . $_->att('Target') )->contents
474             } $wb_rels->find_nodes($string_xpath);
475            
476             my $style_xpath = qq;
477             my $style_target
478             = ( $wb_rels->find_nodes($style_xpath) )[0]->att('Target');
479             my $styles_xml = $self->_parse_xml( $zip, $path_base . $style_target );
480            
481             my %sheet_rels;
482             my $wks_xpath = qq;
483             my %worksheet_xml = map {
484            
485             my $sheet_file = $path_base . $_->att('Target');
486             my $rels_file = $self->_rels_for($sheet_file);
487             my $sheet_rels = '';
488             if ( $zip->memberNamed($rels_file) ) {
489             $sheet_rels = $self->_parse_xml( $zip, $rels_file );
490             }
491            
492             if ( my $contents = $zip->memberNamed($sheet_file)->contents ) {
493             ( $_->att('Id') => { 'xml' => $contents, 'rels' => $sheet_rels } );
494             }
495            
496             } $wb_rels->find_nodes($wks_xpath);
497            
498             my %themes_xml = map {
499             $_->att('Id') =>
500             $self->_parse_xml( $zip, $path_base . $_->att('Target') )
501             } $wb_rels->find_nodes(qq);
502            
503             my $core_base
504             = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata';
505             my $core_full = qq;
506             my $core_name = ( $rels->find_nodes($core_full) )[0]->att('Target');
507             my $core_xml = $self->_parse_xml( $zip, $core_name );
508            
509             return {
510             workbook => $wb_xml,
511             styles => $styles_xml,
512             sheets => \%worksheet_xml,
513             themes => \%themes_xml,
514             core => $core_xml,
515             ( $strings_xml ? ( strings => $strings_xml ) : () ),
516             };
517             }
518             ###############################################################################
519             sub _parse_alignment {
520            
521             =head2 _parse_alignment
522            
523             Parses horizontal and vertical cell alignments in a sheet.
524            
525             =cut
526            
527             my $self = shift;
528             my ($node) = @_;
529            
530             my %align_map = (
531             horizontal => 'align',
532             vertical => 'valign',
533             textRotation => 'rotation',
534             indent => 'indent',
535             wrapText => 'text_wrap',
536             shrinkToFit => 'shrink',
537             );
538             my %align = ();
539             if ( my $alignment = $node->first_child('alignment') ) {
540             map {
541             my $v = $alignment->att($_);
542             if ( defined $v ) {
543             $v = 'vcenter' if ( $_ eq 'vertical' ) and ( $v eq 'center' );
544             $align{ $align_map{$_} } = $v;
545             }
546             } keys %align_map;
547             }
548             return %align;
549             }
550             ###############################################################################
551             sub _parse_borders {
552            
553             =head2 _parse_borders
554            
555             Parses cell borders and diagonal borders.
556            
557             =cut
558            
559             my $self = shift;
560             my ($styles) = @_;
561            
562             my $borders = [];
563             my %border_map = (
564             dashDot => 9,
565             dashDotDot => 11,
566             dashed => 3,
567             dotted => 4,
568             double => 6,
569             hair => 7,
570             medium => 2,
571             mediumDashDot => 10,
572             mediumDashDotDot => 12,
573             mediumDashed => 8,
574             none => 0,
575             slantDashDot => 13,
576             thick => 5,
577             thin => 1,
578             );
579             push @$borders, map {
580             my $border = $_;
581            
582             # XXX specs say "begin" and "end" rather than "left" and "right",
583             # but... that's not what seems to be in the file itself (sigh)
584            
585             my %colors = ();
586             map {
587             my $color
588             = $self->_color(
589             $border->first_child($_)->first_child('color') );
590             $colors{ $_ . '_color' } = $color if $color;
591             } qw(left right top bottom);
592            
593             my %types = ();
594             map {
595             my $style = $border->first_child($_)->att('style');
596             $types{$_} = $border_map{$style} if $style;
597             } qw(left right top bottom);
598            
599             my %diag = ();
600             my $down = $border->att('diagonalDown') // 0;
601             my $up = $border->att('diagonalUp') // 0;
602             $diag{'diag_type'} = 2 * $down + $up if $down + $up;
603             my $dborder = $border->first_child('diagonal')->att('style');
604             $diag{'diag_border'} = $border_map{$dborder} if $dborder;
605             my $dcolor = $border->first_child('diagonal')->first_child('color');
606             $diag{'diag_color'} = $self->_color($dcolor) if $dcolor;
607            
608             my $border_ref = { %colors, %types, %diag };
609             } $styles->find_nodes('//borders/border');
610             return $borders;
611             }
612             ###############################################################################
613             sub _parse_fills {
614            
615             =head2 _parse_fills
616            
617             Parses styles for cell fills (pattern, foreground and background colors.
618             horizontal and horizontal and vertical cell alignments in a sheet.
619            
620             Gradients are parsed, but since EWX does not support gradients, a
621             pattern is substituted.
622            
623             =cut
624            
625             my $self = shift;
626             my ($styles) = @_;
627             my %fill_map = (
628             darkDown => 7,
629             darkGray => 3,
630             darkGrid => 9,
631             darkHorizontal => 5,
632             darkTrellis => 10,
633             darkUp => 8,
634             darkVertical => 6,
635             gray0625 => 18,
636             gray125 => 17,
637             lightDown => 13,
638             lightGray => 4,
639             lightGrid => 15,
640             lightHorizontal => 11,
641             lightTrellis => 16,
642             lightUp => 14,
643             lightVertical => 12,
644             mediumGray => 2,
645             none => 0,
646             solid => 1,
647             );
648            
649             # Pattern Fills / # Gradient Fills
650             # EWX does not support Gradient fills (yet??)
651             # so, substitute a pattern fill to keep indices aligned
652             my $fills = [];
653             push @$fills, map {
654             my ( $fill, @color );
655             my $pat = $_->first_child('patternFill');
656             if ($pat) {
657             for (qw[fg bg]) {
658             my $fgbg = $self->_color( $pat->first_child("${_}Color"), 1 );
659             push @color, ( "${_}_color", $fgbg ) if $fgbg;
660             }
661             $fill = { pattern => $fill_map{ $pat->att('patternType') }, @color };
662             }
663             my $gradient = $_->first_child('gradientFill');
664             if ($gradient) {
665             my @stop_colors = $gradient->find_nodes('stop/color');
666             my $fg = $self->_color( $stop_colors[0], 1 );
667             my $bg = $self->_color( $stop_colors[1], 1 );
668             my %hfg = ( 'fg_color' => ( $fg // 'white' ) );
669             my %hbg = ( 'bg_color' => ( $bg // 'black' ) );
670            
671             ### ?? Create a lightGrid pattern in place of a gradient for now
672             $fill = { pattern => $fill_map{'lightGrid'}, %hfg, %hbg };
673             }
674             $fill;
675             } $styles->find_nodes('//fills/fill');
676             $fills;
677             }
678             ###############################################################################
679             sub _parse_fonts {
680            
681             =head2 _parse_fonts
682            
683             Parses font information (font name, size, super/sub scripts, alignment
684             colors, underline, bold, italic, and strikeout attributes).
685            
686             =cut
687            
688             my $self = shift;
689             my ( $styles, $xpath ) = @_;
690             $xpath //= '//fonts/font';
691            
692             my $fonts = [];
693             @$fonts = map {
694            
695             my $u = $_->first_child('u');
696             my $vert = $_->first_child('vertAlign');
697             my $font;
698            
699             my $size = $_->first_child('sz')->att('val');
700             $font->{'size'} = $size if $size;
701            
702             # XXX if color tag is missing is it black?? '#000000'
703             my $color = $_->first_child('color');
704             $font->{'color'} = $self->_color($color) if $color;
705            
706             my $script_map = {
707             'superscript' => 1,
708             'subscript' => 2,
709             };
710            
711             if ( defined $vert ) {
712             my $script = $vert->att('val');
713             $font->{'font_script'} = $script_map->{$script} if $script;
714             }
715            
716             my $u_map = {
717             'single' => 1,
718             'double' => 2,
719             'singleAccounting' => 33,
720             'doubleAccounting' => 34,
721             };
722             if ( defined $u ) {
723            
724             # XXX sometimes style xml files can contain just with no
725             # val attribute. i think this means single underline, but not sure
726             my $underline = $u->att('val') // 'single';
727             $font->{'underline'} = $u_map->{$underline} if $underline;
728             }
729            
730             my $font_name = $_->first_child('name');
731             $font->{'font'} = $font_name->att('val') if $font_name;
732            
733             # Alternate for rich strings (embedded font)
734             my $rFont = $_->first_child('rFont');
735             $font->{'font'} = $rFont->att('val') if $rFont;
736            
737             my $bold = $_->first_child('b');
738             $font->{'bold'} = 1 if $bold;
739            
740             my $italic = $_->first_child('i');
741             $font->{'italic'} = 1 if $italic;
742            
743             my $strike = $_->first_child('strike');
744             $font->{'font_strikeout'} = 1 if $strike;
745            
746             $font;
747             } $styles->find_nodes($xpath);
748             return $fonts;
749             }
750             ###############################################################################
751             sub _parse_numbers {
752            
753             =head2 _parse_numbers
754            
755             Parses styles for cell number formats (financial, decimal, exponential, date-time, ...)
756            
757             =cut
758            
759             my $self = shift;
760             my ($styles) = @_;
761             my $number_format = { 0 => {} };
762             map {
763             my $id = $_->att('numFmtId') // 0;
764            
765             # defaults are from
766             #http://social.msdn.microsoft.com/Forums/en-US/oxmlsdk/thread/e27aaf16-b900-4654-8210-83c5774a179c
767             # Defaults do not need to be re-created.
768             my $code = $_->att('formatCode') // $id;
769             $number_format->{$id} = { num_format => $code } if $id;
770             } $styles->find_nodes('//numFmts/numFmt');
771             return $number_format;
772             }
773             ###############################################################################
774             sub _parse_protection {
775            
776             =head2 _parse_protection
777            
778             Parses locked and hidden attributes for a cell. These are only
779             useful if the worksheet is locked.
780            
781             This module does not lock the workbook or the worksheet.
782            
783             =cut
784            
785             my $self = shift;
786             my ($node) = @_;
787             my @protection = qw(locked hidden);
788             my %prot = ();
789             if ( my $protection = $_->first_child('protection') ) {
790             map {
791             my $v = $protection->att($_);
792             $prot{$_} = $v if defined $v;
793             } @protection;
794             }
795             return %prot;
796             }
797             ###############################################################################
798             sub _parse_shared_strings {
799            
800             =head2 _parse_shared_strings
801            
802             Parses the shared strings file. Excel does not directly store
803             string values with the cell, but stores an index into the shared
804             strings table instead, to save memory, if a string value is
805             referenced more than once. Shared strings also contain
806             formatting if multiple formats are applied within a cell (See
807             write_rich_string in EWX.
808            
809             =cut
810            
811             my $self = shift;
812             my ($strings) = @_;
813            
814             return unless $strings;
815             my $xml = XML::Twig->new(
816             twig_handlers => {
817             'si' => sub {
818             my ( $twig, $si ) = @_;
819            
820             my $callback = $self->{template_callback};
821             my $call = ref($callback) eq 'CODE';
822            
823             # plain text strings
824             my $t = $si->first_child('t');
825             if ($t) {
826             my $text = $t->text();
827             $call and $self->$callback( \$text );
828             push @{ $self->{SHARED_STRINGS} }, $text;
829             }
830            
831             # rich text strings; String item (si) with multiple
832             # text elements, with optional formatting
833             my $rich = [];
834             for my $r ( $si->find_nodes('r') ) {
835             my $text = $r->first_child('t')->text();
836             $call and $self->$callback( \$text );
837             my $rPr = $r->first_child('rPr');
838            
839             if ($rPr) {
840             my $xml = $r->first_child('rPr')->outer_xml();
841             my $twig = XML::Twig->parse($xml);
842             my $fonts = $self->_parse_fonts( $twig, '//rPr' );
843             my $format = $self->{EWX}->add_format( %{ $fonts->[0] } );
844             push @$rich, $format, $text;
845             }
846             else {
847             push @$rich, $text;
848             }
849             }
850             push( @{ $self->{SHARED_STRINGS} }, $rich ) if scalar(@$rich);
851             $twig->purge;
852             }
853             }
854             ); # } twig_handlers ) new
855             $xml->parse($strings);
856             }
857             ###############################################################################
858             sub _parse_sheet {
859            
860             =head2 _parse_sheet
861            
862             Parses an individual worksheet. This is done in two passes.
863             See _parse_sheet_pass1 and _parse_sheet_pass2 for what elements are
864             parsed. This is necessary because the parse order of XML::Twig callbacks
865             are in the wrong order for some sheet information (header/footer information,
866             hyperlinks, and merged cells).
867            
868             =cut
869            
870             my $self = shift;
871             my ( $sheet, $sheet_file ) = @_;
872            
873             # Hyperlinks are local to each sheet
874             $self->{HYPERLINKS} = {};
875             my $pass1
876             = XML::Twig->new( twig_roots => $self->_parse_sheet_pass1($sheet) );
877             $pass1->parse( $sheet_file->{xml} );
878            
879             # Half time show - track down the URLs for hyperlinks found in pass 1
880             while ( my ( $a1, $rid ) = each %{ $self->{HYPERLINKS} } ) {
881             my $xpath = qq;
882             my $url = ( $sheet_file->{rels}->find_nodes($xpath) )[0];
883             if ($url) {
884             my $target = $url->att('Target');
885             my $mode = lc( $url->att('TargetMode') );
886             $self->{HYPERLINKS}{$a1} = "$target";
887             }
888             }
889            
890             # 2nd pass: cell/row building is dependent on having parsed the merge definitions
891             # beforehand. Also header/footer margins must be parsed before setting header/footer
892             my $pass2
893             = XML::Twig->new( twig_roots => $self->_parse_sheet_pass2($sheet) );
894             $pass2->parse( $sheet_file->{xml} );
895             }
896             ###############################################################################
897             sub _parse_sheet_pass1 {
898            
899             =head2 _parse_sheet_pass1
900            
901             Parses some elements in a worksheet ( pageMargins, headerFooter,
902             hyperlinks, pageSetup, Merged Cells, Sheet Formatting Row and Column
903             heights, Sheet selection, and Tab Color)
904            
905             =cut
906            
907             my $self = shift;
908             my ($sheet) = @_;
909            
910             my $default_row_height = 15;
911             my $default_column_width = 10;
912             my %hf_margin;
913            
914             return {
915             'pageMargins' => sub {
916             my ( $twig, $margin ) = @_;
917             map {
918             my $method = "set_margin_" . $_;
919             $sheet->$method( $margin->att($_) // 0 );
920             } qw( left right top bottom );
921            
922             # Capture header/footer margin, for use with headerFooter callback
923             $hf_margin{Header} = $margin->att('header');
924             $hf_margin{Footer} = $margin->att('footer');
925             $twig->purge;
926             },
927            
928             # Headers/Footers
929             'headerFooter' => sub {
930             my ( $twig, $hf ) = @_;
931            
932             my $callback = $self->{template_callback};
933             my $call = ref($callback) eq 'CODE';
934             for (qw[Header Footer]) {
935             my $child = $hf->first_child( 'odd' . $_ );
936             my $text = $child ? $child->text() : '';
937             $call and $self->$callback( \$text );
938             my $method = 'set_' . lc($_);
939             $sheet->$method( $text, $hf_margin{$_} );
940             }
941            
942             $twig->purge;
943             },
944            
945             # Hyperlinks
946             'hyperlinks/hyperlink ' => sub {
947             my ( $twig, $link ) = @_;
948             my $a1 = $link->att('ref');
949             $self->{HYPERLINKS}{$a1} = $link->att('r:id');
950             $twig->purge;
951             },
952            
953             # Paper/page setup
954             'pageSetup' => sub {
955             my ( $twig, $setup ) = @_;
956             my %lookup = (
957             orientation => => 'set_portrait',
958             firstPageNumber => 'set_start_page',
959             scale => 'set_print_scale',
960             paperSize => 'set_paper'
961            
962             # horizontalDpi ??
963             # verticalDpi
964             );
965            
966             my @page
967             = qw(scale orientation horizontalDpi verticalDpi paperSize firstPageNumber scale);
968             foreach (@page) {
969            
970             # Ignore if we do not have a EWX method for this attribute
971             my $method = $lookup{$_} // next;
972            
973             # Ignore if no value defined for this attribute
974             next unless my $set = $setup->att($_);
975            
976             # Special case; no generic method to set portrait/landscape
977             $method = 'set_landscape' if $set eq 'landscape';
978             $sheet->$method($set);
979             }
980            
981             $twig->purge;
982             },
983            
984             # Merged cells (Create the ranges: content will be added later)
985             'mergeCells/mergeCell' => sub {
986             my ( $twig, $merge_area ) = @_;
987            
988             if ( my $ref = $merge_area->att('ref') ) {
989             my ( $topleft, $bottomright ) = $ref =~ /([^:]+):([^:]+)/;
990             my ( $tr, $lc ) = $self->_cell_to_row_col($topleft);
991             my ( $br, $rc ) = $self->_cell_to_row_col($bottomright);
992            
993             # Merged Ranges/Areas (need to supply blank content and default format)
994             # content and formatting will be added from the parsed cell
995             $sheet->merge_range( $tr, $lc, $br, $rc, '',
996             $self->{DEFAULT_FORMAT} );
997             }
998             $twig->purge;
999             },
1000            
1001             # Default row height
1002             'sheetFormatPr' => sub {
1003             my ( $twig, $format ) = @_;
1004             $default_row_height //= $format->att('defaultRowHeight');
1005             $default_column_width //= $format->att('baseColWidth');
1006             $sheet->set_default_row($default_row_height);
1007             $twig->purge;
1008             },
1009            
1010             'col' => sub {
1011             my ( $twig, $col ) = @_;
1012            
1013             for my $ci ( $col->att('min') .. $col->att('max') ) {
1014             #set_column($first,$last,$width,$fmt,$hide,$level,$collapsed )
1015             $sheet->set_column( $ci - 1, $ci - 1, $col->att('width') );
1016             #?? just sets width, not $col->att('style')
1017             }
1018             $twig->purge;
1019             },
1020            
1021             'row' => sub {
1022             my ( $twig, $row ) = @_;
1023            
1024             # ?? just sets row height. No formatting yet
1025             # set_row( $row, $height, $format, $hidden, $level, $collapsed )
1026             $sheet->set_row( $row->att('r') - 1, $row->att('ht') );
1027             $twig->purge;
1028             },
1029            
1030             'sheetView/selection' => sub {
1031             my ( $twig, $selection ) = @_;
1032             my $range = $selection->att('sqref')
1033             // $selection->att('activeCell') // 'A:1';
1034             $sheet->set_selection($range);
1035             $twig->purge;
1036             },
1037            
1038             'sheetPr/tabColor' => sub {
1039             my ( $twig, $tab_color ) = @_;
1040             $sheet->set_tab_color( $tab_color->att('rgb') );
1041             $twig->purge;
1042             }
1043            
1044             } # return hashref
1045             }
1046             ###############################################################################
1047             sub _parse_sheet_pass2 {
1048            
1049             =head2 _parse_sheet_pass2
1050            
1051             Parses cell contents (first by row, then by column). Cells can contain
1052             inline strings, string references, direct string values, formulas,
1053             and hyperlinks. Each cell may also contain formatting information.
1054             The format is in an index to formatting for borders, shading, alignment,
1055             font, and number formats.
1056            
1057             =cut
1058            
1059             my $self = shift;
1060             my ($sheet) = @_;
1061            
1062             return {
1063             'sheetData/row' => sub {
1064             my ( $twig, $row_elt ) = @_;
1065             for my $cell ( $row_elt->children('c') ) {
1066             my $string_index = 0;
1067             my $a1 = $cell->att('r'); # Cell Address
1068             my $t = $cell->att('t') || 'n'; # Cell Type
1069             my $s = $cell->att('s'); # Cell String Index
1070             my $val_xml
1071             = $t eq 'inlineStr'
1072             ? $cell->first_child('is')->first_child('t')
1073             : $cell->first_child('v');
1074             my $val = $val_xml ? $val_xml->text() : undef;
1075            
1076             my $format_idx = $s || 0;
1077             my $format = $self->{FORMATS}[$format_idx];
1078            
1079             if ( !defined($val) ) {
1080             $val = '';
1081             }
1082             if ( $t eq 's' ) {
1083             $string_index = $val;
1084             $val = $self->{SHARED_STRINGS}[$val];
1085            
1086             # Special case for multiple formats in a cell
1087             # see _parse_shared_strings for rPr nodes
1088             if ( ref($val) eq 'ARRAY' ) {
1089             $sheet->write_rich_string( $a1, @$val );
1090             next;
1091             }
1092             if ( my $url = $self->{HYPERLINKS}{$a1} ) {
1093             $sheet->write_url( $a1, $url, $format, $val );
1094             next;
1095             }
1096             $sheet->write_string( $a1, $val, $format );
1097             next;
1098             }
1099             elsif ( $t eq 'str' ) {
1100             $val = '=' . $cell->first_child('f')->text();
1101             }
1102             elsif ( $t eq 'n' ) {
1103             if ( my $form_child = $cell->first_child('f') ) {
1104             my $isarray = $form_child->att('t');
1105             my $ref = $form_child->att('ref');
1106             my $formula = $form_child->text() // q[="No Formula Found"];
1107             if ($isarray) {
1108             $sheet->write_array_formula( $ref, "=${formula}",
1109             $format, $val );
1110             }
1111             else {
1112             $sheet->write_formula( $a1, "=${formula}", $format,
1113             $val );
1114             }
1115             next;
1116             }
1117            
1118             }
1119             elsif ( $t eq 'b' ) {
1120             $val = $val ? "TRUE" : "FALSE";
1121             }
1122             elsif ( $t eq 'e' ) {
1123             }
1124             elsif ( $t eq 'str' || $t eq 'inlineStr' ) {
1125             }
1126             else {
1127             warn "unimplemented type $t found in cell $a1"; # XXX
1128             }
1129            
1130             $sheet->write( $a1, $val, $format );
1131             }
1132            
1133             $twig->purge;
1134             }
1135             };
1136             }
1137             ###############################################################################
1138             sub _parse_styles {
1139            
1140             =head2 _parse_styles
1141            
1142             Parses style information.
1143             Parses number formats directly. Calls subroutines to parse
1144             fonts, fills, and borders, alignment, and protection.
1145            
1146             Finally, parses Cell Xfs elements to Combine fonts, borders, number formats,
1147             alignment, patterns, into a single format specification.
1148            
1149             Calls EWX add_formats to create a format, and stores the format information
1150             in a FORMAT array within the object.
1151            
1152             =cut
1153            
1154             my $self = shift;
1155             my ($styles) = @_;
1156            
1157             # Number Formats
1158             my $numfmt = $self->_parse_numbers($styles);
1159            
1160             # Fonts / Fills / Borders
1161             my $fonts = $self->_parse_fonts( $styles, '//fonts/font' );
1162             my $fills = $self->_parse_fills($styles);
1163             my $borders = $self->_parse_borders($styles);
1164            
1165             # Cell Xfs
1166             # Combine fonts, borders, number formats, alignment, patterns, into a single format spec
1167             map {
1168             # Also has applyAlignment property, which we do not examine
1169             # same for ApplyFont, ApplyBorder ApplyProtection
1170            
1171             my %halign = $self->_parse_alignment($_);
1172             my %hprot = $self->_parse_protection($_);
1173             my %hfont = %{ $fonts->[ $_->att('fontId') // 0 ] };
1174            
1175             my $numFmtId = $_->att('numFmtId') // 0;
1176            
1177             # Use custom format, or built-in if custom not found
1178             my $ref = $numfmt->{$numFmtId} // { num_format => $numFmtId };
1179             my %hnumfmt = %$ref;
1180            
1181             my %hbord = %{ $borders->[ $_->att('borderId') // 0 ] };
1182             my %hfill = %{ $fills->[ $_->att('fillId') // 0 ] };
1183            
1184             my $fmt
1185             = $self->{EWX}
1186             ->add_format( %hfont, %hnumfmt, %hbord, %halign, %hprot, %hfill );
1187             push @{ $self->{FORMATS} }, $fmt;
1188             } $styles->find_nodes('//cellXfs/xf');
1189             }
1190             ###############################################################################
1191             sub _parse_themes {
1192            
1193             =head2 _parse_themes
1194            
1195             Parses theme information. Some color settings are referenced by an
1196             index to the theme.
1197            
1198             =cut
1199            
1200             my $self = shift;
1201             my ($themes) = @_;
1202            
1203             return {} unless $themes;
1204            
1205             my @color
1206             = map { $_->name eq 'a:sysClr' ? $_->att('lastClr') : $_->att('val') }
1207             $themes->find_nodes('//a:clrScheme/*/*');
1208            
1209             # this shouldn't be necessary, but the documentation is wrong here
1210             # see http://stackoverflow.com/questions/2760976/theme-confusion-in-spreadsheetml
1211             ( $color[0], $color[1] ) = ( $color[1], $color[0] );
1212             ( $color[2], $color[3] ) = ( $color[3], $color[2] );
1213            
1214             return { Color => \@color };
1215             }
1216             ###############################################################################
1217             sub _parse_xml {
1218            
1219             =head2 _parse_xml
1220            
1221             Low level subroutine to parse an entire member of a zip file. Used
1222             for small files, such as xxx.xml.rels, where the entire file is parsed.
1223            
1224             For larger files, XML::Twig::twig_handlers are used.
1225            
1226             =cut
1227            
1228             my $self = shift;
1229             my ( $zip, $subfile ) = @_;
1230            
1231             my $member = $zip->memberNamed($subfile);
1232             die "no subfile named $subfile" unless $member;
1233            
1234             my $xml = XML::Twig->new;
1235             $xml->parse( scalar $member->contents );
1236             return $xml;
1237             }
1238             ###############################################################################
1239             sub _rels_for {
1240            
1241             =head2 _rels_for
1242            
1243             Returns the .rels file name for a sibling workbook or worksheet.
1244            
1245             =cut
1246            
1247             my $self = shift;
1248             my ($file) = @_;
1249            
1250             my @path = split '/', $file;
1251             my $name = pop @path;
1252             $name = '' unless defined $name;
1253             push @path, '_rels';
1254             push @path, "$name.rels";
1255            
1256             return join '/', @path;
1257             }
1258             ###############################################################################
1259             sub zzpodtail {
1260            
1261             =for pod
1262            
1263             =head2 zzpodtail
1264            
1265             Dummy subroutine to allow me to hide pod documentation when using code
1266             folding in the editor.
1267            
1268             =head1 INSTALLATION
1269            
1270             Install with CPAN
1271            
1272             cpan Excel::Template::XLSX
1273            
1274             or, use the standard Unix style installation.
1275            
1276             Unzip and untar the module as follows:
1277            
1278             tar -zxvf Excel::Template::XLSX-nnn.tar.gz
1279            
1280             The module can be installed using the standard Perl procedure:
1281            
1282             perl Makefile.PL
1283             make
1284             make test
1285             make install # As sudo/root
1286            
1287             =head1 BUGS
1288            
1289             =over 4
1290            
1291             =item Large spreadsheets may cause segfaults on perl 5.14 and earlier
1292            
1293             This module internally uses XML::Twig, which makes it potentially subject to
1294             L
1295             on perl versions 5.14 and below (the underlying bug with perl weak references
1296             was fixed in perl 5.15.5). The larger and more complex the spreadsheet, the
1297             more likely to be affected, but the actual size at which it segfaults is
1298             platform dependent. On a 64-bit perl with 7.6gb memory, it was seen on
1299             spreadsheets about 300mb and above. You can work around this adding
1300             C to your code before parsing the spreadsheet,
1301             although this may have other consequences such as memory leaks.
1302            
1303             Please report any bugs to GitHub Issues at
1304             L.
1305            
1306             =back
1307            
1308             =head1 SUPPORT
1309            
1310             You can find this documentation for this module with the perldoc command.
1311            
1312             perldoc Excel::Template::XLSX
1313            
1314             You can also look for information at:
1315            
1316             =over 4
1317            
1318             =item * MetaCPAN
1319            
1320             L
1321            
1322             =item * RT: CPAN's request tracker
1323            
1324             L
1325            
1326             =item * Github
1327            
1328             L
1329            
1330             =item * CPAN Ratings
1331            
1332             L
1333            
1334             =back
1335            
1336             =head1 DEBUGGING TIPS
1337            
1338             Using the Perl debugger gets complicated because of XML::Twig. The objects
1339             created by XML::Twig are HUGE. Also, stepping through the code often results
1340             in exceeding a stack depth of >100. The author found it helpful to take
1341             advantage of the simplify() method in XML::Twig when using the debugger 'x'
1342             command to examine variables.
1343            
1344             x $node->simplify()
1345            
1346             Also, it is helpful to use the 'c' command to jump over XML::Twig subroutine calls and callbacks.
1347            
1348             =head1 BUGS
1349            
1350             Please report any bugs or feature requests to the author.
1351            
1352             =head1 TO DO
1353            
1354             Worksheet Activation
1355             Table Formatting/Styles
1356             Calculation Mode
1357            
1358             =head1 REPOSITORY
1359            
1360             The Excel::Template::XLSX source code is hosted on github:
1361             L.
1362            
1363             =head1 SEE ALSO
1364            
1365             Excel::Writer::XLSX
1366            
1367             This module does not provide much documentation on the capabilites of methods
1368             for creating Excel content. The documentation provided with EWX is excellent,
1369             and also has numerous examples included.
1370            
1371             Spreadsheet::ParseXLSX
1372            
1373             Although this module does not use Spreadsheet::ParseXLSX, the parsing and
1374             comments regarding issues involved with parsing spreadsheets came from this module.
1375            
1376             XML::Twig and Archive::Zip
1377            
1378             Excel .xlsx files are zippped .xml files. These two modules are used to
1379             unzip the .xlsx file, extract the members, and parse the relative portions
1380             of the .xml files inside.
1381            
1382             =head1 ACKNOWLEDGEMENTS
1383            
1384             This module leverages the methods in L, maintained by L
1385             to recreate the template.
1386            
1387             The parser was developed using L as a starting point, maintained by L.
1388             This parser calls methods in EWX directly when a token is resolved rather than building
1389             up an object representing the parsed content.
1390            
1391             =head1 LICENSE AND COPYRIGHT
1392            
1393             Either the Perl Artistic Licence L
1394             or the GPL L.
1395            
1396             AUTHOR
1397            
1398             David Clarke dclarke@cpan.org
1399            
1400             =cut
1401            
1402             }
1403