line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Spreadsheet::ParseXLSX; |
2
|
|
|
|
|
|
|
our $AUTHORITY = 'cpan:DOY'; |
3
|
|
|
|
|
|
|
$Spreadsheet::ParseXLSX::VERSION = '0.25'; |
4
|
31
|
|
|
31
|
|
403346
|
use strict; |
|
31
|
|
|
|
|
49
|
|
|
31
|
|
|
|
|
750
|
|
5
|
31
|
|
|
31
|
|
96
|
use warnings; |
|
31
|
|
|
|
|
30
|
|
|
31
|
|
|
|
|
577
|
|
6
|
31
|
|
|
31
|
|
432
|
use 5.008; |
|
31
|
|
|
|
|
66
|
|
7
|
|
|
|
|
|
|
# ABSTRACT: parse XLSX files |
8
|
|
|
|
|
|
|
|
9
|
31
|
|
|
31
|
|
16687
|
use Archive::Zip; |
|
31
|
|
|
|
|
1922972
|
|
|
31
|
|
|
|
|
1161
|
|
10
|
31
|
|
|
31
|
|
16440
|
use Graphics::ColorUtils 'rgb2hls', 'hls2rgb'; |
|
31
|
|
|
|
|
158037
|
|
|
31
|
|
|
|
|
2025
|
|
11
|
31
|
|
|
31
|
|
162
|
use Scalar::Util 'openhandle'; |
|
31
|
|
|
|
|
37
|
|
|
31
|
|
|
|
|
1258
|
|
12
|
31
|
|
|
31
|
|
22110
|
use Spreadsheet::ParseExcel 0.61; |
|
31
|
|
|
|
|
1118736
|
|
|
31
|
|
|
|
|
868
|
|
13
|
31
|
|
|
31
|
|
57370
|
use XML::Twig; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
use Spreadsheet::ParseXLSX::Decryptor; |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
sub new { |
20
|
|
|
|
|
|
|
my $class = shift; |
21
|
|
|
|
|
|
|
my (%args) = @_; |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
my $self = bless {}, $class; |
24
|
|
|
|
|
|
|
$self->{Password} = $args{Password} if defined $args{Password}; |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
return $self; |
27
|
|
|
|
|
|
|
} |
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
sub parse { |
31
|
|
|
|
|
|
|
my $self = shift; |
32
|
|
|
|
|
|
|
my ($file, $formatter) = @_; |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
my $zip = Archive::Zip->new; |
35
|
|
|
|
|
|
|
my $workbook = Spreadsheet::ParseExcel::Workbook->new; |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
if ($self->_check_signature($file)) { |
38
|
|
|
|
|
|
|
my $decrypted_file = Spreadsheet::ParseXLSX::Decryptor->open( |
39
|
|
|
|
|
|
|
$file, |
40
|
|
|
|
|
|
|
$self->{Password} |
41
|
|
|
|
|
|
|
); |
42
|
|
|
|
|
|
|
$file = $decrypted_file if $decrypted_file; |
43
|
|
|
|
|
|
|
} |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
if (openhandle($file)) { |
46
|
|
|
|
|
|
|
bless $file, 'IO::File' if ref($file) eq 'GLOB'; # sigh |
47
|
|
|
|
|
|
|
my $fh = ref($file) eq 'File::Temp' |
48
|
|
|
|
|
|
|
? IO::File->new("<&=" . fileno($file)) |
49
|
|
|
|
|
|
|
: $file; |
50
|
|
|
|
|
|
|
$zip->readFromFileHandle($fh) == Archive::Zip::AZ_OK |
51
|
|
|
|
|
|
|
or die "Can't open filehandle as a zip file"; |
52
|
|
|
|
|
|
|
$workbook->{File} = undef; |
53
|
|
|
|
|
|
|
$workbook->{__tempfile} = $file; |
54
|
|
|
|
|
|
|
} |
55
|
|
|
|
|
|
|
elsif (ref($file) eq 'SCALAR') { |
56
|
|
|
|
|
|
|
open my $fh, '+<', $file |
57
|
|
|
|
|
|
|
or die "Can't create filehandle from memory data"; |
58
|
|
|
|
|
|
|
$zip->readFromFileHandle($fh) == Archive::Zip::AZ_OK |
59
|
|
|
|
|
|
|
or die "Can't open scalar ref as a zip file"; |
60
|
|
|
|
|
|
|
$workbook->{File} = undef; |
61
|
|
|
|
|
|
|
} |
62
|
|
|
|
|
|
|
elsif (!ref($file)) { |
63
|
|
|
|
|
|
|
$zip->read($file) == Archive::Zip::AZ_OK |
64
|
|
|
|
|
|
|
or die "Can't open file '$file' as a zip file"; |
65
|
|
|
|
|
|
|
$workbook->{File} = $file; |
66
|
|
|
|
|
|
|
} |
67
|
|
|
|
|
|
|
else { |
68
|
|
|
|
|
|
|
die "Argument to 'new' must be a filename, open filehandle, or scalar ref"; |
69
|
|
|
|
|
|
|
} |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
return $self->_parse_workbook($zip, $workbook, $formatter); |
72
|
|
|
|
|
|
|
} |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
sub _check_signature { |
75
|
|
|
|
|
|
|
my $self = shift; |
76
|
|
|
|
|
|
|
my ($file) = @_; |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
my $signature = ''; |
79
|
|
|
|
|
|
|
if (openhandle($file)) { |
80
|
|
|
|
|
|
|
bless $file, 'IO::File' if ref($file) eq 'GLOB'; # sigh |
81
|
|
|
|
|
|
|
$file->read($signature, 2); |
82
|
|
|
|
|
|
|
$file->seek(-2, IO::File::SEEK_CUR); |
83
|
|
|
|
|
|
|
} |
84
|
|
|
|
|
|
|
elsif (ref($file) eq 'SCALAR') { |
85
|
|
|
|
|
|
|
$signature = substr($$file, 0, 2); |
86
|
|
|
|
|
|
|
} |
87
|
|
|
|
|
|
|
elsif (!ref($file)) { |
88
|
|
|
|
|
|
|
my $fh = IO::File->new($file, 'r'); |
89
|
|
|
|
|
|
|
$fh->read($signature, 2); |
90
|
|
|
|
|
|
|
$fh->close; |
91
|
|
|
|
|
|
|
} |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
return $signature eq "\xd0\xcf"; |
94
|
|
|
|
|
|
|
} |
95
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
sub _parse_workbook { |
97
|
|
|
|
|
|
|
my $self = shift; |
98
|
|
|
|
|
|
|
my ($zip, $workbook, $formatter) = @_; |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
my $files = $self->_extract_files($zip); |
101
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
my ($version) = $files->{workbook}->find_nodes('//s:fileVersion'); |
103
|
|
|
|
|
|
|
my ($properties) = $files->{workbook}->find_nodes('//s:workbookPr'); |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
if ($version) { |
106
|
|
|
|
|
|
|
$workbook->{Version} = $version->att('appName') |
107
|
|
|
|
|
|
|
. ($version->att('lowestEdited') |
108
|
|
|
|
|
|
|
? ('-' . $version->att('lowestEdited')) |
109
|
|
|
|
|
|
|
: ("")); |
110
|
|
|
|
|
|
|
} |
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
$workbook->{Flg1904} = $self->_xml_boolean($properties->att('date1904')) |
113
|
|
|
|
|
|
|
if $properties; |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
$workbook->{FmtClass} = $formatter || Spreadsheet::ParseExcel::FmtDefault->new; |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
my $themes = $self->_parse_themes((values %{ $files->{themes} })[0]); # XXX |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
$workbook->{Color} = $themes->{Color}; |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
my $styles = $self->_parse_styles($workbook, $files->{styles}); |
122
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
$workbook->{Format} = $styles->{Format}; |
124
|
|
|
|
|
|
|
$workbook->{FormatStr} = $styles->{FormatStr}; |
125
|
|
|
|
|
|
|
$workbook->{Font} = $styles->{Font}; |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
if ($files->{strings}) { |
128
|
|
|
|
|
|
|
my %string_parse_data = $self->_parse_shared_strings( |
129
|
|
|
|
|
|
|
$files->{strings}, |
130
|
|
|
|
|
|
|
$themes->{Color} |
131
|
|
|
|
|
|
|
); |
132
|
|
|
|
|
|
|
$workbook->{PkgStr} = $string_parse_data{PkgStr}; |
133
|
|
|
|
|
|
|
$workbook->{Rich} = $string_parse_data{Rich}; |
134
|
|
|
|
|
|
|
} |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
# $workbook->{StandardWidth} = ...; |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
# $workbook->{Author} = ...; |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
# $workbook->{PrintArea} = ...; |
141
|
|
|
|
|
|
|
# $workbook->{PrintTitle} = ...; |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
my @sheets = map { |
144
|
|
|
|
|
|
|
my $idx = $_->att('rels:id'); |
145
|
|
|
|
|
|
|
if ($files->{sheets}{$idx}) { |
146
|
|
|
|
|
|
|
my $sheet = Spreadsheet::ParseExcel::Worksheet->new( |
147
|
|
|
|
|
|
|
Name => $_->att('name'), |
148
|
|
|
|
|
|
|
_Book => $workbook, |
149
|
|
|
|
|
|
|
_SheetNo => $idx, |
150
|
|
|
|
|
|
|
); |
151
|
|
|
|
|
|
|
$sheet->{SheetHidden} = 1 if defined $_->att('state') and $_->att('state') eq 'hidden'; |
152
|
|
|
|
|
|
|
$self->_parse_sheet($sheet, $files->{sheets}{$idx}); |
153
|
|
|
|
|
|
|
($sheet) |
154
|
|
|
|
|
|
|
} else { |
155
|
|
|
|
|
|
|
() |
156
|
|
|
|
|
|
|
} |
157
|
|
|
|
|
|
|
} $files->{workbook}->find_nodes('//s:sheets/s:sheet'); |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
$workbook->{Worksheet} = \@sheets; |
160
|
|
|
|
|
|
|
$workbook->{SheetCount} = scalar(@sheets); |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
my ($node) = $files->{workbook}->find_nodes('//s:workbookView'); |
163
|
|
|
|
|
|
|
my $selected = $node ? $node->att('activeTab') : undef; |
164
|
|
|
|
|
|
|
$workbook->{SelectedSheet} = defined($selected) ? 0+$selected : 0; |
165
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
return $workbook; |
167
|
|
|
|
|
|
|
} |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
sub _parse_sheet { |
170
|
|
|
|
|
|
|
my $self = shift; |
171
|
|
|
|
|
|
|
my ($sheet, $sheet_file) = @_; |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
$sheet->{MinRow} = 0; |
174
|
|
|
|
|
|
|
$sheet->{MinCol} = 0; |
175
|
|
|
|
|
|
|
$sheet->{MaxRow} = -1; |
176
|
|
|
|
|
|
|
$sheet->{MaxCol} = -1; |
177
|
|
|
|
|
|
|
$sheet->{Selection} = [ 0, 0 ]; |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
my %merged_cells; |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
my @column_formats; |
182
|
|
|
|
|
|
|
my @column_widths; |
183
|
|
|
|
|
|
|
my @columns_hidden; |
184
|
|
|
|
|
|
|
my @row_heights; |
185
|
|
|
|
|
|
|
my @rows_hidden; |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
my $default_row_height = 15; |
188
|
|
|
|
|
|
|
my $default_column_width = 10; |
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
my %cells; |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
my $sheet_xml = $self->_new_twig( |
193
|
|
|
|
|
|
|
twig_roots => { |
194
|
|
|
|
|
|
|
#XXX need a fallback here, the dimension tag is optional |
195
|
|
|
|
|
|
|
's:dimension' => sub { |
196
|
|
|
|
|
|
|
my ($twig, $dimension) = @_; |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
my ($rmin, $cmin, $rmax, $cmax) = $self->_dimensions( |
199
|
|
|
|
|
|
|
$dimension->att('ref') |
200
|
|
|
|
|
|
|
); |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
$sheet->{MinRow} = $rmin; |
203
|
|
|
|
|
|
|
$sheet->{MinCol} = $cmin; |
204
|
|
|
|
|
|
|
$sheet->{MaxRow} = $rmax ? $rmax : -1; |
205
|
|
|
|
|
|
|
$sheet->{MaxCol} = $cmax ? $cmax : -1; |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
$twig->purge; |
208
|
|
|
|
|
|
|
}, |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
's:headerFooter' => sub { |
211
|
|
|
|
|
|
|
my ($twig, $hf) = @_; |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
my ($helem, $felem) = map { |
214
|
|
|
|
|
|
|
$hf->first_child("s:$_") |
215
|
|
|
|
|
|
|
} qw(oddHeader oddFooter); |
216
|
|
|
|
|
|
|
$sheet->{Header} = $helem->text |
217
|
|
|
|
|
|
|
if $helem; |
218
|
|
|
|
|
|
|
$sheet->{Footer} = $felem->text |
219
|
|
|
|
|
|
|
if $felem; |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
$twig->purge; |
222
|
|
|
|
|
|
|
}, |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
's:pageMargins' => sub { |
225
|
|
|
|
|
|
|
my ($twig, $margin) = @_; |
226
|
|
|
|
|
|
|
map { |
227
|
|
|
|
|
|
|
my $key = "\u${_}Margin"; |
228
|
|
|
|
|
|
|
$sheet->{$key} = defined $margin->att($_) |
229
|
|
|
|
|
|
|
? $margin->att($_) : 0 |
230
|
|
|
|
|
|
|
} qw(left right top bottom header footer); |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
$twig->purge; |
233
|
|
|
|
|
|
|
}, |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
's:pageSetup' => sub { |
236
|
|
|
|
|
|
|
my ($twig, $setup) = @_; |
237
|
|
|
|
|
|
|
$sheet->{Scale} = defined $setup->att('scale') |
238
|
|
|
|
|
|
|
? $setup->att('scale') |
239
|
|
|
|
|
|
|
: 100; |
240
|
|
|
|
|
|
|
$sheet->{Landscape} = ($setup->att('orientation') || '') ne 'landscape'; |
241
|
|
|
|
|
|
|
$sheet->{PaperSize} = defined $setup->att('paperSize') |
242
|
|
|
|
|
|
|
? $setup->att('paperSize') |
243
|
|
|
|
|
|
|
: 1; |
244
|
|
|
|
|
|
|
$sheet->{PageStart} = $setup->att('firstPageNumber'); |
245
|
|
|
|
|
|
|
$sheet->{UsePage} = $self->_xml_boolean($setup->att('useFirstPageNumber')); |
246
|
|
|
|
|
|
|
$sheet->{HorizontalDPI} = $setup->att('horizontalDpi'); |
247
|
|
|
|
|
|
|
$sheet->{VerticalDPI} = $setup->att('verticalDpi'); |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
$twig->purge; |
250
|
|
|
|
|
|
|
}, |
251
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
's:mergeCells/s:mergeCell' => sub { |
253
|
|
|
|
|
|
|
my ( $twig, $merge_area ) = @_; |
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
if (my $ref = $merge_area->att('ref')) { |
256
|
|
|
|
|
|
|
my ($topleft, $bottomright) = $ref =~ /([^:]+):([^:]+)/; |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
my ($toprow, $leftcol) = $self->_cell_to_row_col($topleft); |
259
|
|
|
|
|
|
|
my ($bottomrow, $rightcol) = $self->_cell_to_row_col($bottomright); |
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
push @{ $sheet->{MergedArea} }, [ |
262
|
|
|
|
|
|
|
$toprow, $leftcol, |
263
|
|
|
|
|
|
|
$bottomrow, $rightcol, |
264
|
|
|
|
|
|
|
]; |
265
|
|
|
|
|
|
|
for my $row ($toprow .. $bottomrow) { |
266
|
|
|
|
|
|
|
for my $col ($leftcol .. $rightcol) { |
267
|
|
|
|
|
|
|
$merged_cells{"$row;$col"} = 1; |
268
|
|
|
|
|
|
|
} |
269
|
|
|
|
|
|
|
} |
270
|
|
|
|
|
|
|
} |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
$twig->purge; |
273
|
|
|
|
|
|
|
}, |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
's:sheetFormatPr' => sub { |
276
|
|
|
|
|
|
|
my ( $twig, $format ) = @_; |
277
|
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
$default_row_height = $format->att('defaultRowHeight') |
279
|
|
|
|
|
|
|
unless defined $default_row_height; |
280
|
|
|
|
|
|
|
$default_column_width = $format->att('baseColWidth') |
281
|
|
|
|
|
|
|
unless defined $default_column_width; |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
$twig->purge; |
284
|
|
|
|
|
|
|
}, |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
's:col' => sub { |
287
|
|
|
|
|
|
|
my ( $twig, $col ) = @_; |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
for my $colnum ($col->att('min')..$col->att('max')) { |
290
|
|
|
|
|
|
|
$column_widths[$colnum - 1] = $col->att('width'); |
291
|
|
|
|
|
|
|
$column_formats[$colnum - 1] = $col->att('style'); |
292
|
|
|
|
|
|
|
$columns_hidden[$colnum - 1] = $self->_xml_boolean($col->att('hidden')); |
293
|
|
|
|
|
|
|
} |
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
$twig->purge; |
296
|
|
|
|
|
|
|
}, |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
's:row' => sub { |
299
|
|
|
|
|
|
|
my ( $twig, $row ) = @_; |
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
$row_heights[ $row->att('r') - 1 ] = $row->att('ht'); |
302
|
|
|
|
|
|
|
$rows_hidden[ $row->att('r') - 1 ] = $self->_xml_boolean($row->att('hidden')); |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
$twig->purge; |
305
|
|
|
|
|
|
|
}, |
306
|
|
|
|
|
|
|
|
307
|
|
|
|
|
|
|
's:selection' => sub { |
308
|
|
|
|
|
|
|
my ( $twig, $selection ) = @_; |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
if (my $cell = $selection->att('activeCell')) { |
311
|
|
|
|
|
|
|
$sheet->{Selection} = [ $self->_cell_to_row_col($cell) ]; |
312
|
|
|
|
|
|
|
} |
313
|
|
|
|
|
|
|
elsif (my $range = $selection->att('sqref')) { |
314
|
|
|
|
|
|
|
my ($topleft, $bottomright) = $range =~ /([^:]+):([^:]+)/; |
315
|
|
|
|
|
|
|
$sheet->{Selection} = [ |
316
|
|
|
|
|
|
|
$self->_cell_to_row_col($topleft), |
317
|
|
|
|
|
|
|
$self->_cell_to_row_col($bottomright), |
318
|
|
|
|
|
|
|
]; |
319
|
|
|
|
|
|
|
} |
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
$twig->purge; |
322
|
|
|
|
|
|
|
}, |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
's:sheetPr/s:tabColor' => sub { |
325
|
|
|
|
|
|
|
my ( $twig, $tab_color ) = @_; |
326
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
$sheet->{TabColor} = $self->_color($sheet->{_Book}{Color}, $tab_color); |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
$twig->purge; |
330
|
|
|
|
|
|
|
}, |
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
's:sheetData/s:row' => sub { |
333
|
|
|
|
|
|
|
my ( $twig, $row_elt ) = @_; |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
for my $cell ( $row_elt->children('s:c') ){ |
336
|
|
|
|
|
|
|
my ($row, $col) = $self->_cell_to_row_col($cell->att('r')); |
337
|
|
|
|
|
|
|
$sheet->{MaxRow} = $row |
338
|
|
|
|
|
|
|
if $sheet->{MaxRow} < $row; |
339
|
|
|
|
|
|
|
$sheet->{MaxCol} = $col |
340
|
|
|
|
|
|
|
if $sheet->{MaxCol} < $col; |
341
|
|
|
|
|
|
|
my $type = $cell->att('t') || 'n'; |
342
|
|
|
|
|
|
|
my $val_xml; |
343
|
|
|
|
|
|
|
if ($type ne 'inlineStr') { |
344
|
|
|
|
|
|
|
$val_xml = $cell->first_child('s:v'); |
345
|
|
|
|
|
|
|
} |
346
|
|
|
|
|
|
|
elsif (defined $cell->first_child('s:is')) { |
347
|
|
|
|
|
|
|
$val_xml = ($cell->find_nodes('.//s:t'))[0]; |
348
|
|
|
|
|
|
|
} |
349
|
|
|
|
|
|
|
my $val = $val_xml ? $val_xml->text : undef; |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
my $long_type; |
352
|
|
|
|
|
|
|
my $Rich; |
353
|
|
|
|
|
|
|
if (!defined($val)) { |
354
|
|
|
|
|
|
|
$long_type = 'Text'; |
355
|
|
|
|
|
|
|
$val = ''; |
356
|
|
|
|
|
|
|
} |
357
|
|
|
|
|
|
|
elsif ($type eq 's') { |
358
|
|
|
|
|
|
|
$long_type = 'Text'; |
359
|
|
|
|
|
|
|
$Rich = $sheet->{_Book}{Rich}->{$val}; |
360
|
|
|
|
|
|
|
$val = $sheet->{_Book}{PkgStr}[$val]; |
361
|
|
|
|
|
|
|
} |
362
|
|
|
|
|
|
|
elsif ($type eq 'n') { |
363
|
|
|
|
|
|
|
$long_type = 'Numeric'; |
364
|
|
|
|
|
|
|
$val = defined($val) ? 0+$val : undef; |
365
|
|
|
|
|
|
|
} |
366
|
|
|
|
|
|
|
elsif ($type eq 'd') { |
367
|
|
|
|
|
|
|
$long_type = 'Date'; |
368
|
|
|
|
|
|
|
} |
369
|
|
|
|
|
|
|
elsif ($type eq 'b') { |
370
|
|
|
|
|
|
|
$long_type = 'Text'; |
371
|
|
|
|
|
|
|
$val = $val ? "TRUE" : "FALSE"; |
372
|
|
|
|
|
|
|
} |
373
|
|
|
|
|
|
|
elsif ($type eq 'e') { |
374
|
|
|
|
|
|
|
$long_type = 'Text'; |
375
|
|
|
|
|
|
|
} |
376
|
|
|
|
|
|
|
elsif ($type eq 'str' || $type eq 'inlineStr') { |
377
|
|
|
|
|
|
|
$long_type = 'Text'; |
378
|
|
|
|
|
|
|
} |
379
|
|
|
|
|
|
|
else { |
380
|
|
|
|
|
|
|
die "unimplemented type $type"; # XXX |
381
|
|
|
|
|
|
|
} |
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
my $format_idx = $cell->att('s') || 0; |
384
|
|
|
|
|
|
|
my $format = $sheet->{_Book}{Format}[$format_idx]; |
385
|
|
|
|
|
|
|
die "unknown format $format_idx" unless $format; |
386
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
# see the list of built-in formats below in _parse_styles |
388
|
|
|
|
|
|
|
# XXX probably should figure this out from the actual format string, |
389
|
|
|
|
|
|
|
# but that's not entirely trivial |
390
|
|
|
|
|
|
|
if (grep { $format->{FmtIdx} == $_ } 14..22, 45..47) { |
391
|
|
|
|
|
|
|
$long_type = 'Date'; |
392
|
|
|
|
|
|
|
} |
393
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
my $formula = $cell->first_child('s:f'); |
395
|
|
|
|
|
|
|
my $cell = Spreadsheet::ParseExcel::Cell->new( |
396
|
|
|
|
|
|
|
Val => $val, |
397
|
|
|
|
|
|
|
Type => $long_type, |
398
|
|
|
|
|
|
|
Merged => undef, # fix up later |
399
|
|
|
|
|
|
|
Format => $format, |
400
|
|
|
|
|
|
|
FormatNo => $format_idx, |
401
|
|
|
|
|
|
|
($formula |
402
|
|
|
|
|
|
|
? (Formula => $formula->text) |
403
|
|
|
|
|
|
|
: ()), |
404
|
|
|
|
|
|
|
Rich => $Rich, |
405
|
|
|
|
|
|
|
); |
406
|
|
|
|
|
|
|
$cell->{_Value} = $sheet->{_Book}{FmtClass}->ValFmt( |
407
|
|
|
|
|
|
|
$cell, $sheet->{_Book} |
408
|
|
|
|
|
|
|
); |
409
|
|
|
|
|
|
|
$cells{"$row;$col"} = $cell; |
410
|
|
|
|
|
|
|
$sheet->{Cells}[$row][$col] = $cell; |
411
|
|
|
|
|
|
|
} |
412
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
$twig->purge; |
414
|
|
|
|
|
|
|
}, |
415
|
|
|
|
|
|
|
} |
416
|
|
|
|
|
|
|
); |
417
|
|
|
|
|
|
|
|
418
|
|
|
|
|
|
|
$sheet_xml->parse( $sheet_file ); |
419
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
for my $key (keys %merged_cells) { |
421
|
|
|
|
|
|
|
$cells{$key}{Merged} = 1 if $cells{$key}; |
422
|
|
|
|
|
|
|
} |
423
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
if ( ! $sheet->{Cells} ){ |
425
|
|
|
|
|
|
|
$sheet->{MaxRow} = $sheet->{MaxCol} = -1; |
426
|
|
|
|
|
|
|
} |
427
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
$sheet->{DefRowHeight} = 0+$default_row_height; |
429
|
|
|
|
|
|
|
$sheet->{DefColWidth} = 0+$default_column_width; |
430
|
|
|
|
|
|
|
$sheet->{RowHeight} = [ |
431
|
|
|
|
|
|
|
map { defined $_ ? 0+$_ : 0+$default_row_height } @row_heights |
432
|
|
|
|
|
|
|
]; |
433
|
|
|
|
|
|
|
$sheet->{RowHidden} = \@rows_hidden; |
434
|
|
|
|
|
|
|
$sheet->{ColWidth} = [ |
435
|
|
|
|
|
|
|
map { defined $_ ? 0+$_ : 0+$default_column_width } @column_widths |
436
|
|
|
|
|
|
|
]; |
437
|
|
|
|
|
|
|
$sheet->{ColFmtNo} = \@column_formats; |
438
|
|
|
|
|
|
|
$sheet->{ColHidden} = \@columns_hidden; |
439
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
} |
441
|
|
|
|
|
|
|
|
442
|
|
|
|
|
|
|
sub _get_text_and_rich_font_by_cell { |
443
|
|
|
|
|
|
|
my $self = shift; |
444
|
|
|
|
|
|
|
my ($si, $theme_colors) = @_; |
445
|
|
|
|
|
|
|
|
446
|
|
|
|
|
|
|
# XXX |
447
|
|
|
|
|
|
|
my %default_font_opts = ( |
448
|
|
|
|
|
|
|
Height => 12, |
449
|
|
|
|
|
|
|
Color => '#000000', |
450
|
|
|
|
|
|
|
Name => '', |
451
|
|
|
|
|
|
|
Bold => 0, |
452
|
|
|
|
|
|
|
Italic => 0, |
453
|
|
|
|
|
|
|
Underline => 0, |
454
|
|
|
|
|
|
|
UnderlineStyle => 0, |
455
|
|
|
|
|
|
|
Strikeout => 0, |
456
|
|
|
|
|
|
|
Super => 0, |
457
|
|
|
|
|
|
|
); |
458
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
my $string_text = ''; |
460
|
|
|
|
|
|
|
my @rich_font_by_cell; |
461
|
|
|
|
|
|
|
my @nodes_r = $si->find_nodes('.//s:r'); |
462
|
|
|
|
|
|
|
if (@nodes_r > 0) { |
463
|
|
|
|
|
|
|
for my $chunk (map { $_->children } @nodes_r) { |
464
|
|
|
|
|
|
|
my $string_length = length($string_text); |
465
|
|
|
|
|
|
|
if ($chunk->name eq 's:t') { |
466
|
|
|
|
|
|
|
if (!@rich_font_by_cell) { |
467
|
|
|
|
|
|
|
push @rich_font_by_cell, [ |
468
|
|
|
|
|
|
|
$string_length, |
469
|
|
|
|
|
|
|
Spreadsheet::ParseExcel::Font->new(%default_font_opts) |
470
|
|
|
|
|
|
|
]; |
471
|
|
|
|
|
|
|
} |
472
|
|
|
|
|
|
|
$string_text .= $chunk->text; |
473
|
|
|
|
|
|
|
} |
474
|
|
|
|
|
|
|
elsif ($chunk->name eq 's:rPr') { |
475
|
|
|
|
|
|
|
my %format_text = %default_font_opts; |
476
|
|
|
|
|
|
|
for my $node_format ($chunk->children) { |
477
|
|
|
|
|
|
|
if ($node_format->name eq 's:sz') { |
478
|
|
|
|
|
|
|
$format_text{Height} = $node_format->att('val'); |
479
|
|
|
|
|
|
|
} |
480
|
|
|
|
|
|
|
elsif ($node_format->name eq 's:color') { |
481
|
|
|
|
|
|
|
$format_text{Color} = $self->_color( |
482
|
|
|
|
|
|
|
$theme_colors, |
483
|
|
|
|
|
|
|
$node_format |
484
|
|
|
|
|
|
|
); |
485
|
|
|
|
|
|
|
} |
486
|
|
|
|
|
|
|
elsif ($node_format->name eq 's:rFont') { |
487
|
|
|
|
|
|
|
$format_text{Name} = $node_format->att('val'); |
488
|
|
|
|
|
|
|
} |
489
|
|
|
|
|
|
|
elsif ($node_format->name eq 's:b') { |
490
|
|
|
|
|
|
|
$format_text{Bold} = 1; |
491
|
|
|
|
|
|
|
} |
492
|
|
|
|
|
|
|
elsif ($node_format->name eq 's:i') { |
493
|
|
|
|
|
|
|
$format_text{Italic} = 1; |
494
|
|
|
|
|
|
|
} |
495
|
|
|
|
|
|
|
elsif ($node_format->name eq 's:u') { |
496
|
|
|
|
|
|
|
$format_text{Underline} = 1; |
497
|
|
|
|
|
|
|
if (defined $node_format->att('val')) { |
498
|
|
|
|
|
|
|
$format_text{UnderlineStyle} = 2; |
499
|
|
|
|
|
|
|
} else { |
500
|
|
|
|
|
|
|
$format_text{UnderlineStyle} = 1; |
501
|
|
|
|
|
|
|
} |
502
|
|
|
|
|
|
|
} |
503
|
|
|
|
|
|
|
elsif ($node_format->name eq 's:strike') { |
504
|
|
|
|
|
|
|
$format_text{Strikeout} = 1; |
505
|
|
|
|
|
|
|
} |
506
|
|
|
|
|
|
|
elsif ($node_format->name eq 's:vertAlign') { |
507
|
|
|
|
|
|
|
if ($node_format->att('val') eq 'superscript') { |
508
|
|
|
|
|
|
|
$format_text{Super} = 1; |
509
|
|
|
|
|
|
|
} |
510
|
|
|
|
|
|
|
elsif ($node_format->att('val') eq 'subscript') { |
511
|
|
|
|
|
|
|
$format_text{Super} = 2; |
512
|
|
|
|
|
|
|
} |
513
|
|
|
|
|
|
|
} |
514
|
|
|
|
|
|
|
} |
515
|
|
|
|
|
|
|
push @rich_font_by_cell, [ |
516
|
|
|
|
|
|
|
$string_length, |
517
|
|
|
|
|
|
|
Spreadsheet::ParseExcel::Font->new(%format_text) |
518
|
|
|
|
|
|
|
]; |
519
|
|
|
|
|
|
|
} |
520
|
|
|
|
|
|
|
} |
521
|
|
|
|
|
|
|
} |
522
|
|
|
|
|
|
|
else { |
523
|
|
|
|
|
|
|
$string_text = join '', map { $_->text } $si->find_nodes('.//s:t'); |
524
|
|
|
|
|
|
|
} |
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
return ( |
527
|
|
|
|
|
|
|
String => $string_text, |
528
|
|
|
|
|
|
|
Rich => \@rich_font_by_cell, |
529
|
|
|
|
|
|
|
); |
530
|
|
|
|
|
|
|
} |
531
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
sub _parse_shared_strings { |
533
|
|
|
|
|
|
|
my $self = shift; |
534
|
|
|
|
|
|
|
my ($strings, $theme_colors) = @_; |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
my $PkgStr = []; |
537
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
my %richfonts; |
539
|
|
|
|
|
|
|
if ($strings) { |
540
|
|
|
|
|
|
|
my $xml = $self->_new_twig( |
541
|
|
|
|
|
|
|
twig_handlers => { |
542
|
|
|
|
|
|
|
's:si' => sub { |
543
|
|
|
|
|
|
|
my ( $twig, $si ) = @_; |
544
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
my %text_rich = $self->_get_text_and_rich_font_by_cell( |
546
|
|
|
|
|
|
|
$si, |
547
|
|
|
|
|
|
|
$theme_colors |
548
|
|
|
|
|
|
|
); |
549
|
|
|
|
|
|
|
$richfonts{scalar @$PkgStr} = $text_rich{Rich}; |
550
|
|
|
|
|
|
|
push @$PkgStr, $text_rich{String}; |
551
|
|
|
|
|
|
|
$twig->purge; |
552
|
|
|
|
|
|
|
}, |
553
|
|
|
|
|
|
|
} |
554
|
|
|
|
|
|
|
); |
555
|
|
|
|
|
|
|
$xml->parse( $strings ); |
556
|
|
|
|
|
|
|
} |
557
|
|
|
|
|
|
|
return ( |
558
|
|
|
|
|
|
|
Rich => \%richfonts, |
559
|
|
|
|
|
|
|
PkgStr => $PkgStr, |
560
|
|
|
|
|
|
|
); |
561
|
|
|
|
|
|
|
} |
562
|
|
|
|
|
|
|
|
563
|
|
|
|
|
|
|
sub _parse_themes { |
564
|
|
|
|
|
|
|
my $self = shift; |
565
|
|
|
|
|
|
|
my ($themes) = @_; |
566
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
return {} unless $themes; |
568
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
my @color = map { |
570
|
|
|
|
|
|
|
$_->name eq 'drawmain:sysClr' ? $_->att('lastClr') : $_->att('val') |
571
|
|
|
|
|
|
|
} $themes->find_nodes('//drawmain:clrScheme/*/*'); |
572
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
# this shouldn't be necessary, but the documentation is wrong here |
574
|
|
|
|
|
|
|
# see http://stackoverflow.com/questions/2760976/theme-confusion-in-spreadsheetml |
575
|
|
|
|
|
|
|
($color[0], $color[1]) = ($color[1], $color[0]); |
576
|
|
|
|
|
|
|
($color[2], $color[3]) = ($color[3], $color[2]); |
577
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
return { |
579
|
|
|
|
|
|
|
Color => \@color, |
580
|
|
|
|
|
|
|
} |
581
|
|
|
|
|
|
|
} |
582
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
sub _parse_styles { |
584
|
|
|
|
|
|
|
my $self = shift; |
585
|
|
|
|
|
|
|
my ($workbook, $styles) = @_; |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
# these defaults are from |
588
|
|
|
|
|
|
|
# http://social.msdn.microsoft.com/Forums/en-US/oxmlsdk/thread/e27aaf16-b900-4654-8210-83c5774a179c |
589
|
|
|
|
|
|
|
my %default_format_str = ( |
590
|
|
|
|
|
|
|
0 => 'GENERAL', |
591
|
|
|
|
|
|
|
1 => '0', |
592
|
|
|
|
|
|
|
2 => '0.00', |
593
|
|
|
|
|
|
|
3 => '#,##0', |
594
|
|
|
|
|
|
|
4 => '#,##0.00', |
595
|
|
|
|
|
|
|
5 => '$#,##0_);($#,##0)', |
596
|
|
|
|
|
|
|
6 => '$#,##0_);[Red]($#,##0)', |
597
|
|
|
|
|
|
|
7 => '$#,##0.00_);($#,##0.00)', |
598
|
|
|
|
|
|
|
8 => '$#,##0.00_);[Red]($#,##0.00)', |
599
|
|
|
|
|
|
|
9 => '0%', |
600
|
|
|
|
|
|
|
10 => '0.00%', |
601
|
|
|
|
|
|
|
11 => '0.00E+00', |
602
|
|
|
|
|
|
|
12 => '# ?/?', |
603
|
|
|
|
|
|
|
13 => '# ??/??', |
604
|
|
|
|
|
|
|
14 => 'm/d/yyyy', |
605
|
|
|
|
|
|
|
15 => 'd-mmm-yy', |
606
|
|
|
|
|
|
|
16 => 'd-mmm', |
607
|
|
|
|
|
|
|
17 => 'mmm-yy', |
608
|
|
|
|
|
|
|
18 => 'h:mm AM/PM', |
609
|
|
|
|
|
|
|
19 => 'h:mm:ss AM/PM', |
610
|
|
|
|
|
|
|
20 => 'h:mm', |
611
|
|
|
|
|
|
|
21 => 'h:mm:ss', |
612
|
|
|
|
|
|
|
22 => 'm/d/yyyy h:mm', |
613
|
|
|
|
|
|
|
37 => '#,##0_);(#,##0)', |
614
|
|
|
|
|
|
|
38 => '#,##0_);[Red](#,##0)', |
615
|
|
|
|
|
|
|
39 => '#,##0.00_);(#,##0.00)', |
616
|
|
|
|
|
|
|
40 => '#,##0.00_);[Red](#,##0.00)', |
617
|
|
|
|
|
|
|
45 => 'mm:ss', |
618
|
|
|
|
|
|
|
46 => '[h]:mm:ss', |
619
|
|
|
|
|
|
|
47 => 'mm:ss.0', |
620
|
|
|
|
|
|
|
48 => '##0.0E+0', |
621
|
|
|
|
|
|
|
49 => '@', |
622
|
|
|
|
|
|
|
); |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
my %default_format_opts = ( |
625
|
|
|
|
|
|
|
IgnoreFont => 1, |
626
|
|
|
|
|
|
|
IgnoreFill => 1, |
627
|
|
|
|
|
|
|
IgnoreBorder => 1, |
628
|
|
|
|
|
|
|
IgnoreAlignment => 1, |
629
|
|
|
|
|
|
|
IgnoreNumberFormat => 1, |
630
|
|
|
|
|
|
|
IgnoreProtection => 1, |
631
|
|
|
|
|
|
|
FontNo => 0, |
632
|
|
|
|
|
|
|
FmtIdx => 0, |
633
|
|
|
|
|
|
|
Lock => 1, |
634
|
|
|
|
|
|
|
Hidden => 0, |
635
|
|
|
|
|
|
|
AlignH => 0, |
636
|
|
|
|
|
|
|
Wrap => 0, |
637
|
|
|
|
|
|
|
AlignV => 2, |
638
|
|
|
|
|
|
|
Rotate => 0, |
639
|
|
|
|
|
|
|
Indent => 0, |
640
|
|
|
|
|
|
|
Shrink => 0, |
641
|
|
|
|
|
|
|
BdrStyle => [0, 0, 0, 0], |
642
|
|
|
|
|
|
|
BdrColor => [undef, undef, undef, undef], |
643
|
|
|
|
|
|
|
BdrDiag => [0, 0, undef], |
644
|
|
|
|
|
|
|
Fill => [0, undef, undef], |
645
|
|
|
|
|
|
|
); |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
if (!$styles) { |
648
|
|
|
|
|
|
|
# XXX i guess? |
649
|
|
|
|
|
|
|
my $font = Spreadsheet::ParseExcel::Font->new( |
650
|
|
|
|
|
|
|
Height => 12, |
651
|
|
|
|
|
|
|
Color => '#000000', |
652
|
|
|
|
|
|
|
Name => '', |
653
|
|
|
|
|
|
|
); |
654
|
|
|
|
|
|
|
my $format = Spreadsheet::ParseExcel::Format->new( |
655
|
|
|
|
|
|
|
%default_format_opts, |
656
|
|
|
|
|
|
|
Font => $font, |
657
|
|
|
|
|
|
|
); |
658
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
return { |
660
|
|
|
|
|
|
|
FormatStr => \%default_format_str, |
661
|
|
|
|
|
|
|
Font => [ $font ], |
662
|
|
|
|
|
|
|
Format => [ $format ], |
663
|
|
|
|
|
|
|
}; |
664
|
|
|
|
|
|
|
} |
665
|
|
|
|
|
|
|
|
666
|
|
|
|
|
|
|
my %halign = ( |
667
|
|
|
|
|
|
|
center => 2, |
668
|
|
|
|
|
|
|
centerContinuous => 6, |
669
|
|
|
|
|
|
|
distributed => 7, |
670
|
|
|
|
|
|
|
fill => 4, |
671
|
|
|
|
|
|
|
general => 0, |
672
|
|
|
|
|
|
|
justify => 5, |
673
|
|
|
|
|
|
|
left => 1, |
674
|
|
|
|
|
|
|
right => 3, |
675
|
|
|
|
|
|
|
); |
676
|
|
|
|
|
|
|
|
677
|
|
|
|
|
|
|
my %valign = ( |
678
|
|
|
|
|
|
|
bottom => 2, |
679
|
|
|
|
|
|
|
center => 1, |
680
|
|
|
|
|
|
|
distributed => 4, |
681
|
|
|
|
|
|
|
justify => 3, |
682
|
|
|
|
|
|
|
top => 0, |
683
|
|
|
|
|
|
|
); |
684
|
|
|
|
|
|
|
|
685
|
|
|
|
|
|
|
my %border = ( |
686
|
|
|
|
|
|
|
dashDot => 9, |
687
|
|
|
|
|
|
|
dashDotDot => 11, |
688
|
|
|
|
|
|
|
dashed => 3, |
689
|
|
|
|
|
|
|
dotted => 4, |
690
|
|
|
|
|
|
|
double => 6, |
691
|
|
|
|
|
|
|
hair => 7, |
692
|
|
|
|
|
|
|
medium => 2, |
693
|
|
|
|
|
|
|
mediumDashDot => 10, |
694
|
|
|
|
|
|
|
mediumDashDotDot => 12, |
695
|
|
|
|
|
|
|
mediumDashed => 8, |
696
|
|
|
|
|
|
|
none => 0, |
697
|
|
|
|
|
|
|
slantDashDot => 13, |
698
|
|
|
|
|
|
|
thick => 5, |
699
|
|
|
|
|
|
|
thin => 1, |
700
|
|
|
|
|
|
|
); |
701
|
|
|
|
|
|
|
|
702
|
|
|
|
|
|
|
my %fill = ( |
703
|
|
|
|
|
|
|
darkDown => 7, |
704
|
|
|
|
|
|
|
darkGray => 3, |
705
|
|
|
|
|
|
|
darkGrid => 9, |
706
|
|
|
|
|
|
|
darkHorizontal => 5, |
707
|
|
|
|
|
|
|
darkTrellis => 10, |
708
|
|
|
|
|
|
|
darkUp => 8, |
709
|
|
|
|
|
|
|
darkVertical => 6, |
710
|
|
|
|
|
|
|
gray0625 => 18, |
711
|
|
|
|
|
|
|
gray125 => 17, |
712
|
|
|
|
|
|
|
lightDown => 13, |
713
|
|
|
|
|
|
|
lightGray => 4, |
714
|
|
|
|
|
|
|
lightGrid => 15, |
715
|
|
|
|
|
|
|
lightHorizontal => 11, |
716
|
|
|
|
|
|
|
lightTrellis => 16, |
717
|
|
|
|
|
|
|
lightUp => 14, |
718
|
|
|
|
|
|
|
lightVertical => 12, |
719
|
|
|
|
|
|
|
mediumGray => 2, |
720
|
|
|
|
|
|
|
none => 0, |
721
|
|
|
|
|
|
|
solid => 1, |
722
|
|
|
|
|
|
|
); |
723
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
my @fills = map { |
725
|
|
|
|
|
|
|
my $pattern_type = $_->att('patternType'); |
726
|
|
|
|
|
|
|
[ |
727
|
|
|
|
|
|
|
($pattern_type ? $fill{$pattern_type} : 0), |
728
|
|
|
|
|
|
|
$self->_color($workbook->{Color}, $_->first_child('s:fgColor'), 1), |
729
|
|
|
|
|
|
|
$self->_color($workbook->{Color}, $_->first_child('s:bgColor'), 1), |
730
|
|
|
|
|
|
|
] |
731
|
|
|
|
|
|
|
} $styles->find_nodes('//s:fills/s:fill/s:patternFill'); |
732
|
|
|
|
|
|
|
|
733
|
|
|
|
|
|
|
my @borders = map { |
734
|
|
|
|
|
|
|
my $border = $_; |
735
|
|
|
|
|
|
|
my ($ddiag, $udiag) = map { |
736
|
|
|
|
|
|
|
$self->_xml_boolean($border->att($_)) |
737
|
|
|
|
|
|
|
} qw(diagonalDown diagonalUp); |
738
|
|
|
|
|
|
|
my %borderstyles = map { |
739
|
|
|
|
|
|
|
my $e = $border->first_child("s:$_"); |
740
|
|
|
|
|
|
|
$_ => ($e ? $e->att('style') || 'none' : 'none') |
741
|
|
|
|
|
|
|
} qw(left right top bottom diagonal); |
742
|
|
|
|
|
|
|
my %bordercolors = map { |
743
|
|
|
|
|
|
|
my $e = $border->first_child("s:$_"); |
744
|
|
|
|
|
|
|
$_ => ($e ? $e->first_child('s:color') : undef) |
745
|
|
|
|
|
|
|
} qw(left right top bottom diagonal); |
746
|
|
|
|
|
|
|
# XXX specs say "begin" and "end" rather than "left" and "right", |
747
|
|
|
|
|
|
|
# but... that's not what seems to be in the file itself (sigh) |
748
|
|
|
|
|
|
|
{ |
749
|
|
|
|
|
|
|
colors => [ |
750
|
|
|
|
|
|
|
map { |
751
|
|
|
|
|
|
|
$self->_color($workbook->{Color}, $bordercolors{$_}) |
752
|
|
|
|
|
|
|
} qw(left right top bottom) |
753
|
|
|
|
|
|
|
], |
754
|
|
|
|
|
|
|
styles => [ |
755
|
|
|
|
|
|
|
map { |
756
|
|
|
|
|
|
|
$border{$borderstyles{$_}} |
757
|
|
|
|
|
|
|
} qw(left right top bottom) |
758
|
|
|
|
|
|
|
], |
759
|
|
|
|
|
|
|
diagonal => [ |
760
|
|
|
|
|
|
|
( $ddiag && $udiag ? 3 |
761
|
|
|
|
|
|
|
: $ddiag && !$udiag ? 2 |
762
|
|
|
|
|
|
|
: !$ddiag && $udiag ? 1 |
763
|
|
|
|
|
|
|
: 0), |
764
|
|
|
|
|
|
|
$border{$borderstyles{diagonal}}, |
765
|
|
|
|
|
|
|
$self->_color($workbook->{Color}, $bordercolors{diagonal}), |
766
|
|
|
|
|
|
|
], |
767
|
|
|
|
|
|
|
} |
768
|
|
|
|
|
|
|
} $styles->find_nodes('//s:borders/s:border'); |
769
|
|
|
|
|
|
|
|
770
|
|
|
|
|
|
|
my %format_str = ( |
771
|
|
|
|
|
|
|
%default_format_str, |
772
|
|
|
|
|
|
|
(map { |
773
|
|
|
|
|
|
|
$_->att('numFmtId') => $_->att('formatCode') |
774
|
|
|
|
|
|
|
} $styles->find_nodes('//s:numFmts/s:numFmt')), |
775
|
|
|
|
|
|
|
); |
776
|
|
|
|
|
|
|
|
777
|
|
|
|
|
|
|
my @font = map { |
778
|
|
|
|
|
|
|
my $vert = $_->first_child('s:vertAlign'); |
779
|
|
|
|
|
|
|
my $under = $_->first_child('s:u'); |
780
|
|
|
|
|
|
|
my $heightelem = $_->first_child('s:sz'); |
781
|
|
|
|
|
|
|
# XXX i guess 12 is okay? |
782
|
|
|
|
|
|
|
my $height = 0+($heightelem ? $heightelem->att('val') : 12); |
783
|
|
|
|
|
|
|
my $nameelem = $_->first_child('s:name'); |
784
|
|
|
|
|
|
|
my $name = $nameelem ? $nameelem->att('val') : ''; |
785
|
|
|
|
|
|
|
Spreadsheet::ParseExcel::Font->new( |
786
|
|
|
|
|
|
|
Height => $height, |
787
|
|
|
|
|
|
|
# Attr => $iAttr, |
788
|
|
|
|
|
|
|
# XXX not sure if there's a better way to keep the indexing stuff |
789
|
|
|
|
|
|
|
# intact rather than just going straight to #xxxxxx |
790
|
|
|
|
|
|
|
# XXX also not sure what it means for the color tag to be missing, |
791
|
|
|
|
|
|
|
# just assuming black for now |
792
|
|
|
|
|
|
|
Color => ($_->first_child('s:color') |
793
|
|
|
|
|
|
|
? $self->_color( |
794
|
|
|
|
|
|
|
$workbook->{Color}, |
795
|
|
|
|
|
|
|
$_->first_child('s:color') |
796
|
|
|
|
|
|
|
) |
797
|
|
|
|
|
|
|
: '#000000' |
798
|
|
|
|
|
|
|
), |
799
|
|
|
|
|
|
|
Super => ($vert |
800
|
|
|
|
|
|
|
? ($vert->att('val') eq 'superscript' ? 1 |
801
|
|
|
|
|
|
|
: $vert->att('val') eq 'subscript' ? 2 |
802
|
|
|
|
|
|
|
: 0) |
803
|
|
|
|
|
|
|
: 0 |
804
|
|
|
|
|
|
|
), |
805
|
|
|
|
|
|
|
# XXX not sure what the single accounting and double accounting |
806
|
|
|
|
|
|
|
# underline styles map to in xlsx. also need to map the new |
807
|
|
|
|
|
|
|
# underline styles |
808
|
|
|
|
|
|
|
UnderlineStyle => ($under |
809
|
|
|
|
|
|
|
# XXX sometimes style xml files can contain just with no |
810
|
|
|
|
|
|
|
# val attribute. i think this means single underline, but not |
811
|
|
|
|
|
|
|
# sure |
812
|
|
|
|
|
|
|
? (!$under->att('val') ? 1 |
813
|
|
|
|
|
|
|
: $under->att('val') eq 'single' ? 1 |
814
|
|
|
|
|
|
|
: $under->att('val') eq 'double' ? 2 |
815
|
|
|
|
|
|
|
: 0) |
816
|
|
|
|
|
|
|
: 0 |
817
|
|
|
|
|
|
|
), |
818
|
|
|
|
|
|
|
Name => $name, |
819
|
|
|
|
|
|
|
|
820
|
|
|
|
|
|
|
Bold => $_->has_child('s:b') ? 1 : 0, |
821
|
|
|
|
|
|
|
Italic => $_->has_child('s:i') ? 1 : 0, |
822
|
|
|
|
|
|
|
Underline => $_->has_child('s:u') ? 1 : 0, |
823
|
|
|
|
|
|
|
Strikeout => $_->has_child('s:strike') ? 1 : 0, |
824
|
|
|
|
|
|
|
) |
825
|
|
|
|
|
|
|
} $styles->find_nodes('//s:fonts/s:font'); |
826
|
|
|
|
|
|
|
|
827
|
|
|
|
|
|
|
my @format = map { |
828
|
|
|
|
|
|
|
my $xml_fmt = $_; |
829
|
|
|
|
|
|
|
my $alignment = $xml_fmt->first_child('s:alignment'); |
830
|
|
|
|
|
|
|
my $protection = $xml_fmt->first_child('s:protection'); |
831
|
|
|
|
|
|
|
my %ignore = map { |
832
|
|
|
|
|
|
|
("Ignore$_" => !$self->_xml_boolean($xml_fmt->att("apply$_"))) |
833
|
|
|
|
|
|
|
} qw(Font Fill Border Alignment NumberFormat Protection); |
834
|
|
|
|
|
|
|
my %opts = ( |
835
|
|
|
|
|
|
|
%default_format_opts, |
836
|
|
|
|
|
|
|
%ignore, |
837
|
|
|
|
|
|
|
); |
838
|
|
|
|
|
|
|
|
839
|
|
|
|
|
|
|
$opts{FmtIdx} = 0+($xml_fmt->att('numFmtId')||0); |
840
|
|
|
|
|
|
|
$opts{FontNo} = 0+($xml_fmt->att('fontId')||0); |
841
|
|
|
|
|
|
|
$opts{Font} = $font[$opts{FontNo}]; |
842
|
|
|
|
|
|
|
$opts{Fill} = $fills[$xml_fmt->att('fillId')||0]; |
843
|
|
|
|
|
|
|
$opts{BdrStyle} = $borders[$xml_fmt->att('borderId')||0]{styles}; |
844
|
|
|
|
|
|
|
$opts{BdrColor} = $borders[$xml_fmt->att('borderId')||0]{colors}; |
845
|
|
|
|
|
|
|
$opts{BdrDiag} = $borders[$xml_fmt->att('borderId')||0]{diagonal}; |
846
|
|
|
|
|
|
|
|
847
|
|
|
|
|
|
|
if ($alignment) { |
848
|
|
|
|
|
|
|
$opts{AlignH} = $halign{$alignment->att('horizontal') || 'general'}; |
849
|
|
|
|
|
|
|
$opts{Wrap} = $self->_xml_boolean($alignment->att('wrapText')); |
850
|
|
|
|
|
|
|
$opts{AlignV} = $valign{$alignment->att('vertical') || 'bottom'}; |
851
|
|
|
|
|
|
|
$opts{Rotate} = $alignment->att('textRotation'); |
852
|
|
|
|
|
|
|
$opts{Indent} = $alignment->att('indent'); |
853
|
|
|
|
|
|
|
$opts{Shrink} = $self->_xml_boolean($alignment->att('shrinkToFit')); |
854
|
|
|
|
|
|
|
# JustLast => $iJustL, |
855
|
|
|
|
|
|
|
} |
856
|
|
|
|
|
|
|
|
857
|
|
|
|
|
|
|
if ($protection) { |
858
|
|
|
|
|
|
|
$opts{Lock} = defined $protection->att('locked') |
859
|
|
|
|
|
|
|
? $self->_xml_boolean($protection->att('locked')) |
860
|
|
|
|
|
|
|
: 1; |
861
|
|
|
|
|
|
|
$opts{Hidden} = $self->_xml_boolean($protection->att('hidden')); |
862
|
|
|
|
|
|
|
} |
863
|
|
|
|
|
|
|
|
864
|
|
|
|
|
|
|
# Style => $iStyle, |
865
|
|
|
|
|
|
|
# Key123 => $i123, |
866
|
|
|
|
|
|
|
# Merge => $iMerge, |
867
|
|
|
|
|
|
|
# ReadDir => $iReadDir, |
868
|
|
|
|
|
|
|
Spreadsheet::ParseExcel::Format->new(%opts) |
869
|
|
|
|
|
|
|
} $styles->find_nodes('//s:cellXfs/s:xf'); |
870
|
|
|
|
|
|
|
|
871
|
|
|
|
|
|
|
return { |
872
|
|
|
|
|
|
|
FormatStr => \%format_str, |
873
|
|
|
|
|
|
|
Font => \@font, |
874
|
|
|
|
|
|
|
Format => \@format, |
875
|
|
|
|
|
|
|
} |
876
|
|
|
|
|
|
|
} |
877
|
|
|
|
|
|
|
|
878
|
|
|
|
|
|
|
sub _extract_files { |
879
|
|
|
|
|
|
|
my $self = shift; |
880
|
|
|
|
|
|
|
my ($zip) = @_; |
881
|
|
|
|
|
|
|
|
882
|
|
|
|
|
|
|
my $type_base = |
883
|
|
|
|
|
|
|
'http://schemas.openxmlformats.org/officeDocument/2006/relationships'; |
884
|
|
|
|
|
|
|
|
885
|
|
|
|
|
|
|
my $rels = $self->_parse_xml( |
886
|
|
|
|
|
|
|
$zip, |
887
|
|
|
|
|
|
|
$self->_rels_for(''), |
888
|
|
|
|
|
|
|
); |
889
|
|
|
|
|
|
|
my $wb_name = ($rels->find_nodes( |
890
|
|
|
|
|
|
|
qq/packagerels:Relationship[\@Type="$type_base/officeDocument"]> |
891
|
|
|
|
|
|
|
))[0]->att('Target'); |
892
|
|
|
|
|
|
|
$wb_name =~ s{^/}{}; |
893
|
|
|
|
|
|
|
my $wb_xml = $self->_parse_xml($zip, $wb_name); |
894
|
|
|
|
|
|
|
|
895
|
|
|
|
|
|
|
my $path_base = $self->_base_path_for($wb_name); |
896
|
|
|
|
|
|
|
my $wb_rels = $self->_parse_xml( |
897
|
|
|
|
|
|
|
$zip, |
898
|
|
|
|
|
|
|
$self->_rels_for($wb_name), |
899
|
|
|
|
|
|
|
); |
900
|
|
|
|
|
|
|
|
901
|
|
|
|
|
|
|
my $get_path = sub { |
902
|
|
|
|
|
|
|
my ($p) = @_; |
903
|
|
|
|
|
|
|
|
904
|
|
|
|
|
|
|
return $p =~ s{^/}{} |
905
|
|
|
|
|
|
|
? $p |
906
|
|
|
|
|
|
|
: $path_base . $p; |
907
|
|
|
|
|
|
|
}; |
908
|
|
|
|
|
|
|
|
909
|
|
|
|
|
|
|
my ($strings_xml) = map { |
910
|
|
|
|
|
|
|
$self->_zip_file_member($zip, $get_path->($_->att('Target'))) |
911
|
|
|
|
|
|
|
} $wb_rels->find_nodes(qq/packagerels:Relationship[\@Type="$type_base/sharedStrings"]>); |
912
|
|
|
|
|
|
|
|
913
|
|
|
|
|
|
|
my ($styles_xml) = map { |
914
|
|
|
|
|
|
|
$self->_parse_xml( |
915
|
|
|
|
|
|
|
$zip, |
916
|
|
|
|
|
|
|
$get_path->($_->att('Target')) |
917
|
|
|
|
|
|
|
) |
918
|
|
|
|
|
|
|
} $wb_rels->find_nodes(qq/packagerels:Relationship[\@Type="$type_base/styles"]>); |
919
|
|
|
|
|
|
|
|
920
|
|
|
|
|
|
|
my %worksheet_xml = map { |
921
|
|
|
|
|
|
|
($_->att('Id') => $self->_zip_file_member($zip, $get_path->($_->att('Target')))) |
922
|
|
|
|
|
|
|
} $wb_rels->find_nodes(qq/packagerels:Relationship[\@Type="$type_base/worksheet"]>); |
923
|
|
|
|
|
|
|
|
924
|
|
|
|
|
|
|
my %themes_xml = map { |
925
|
|
|
|
|
|
|
$_->att('Id') => $self->_parse_xml($zip, $get_path->($_->att('Target'))) |
926
|
|
|
|
|
|
|
} $wb_rels->find_nodes(qq/packagerels:Relationship[\@Type="$type_base/theme"]>); |
927
|
|
|
|
|
|
|
|
928
|
|
|
|
|
|
|
return { |
929
|
|
|
|
|
|
|
workbook => $wb_xml, |
930
|
|
|
|
|
|
|
sheets => \%worksheet_xml, |
931
|
|
|
|
|
|
|
themes => \%themes_xml, |
932
|
|
|
|
|
|
|
($styles_xml |
933
|
|
|
|
|
|
|
? (styles => $styles_xml) |
934
|
|
|
|
|
|
|
: ()), |
935
|
|
|
|
|
|
|
($strings_xml |
936
|
|
|
|
|
|
|
? (strings => $strings_xml) |
937
|
|
|
|
|
|
|
: ()), |
938
|
|
|
|
|
|
|
}; |
939
|
|
|
|
|
|
|
} |
940
|
|
|
|
|
|
|
|
941
|
|
|
|
|
|
|
sub _parse_xml { |
942
|
|
|
|
|
|
|
my $self = shift; |
943
|
|
|
|
|
|
|
my ($zip, $subfile, $map_xmlns) = @_; |
944
|
|
|
|
|
|
|
|
945
|
|
|
|
|
|
|
my $xml = $self->_new_twig; |
946
|
|
|
|
|
|
|
$xml->parse($self->_zip_file_member($zip, $subfile)); |
947
|
|
|
|
|
|
|
|
948
|
|
|
|
|
|
|
return $xml; |
949
|
|
|
|
|
|
|
} |
950
|
|
|
|
|
|
|
|
951
|
|
|
|
|
|
|
sub _zip_file_member { |
952
|
|
|
|
|
|
|
my $self = shift; |
953
|
|
|
|
|
|
|
my ($zip, $name) = @_; |
954
|
|
|
|
|
|
|
|
955
|
|
|
|
|
|
|
my @members = $zip->membersMatching(qr/^$name$/i); |
956
|
|
|
|
|
|
|
die "no subfile named $name" unless @members; |
957
|
|
|
|
|
|
|
|
958
|
|
|
|
|
|
|
return scalar $members[0]->contents; |
959
|
|
|
|
|
|
|
} |
960
|
|
|
|
|
|
|
|
961
|
|
|
|
|
|
|
sub _rels_for { |
962
|
|
|
|
|
|
|
my $self = shift; |
963
|
|
|
|
|
|
|
my ($file) = @_; |
964
|
|
|
|
|
|
|
|
965
|
|
|
|
|
|
|
my @path = split '/', $file; |
966
|
|
|
|
|
|
|
my $name = pop @path; |
967
|
|
|
|
|
|
|
$name = '' unless defined $name; |
968
|
|
|
|
|
|
|
push @path, '_rels'; |
969
|
|
|
|
|
|
|
push @path, "$name.rels"; |
970
|
|
|
|
|
|
|
|
971
|
|
|
|
|
|
|
return join '/', @path; |
972
|
|
|
|
|
|
|
} |
973
|
|
|
|
|
|
|
|
974
|
|
|
|
|
|
|
sub _base_path_for { |
975
|
|
|
|
|
|
|
my $self = shift; |
976
|
|
|
|
|
|
|
my ($file) = @_; |
977
|
|
|
|
|
|
|
|
978
|
|
|
|
|
|
|
my @path = split '/', $file; |
979
|
|
|
|
|
|
|
pop @path; |
980
|
|
|
|
|
|
|
|
981
|
|
|
|
|
|
|
return join('/', @path) . '/'; |
982
|
|
|
|
|
|
|
} |
983
|
|
|
|
|
|
|
|
984
|
|
|
|
|
|
|
sub _dimensions { |
985
|
|
|
|
|
|
|
my $self = shift; |
986
|
|
|
|
|
|
|
my ($dim) = @_; |
987
|
|
|
|
|
|
|
|
988
|
|
|
|
|
|
|
my ($topleft, $bottomright) = split ':', $dim; |
989
|
|
|
|
|
|
|
$bottomright = $topleft unless defined $bottomright; |
990
|
|
|
|
|
|
|
|
991
|
|
|
|
|
|
|
my ($rmin, $cmin) = $self->_cell_to_row_col($topleft); |
992
|
|
|
|
|
|
|
my ($rmax, $cmax) = $self->_cell_to_row_col($bottomright); |
993
|
|
|
|
|
|
|
|
994
|
|
|
|
|
|
|
return ($rmin, $cmin, $rmax, $cmax); |
995
|
|
|
|
|
|
|
} |
996
|
|
|
|
|
|
|
|
997
|
|
|
|
|
|
|
sub _cell_to_row_col { |
998
|
|
|
|
|
|
|
my $self = shift; |
999
|
|
|
|
|
|
|
my ($cell) = @_; |
1000
|
|
|
|
|
|
|
|
1001
|
|
|
|
|
|
|
my ($col, $row) = $cell =~ /([A-Z]+)([0-9]+)/; |
1002
|
|
|
|
|
|
|
|
1003
|
|
|
|
|
|
|
my $ncol = 0; |
1004
|
|
|
|
|
|
|
for my $char (split //, $col) { |
1005
|
|
|
|
|
|
|
$ncol *= 26; |
1006
|
|
|
|
|
|
|
$ncol += ord($char) - ord('A') + 1; |
1007
|
|
|
|
|
|
|
} |
1008
|
|
|
|
|
|
|
$ncol = $ncol - 1; |
1009
|
|
|
|
|
|
|
|
1010
|
|
|
|
|
|
|
my $nrow = $row - 1; |
1011
|
|
|
|
|
|
|
|
1012
|
|
|
|
|
|
|
return ($nrow, $ncol); |
1013
|
|
|
|
|
|
|
} |
1014
|
|
|
|
|
|
|
|
1015
|
|
|
|
|
|
|
sub _xml_boolean { |
1016
|
|
|
|
|
|
|
my $self = shift; |
1017
|
|
|
|
|
|
|
my ($bool) = @_; |
1018
|
|
|
|
|
|
|
return defined($bool) && ($bool eq 'true' || $bool eq '1'); |
1019
|
|
|
|
|
|
|
} |
1020
|
|
|
|
|
|
|
|
1021
|
|
|
|
|
|
|
sub _color { |
1022
|
|
|
|
|
|
|
my $self = shift; |
1023
|
|
|
|
|
|
|
my ($colors, $color_node, $fill) = @_; |
1024
|
|
|
|
|
|
|
|
1025
|
|
|
|
|
|
|
my $color; |
1026
|
|
|
|
|
|
|
if ($color_node && !$self->_xml_boolean($color_node->att('auto'))) { |
1027
|
|
|
|
|
|
|
if (defined $color_node->att('indexed')) { |
1028
|
|
|
|
|
|
|
# see https://rt.cpan.org/Public/Bug/Display.html?id=93065 |
1029
|
|
|
|
|
|
|
if ($fill && $color_node->att('indexed') == 64) { |
1030
|
|
|
|
|
|
|
return '#FFFFFF'; |
1031
|
|
|
|
|
|
|
} |
1032
|
|
|
|
|
|
|
else { |
1033
|
|
|
|
|
|
|
$color = '#' . Spreadsheet::ParseExcel->ColorIdxToRGB( |
1034
|
|
|
|
|
|
|
$color_node->att('indexed') |
1035
|
|
|
|
|
|
|
); |
1036
|
|
|
|
|
|
|
} |
1037
|
|
|
|
|
|
|
} |
1038
|
|
|
|
|
|
|
elsif (defined $color_node->att('rgb')) { |
1039
|
|
|
|
|
|
|
$color = '#' . substr($color_node->att('rgb'), 2, 6); |
1040
|
|
|
|
|
|
|
} |
1041
|
|
|
|
|
|
|
elsif (defined $color_node->att('theme')) { |
1042
|
|
|
|
|
|
|
my $theme = $colors->[$color_node->att('theme')]; |
1043
|
|
|
|
|
|
|
if (defined $theme) { |
1044
|
|
|
|
|
|
|
$color = "#$theme"; |
1045
|
|
|
|
|
|
|
} |
1046
|
|
|
|
|
|
|
else { |
1047
|
|
|
|
|
|
|
return undef; |
1048
|
|
|
|
|
|
|
} |
1049
|
|
|
|
|
|
|
} |
1050
|
|
|
|
|
|
|
|
1051
|
|
|
|
|
|
|
$color = $self->_apply_tint($color, $color_node->att('tint')) |
1052
|
|
|
|
|
|
|
if $color_node->att('tint'); |
1053
|
|
|
|
|
|
|
} |
1054
|
|
|
|
|
|
|
|
1055
|
|
|
|
|
|
|
return $color; |
1056
|
|
|
|
|
|
|
} |
1057
|
|
|
|
|
|
|
|
1058
|
|
|
|
|
|
|
sub _apply_tint { |
1059
|
|
|
|
|
|
|
my $self = shift; |
1060
|
|
|
|
|
|
|
my ($color, $tint) = @_; |
1061
|
|
|
|
|
|
|
|
1062
|
|
|
|
|
|
|
my ($r, $g, $b) = map { oct("0x$_") } $color =~ /#(..)(..)(..)/; |
1063
|
|
|
|
|
|
|
my ($h, $l, $s) = rgb2hls($r, $g, $b); |
1064
|
|
|
|
|
|
|
|
1065
|
|
|
|
|
|
|
if ($tint < 0) { |
1066
|
|
|
|
|
|
|
$l = $l * (1.0 + $tint); |
1067
|
|
|
|
|
|
|
} |
1068
|
|
|
|
|
|
|
else { |
1069
|
|
|
|
|
|
|
$l = $l * (1.0 - $tint) + (1.0 - 1.0 * (1.0 - $tint)); |
1070
|
|
|
|
|
|
|
} |
1071
|
|
|
|
|
|
|
|
1072
|
|
|
|
|
|
|
return scalar hls2rgb($h, $l, $s); |
1073
|
|
|
|
|
|
|
} |
1074
|
|
|
|
|
|
|
|
1075
|
|
|
|
|
|
|
sub _new_twig { |
1076
|
|
|
|
|
|
|
my $self = shift; |
1077
|
|
|
|
|
|
|
my %opts = @_; |
1078
|
|
|
|
|
|
|
|
1079
|
|
|
|
|
|
|
return XML::Twig->new( |
1080
|
|
|
|
|
|
|
map_xmlns => { |
1081
|
|
|
|
|
|
|
'http://schemas.openxmlformats.org/spreadsheetml/2006/main' => 's', |
1082
|
|
|
|
|
|
|
'http://schemas.openxmlformats.org/package/2006/relationships' => 'packagerels', |
1083
|
|
|
|
|
|
|
'http://schemas.openxmlformats.org/officeDocument/2006/relationships' => 'rels', |
1084
|
|
|
|
|
|
|
'http://schemas.openxmlformats.org/drawingml/2006/main' => 'drawmain', |
1085
|
|
|
|
|
|
|
}, |
1086
|
|
|
|
|
|
|
keep_original_prefix => 1, |
1087
|
|
|
|
|
|
|
%opts, |
1088
|
|
|
|
|
|
|
); |
1089
|
|
|
|
|
|
|
} |
1090
|
|
|
|
|
|
|
|
1091
|
|
|
|
|
|
|
|
1092
|
|
|
|
|
|
|
1; |
1093
|
|
|
|
|
|
|
|
1094
|
|
|
|
|
|
|
__END__ |