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