File Coverage

blib/lib/Spreadsheet/ReadSXC.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Spreadsheet::ReadSXC;
2              
3 1     1   27489 use 5.006;
  1         5  
  1         35  
4 1     1   5 use strict;
  1         2  
  1         35  
5 1     1   5 use warnings;
  1         7  
  1         140  
6              
7             require Exporter;
8              
9             our @ISA = qw(Exporter);
10             our @EXPORT_OK = qw(read_sxc read_xml_file read_xml_string);
11             our $VERSION = '0.20';
12              
13 1     1   1131 use Archive::Zip;
  1         86427  
  1         39  
14 1     1   504 use XML::Parser;
  0            
  0            
15              
16             my %workbook = ();
17             my @worksheets = ();
18             my @sheet_order = ();
19             my $table = "";
20             my $row = -1;
21             my $col = -1;
22             my $text_p = -1;
23             my @cell = ();
24             my $repeat_cells = 1;
25             my $repeat_rows = 1;
26             my $row_hidden = 0;
27             my $date_value = '';
28             my $time_value = '';
29             my $max_datarow = -1;
30             my $max_datacol = -1;
31             my $col_count = -1;
32             my @hidden_cols = ();
33             my %options = ();
34              
35             sub zip_error_handler {}
36              
37             sub read_sxc ($;$) {
38             my ($sxc_file, $options_ref) = @_;
39             -f $sxc_file && -s _ or return undef;
40             Archive::Zip::setErrorHandler(\&zip_error_handler);
41             eval {
42             my $zip = Archive::Zip->new($sxc_file);
43             my $xml_string = $zip->contents('content.xml');
44             return read_xml_string($xml_string, $options_ref);
45             };
46             }
47              
48             sub read_xml_file ($;$) {
49             my ($xml_file, $options_ref) = @_;
50             -f $xml_file && -s _ or return undef;
51             local $/;
52             open CONTENT, "<$xml_file" or die "$xml_file: $!\n";
53             my $xml_string = ;
54             close CONTENT;
55             return read_xml_string($xml_string, $options_ref);
56             }
57              
58             sub read_xml_string ($;$) {
59             my ($xml_string, $options_ref) = @_;
60             %workbook = ();
61             @worksheets = ();
62             if ( defined $options_ref ) { %options = %{$options_ref}}
63             eval {
64             my $p = XML::Parser->new(Handlers => {Start => \&handle_start,
65             End => \&handle_end,
66             Char => \&char_start});
67             $p->parse($xml_string);
68             };
69             if ( $options{OrderBySheet} ) { return [@worksheets] } else { return {%workbook} }
70             }
71              
72             sub handle_start {
73             my ($expat, $element, %attributes) = @_;
74             if ( $element eq "text:p" ) {
75             # increase paragraph count if not part of an annotation
76             if ( ! $expat->within_element('office:annotation') ) { $text_p++ }
77             }
78             elsif ( ( $element eq "table:table-cell" ) or ( $element eq "table:covered-table-cell" ) ) {
79             # increase cell count
80             $col++;
81             # if number-columns-repeated is set, set $repeat_cells value accordingly for later use
82             if ( exists $attributes{'table:number-columns-repeated'} ) {
83             $repeat_cells = $attributes{'table:number-columns-repeated'};
84             }
85             # if cell contains date or time values, set boolean variable for later use
86             if (exists $attributes{'table:date-value'} ) {
87             $date_value = $attributes{'table:date-value'};
88             }
89             elsif (exists $attributes{'table:time-value'} ) {
90             $time_value = $attributes{'table:time-value'};
91             }
92             }
93             elsif ( $element eq "table:table-row" ) {
94             # increase row count
95             $row++;
96             # if row is hidden, set $row_hidden for later use
97             if ( exists $attributes{'table:visibility'} ) { $row_hidden = 1 } else { $row_hidden = 0 }
98             # if number-rows-repeated is set, set $repeat_rows value accordingly for later use
99             if ( exists $attributes{'table:number-rows-repeated'} ) {
100             $repeat_rows = $attributes{'table:number-rows-repeated'};
101             }
102             }
103             elsif ( $element eq "table:table-column" ) {
104             # increase column count
105             $col_count++;
106             # if columns is hidden, add column number to @hidden_cols array for later use
107             if ( exists $attributes{'table:visibility'} ) {
108             push @hidden_cols, $col_count;
109             }
110             # if number-columns-repeated is set and column is hidden, add affected columns to @hidden_cols
111             if ( exists $attributes{'table:number-columns-repeated'} ) {
112             $col_count++;
113             if ( exists $attributes{'table:visibility'} ) {
114             for (2..$attributes{'table:number-columns-repeated'}) {
115             push @hidden_cols, $hidden_cols[$#hidden_cols] + 1;
116             }
117             }
118             }
119             }
120             elsif ( $element eq "table:table" ) {
121             # get name of current table
122             $table = $attributes{'table:name'};
123             }
124             }
125              
126             sub handle_end {
127             my ($expat, $element) = @_;
128             if ( $element eq "table:table") {
129             # decrease $max_datacol if hidden columns within range
130             if ( ( ! $options{NoTruncate} ) and ( $options{DropHiddenColumns} ) ) {
131             for ( 1..scalar grep { $_ <= $max_datacol } @hidden_cols ) {
132             $max_datacol--;
133             }
134             }
135             # truncate table to $max_datarow and $max_datacol
136             if ( ! $options{NoTruncate} ) {
137             $#{$workbook{$table}} = $max_datarow;
138             foreach ( @{$workbook{$table}} ) {
139             $#{$_} = $max_datacol;
140             }
141             }
142             # set up alternative data structure
143             if ( $options{OrderBySheet} ) {
144             push @worksheets, (
145             {
146             label => $table,
147             data => \@{$workbook{$table}},
148             }
149             );
150             }
151             # reset table, column, and row values to default for next table
152             $row = -1;
153             $max_datarow = -1;
154             $max_datacol = -1;
155             $table = "";
156             $col_count = -1;
157             @hidden_cols = ();
158             }
159             elsif ( $element eq "table:table-row" ) {
160             # drop hidden columns from current row
161             if ( $options{DropHiddenColumns} ) {
162             foreach ( reverse @hidden_cols ) {
163             splice @{$workbook{$table}[$row]}, $_, 1;
164             }
165             }
166             # drop current row, if hidden
167             if ( ( $options{DropHiddenRows} ) and ( $row_hidden == 1 ) ) {
168             pop @{$workbook{$table}};
169             $row--;
170             }
171             # repeat current row, if necessary
172             else {
173             for (2..$repeat_rows) {
174             $row++;
175             $workbook{$table}[$row] = $workbook{$table}[$row - 1] # copy reference, not data
176             }
177             # set max_datarow, if row not empty
178             if ( grep { defined $_ } @{$workbook{$table}[$row]} ) {
179             $max_datarow = $row;
180             }
181             }
182             # reset row and col values to default for next row
183             $repeat_rows = 1;
184             $col = -1;
185             }
186             elsif ( ( $element eq "table:table-cell" ) or ( $element eq "table:covered-table-cell" ) ) {
187             # assign date or time value to current workbook cell if requested
188             if ( ( $options{StandardDate} ) and ( $date_value ) ) {
189             $workbook{$table}[$row][$col] = $date_value;
190             $date_value = '';
191             }
192             elsif ( ( $options{StandardTime} ) and ( $time_value ) ) {
193             $workbook{$table}[$row][$col] = $time_value;
194             $time_value = '';
195             }
196             # join cell contents and assign to current workbook cell
197             else {
198             $workbook{$table}[$row][$col] = @cell ? join $options{ReplaceNewlineWith} || "", @cell : undef;
199             }
200             # repeat current cell, if necessary
201             for (2..$repeat_cells) {
202             $col++;
203             $workbook{$table}[$row][$col] = $workbook{$table}[$row][$col - 1];
204             }
205             # reset cell and paragraph values to default for next cell
206             @cell = ();
207             $repeat_cells = 1;
208             $text_p = -1;
209             }
210             }
211              
212             sub char_start {
213             my ($expat, $content) = @_;
214             # don't include paragraph if part of an annotation
215             if ( $expat->within_element('office:annotation') ) {
216             return;
217             }
218             # don't include covered cells, if not requested
219             if ( ( $expat->within_element('table:covered-table-cell') ) and ( ! $options{IncludeCoveredCells} ) ) {
220             return;
221             }
222             # add paragraph or textspan to current @cell array
223             if ( $table ) {
224             $cell[$text_p] .= $content;
225             # set $max_datarow and $max_datacol to current values
226             $max_datarow = $row;
227             if ( $col > $max_datacol ) { $max_datacol = $col }
228             }
229             }
230              
231             1;
232             __END__