| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Spreadsheet::ParseXLSX; |
|
2
|
|
|
|
|
|
|
our $AUTHORITY = 'cpan:DOY'; |
|
3
|
|
|
|
|
|
|
$Spreadsheet::ParseXLSX::VERSION = '0.26'; |
|
4
|
31
|
|
|
31
|
|
402865
|
use strict; |
|
|
31
|
|
|
|
|
47
|
|
|
|
31
|
|
|
|
|
718
|
|
|
5
|
31
|
|
|
31
|
|
89
|
use warnings; |
|
|
31
|
|
|
|
|
31
|
|
|
|
31
|
|
|
|
|
553
|
|
|
6
|
31
|
|
|
31
|
|
459
|
use 5.010; |
|
|
31
|
|
|
|
|
69
|
|
|
7
|
|
|
|
|
|
|
# ABSTRACT: parse XLSX files |
|
8
|
|
|
|
|
|
|
|
|
9
|
31
|
|
|
31
|
|
16880
|
use Archive::Zip; |
|
|
31
|
|
|
|
|
1896251
|
|
|
|
31
|
|
|
|
|
1122
|
|
|
10
|
31
|
|
|
31
|
|
16023
|
use Graphics::ColorUtils 'rgb2hls', 'hls2rgb'; |
|
|
31
|
|
|
|
|
153483
|
|
|
|
31
|
|
|
|
|
1923
|
|
|
11
|
31
|
|
|
31
|
|
161
|
use Scalar::Util 'openhandle'; |
|
|
31
|
|
|
|
|
34
|
|
|
|
31
|
|
|
|
|
1227
|
|
|
12
|
31
|
|
|
31
|
|
21768
|
use Spreadsheet::ParseExcel 0.61; |
|
|
31
|
|
|
|
|
1103845
|
|
|
|
31
|
|
|
|
|
797
|
|
|
13
|
31
|
|
|
31
|
|
56155
|
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__ |