File Coverage

blib/lib/Spreadsheet/WriteExcel/Extended/FitColumnWidth.pm
Criterion Covered Total %
statement 114 151 75.5
branch 27 52 51.9
condition 7 12 58.3
subroutine 11 15 73.3
pod 10 10 100.0
total 169 240 70.4


line stmt bran cond sub pod time code
1             package Spreadsheet::WriteExcel::Extended::FitColumnWidth;
2            
3 2     2   26207 use warnings;
  2         5  
  2         68  
4 2     2   11 use strict;
  2         4  
  2         62  
5 2     2   9 use Carp;
  2         9  
  2         149  
6 2     2   10 use base qw(Spreadsheet::WriteExcel);
  2         4  
  2         6176  
7 2     2   310033 use Font::TTFMetrics;
  2         18872  
  2         3597  
8            
9             our $arial; # Default font
10             our $arialbd; # Default bold font
11             our $VERSION = sprintf("%d.%02d", q'$Revision: 1.2 $' =~ /(\d+)\.(\d+)/);
12            
13            
14             sub new
15             {
16 1     1 1 780 my( $class, @args ) = @_;
17            
18 1         2 my $workbook;
19             my $hr;
20 1 50 33     12 if (@args == 1 && ref($args[0]) eq 'HASH')
21             {
22 1         2 $hr = $args[0];
23 1 50 33     10 carp "Expect hash ref with filename defined against key 'filename'" unless ( $hr->{filename} && $hr->{filename} ne '');
24 1         13 $workbook = $class->SUPER::new( $hr->{filename} );
25             }
26             else
27             {
28             # This is not how we expect to use this subclass, ie default behaviour only
29             # Assume default of just filename with possibly other arguments
30 0         0 $workbook = $class->SUPER::new( @args );
31 0         0 return $workbook;
32             }
33            
34             # Create some formats for later use
35 1         18031 my $format_heading = $workbook->add_format();
36 1         80 $format_heading->set_bold();
37 1         26 $format_heading->set_bg_color('silver');
38 1         65 $format_heading->set_color('blue');
39 1         20 $format_heading->set_align('center');
40 1         54 $workbook->{__extended_format_heading__} = $format_heading;
41            
42 1         4 my $format_bold = $workbook->add_format();
43 1         30 $format_bold->set_bold();
44 1         9 $workbook->{__extended_format_bold__} = $format_bold;
45            
46             # Set font and background colour formats (standard)
47 1         4 foreach my $colour ('blue', 'brown', 'cyan', 'gray', 'green', 'lime', 'magenta', 'navy', 'orange', 'pink', 'purple', 'red', 'silver', 'white', 'yellow',)
48             {
49 15         42 my $fmt = $workbook->add_format();
50 15         585 $fmt->set_color($colour);
51 15         318 $workbook->{'__extended_format_' . $colour . '__'} = $fmt;
52            
53 15         53 my $fmt_bold = $workbook->add_format();
54 15         455 $fmt_bold->set_color($colour);
55 15         302 $fmt_bold->set_bold();
56 15         155 $workbook->{'__extended_format_' . $colour . '_bold__'} = $fmt_bold;
57            
58 15         39 my $fmt_bg = $workbook->add_format();
59 15         549 $fmt_bg->set_bg_color($colour);
60 15         287 $workbook->{'__extended_format_' . $colour . '_bg__'} = $fmt_bg;
61             }
62             # Set font and background colour formats (special)
63 1         6 foreach my $colour ([ 'lightblue', 0x1A], [ 'lightyellow', 0x1B], [ 'lightgreen', 0x2A], [ 'lightpurple', 0x2E],)
64             {
65 4         13 my $fmt = $workbook->add_format();
66 4         138 $fmt->set_color($colour->[1]);
67 4         123 $workbook->{'__extended_format_' . $colour->[0] . '__'} = $fmt;
68            
69 4         15 my $fmt_bold = $workbook->add_format();
70 4         152 $fmt_bold->set_color($colour->[1]);
71 4         103 $fmt_bold->set_bold();
72 4         48 $workbook->{'__extended_format_' . $colour->[0] . '_bold__'} = $fmt_bold;
73            
74 4         12 my $fmt_bg = $workbook->add_format();
75 4         169 $fmt_bg->set_bg_color($colour->[1]);
76 4         102 $workbook->{'__extended_format_' . $colour->[0] . '_bg__'} = $fmt_bg;
77             }
78             # Finally a special light gray
79 1         13 my $lgray = $workbook->set_custom_color(62, 231, 231, 231);
80 1         27 $workbook->{'__extended_format_lightgray_bg__'} = $workbook->add_format(bg_color => $lgray);
81 1         93 $workbook->{'__extended_format_lightgray__'} = $workbook->add_format(color => $lgray);
82            
83            
84             # Setup any sheets (after all that's what this subclass is for)
85 1 50       92 if ($hr->{sheets})
86             {
87 1 50       6 carp "Expect sheets value to be an array ref" unless (ref($hr->{sheets}) eq 'ARRAY');
88            
89 1         1 my $cnt = 1;
90 1         2 foreach my $sht (@{ $hr->{sheets} })
  1         3  
91             {
92             # Each sheet can be either a name or a hash ref,
93             # If a hash ref, it should contain keys: name, headings
94             # the headings value should be an array ref of column headings for the first row
95            
96 1         1 my $worksheet;
97 1 50       5 if (ref($sht) eq 'HASH')
98             {
99 1         11 $worksheet = $workbook->add_worksheet($sht->{name});
100 1         658 $worksheet->add_write_handler(qr[\w], \&extended_store_string_widths); # Based on jMcNamara example code
101 1         20 $worksheet->write_row(0, 0, $sht->{headings}, $format_heading);
102             }
103             else
104             {
105 0         0 $worksheet = $workbook->add_worksheet($sht);
106 0         0 $worksheet->add_write_handler(qr[\w], \&extended_store_string_widths); # Based on jMcNamara example code
107             }
108            
109 1         80 $worksheet->keep_leading_zeros(); # Keep leading zeros on data (good for entry_ID)
110 1         9 $worksheet->freeze_panes(1, 0); # Freeze the first row
111            
112             # Save it into the object
113 1         10 push @{$workbook->{__extended_sheets__}}, $worksheet;
  1         5  
114             }
115             }
116            
117             # Should expand this to cater for other fonts, font sizes and location of TTF's
118 1         2 my $font_file = 'c:\windows\fonts\arial.ttf';
119 1         2 my $font_file_bold = 'c:\windows\fonts\arialbd.ttf';
120            
121 1 50       4 if ($hr->{font})
122             {
123 0 0       0 if (-f $hr->{font})
124             {
125 0         0 $font_file = $hr->{font};
126             }
127             else
128             {
129 0         0 carp "Specified font file $hr->{font} does not exist\n";
130             }
131             }
132 1 50       6 if ($hr->{font_bold})
133             {
134 0 0       0 if (-f $hr->{font_bold})
135             {
136 0         0 $font_file_bold = $hr->{font_bold};
137             }
138             else
139             {
140 0         0 carp "Specified font file $hr->{font_bold} does not exist\n";
141             }
142             }
143            
144 1 50       13 unless (-f $font_file)
145             {
146 1         277 carp "Could not find font file $font_file";
147             }
148             else
149             {
150 0         0 $arial = Font::TTFMetrics->new($font_file);
151             }
152 1 50       177 unless (-f $font_file_bold)
153             {
154 1         176 carp "Could not find font file $font_file_bold";
155             }
156             else
157             {
158 0         0 $arialbd = Font::TTFMetrics->new($font_file_bold);
159             }
160            
161 1         81 return $workbook;
162             }
163            
164            
165             sub get_formats
166             {
167 0     0 1 0 my $workbook = shift;
168            
169 0         0 return sort grep { /^__extended_format/ } keys %$workbook;
  0         0  
170             }
171            
172             sub get_format
173             {
174 1     1 1 14 my ($workbook, $name) = @_;
175            
176 1 50       7 if ($workbook->{'__extended_format_' . lc($name) . '__'})
177             {
178 1         8 return $workbook->{'__extended_format_' . lc($name) . '__'};
179             }
180            
181 0         0 my $msg = "Extended format $name does not exist, valid values are:\n";
182 0         0 $msg .= join("\n", map { /__extended_format_(.*)__/; $1 } $workbook->get_formats());
  0         0  
  0         0  
183            
184 0         0 croak $msg;
185             }
186            
187             sub get_number_sheets
188             {
189 0     0 1 0 my $workbook = shift;
190            
191 0         0 return scalar(@{$workbook->{__extended_sheets__}});
  0         0  
192             }
193            
194             sub get_worksheets_extended
195             {
196 0     0 1 0 my $workbook = shift;
197            
198 0         0 return @{ $workbook->{__extended_sheets__} };
  0         0  
199             }
200            
201            
202            
203             ###############################################################################
204             #
205             # The following function is a callback that was added via add_write_handler()
206             # above. It modifies the write() function so that it stores the maximum
207             # unwrapped width of a string in a column.
208             #
209             sub extended_store_string_widths
210             {
211 15     15 1 1148 my $worksheet = shift;
212 15         19 my $row = $_[0];
213 15         17 my $col = $_[1];
214 15         13 my $token = $_[2];
215            
216             # Ignore some tokens that we aren't interested in.
217 15 50       33 return if not defined $token; # Ignore undefs.
218 15 50       37 return if $token eq ''; # Ignore blank cells.
219 15 50       30 return if ref $token eq 'ARRAY'; # Ignore array refs.
220 15 50       30 return if $token =~ /^=/; # Ignore formula
221            
222             # Ignore numbers
223 15 100       57 return if $token =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/;
224            
225             # Ignore various internal and external hyperlinks. In a real scenario
226             # you may wish to track the length of the optional strings used with
227             # urls.
228 11 50       21 return if $token =~ m{^[fh]tt?ps?://};
229 11 50       19 return if $token =~ m{^mailto:};
230 11 50       19 return if $token =~ m{^(?:in|ex)ternal:};
231            
232            
233             # We store the string width as data in the Worksheet object. We use
234             # a double underscore key name to avoid conflicts with future names.
235             #
236 11         17 my $old_width = $worksheet->{__extended_col_widths}->[$col];
237 11         10 my $string_width;
238 11 50       21 if ($arial)
239             {
240 0         0 $string_width = string_width_fancy($token, $arial);
241             }
242             else
243             {
244 11         24 $string_width = string_width_simple($token);
245             }
246            
247             # Hack to cater for header row being bold - this needs to be done better
248 11 50 66     39 if ($row == 0 && $arialbd)
    100          
249             {
250 0         0 $string_width = string_width_fancy($token, $arialbd);
251             }
252             elsif ($row == 0)
253             {
254 3         4 $string_width *= 1.15;
255             }
256            
257 11 100 100     39 if (not defined $old_width or $string_width > $old_width)
258             {
259             # Set a minium of 4 - this should be configurable
260 6 50       18 $worksheet->{__extended_col_widths}->[$col] = $string_width > 4 ? $string_width : 4;
261             }
262            
263             # Return control to write();
264 11         29 return undef;
265             }
266            
267            
268             ###############################################################################
269             #
270             # Very simple conversion between string length and string width for Arial 10.
271             # See below for a more sophisticated method.
272             #
273             sub string_width_simple
274             {
275 11     11 1 23 return 0.9 * length $_[0];
276             }
277            
278            
279             ###############################################################################
280             #
281             # This function uses an external module to get a more accurate width for a
282             # string. Note that in a real program you could "use" the module instead of
283             # "require"-ing it and you could make the Font object global to avoid repeated
284             # initialisation.
285             #
286             # Note also that the $pixel_width to $cell_width is specific to Arial. For
287             # other fonts you should calculate appropriate relationships. A future version
288             # of S::WE will provide a way of specifying column widths in pixels instead of
289             # cell units in order to simplify this conversion.
290             #
291             sub string_width_fancy
292             {
293 0     0 1 0 my ($str, $font_metrics) = @_;
294            
295 0         0 my $font_size = 10;
296 0         0 my $dpi = 96;
297 0         0 my $units_per_em = $font_metrics->get_units_per_em();
298 0         0 my $font_width = $font_metrics->string_width($str);
299            
300             # Convert to pixels as per TTFMetrics docs.
301 0         0 my $pixel_width = 6 + $font_width *$font_size *$dpi /(72 *$units_per_em);
302            
303             # Add extra pixels for border around text.
304 0         0 $pixel_width += 6;
305            
306             # Convert to cell width (for Arial) and for cell widths > 1.
307 0         0 my $cell_width = ($pixel_width -5) /7;
308            
309 0         0 return $cell_width;
310             }
311            
312            
313             ###############################################################################
314             #
315             # Adjust the column widths to fit the longest string in the column.
316             #
317             sub extended_autofit_columns
318             {
319 1     1 1 1 my $worksheet = shift;
320 1         2 my $col = 0;
321            
322 1         2 for my $width (@{$worksheet->{__extended_col_widths}})
  1         2  
323             {
324 3 50       15 $worksheet->set_column($col, $col, $width) if $width;
325 3         73 $col++;
326             }
327             }
328            
329            
330             sub close
331             {
332 1     1 1 3 my $workbook = shift;
333            
334             # Do the autofit of columns
335 1         2 foreach my $worksheet (@{$workbook->{__extended_sheets__}})
  1         3  
336             {
337 1         4 extended_autofit_columns($worksheet);
338             }
339            
340             # Now close
341 1         17 $workbook->SUPER::close();
342             }
343            
344            
345             #####################################################################
346             # DO NOT REMOVE THE FOLLOWING LINE, IT IS NEEDED TO LOAD THIS LIBRARY
347             1;
348            
349            
350             __END__