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