Server IP : 103.119.228.120 / Your IP : 18.188.107.57 Web Server : Apache System : Linux v8.techscape8.com 3.10.0-1160.119.1.el7.tuxcare.els2.x86_64 #1 SMP Mon Jul 15 12:09:18 UTC 2024 x86_64 User : nobody ( 99) PHP Version : 5.6.40 Disable Function : shell_exec,symlink,system,exec,proc_get_status,proc_nice,proc_terminate,define_syslog_variables,syslog,openlog,closelog,escapeshellcmd,passthru,ocinum cols,ini_alter,leak,listen,chgrp,apache_note,apache_setenv,debugger_on,debugger_off,ftp_exec,dl,dll,myshellexec,proc_open,socket_bind,proc_close,escapeshellarg,parse_ini_filepopen,fpassthru,exec,passthru,escapeshellarg,escapeshellcmd,proc_close,proc_open,ini_alter,popen,show_source,proc_nice,proc_terminate,proc_get_status,proc_close,pfsockopen,leak,apache_child_terminate,posix_kill,posix_mkfifo,posix_setpgid,posix_setsid,posix_setuid,dl,symlink,shell_exec,system,dl,passthru,escapeshellarg,escapeshellcmd,myshellexec,c99_buff_prepare,c99_sess_put,fpassthru,getdisfunc,fx29exec,fx29exec2,is_windows,disp_freespace,fx29sh_getupdate,fx29_buff_prepare,fx29_sess_put,fx29shexit,fx29fsearch,fx29ftpbrutecheck,fx29sh_tools,fx29sh_about,milw0rm,imagez,sh_name,myshellexec,checkproxyhost,dosyayicek,c99_buff_prepare,c99_sess_put,c99getsource,c99sh_getupdate,c99fsearch,c99shexit,view_perms,posix_getpwuid,posix_getgrgid,posix_kill,parse_perms,parsesort,view_perms_color,set_encoder_input,ls_setcheckboxall,ls_reverse_all,rsg_read,rsg_glob,selfURL,dispsecinfo,unix2DosTime,addFile,system,get_users,view_size,DirFiles,DirFilesWide,DirPrintHTMLHeaders,GetFilesTotal,GetTitles,GetTimeTotal,GetMatchesCount,GetFileMatchesCount,GetResultFiles,fs_copy_dir,fs_copy_obj,fs_move_dir,fs_move_obj,fs_rmdir,SearchText,getmicrotime MySQL : ON | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /usr/local/ssl/local/ssl/local/ssl/local/ssl/local/share/perl5/Spreadsheet/WriteExcel/ |
Upload File : |
package Spreadsheet::WriteExcel::Workbook; ############################################################################### # # Workbook - A writer class for Excel Workbooks. # # # Used in conjunction with Spreadsheet::WriteExcel # # Copyright 2000-2010, John McNamara, jmcnamara@cpan.org # # Documentation after __END__ # use Exporter; use strict; use Carp; use Spreadsheet::WriteExcel::BIFFwriter; use Spreadsheet::WriteExcel::OLEwriter; use Spreadsheet::WriteExcel::Worksheet; use Spreadsheet::WriteExcel::Format; use Spreadsheet::WriteExcel::Chart; use Spreadsheet::WriteExcel::Properties ':property_sets'; use vars qw($VERSION @ISA); @ISA = qw(Spreadsheet::WriteExcel::BIFFwriter Exporter); $VERSION = '2.40'; ############################################################################### # # new() # # Constructor. Creates a new Workbook object from a BIFFwriter object. # sub new { my $class = shift; my $self = Spreadsheet::WriteExcel::BIFFwriter->new(); my $byte_order = $self->{_byte_order}; my $parser = Spreadsheet::WriteExcel::Formula->new($byte_order); $self->{_filename} = $_[0] || ''; $self->{_parser} = $parser; $self->{_tempdir} = undef; $self->{_1904} = 0; $self->{_activesheet} = 0; $self->{_firstsheet} = 0; $self->{_selected} = 0; $self->{_xf_index} = 0; $self->{_fileclosed} = 0; $self->{_biffsize} = 0; $self->{_sheet_name} = 'Sheet'; $self->{_chart_name} = 'Chart'; $self->{_sheet_count} = 0; $self->{_chart_count} = 0; $self->{_url_format} = ''; $self->{_codepage} = 0x04E4; $self->{_country} = 1; $self->{_worksheets} = []; $self->{_sheetnames} = []; $self->{_formats} = []; $self->{_palette} = []; $self->{_using_tmpfile} = 1; $self->{_filehandle} = ""; $self->{_temp_file} = ""; $self->{_internal_fh} = 0; $self->{_fh_out} = ""; $self->{_str_total} = 0; $self->{_str_unique} = 0; $self->{_str_table} = {}; $self->{_str_array} = []; $self->{_str_block_sizes} = []; $self->{_extsst_offsets} = []; $self->{_extsst_buckets} = 0; $self->{_extsst_bucket_size} = 0; $self->{_ext_ref_count} = 0; $self->{_ext_refs} = {}; $self->{_mso_clusters} = []; $self->{_mso_size} = 0; $self->{_hideobj} = 0; $self->{_compatibility} = 0; $self->{_add_doc_properties} = 0; $self->{_localtime} = [localtime()]; $self->{_defined_names} = []; bless $self, $class; # Add the in-built style formats and the default cell format. $self->add_format(type => 1); # 0 Normal $self->add_format(type => 1); # 1 RowLevel 1 $self->add_format(type => 1); # 2 RowLevel 2 $self->add_format(type => 1); # 3 RowLevel 3 $self->add_format(type => 1); # 4 RowLevel 4 $self->add_format(type => 1); # 5 RowLevel 5 $self->add_format(type => 1); # 6 RowLevel 6 $self->add_format(type => 1); # 7 RowLevel 7 $self->add_format(type => 1); # 8 ColLevel 1 $self->add_format(type => 1); # 9 ColLevel 2 $self->add_format(type => 1); # 10 ColLevel 3 $self->add_format(type => 1); # 11 ColLevel 4 $self->add_format(type => 1); # 12 ColLevel 5 $self->add_format(type => 1); # 13 ColLevel 6 $self->add_format(type => 1); # 14 ColLevel 7 $self->add_format(); # 15 Cell XF $self->add_format(type => 1, num_format => 0x2B); # 16 Comma $self->add_format(type => 1, num_format => 0x29); # 17 Comma[0] $self->add_format(type => 1, num_format => 0x2C); # 18 Currency $self->add_format(type => 1, num_format => 0x2A); # 19 Currency[0] $self->add_format(type => 1, num_format => 0x09); # 20 Percent # Add the default format for hyperlinks $self->{_url_format} = $self->add_format(color => 'blue', underline => 1); # Check for a filename unless it is an existing filehandle if (not ref $self->{_filename} and $self->{_filename} eq '') { carp 'Filename required by Spreadsheet::WriteExcel->new()'; return undef; } # Convert the filename to a filehandle to pass to the OLE writer when the # file is closed. If the filename is a reference it is assumed that it is # a valid filehandle. # if (not ref $self->{_filename}) { my $fh = FileHandle->new('>'. $self->{_filename}); if (not defined $fh) { carp "Can't open " . $self->{_filename} . ". It may be in use or protected"; return undef; } # binmode file whether platform requires it or not binmode($fh); $self->{_internal_fh} = 1; $self->{_fh_out} = $fh; } else { $self->{_internal_fh} = 0; $self->{_fh_out} = $self->{_filename}; } # Set colour palette. $self->set_palette_xl97(); # Load Encode if we can. require Encode if $] >= 5.008; $self->_initialize(); $self->_get_checksum_method(); return $self; } ############################################################################### # # _initialize() # # Open a tmp file to store the majority of the Worksheet data. If this fails, # for example due to write permissions, store the data in memory. This can be # slow for large files. # # TODO: Move this and other methods shared with Worksheet up into BIFFwriter. # sub _initialize { my $self = shift; my $fh; my $tmp_dir; # The following code is complicated by Windows limitations. Porters can # choose a more direct method. # In the default case we use IO::File->new_tmpfile(). This may fail, in # particular with IIS on Windows, so we allow the user to specify a temp # directory via File::Temp. # if (defined $self->{_tempdir}) { # Delay loading File:Temp to reduce the module dependencies. eval { require File::Temp }; die "The File::Temp module must be installed in order ". "to call set_tempdir().\n" if $@; # Trap but ignore File::Temp errors. eval { $fh = File::Temp::tempfile(DIR => $self->{_tempdir}) }; # Store the failed tmp dir in case of errors. $tmp_dir = $self->{_tempdir} || File::Spec->tmpdir if not $fh; } else { $fh = IO::File->new_tmpfile(); # Store the failed tmp dir in case of errors. $tmp_dir = "POSIX::tmpnam() directory" if not $fh; } # Check if the temp file creation was successful. Else store data in memory. if ($fh) { # binmode file whether platform requires it or not. binmode($fh); # Store filehandle $self->{_filehandle} = $fh; } else { # Set flag to store data in memory if XX::tempfile() failed. $self->{_using_tmpfile} = 0; if ($^W) { my $dir = $self->{_tempdir} || File::Spec->tmpdir(); warn "Unable to create temp files in $tmp_dir. Data will be ". "stored in memory. Refer to set_tempdir() in the ". "Spreadsheet::WriteExcel documentation.\n" ; } } } ############################################################################### # # _get_checksum_method. # # Check for modules available to calculate image checksum. Excel uses MD4 but # MD5 will also work. # sub _get_checksum_method { my $self = shift; eval { require Digest::MD4}; if (not $@) { $self->{_checksum_method} = 1; return; } eval { require Digest::Perl::MD4}; if (not $@) { $self->{_checksum_method} = 2; return; } eval { require Digest::MD5}; if (not $@) { $self->{_checksum_method} = 3; return; } # Default. $self->{_checksum_method} = 0; } ############################################################################### # # _append(), overridden. # # Store Worksheet data in memory using the base class _append() or to a # temporary file, the default. # sub _append { my $self = shift; my $data = ''; if ($self->{_using_tmpfile}) { $data = join('', @_); # Add CONTINUE records if necessary $data = $self->_add_continue($data) if length($data) > $self->{_limit}; # Protect print() from -l on the command line. local $\ = undef; print {$self->{_filehandle}} $data; $self->{_datasize} += length($data); } else { $data = $self->SUPER::_append(@_); } return $data; } ############################################################################### # # get_data(). # # Retrieves data from memory in one chunk, or from disk in $buffer # sized chunks. # sub get_data { my $self = shift; my $buffer = 4096; my $tmp; # Return data stored in memory if (defined $self->{_data}) { $tmp = $self->{_data}; $self->{_data} = undef; my $fh = $self->{_filehandle}; seek($fh, 0, 0) if $self->{_using_tmpfile}; return $tmp; } # Return data stored on disk if ($self->{_using_tmpfile}) { return $tmp if read($self->{_filehandle}, $tmp, $buffer); } # No data to return return undef; } ############################################################################### # # close() # # Calls finalization methods and explicitly close the OLEwriter file # handle. # sub close { my $self = shift; return if $self->{_fileclosed}; # Prevent close() from being called twice. $self->{_fileclosed} = 1; return $self->_store_workbook(); } ############################################################################### # # DESTROY() # # Close the workbook if it hasn't already been explicitly closed. # sub DESTROY { my $self = shift; local ($@, $!, $^E, $?); $self->close() if not $self->{_fileclosed}; } ############################################################################### # # sheets(slice,...) # # An accessor for the _worksheets[] array # # Returns: an optionally sliced list of the worksheet objects in a workbook. # sub sheets { my $self = shift; if (@_) { # Return a slice of the array return @{$self->{_worksheets}}[@_]; } else { # Return the entire list return @{$self->{_worksheets}}; } } ############################################################################### # # worksheets() # # An accessor for the _worksheets[] array. # This method is now deprecated. Use the sheets() method instead. # # Returns: an array reference # sub worksheets { my $self = shift; return $self->{_worksheets}; } ############################################################################### # # add_worksheet($name, $encoding) # # Add a new worksheet to the Excel workbook. # # Returns: reference to a worksheet object # sub add_worksheet { my $self = shift; my $index = @{$self->{_worksheets}}; my ($name, $encoding) = $self->_check_sheetname($_[0], $_[1]); # Porters take note, the following scheme of passing references to Workbook # data (in the \$self->{_foo} cases) instead of a reference to the Workbook # itself is a workaround to avoid circular references between Workbook and # Worksheet objects. Feel free to implement this in any way the suits your # language. # my @init_data = ( $name, $index, $encoding, \$self->{_activesheet}, \$self->{_firstsheet}, $self->{_url_format}, $self->{_parser}, $self->{_tempdir}, \$self->{_str_total}, \$self->{_str_unique}, \$self->{_str_table}, $self->{_1904}, $self->{_compatibility}, undef, # Palette. Not used yet. See add_chart(). ); my $worksheet = Spreadsheet::WriteExcel::Worksheet->new(@init_data); $self->{_worksheets}->[$index] = $worksheet; # Store ref for iterator $self->{_sheetnames}->[$index] = $name; # Store EXTERNSHEET names $self->{_parser}->set_ext_sheets($name, $index); # Store names in Formula.pm return $worksheet; } # Older method name for backwards compatibility. *addworksheet = *add_worksheet; ############################################################################### # # add_chart(%args) # # Create a chart for embedding or as a new sheet. # # sub add_chart { my $self = shift; my %arg = @_; my $name = ''; my $encoding = 0; my $index = @{ $self->{_worksheets} }; # Type must be specified so we can create the required chart instance. my $type = $arg{type}; if ( !defined $type ) { croak "Must define chart type in add_chart()"; } # Ensure that the chart defaults to non embedded. my $embedded = $arg{embedded} ||= 0; # Check the worksheet name for non-embedded charts. if ( !$embedded ) { ( $name, $encoding ) = $self->_check_sheetname( $arg{name}, $arg{name_encoding}, 1 ); } my @init_data = ( $name, $index, $encoding, \$self->{_activesheet}, \$self->{_firstsheet}, $self->{_url_format}, $self->{_parser}, $self->{_tempdir}, \$self->{_str_total}, \$self->{_str_unique}, \$self->{_str_table}, $self->{_1904}, $self->{_compatibility}, $self->{_palette}, ); my $chart = Spreadsheet::WriteExcel::Chart->factory( $type, @init_data ); # If the chart isn't embedded let the workbook control it. if ( !$embedded ) { $self->{_worksheets}->[$index] = $chart; # Store ref for iterator $self->{_sheetnames}->[$index] = $name; # Store EXTERNSHEET names } else { # Set index to 0 so that the activate() and set_first_sheet() methods # point back to the first worksheet if used for embedded charts. $chart->{_index} = 0; $chart->_set_embedded_config_data(); } return $chart; } ############################################################################### # # add_chart_ext($filename, $name) # # Add an externally created chart. # # sub add_chart_ext { my $self = shift; my $filename = $_[0]; my $index = @{$self->{_worksheets}}; my $type = 'external'; my ($name, $encoding) = $self->_check_sheetname($_[1], $_[2]); my @init_data = ( $filename, $name, $index, $encoding, \$self->{_activesheet}, \$self->{_firstsheet}, ); my $chart = Spreadsheet::WriteExcel::Chart->factory($type, @init_data); $self->{_worksheets}->[$index] = $chart; # Store ref for iterator $self->{_sheetnames}->[$index] = $name; # Store EXTERNSHEET names return $chart; } ############################################################################### # # _check_sheetname($name, $encoding) # # Check for valid worksheet names. We check the length, if it contains any # invalid characters and if the name is unique in the workbook. # sub _check_sheetname { my $self = shift; my $name = $_[0] || ""; my $encoding = $_[1] || 0; my $chart = $_[2] || 0; my $limit = $encoding ? 62 : 31; my $invalid_char = qr([\[\]:*?/\\]); # Increment the Sheet/Chart number used for default sheet names below. if ( $chart ) { $self->{_chart_count}++; } else { $self->{_sheet_count}++; } # Supply default Sheet/Chart name if none has been defined. if ( $name eq "" ) { $encoding = 0; if ( $chart ) { $name = $self->{_chart_name} . $self->{_chart_count}; } else { $name = $self->{_sheet_name} . $self->{_sheet_count}; } } # Check that sheetname is <= 31 (1 or 2 byte chars). Excel limit. croak "Sheetname $name must be <= 31 chars" if length $name > $limit; # Check that Unicode sheetname has an even number of bytes croak 'Odd number of bytes in Unicode worksheet name:' . $name if $encoding == 1 and length($name) % 2; # Check that sheetname doesn't contain any invalid characters if ($encoding != 1 and $name =~ $invalid_char) { # Check ASCII names croak 'Invalid character []:*?/\\ in worksheet name: ' . $name; } else { # Extract any 8bit clean chars from the UTF16 name and validate them. for my $wchar ($name =~ /../sg) { my ($hi, $lo) = unpack "aa", $wchar; if ($hi eq "\0" and $lo =~ $invalid_char) { croak 'Invalid character []:*?/\\ in worksheet name: ' . $name; } } } # Handle utf8 strings in perl 5.8. if ($] >= 5.008) { require Encode; if (Encode::is_utf8($name)) { $name = Encode::encode("UTF-16BE", $name); $encoding = 1; } } # Check that the worksheet name doesn't already exist since this is a fatal # error in Excel 97. The check must also exclude case insensitive matches # since the names 'Sheet1' and 'sheet1' are equivalent. The tests also have # to take the encoding into account. # foreach my $worksheet (@{$self->{_worksheets}}) { my $name_a = $name; my $encd_a = $encoding; my $name_b = $worksheet->{_name}; my $encd_b = $worksheet->{_encoding}; my $error = 0; if ($encd_a == 0 and $encd_b == 0) { $error = 1 if lc($name_a) eq lc($name_b); } elsif ($encd_a == 0 and $encd_b == 1) { $name_a = pack "n*", unpack "C*", $name_a; $error = 1 if lc($name_a) eq lc($name_b); } elsif ($encd_a == 1 and $encd_b == 0) { $name_b = pack "n*", unpack "C*", $name_b; $error = 1 if lc($name_a) eq lc($name_b); } elsif ($encd_a == 1 and $encd_b == 1) { # We can do a true case insensitive test with Perl 5.8 and utf8. if ($] >= 5.008) { $name_a = Encode::decode("UTF-16BE", $name_a); $name_b = Encode::decode("UTF-16BE", $name_b); $error = 1 if lc($name_a) eq lc($name_b); } else { # We can't easily do a case insensitive test of the UTF16 names. # As a special case we check if all of the high bytes are nulls and # then do an ASCII style case insensitive test. # Strip out the high bytes (funkily). my $hi_a = grep {ord} $name_a =~ /(.)./sg; my $hi_b = grep {ord} $name_b =~ /(.)./sg; if ($hi_a or $hi_b) { $error = 1 if $name_a eq $name_b; } else { $error = 1 if lc($name_a) eq lc($name_b); } } } # If any of the cases failed we throw the error here. if ($error) { croak "Worksheet name '$name', with case ignored, " . "is already in use"; } } return ($name, $encoding); } ############################################################################### # # add_format(%properties) # # Add a new format to the Excel workbook. This adds an XF record and # a FONT record. Also, pass any properties to the Format::new(). # sub add_format { my $self = shift; my $format = Spreadsheet::WriteExcel::Format->new($self->{_xf_index}, @_); $self->{_xf_index} += 1; push @{$self->{_formats}}, $format; # Store format reference return $format; } # Older method name for backwards compatibility. *addformat = *add_format; ############################################################################### # # compatibility_mode() # # Set the compatibility mode. # # Excel doesn't require every possible Biff record to be present in a file. # In particular if the indexing records INDEX, ROW and DBCELL aren't present # it just ignores the fact and reads the cells anyway. This is also true of # the EXTSST record. Gnumeric and OOo also take this approach. This allows # WriteExcel to ignore these records in order to minimise the amount of data # stored in memory. However, other third party applications that read Excel # files often expect these records to be present. In "compatibility mode" # WriteExcel writes these records and tries to be as close to an Excel # generated file as possible. # # This requires additional data to be stored in memory until the file is # about to be written. This incurs a memory and speed penalty and may not be # suitable for very large files. # sub compatibility_mode { my $self = shift; croak "compatibility_mode() must be called before add_worksheet()" if $self->sheets(); if (defined($_[0])) { $self->{_compatibility} = $_[0]; } else { $self->{_compatibility} = 1; } } ############################################################################### # # set_1904() # # Set the date system: 0 = 1900 (the default), 1 = 1904 # sub set_1904 { my $self = shift; croak "set_1904() must be called before add_worksheet()" if $self->sheets(); if (defined($_[0])) { $self->{_1904} = $_[0]; } else { $self->{_1904} = 1; } } ############################################################################### # # get_1904() # # Return the date system: 0 = 1900, 1 = 1904 # sub get_1904 { my $self = shift; return $self->{_1904}; } ############################################################################### # # set_custom_color() # # Change the RGB components of the elements in the colour palette. # sub set_custom_color { my $self = shift; # Match a HTML #xxyyzz style parameter if (defined $_[1] and $_[1] =~ /^#(\w\w)(\w\w)(\w\w)/ ) { @_ = ($_[0], hex $1, hex $2, hex $3); } my $index = $_[0] || 0; my $red = $_[1] || 0; my $green = $_[2] || 0; my $blue = $_[3] || 0; my $aref = $self->{_palette}; # Check that the colour index is the right range if ($index < 8 or $index > 64) { carp "Color index $index outside range: 8 <= index <= 64"; return 0; } # Check that the colour components are in the right range if ( ($red < 0 or $red > 255) || ($green < 0 or $green > 255) || ($blue < 0 or $blue > 255) ) { carp "Color component outside range: 0 <= color <= 255"; return 0; } $index -=8; # Adjust colour index (wingless dragonfly) # Set the RGB value $aref->[$index] = [$red, $green, $blue, 0]; return $index +8; } ############################################################################### # # set_palette_xl97() # # Sets the colour palette to the Excel 97+ default. # sub set_palette_xl97 { my $self = shift; $self->{_palette} = [ [0x00, 0x00, 0x00, 0x00], # 8 [0xff, 0xff, 0xff, 0x00], # 9 [0xff, 0x00, 0x00, 0x00], # 10 [0x00, 0xff, 0x00, 0x00], # 11 [0x00, 0x00, 0xff, 0x00], # 12 [0xff, 0xff, 0x00, 0x00], # 13 [0xff, 0x00, 0xff, 0x00], # 14 [0x00, 0xff, 0xff, 0x00], # 15 [0x80, 0x00, 0x00, 0x00], # 16 [0x00, 0x80, 0x00, 0x00], # 17 [0x00, 0x00, 0x80, 0x00], # 18 [0x80, 0x80, 0x00, 0x00], # 19 [0x80, 0x00, 0x80, 0x00], # 20 [0x00, 0x80, 0x80, 0x00], # 21 [0xc0, 0xc0, 0xc0, 0x00], # 22 [0x80, 0x80, 0x80, 0x00], # 23 [0x99, 0x99, 0xff, 0x00], # 24 [0x99, 0x33, 0x66, 0x00], # 25 [0xff, 0xff, 0xcc, 0x00], # 26 [0xcc, 0xff, 0xff, 0x00], # 27 [0x66, 0x00, 0x66, 0x00], # 28 [0xff, 0x80, 0x80, 0x00], # 29 [0x00, 0x66, 0xcc, 0x00], # 30 [0xcc, 0xcc, 0xff, 0x00], # 31 [0x00, 0x00, 0x80, 0x00], # 32 [0xff, 0x00, 0xff, 0x00], # 33 [0xff, 0xff, 0x00, 0x00], # 34 [0x00, 0xff, 0xff, 0x00], # 35 [0x80, 0x00, 0x80, 0x00], # 36 [0x80, 0x00, 0x00, 0x00], # 37 [0x00, 0x80, 0x80, 0x00], # 38 [0x00, 0x00, 0xff, 0x00], # 39 [0x00, 0xcc, 0xff, 0x00], # 40 [0xcc, 0xff, 0xff, 0x00], # 41 [0xcc, 0xff, 0xcc, 0x00], # 42 [0xff, 0xff, 0x99, 0x00], # 43 [0x99, 0xcc, 0xff, 0x00], # 44 [0xff, 0x99, 0xcc, 0x00], # 45 [0xcc, 0x99, 0xff, 0x00], # 46 [0xff, 0xcc, 0x99, 0x00], # 47 [0x33, 0x66, 0xff, 0x00], # 48 [0x33, 0xcc, 0xcc, 0x00], # 49 [0x99, 0xcc, 0x00, 0x00], # 50 [0xff, 0xcc, 0x00, 0x00], # 51 [0xff, 0x99, 0x00, 0x00], # 52 [0xff, 0x66, 0x00, 0x00], # 53 [0x66, 0x66, 0x99, 0x00], # 54 [0x96, 0x96, 0x96, 0x00], # 55 [0x00, 0x33, 0x66, 0x00], # 56 [0x33, 0x99, 0x66, 0x00], # 57 [0x00, 0x33, 0x00, 0x00], # 58 [0x33, 0x33, 0x00, 0x00], # 59 [0x99, 0x33, 0x00, 0x00], # 60 [0x99, 0x33, 0x66, 0x00], # 61 [0x33, 0x33, 0x99, 0x00], # 62 [0x33, 0x33, 0x33, 0x00], # 63 ]; return 0; } ############################################################################### # # set_tempdir() # # Change the default temp directory used by _initialize() in Worksheet.pm. # sub set_tempdir { my $self = shift; # Windows workaround. See Worksheet::_initialize() my $dir = shift || ''; croak "$dir is not a valid directory" if $dir ne '' and not -d $dir; croak "set_tempdir must be called before add_worksheet" if $self->sheets(); $self->{_tempdir} = $dir ; } ############################################################################### # # set_codepage() # # See also the _store_codepage method. This is used to store the code page, i.e. # the character set used in the workbook. # sub set_codepage { my $self = shift; my $codepage = $_[0] || 1; $codepage = 0x04E4 if $codepage == 1; $codepage = 0x8000 if $codepage == 2; $self->{_codepage} = $codepage; } ############################################################################### # # set_country() # # See also the _store_country method. This is used to store the country code. # Some non-english versions of Excel may need this set to some value other # than 1 = "United States". In general the country code is equal to the # international dialling code. # sub set_country { my $self = shift; $self->{_country} = $_[0] || 1; } ############################################################################### # # define_name() # # TODO. # sub define_name { my $self = shift; my $name = shift; my $formula = shift; my $encoding = shift || 0; my $sheet_index = 0; my @tokens; my $full_name = $name; if ($name =~ /^(.*)!(.*)$/) { my $sheetname = $1; $name = $2; $sheet_index = 1 + $self->{_parser}->_get_sheet_index($sheetname); } # Strip the = sign at the beginning of the formula string $formula =~ s(^=)(); # Parse the formula using the parser in Formula.pm my $parser = $self->{_parser}; # In order to raise formula errors from the point of view of the calling # program we use an eval block and re-raise the error from here. # eval { @tokens = $parser->parse_formula($formula) }; if ($@) { $@ =~ s/\n$//; # Strip the \n used in the Formula.pm die() croak $@; # Re-raise the error } # Force 2d ranges to be a reference class. s/_ref3d/_ref3dR/ for @tokens; s/_range3d/_range3dR/ for @tokens; # Parse the tokens into a formula string. $formula = $parser->parse_tokens(@tokens); $full_name = lc $full_name; push @{$self->{_defined_names}}, { name => $name, encoding => $encoding, sheet_index => $sheet_index, formula => $formula, }; my $index = scalar @{$self->{_defined_names}}; $parser->set_ext_name($name, $index); } ############################################################################### # # set_properties() # # Set the document properties such as Title, Author etc. These are written to # property sets in the OLE container. # sub set_properties { my $self = shift; my %param; # Ignore if no args were passed. return -1 unless @_; # Allow the parameters to be passed as a hash or hash ref. if (ref $_[0] eq 'HASH') { %param = %{$_[0]}; } else { %param = @_; } # List of valid input parameters. my %properties = ( codepage => [0x0001, 'VT_I2' ], title => [0x0002, 'VT_LPSTR' ], subject => [0x0003, 'VT_LPSTR' ], author => [0x0004, 'VT_LPSTR' ], keywords => [0x0005, 'VT_LPSTR' ], comments => [0x0006, 'VT_LPSTR' ], last_author => [0x0008, 'VT_LPSTR' ], created => [0x000C, 'VT_FILETIME'], category => [0x0002, 'VT_LPSTR' ], manager => [0x000E, 'VT_LPSTR' ], company => [0x000F, 'VT_LPSTR' ], utf8 => 1, ); # Check for valid input parameters. for my $parameter (keys %param) { if (not exists $properties{$parameter}) { carp "Unknown parameter '$parameter' in set_properties()"; return -1; } } # Set the creation time unless specified by the user. if (!exists $param{created}){ $param{created} = $self->{_localtime}; } # # Create the SummaryInformation property set. # # Get the codepage of the strings in the property set. my @strings = qw(title subject author keywords comments last_author); $param{codepage} = $self->_get_property_set_codepage(\%param, \@strings); # Create an array of property set values. my @property_sets; for my $property (qw(codepage title subject author keywords comments last_author created)) { if (exists $param{$property} && defined $param{$property}) { push @property_sets, [ $properties{$property}->[0], $properties{$property}->[1], $param{$property} ]; } } # Pack the property sets. $self->{summary} = create_summary_property_set(\@property_sets); # # Create the DocSummaryInformation property set. # # Get the codepage of the strings in the property set. @strings = qw(category manager company); $param{codepage} = $self->_get_property_set_codepage(\%param, \@strings); # Create an array of property set values. @property_sets = (); for my $property (qw(codepage category manager company)) { if (exists $param{$property} && defined $param{$property}) { push @property_sets, [ $properties{$property}->[0], $properties{$property}->[1], $param{$property} ]; } } # Pack the property sets. $self->{doc_summary} = create_doc_summary_property_set(\@property_sets); # Set a flag for when the files is written. $self->{_add_doc_properties} = 1; } ############################################################################### # # _get_property_set_codepage() # # Get the character codepage used by the strings in a property set. If one of # the strings used is utf8 then the codepage is marked as utf8. Otherwise # Latin 1 is used (although in our case this is limited to 7bit ASCII). # sub _get_property_set_codepage { my $self = shift; my $params = $_[0]; my $strings = $_[1]; # Allow for manually marked utf8 strings. return 0xFDE9 if defined $params->{utf8}; # Check for utf8 strings in perl 5.8. if ($] >= 5.008) { require Encode; for my $string (@{$strings }) { next unless exists $params->{$string}; return 0xFDE9 if Encode::is_utf8($params->{$string}); } } return 0x04E4; # Default codepage, Latin 1. } ############################################################################### # # _store_workbook() # # Assemble worksheets into a workbook and send the BIFF data to an OLE # storage. # sub _store_workbook { my $self = shift; # Add a default worksheet if non have been added. $self->add_worksheet() if not @{$self->{_worksheets}}; # Calculate size required for MSO records and update worksheets. $self->_calc_mso_sizes(); # Ensure that at least one worksheet has been selected. if ($self->{_activesheet} == 0) { @{$self->{_worksheets}}[0]->{_selected} = 1; @{$self->{_worksheets}}[0]->{_hidden} = 0; } # Calculate the number of selected sheet tabs and set the active sheet. foreach my $sheet (@{$self->{_worksheets}}) { $self->{_selected}++ if $sheet->{_selected}; $sheet->{_active} = 1 if $sheet->{_index} == $self->{_activesheet}; } # Add Workbook globals $self->_store_bof(0x0005); $self->_store_codepage(); $self->_store_window1(); $self->_store_hideobj(); $self->_store_1904(); $self->_store_all_fonts(); $self->_store_all_num_formats(); $self->_store_all_xfs(); $self->_store_all_styles(); $self->_store_palette(); # Calculate the offsets required by the BOUNDSHEET records $self->_calc_sheet_offsets(); # Add BOUNDSHEET records. foreach my $sheet (@{$self->{_worksheets}}) { $self->_store_boundsheet($sheet->{_name}, $sheet->{_offset}, $sheet->{_sheet_type}, $sheet->{_hidden}, $sheet->{_encoding}); } # NOTE: If any records are added between here and EOF the # _calc_sheet_offsets() should be updated to include the new length. $self->_store_country(); if ($self->{_ext_ref_count}) { $self->_store_supbook(); $self->_store_externsheet(); $self->_store_names(); } $self->_add_mso_drawing_group(); $self->_store_shared_strings(); $self->_store_extsst(); # End Workbook globals $self->_store_eof(); # Store the workbook in an OLE container return $self->_store_OLE_file(); } ############################################################################### # # _store_OLE_file() # # Store the workbook in an OLE container using the default handler or using # OLE::Storage_Lite if the workbook data is > ~ 7MB. # sub _store_OLE_file { my $self = shift; my $maxsize = 7_087_104; if (!$self->{_add_doc_properties} && $self->{_biffsize} <= $maxsize) { # Write the OLE file using OLEwriter if data <= 7MB my $OLE = Spreadsheet::WriteExcel::OLEwriter->new($self->{_fh_out}); # Write the BIFF data without the OLE container for testing. $OLE->{_biff_only} = $self->{_biff_only}; # Indicate that we created the filehandle and want to close it. $OLE->{_internal_fh} = $self->{_internal_fh}; $OLE->set_size($self->{_biffsize}); $OLE->write_header(); while (my $tmp = $self->get_data()) { $OLE->write($tmp); } foreach my $worksheet (@{$self->{_worksheets}}) { while (my $tmp = $worksheet->get_data()) { $OLE->write($tmp); } } return $OLE->close(); } else { # Write the OLE file using OLE::Storage_Lite if data > 7MB eval { require OLE::Storage_Lite }; if (not $@) { # Protect print() from -l on the command line. local $\ = undef; my @streams; # Create the Workbook stream. my $stream = pack 'v*', unpack 'C*', 'Workbook'; my $workbook = OLE::Storage_Lite::PPS::File->newFile($stream); while (my $tmp = $self->get_data()) { $workbook->append($tmp); } foreach my $worksheet (@{$self->{_worksheets}}) { while (my $tmp = $worksheet->get_data()) { $workbook->append($tmp); } } push @streams, $workbook; # Create the properties streams, if any. if ($self->{_add_doc_properties}) { my $stream; my $summary; $stream = pack 'v*', unpack 'C*', "\5SummaryInformation"; $summary = $self->{summary}; $summary = OLE::Storage_Lite::PPS::File->new($stream, $summary); push @streams, $summary; $stream = pack 'v*', unpack 'C*', "\5DocumentSummaryInformation"; $summary = $self->{doc_summary}; $summary = OLE::Storage_Lite::PPS::File->new($stream, $summary); push @streams, $summary; } # Create the OLE root document and add the substreams. my @localtime = @{ $self->{_localtime} }; splice(@localtime, 6); my $ole_root = OLE::Storage_Lite::PPS::Root->new(\@localtime, \@localtime, \@streams); $ole_root->save($self->{_filename}); # Close the filehandle if it was created internally. return CORE::close($self->{_fh_out}) if $self->{_internal_fh}; } else { # File in greater than limit, set $! to "File too large" $! = 27; # Perl error code "File too large" croak "Maximum Spreadsheet::WriteExcel filesize, $maxsize bytes, ". "exceeded. To create files bigger than this limit please " . "install OLE::Storage_Lite\n"; # return 0; } } } ############################################################################### # # _calc_sheet_offsets() # # Calculate Worksheet BOF offsets records for use in the BOUNDSHEET records. # sub _calc_sheet_offsets { my $self = shift; my $BOF = 12; my $EOF = 4; my $offset = $self->{_datasize}; # Add the length of the COUNTRY record $offset += 8; # Add the length of the SST and associated CONTINUEs $offset += $self->_calculate_shared_string_sizes(); # Add the length of the EXTSST record. $offset += $self->_calculate_extsst_size(); # Add the length of the SUPBOOK, EXTERNSHEET and NAME records $offset += $self->_calculate_extern_sizes(); # Add the length of the MSODRAWINGGROUP records including an extra 4 bytes # for any CONTINUE headers. See _add_mso_drawing_group_continue(). my $mso_size = $self->{_mso_size}; $mso_size += 4 * int(($mso_size -1) / $self->{_limit}); $offset += $mso_size ; foreach my $sheet (@{$self->{_worksheets}}) { $offset += $BOF + length($sheet->{_name}); } $offset += $EOF; foreach my $sheet (@{$self->{_worksheets}}) { $sheet->{_offset} = $offset; $sheet->_close(); $offset += $sheet->{_datasize}; } $self->{_biffsize} = $offset; } ############################################################################### # # _calc_mso_sizes() # # Calculate the MSODRAWINGGROUP sizes and the indexes of the Worksheet # MSODRAWING records. # # In the following SPID is shape id, according to Escher nomenclature. # sub _calc_mso_sizes { my $self = shift; my $mso_size = 0; # Size of the MSODRAWINGGROUP record my $start_spid = 1024; # Initial spid for each sheet my $max_spid = 1024; # spidMax my $num_clusters = 1; # cidcl my $shapes_saved = 0; # cspSaved my $drawings_saved = 0; # cdgSaved my @clusters = (); $self->_process_images(); # Add Bstore container size if there are images. $mso_size += 8 if @{$self->{_images_data}}; # Iterate through the worksheets, calculate the MSODRAWINGGROUP parameters # and space required to store the record and the MSODRAWING parameters # required by each worksheet. # foreach my $sheet (@{$self->{_worksheets}}) { next unless $sheet->{_sheet_type} == 0x0000; # Ignore charts. my $num_images = $sheet->{_num_images} || 0; my $image_mso_size = $sheet->{_image_mso_size} || 0; my $num_comments = $sheet->_prepare_comments(); my $num_charts = $sheet->_prepare_charts(); my $num_filters = $sheet->{_filter_count}; next unless $num_images + $num_comments + $num_charts +$num_filters; # Include 1 parent MSODRAWING shape, per sheet, in the shape count. my $num_shapes = 1 + $num_images + $num_comments + $num_charts + $num_filters; $shapes_saved += $num_shapes; $mso_size += $image_mso_size; # Add a drawing object for each sheet with comments. $drawings_saved++; # For each sheet start the spids at the next 1024 interval. $max_spid = 1024 * (1 + int(($max_spid -1)/1024)); $start_spid = $max_spid; # Max spid for each sheet and eventually for the workbook. $max_spid += $num_shapes; # Store the cluster ids for (my $i = $num_shapes; $i > 0; $i -= 1024) { $num_clusters += 1; $mso_size += 8; my $size = $i > 1024 ? 1024 : $i; push @clusters, [$drawings_saved, $size]; } # Pass calculated values back to the worksheet $sheet->{_object_ids} = [$start_spid, $drawings_saved, $num_shapes, $max_spid -1]; } # Calculate the MSODRAWINGGROUP size if we have stored some shapes. $mso_size += 86 if $mso_size; # Smallest size is 86+8=94 $self->{_mso_size} = $mso_size; $self->{_mso_clusters} = [ $max_spid, $num_clusters, $shapes_saved, $drawings_saved, [@clusters] ]; } ############################################################################### # # _process_images() # # We need to process each image in each worksheet and extract information. # Some of this information is stored and used in the Workbook and some is # passed back into each Worksheet. The overall size for the image related # BIFF structures in the Workbook is calculated here. # # MSO size = 8 bytes for bstore_container + # 44 bytes for blip_store_entry + # 25 bytes for blip # = 77 + image size. # sub _process_images { my $self = shift; my %images_seen; my @image_data; my @previous_images; my $image_id = 1; my $images_size = 0; foreach my $sheet (@{$self->{_worksheets}}) { next unless $sheet->{_sheet_type} == 0x0000; # Ignore charts. next unless $sheet->_prepare_images(); my $num_images = 0; my $image_mso_size = 0; for my $image_ref (@{$sheet->{_images_array}}) { my $filename = $image_ref->[2]; $num_images++; # # For each Worksheet image we get a structure like this # [ # $row, # $col, # $name, # $x_offset, # $y_offset, # $scale_x, # $scale_y, # ] # # And we add additional information: # # $image_id, # $type, # $width, # $height; if (not exists $images_seen{$filename}) { # TODO should also match seen images based on checksum. # Open the image file and import the data. my $fh = FileHandle->new($filename); croak "Couldn't import $filename: $!" unless defined $fh; binmode $fh; # Slurp the file into a string and do some size calcs. my $data = do {local $/; <$fh>}; my $size = length $data; my $checksum1 = $self->_image_checksum($data, $image_id); my $checksum2 = $checksum1; my $ref_count = 1; # Process the image and extract dimensions. my ($type, $width, $height); # Test for PNGs... if (unpack('x A3', $data) eq 'PNG') { ($type, $width, $height) = $self->_process_png($data); } # Test for JFIF and Exif JPEGs... elsif ( (unpack('n', $data) == 0xFFD8) && ( (unpack('x6 A4', $data) eq 'JFIF') || (unpack('x6 A4', $data) eq 'Exif') ) ) { ($type, $width, $height) = $self->_process_jpg($data, $filename); } # Test for BMPs... elsif (unpack('A2', $data) eq 'BM') { ($type, $width, $height) = $self->_process_bmp($data, $filename); # The 14 byte header of the BMP is stripped off. $data = substr $data, 14; # A checksum of the new image data is also required. $checksum2 = $self->_image_checksum($data, $image_id, $image_id ); # Adjust size -14 (header) + 16 (extra checksum). $size += 2; } else { croak "Unsupported image format for file: $filename\n"; } # Push the new data back into the Worksheet array; push @$image_ref, $image_id, $type, $width, $height; # Also store new data for use in duplicate images. push @previous_images, [$image_id, $type, $width, $height]; # Store information required by the Workbook. push @image_data, [$ref_count, $type, $data, $size, $checksum1, $checksum2]; # Keep track of overall data size. $images_size += $size +61; # Size for bstore container. $image_mso_size += $size +69; # Size for dgg container. $images_seen{$filename} = $image_id++; $fh->close; } else { # We've processed this file already. my $index = $images_seen{$filename} -1; # Increase image reference count. $image_data[$index]->[0]++; # Add previously calculated data back onto the Worksheet array. # $image_id, $type, $width, $height my $a_ref = $sheet->{_images_array}->[$index]; push @$image_ref, @{$previous_images[$index]}; } } # Store information required by the Worksheet. $sheet->{_num_images} = $num_images; $sheet->{_image_mso_size} = $image_mso_size; } # Store information required by the Workbook. $self->{_images_size} = $images_size; $self->{_images_data} = \@image_data; # Store the data for MSODRAWINGGROUP. } ############################################################################### # # _image_checksum() # # Generate a checksum for the image using whichever module is available..The # available modules are checked in _get_checksum_method(). Excel uses an MD4 # checksum but any other will do. In the event of no checksum module being # available we simulate a checksum using the image index. # sub _image_checksum { my $self = shift; my $data = $_[0]; my $index1 = $_[1]; my $index2 = $_[2] || 0; if ($self->{_checksum_method} == 1) { # Digest::MD4 return Digest::MD4::md4_hex($data); } elsif ($self->{_checksum_method} == 2) { # Digest::Perl::MD4 return Digest::Perl::MD4::md4_hex($data); } elsif ($self->{_checksum_method} == 3) { # Digest::MD5 return Digest::MD5::md5_hex($data); } else { # Default return sprintf '%016X%016X', $index2, $index1; } } ############################################################################### # # _process_png() # # Extract width and height information from a PNG file. # sub _process_png { my $self = shift; my $type = 6; # Excel Blip type (MSOBLIPTYPE). my $width = unpack "N", substr $_[0], 16, 4; my $height = unpack "N", substr $_[0], 20, 4; return ($type, $width, $height); } ############################################################################### # # _process_bmp() # # Extract width and height information from a BMP file. # # Most of these checks came from the old Worksheet::_process_bitmap() method. # sub _process_bmp { my $self = shift; my $data = $_[0]; my $filename = $_[1]; my $type = 7; # Excel Blip type (MSOBLIPTYPE). # Check that the file is big enough to be a bitmap. if (length $data <= 0x36) { croak "$filename doesn't contain enough data."; } # Read the bitmap width and height. Verify the sizes. my ($width, $height) = unpack "x18 V2", $data; if ($width > 0xFFFF) { croak "$filename: largest image width $width supported is 65k."; } if ($height > 0xFFFF) { croak "$filename: largest image height supported is 65k."; } # Read the bitmap planes and bpp data. Verify them. my ($planes, $bitcount) = unpack "x26 v2", $data; if ($bitcount != 24) { croak "$filename isn't a 24bit true color bitmap."; } if ($planes != 1) { croak "$filename: only 1 plane supported in bitmap image."; } # Read the bitmap compression. Verify compression. my $compression = unpack "x30 V", $data; if ($compression != 0) { croak "$filename: compression not supported in bitmap image."; } return ($type, $width, $height); } ############################################################################### # # _process_jpg() # # Extract width and height information from a JPEG file. # sub _process_jpg { my $self = shift; my $data = $_[0]; my $filename = $_[1]; my $type = 5; # Excel Blip type (MSOBLIPTYPE). my $width; my $height; my $offset = 2; my $data_length = length $data; # Search through the image data to find the 0xFFC0 marker. The height and # width are contained in the data for that sub element. while ($offset < $data_length) { my $marker = unpack "n", substr $data, $offset, 2; my $length = unpack "n", substr $data, $offset +2, 2; if ($marker == 0xFFC0 || $marker == 0xFFC2) { $height = unpack "n", substr $data, $offset +5, 2; $width = unpack "n", substr $data, $offset +7, 2; last; } $offset = $offset + $length + 2; last if $marker == 0xFFDA; } if (not defined $height) { croak "$filename: no size data found in jpeg image.\n"; } return ($type, $width, $height); } ############################################################################### # # _store_all_fonts() # # Store the Excel FONT records. # sub _store_all_fonts { my $self = shift; my $format = $self->{_formats}->[15]; # The default cell format. my $font = $format->get_font(); # Fonts are 0-indexed. According to the SDK there is no index 4, for (0..3) { $self->_append($font); } # Add the default fonts for charts and comments. This aren't connected # to XF formats. Note, the font size, and some other properties of # chart fonts are set in the FBI record of the chart. my $tmp_format; # Index 5. Axis numbers. $tmp_format = Spreadsheet::WriteExcel::Format->new( undef, font_only => 1, ); $self->_append( $tmp_format->get_font() ); # Index 6. Series names. $tmp_format = Spreadsheet::WriteExcel::Format->new( undef, font_only => 1, ); $self->_append( $tmp_format->get_font() ); # Index 7. Title. $tmp_format = Spreadsheet::WriteExcel::Format->new( undef, font_only => 1, bold => 1, ); $self->_append( $tmp_format->get_font() ); # Index 8. Axes. $tmp_format = Spreadsheet::WriteExcel::Format->new( undef, font_only => 1, bold => 1, ); $self->_append( $tmp_format->get_font() ); # Index 9. Comments. $tmp_format = Spreadsheet::WriteExcel::Format->new( undef, font_only => 1, font => 'Tahoma', size => 8, ); $self->_append( $tmp_format->get_font() ); # Iterate through the XF objects and write a FONT record if it isn't the # same as the default FONT and if it hasn't already been used. # my %fonts; my $key; my $index = 10; # The first user defined FONT $key = $format->get_font_key(); # The default font for cell formats. $fonts{$key} = 0; # Index of the default font # Fonts that are marked as '_font_only' are always stored. These are used # mainly for charts and may not have an associated XF record. foreach $format (@{$self->{_formats}}) { $key = $format->get_font_key(); if (not $format->{_font_only} and exists $fonts{$key}) { # FONT has already been used $format->{_font_index} = $fonts{$key}; } else { # Add a new FONT record if (not $format->{_font_only}) { $fonts{$key} = $index; } $format->{_font_index} = $index; $index++; $font = $format->get_font(); $self->_append($font); } } } ############################################################################### # # _store_all_num_formats() # # Store user defined numerical formats i.e. FORMAT records # sub _store_all_num_formats { my $self = shift; my %num_formats; my @num_formats; my $num_format; my $index = 164; # User defined FORMAT records start from 0xA4 # Iterate through the XF objects and write a FORMAT record if it isn't a # built-in format type and if the FORMAT string hasn't already been used. # foreach my $format (@{$self->{_formats}}) { my $num_format = $format->{_num_format}; my $encoding = $format->{_num_format_enc}; # Check if $num_format is an index to a built-in format. # Also check for a string of zeros, which is a valid format string # but would evaluate to zero. # if ($num_format !~ m/^0+\d/) { next if $num_format =~ m/^\d+$/; # built-in } if (exists($num_formats{$num_format})) { # FORMAT has already been used $format->{_num_format} = $num_formats{$num_format}; } else{ # Add a new FORMAT $num_formats{$num_format} = $index; $format->{_num_format} = $index; $self->_store_num_format($num_format, $index, $encoding); $index++; } } } ############################################################################### # # _store_all_xfs() # # Write all XF records. # sub _store_all_xfs { my $self = shift; foreach my $format (@{$self->{_formats}}) { my $xf = $format->get_xf(); $self->_append($xf); } } ############################################################################### # # _store_all_styles() # # Write all STYLE records. # sub _store_all_styles { my $self = shift; # Excel adds the built-in styles in alphabetical order. my @built_ins = ( [0x03, 16], # Comma [0x06, 17], # Comma[0] [0x04, 18], # Currency [0x07, 19], # Currency[0] [0x00, 0], # Normal [0x05, 20], # Percent # We don't deal with these styles yet. #[0x08, 21], # Hyperlink #[0x02, 8], # ColLevel_n #[0x01, 1], # RowLevel_n ); for my $aref (@built_ins) { my $type = $aref->[0]; my $xf_index = $aref->[1]; $self->_store_style($type, $xf_index); } } ############################################################################### # # _store_names() # # Write the NAME record to define the print area and the repeat rows and cols. # sub _store_names { my $self = shift; my $index; my %ext_refs = %{$self->{_ext_refs}}; # Create the user defined names. for my $defined_name (@{$self->{_defined_names}}) { $self->_store_name( $defined_name->{name}, $defined_name->{encoding}, $defined_name->{sheet_index}, $defined_name->{formula}, ); } # Sort the worksheets into alphabetical order by name. This is a # requirement for some non-English language Excel patch levels. my @worksheets = @{$self->{_worksheets}}; @worksheets = sort { $a->{_name} cmp $b->{_name} } @worksheets; # Create the autofilter NAME records foreach my $worksheet (@worksheets) { $index = $worksheet->{_index}; my $key = "$index:$index"; my $ref = $ext_refs{$key}; # Write a Name record if Autofilter has been defined if ($worksheet->{_filter_count}) { $self->_store_name_short( $worksheet->{_index}, 0x0D, # NAME type = Filter Database $ref, $worksheet->{_filter_area}->[0], $worksheet->{_filter_area}->[1], $worksheet->{_filter_area}->[2], $worksheet->{_filter_area}->[3], 1, # Hidden ); } } # Create the print area NAME records foreach my $worksheet (@worksheets) { $index = $worksheet->{_index}; my $key = "$index:$index"; my $ref = $ext_refs{$key}; # Write a Name record if the print area has been defined if (defined $worksheet->{_print_rowmin}) { $self->_store_name_short( $worksheet->{_index}, 0x06, # NAME type = Print_Area $ref, $worksheet->{_print_rowmin}, $worksheet->{_print_rowmax}, $worksheet->{_print_colmin}, $worksheet->{_print_colmax} ); } } # Create the print title NAME records foreach my $worksheet (@worksheets) { $index = $worksheet->{_index}; my $rowmin = $worksheet->{_title_rowmin}; my $rowmax = $worksheet->{_title_rowmax}; my $colmin = $worksheet->{_title_colmin}; my $colmax = $worksheet->{_title_colmax}; my $key = "$index:$index"; my $ref = $ext_refs{$key}; # Determine if row + col, row, col or nothing has been defined # and write the appropriate record # if (defined $rowmin && defined $colmin) { # Row and column titles have been defined. # Row title has been defined. $self->_store_name_long( $worksheet->{_index}, 0x07, # NAME type = Print_Titles $ref, $rowmin, $rowmax, $colmin, $colmax ); } elsif (defined $rowmin) { # Row title has been defined. $self->_store_name_short( $worksheet->{_index}, 0x07, # NAME type = Print_Titles $ref, $rowmin, $rowmax, 0x00, 0xff ); } elsif (defined $colmin) { # Column title has been defined. $self->_store_name_short( $worksheet->{_index}, 0x07, # NAME type = Print_Titles $ref, 0x0000, 0xffff, $colmin, $colmax ); } else { # Nothing left to do } } } ############################################################################### ############################################################################### # # BIFF RECORDS # ############################################################################### # # _store_window1() # # Write Excel BIFF WINDOW1 record. # sub _store_window1 { my $self = shift; my $record = 0x003D; # Record identifier my $length = 0x0012; # Number of bytes to follow my $xWn = 0x0000; # Horizontal position of window my $yWn = 0x0000; # Vertical position of window my $dxWn = 0x355C; # Width of window my $dyWn = 0x30ED; # Height of window my $grbit = 0x0038; # Option flags my $ctabsel = $self->{_selected}; # Number of workbook tabs selected my $wTabRatio = 0x0258; # Tab to scrollbar ratio my $itabFirst = $self->{_firstsheet}; # 1st displayed worksheet my $itabCur = $self->{_activesheet}; # Active worksheet my $header = pack("vv", $record, $length); my $data = pack("vvvvvvvvv", $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio); $self->_append($header, $data); } ############################################################################### # # _store_boundsheet() # # Writes Excel BIFF BOUNDSHEET record. # sub _store_boundsheet { my $self = shift; my $record = 0x0085; # Record identifier my $length = 0x08 + length($_[0]); # Number of bytes to follow my $sheetname = $_[0]; # Worksheet name my $offset = $_[1]; # Location of worksheet BOF my $type = $_[2]; # Worksheet type my $hidden = $_[3]; # Worksheet hidden flag my $encoding = $_[4]; # Sheet name encoding my $cch = length($sheetname); # Length of sheet name my $grbit = $type | $hidden; # Character length is num of chars not num of bytes $cch /= 2 if $encoding; # Change the UTF-16 name from BE to LE $sheetname = pack 'n*', unpack 'v*', $sheetname if $encoding; my $header = pack("vv", $record, $length); my $data = pack("VvCC", $offset, $grbit, $cch, $encoding); $self->_append($header, $data, $sheetname); } ############################################################################### # # _store_style() # # Write Excel BIFF STYLE records. # sub _store_style { my $self = shift; my $record = 0x0293; # Record identifier my $length = 0x0004; # Bytes to follow my $type = $_[0]; # Built-in style my $xf_index = $_[1]; # Index to style XF my $level = 0xff; # Outline style level $xf_index |= 0x8000; # Add flag to indicate built-in style. my $header = pack("vv", $record, $length); my $data = pack("vCC", $xf_index, $type, $level); $self->_append($header, $data); } ############################################################################### # # _store_num_format() # # Writes Excel FORMAT record for non "built-in" numerical formats. # sub _store_num_format { my $self = shift; my $record = 0x041E; # Record identifier my $length; # Number of bytes to follow my $format = $_[0]; # Custom format string my $ifmt = $_[1]; # Format index code my $encoding = $_[2]; # Char encoding for format string # Handle utf8 strings in perl 5.8. if ($] >= 5.008) { require Encode; if (Encode::is_utf8($format)) { $format = Encode::encode("UTF-16BE", $format); $encoding = 1; } } # Char length of format string my $cch = length $format; # Handle Unicode format strings. if ($encoding == 1) { croak "Uneven number of bytes in Unicode font name" if $cch % 2; $cch /= 2 if $encoding; $format = pack 'v*', unpack 'n*', $format; } # Special case to handle Euro symbol, 0x80, in non-Unicode strings. if ($encoding == 0 and $format =~ /\x80/) { $format = pack 'v*', unpack 'C*', $format; $format =~ s/\x80\x00/\xAC\x20/g; $encoding = 1; } $length = 0x05 + length $format; my $header = pack("vv", $record, $length); my $data = pack("vvC", $ifmt, $cch, $encoding); $self->_append($header, $data, $format); } ############################################################################### # # _store_1904() # # Write Excel 1904 record to indicate the date system in use. # sub _store_1904 { my $self = shift; my $record = 0x0022; # Record identifier my $length = 0x0002; # Bytes to follow my $f1904 = $self->{_1904}; # Flag for 1904 date system my $header = pack("vv", $record, $length); my $data = pack("v", $f1904); $self->_append($header, $data); } ############################################################################### # # _store_supbook() # # Write BIFF record SUPBOOK to indicate that the workbook contains external # references, in our case, formula, print area and print title refs. # sub _store_supbook { my $self = shift; my $record = 0x01AE; # Record identifier my $length = 0x0004; # Number of bytes to follow my $ctabs = @{$self->{_worksheets}}; # Number of worksheets my $StVirtPath = 0x0401; # Encoded workbook filename my $header = pack("vv", $record, $length); my $data = pack("vv", $ctabs, $StVirtPath); $self->_append($header, $data); } ############################################################################### # # _store_externsheet() # # Writes the Excel BIFF EXTERNSHEET record. These references are used by # formulas. TODO NAME record is required to define the print area and the # repeat rows and columns. # sub _store_externsheet { my $self = shift; my $record = 0x0017; # Record identifier my $length; # Number of bytes to follow # Get the external refs my %ext_refs = %{$self->{_ext_refs}}; my @ext_refs = sort {$ext_refs{$a} <=> $ext_refs{$b}} keys %ext_refs; # Change the external refs from stringified "1:1" to [1, 1] foreach my $ref (@ext_refs) { $ref = [split /:/, $ref]; } my $cxti = scalar @ext_refs; # Number of Excel XTI structures my $rgxti = ''; # Array of XTI structures # Write the XTI structs foreach my $ext_ref (@ext_refs) { $rgxti .= pack("vvv", 0, $ext_ref->[0], $ext_ref->[1]) } my $data = pack("v", $cxti) . $rgxti; my $header = pack("vv", $record, length $data); $self->_append($header, $data); } ############################################################################### # # _store_name() # # # Store the NAME record used for storing the print area, repeat rows, repeat # columns, autofilters and defined names. # # TODO. This is a more generic version that will replace _store_name_short() # and _store_name_long(). # sub _store_name { my $self = shift; my $record = 0x0018; # Record identifier my $length; # Number of bytes to follow my $name = shift; my $encoding = shift; my $sheet_index = shift; my $formula = shift; my $text_length = length $name; my $formula_length = length $formula; # UTF-16 string length is in characters not bytes. $text_length /= 2 if $encoding; my $grbit = 0x0000; # Option flags my $shortcut = 0x00; # Keyboard shortcut my $ixals = 0x0000; # Unused index. my $menu_length = 0x00; # Length of cust menu text my $desc_length = 0x00; # Length of description text my $help_length = 0x00; # Length of help topic text my $status_length = 0x00; # Length of status bar text # Set grbit built-in flag and the hidden flag for autofilters. if ($text_length == 1) { $grbit = 0x0020 if ord $name == 0x06; # Print area $grbit = 0x0020 if ord $name == 0x07; # Print titles $grbit = 0x0021 if ord $name == 0x0D; # Autofilter } my $data = pack "v", $grbit; $data .= pack "C", $shortcut; $data .= pack "C", $text_length; $data .= pack "v", $formula_length; $data .= pack "v", $ixals; $data .= pack "v", $sheet_index; $data .= pack "C", $menu_length; $data .= pack "C", $desc_length; $data .= pack "C", $help_length; $data .= pack "C", $status_length; $data .= pack "C", $encoding; $data .= $name; $data .= $formula; my $header = pack "vv", $record, length $data; $self->_append($header, $data); } ############################################################################### # # _store_name_short() # # # Store the NAME record in the short format that is used for storing the print # area, repeat rows only and repeat columns only. # sub _store_name_short { my $self = shift; my $record = 0x0018; # Record identifier my $length = 0x001b; # Number of bytes to follow my $index = shift; # Sheet index my $type = shift; my $ext_ref = shift; # TODO my $grbit = 0x0020; # Option flags my $chKey = 0x00; # Keyboard shortcut my $cch = 0x01; # Length of text name my $cce = 0x000b; # Length of text definition my $unknown01 = 0x0000; # my $ixals = $index +1; # Sheet index my $unknown02 = 0x00; # my $cchCustMenu = 0x00; # Length of cust menu text my $cchDescription = 0x00; # Length of description text my $cchHelptopic = 0x00; # Length of help topic text my $cchStatustext = 0x00; # Length of status bar text my $rgch = $type; # Built-in name type my $unknown03 = 0x3b; # my $rowmin = $_[0]; # Start row my $rowmax = $_[1]; # End row my $colmin = $_[2]; # Start column my $colmax = $_[3]; # end column my $hidden = $_[4]; # Name is hidden $grbit = 0x0021 if $hidden; my $header = pack("vv", $record, $length); my $data = pack("v", $grbit); $data .= pack("C", $chKey); $data .= pack("C", $cch); $data .= pack("v", $cce); $data .= pack("v", $unknown01); $data .= pack("v", $ixals); $data .= pack("C", $unknown02); $data .= pack("C", $cchCustMenu); $data .= pack("C", $cchDescription); $data .= pack("C", $cchHelptopic); $data .= pack("C", $cchStatustext); $data .= pack("C", $rgch); $data .= pack("C", $unknown03); $data .= pack("v", $ext_ref); $data .= pack("v", $rowmin); $data .= pack("v", $rowmax); $data .= pack("v", $colmin); $data .= pack("v", $colmax); $self->_append($header, $data); } ############################################################################### # # _store_name_long() # # # Store the NAME record in the long format that is used for storing the repeat # rows and columns when both are specified. This share a lot of code with # _store_name_short() but we use a separate method to keep the code clean. # Code abstraction for reuse can be carried too far, and I should know. ;-) # sub _store_name_long { my $self = shift; my $record = 0x0018; # Record identifier my $length = 0x002a; # Number of bytes to follow my $index = shift; # Sheet index my $type = shift; my $ext_ref = shift; # TODO my $grbit = 0x0020; # Option flags my $chKey = 0x00; # Keyboard shortcut my $cch = 0x01; # Length of text name my $cce = 0x001a; # Length of text definition my $unknown01 = 0x0000; # my $ixals = $index +1; # Sheet index my $unknown02 = 0x00; # my $cchCustMenu = 0x00; # Length of cust menu text my $cchDescription = 0x00; # Length of description text my $cchHelptopic = 0x00; # Length of help topic text my $cchStatustext = 0x00; # Length of status bar text my $rgch = $type; # Built-in name type my $unknown03 = 0x29; my $unknown04 = 0x0017; my $unknown05 = 0x3b; my $rowmin = $_[0]; # Start row my $rowmax = $_[1]; # End row my $colmin = $_[2]; # Start column my $colmax = $_[3]; # end column my $header = pack("vv", $record, $length); my $data = pack("v", $grbit); $data .= pack("C", $chKey); $data .= pack("C", $cch); $data .= pack("v", $cce); $data .= pack("v", $unknown01); $data .= pack("v", $ixals); $data .= pack("C", $unknown02); $data .= pack("C", $cchCustMenu); $data .= pack("C", $cchDescription); $data .= pack("C", $cchHelptopic); $data .= pack("C", $cchStatustext); $data .= pack("C", $rgch); # Column definition $data .= pack("C", $unknown03); $data .= pack("v", $unknown04); $data .= pack("C", $unknown05); $data .= pack("v", $ext_ref); $data .= pack("v", 0x0000); $data .= pack("v", 0xffff); $data .= pack("v", $colmin); $data .= pack("v", $colmax); # Row definition $data .= pack("C", $unknown05); $data .= pack("v", $ext_ref); $data .= pack("v", $rowmin); $data .= pack("v", $rowmax); $data .= pack("v", 0x00); $data .= pack("v", 0xff); # End of data $data .= pack("C", 0x10); $self->_append($header, $data); } ############################################################################### # # _store_palette() # # Stores the PALETTE biff record. # sub _store_palette { my $self = shift; my $aref = $self->{_palette}; my $record = 0x0092; # Record identifier my $length = 2 + 4 * @$aref; # Number of bytes to follow my $ccv = @$aref; # Number of RGB values to follow my $data; # The RGB data # Pack the RGB data $data .= pack "CCCC", @$_ for @$aref; my $header = pack("vvv", $record, $length, $ccv); $self->_append($header, $data); } ############################################################################### # # _store_codepage() # # Stores the CODEPAGE biff record. # sub _store_codepage { my $self = shift; my $record = 0x0042; # Record identifier my $length = 0x0002; # Number of bytes to follow my $cv = $self->{_codepage}; # The code page my $header = pack("vv", $record, $length); my $data = pack("v", $cv); $self->_append($header, $data); } ############################################################################### # # _store_country() # # Stores the COUNTRY biff record. # sub _store_country { my $self = shift; my $record = 0x008C; # Record identifier my $length = 0x0004; # Number of bytes to follow my $country_default = $self->{_country}; my $country_win_ini = $self->{_country}; my $header = pack("vv", $record, $length); my $data = pack("vv", $country_default, $country_win_ini); $self->_append($header, $data); } ############################################################################### # # _store_hideobj() # # Stores the HIDEOBJ biff record. # sub _store_hideobj { my $self = shift; my $record = 0x008D; # Record identifier my $length = 0x0002; # Number of bytes to follow my $hide = $self->{_hideobj}; # Option to hide objects my $header = pack("vv", $record, $length); my $data = pack("v", $hide); $self->_append($header, $data); } ############################################################################### ############################################################################### ############################################################################### ############################################################################### # # _calculate_extern_sizes() # # We need to calculate the space required by the SUPBOOK, EXTERNSHEET and NAME # records so that it can be added to the BOUNDSHEET offsets. # sub _calculate_extern_sizes { my $self = shift; my %ext_refs = $self->{_parser}->get_ext_sheets(); my $ext_ref_count = scalar keys %ext_refs; my $length = 0; my $index = 0; if (@{$self->{_defined_names}}) { my $index = 0; my $key = "$index:$index"; if (not exists $ext_refs{$key}) { $ext_refs{$key} = $ext_ref_count++; } } for my $defined_name (@{$self->{_defined_names}}) { $length += 19 + length($defined_name->{name}) + length($defined_name->{formula}); } foreach my $worksheet (@{$self->{_worksheets}}) { my $rowmin = $worksheet->{_title_rowmin}; my $colmin = $worksheet->{_title_colmin}; my $filter = $worksheet->{_filter_count}; my $key = "$index:$index"; $index++; # Add area NAME records # if (defined $worksheet->{_print_rowmin}) { $ext_refs{$key} = $ext_ref_count++ if not exists $ext_refs{$key}; $length += 31 ; } # Add title NAME records # if (defined $rowmin and defined $colmin) { $ext_refs{$key} = $ext_ref_count++ if not exists $ext_refs{$key}; $length += 46; } elsif (defined $rowmin or defined $colmin) { $ext_refs{$key} = $ext_ref_count++ if not exists $ext_refs{$key}; $length += 31; } else { # TODO, may need this later. } # Add Autofilter NAME records # if ($filter) { $ext_refs{$key} = $ext_ref_count++ if not exists $ext_refs{$key}; $length += 31; } } # Update the ref counts. $self->{_ext_ref_count} = $ext_ref_count; $self->{_ext_refs} = {%ext_refs}; # If there are no external refs then we don't write, SUPBOOK, EXTERNSHEET # and NAME. Therefore the length is 0. return $length = 0 if $ext_ref_count == 0; # The SUPBOOK record is 8 bytes $length += 8; # The EXTERNSHEET record is 6 bytes + 6 bytes for each external ref $length += 6 * (1 + $ext_ref_count); return $length; } ############################################################################### # # _calculate_shared_string_sizes() # # Handling of the SST continue blocks is complicated by the need to include an # additional continuation byte depending on whether the string is split between # blocks or whether it starts at the beginning of the block. (There are also # additional complications that will arise later when/if Rich Strings are # supported). As such we cannot use the simple CONTINUE mechanism provided by # the _add_continue() method in BIFFwriter.pm. Thus we have to make two passes # through the strings data. The first is to calculate the required block sizes # and the second, in _store_shared_strings(), is to write the actual strings. # The first pass through the data is also used to calculate the size of the SST # and CONTINUE records for use in setting the BOUNDSHEET record offsets. The # downside of this is that the same algorithm repeated in _store_shared_strings. # sub _calculate_shared_string_sizes { my $self = shift; my @strings; $#strings = $self->{_str_unique} -1; # Pre-extend array while (my $key = each %{$self->{_str_table}}) { $strings[$self->{_str_table}->{$key}] = $key; } # The SST data could be very large, free some memory (maybe). $self->{_str_table} = undef; $self->{_str_array} = [@strings]; # Iterate through the strings to calculate the CONTINUE block sizes. # # The SST blocks requires a specialised CONTINUE block, so we have to # ensure that the maximum data block size is less than the limit used by # _add_continue() in BIFFwriter.pm. For simplicity we use the same size # for the SST and CONTINUE records: # 8228 : Maximum Excel97 block size # -4 : Length of block header # -8 : Length of additional SST header information # -8 : Arbitrary number to keep within _add_continue() limit # = 8208 # my $continue_limit = 8208; my $block_length = 0; my $written = 0; my @block_sizes; my $continue = 0; for my $string (@strings) { my $string_length = length $string; my $encoding = unpack "xx C", $string; my $split_string = 0; # Block length is the total length of the strings that will be # written out in a single SST or CONTINUE block. # $block_length += $string_length; # We can write the string if it doesn't cross a CONTINUE boundary if ($block_length < $continue_limit) { $written += $string_length; next; } # Deal with the cases where the next string to be written will exceed # the CONTINUE boundary. If the string is very long it may need to be # written in more than one CONTINUE record. # while ($block_length >= $continue_limit) { # We need to avoid the case where a string is continued in the first # n bytes that contain the string header information. # my $header_length = 3; # Min string + header size -1 my $space_remaining = $continue_limit -$written -$continue; # Unicode data should only be split on char (2 byte) boundaries. # Therefore, in some cases we need to reduce the amount of available # space by 1 byte to ensure the correct alignment. my $align = 0; # Only applies to Unicode strings if ($encoding == 1) { # Min string + header size -1 $header_length = 4; if ($space_remaining > $header_length) { # String contains 3 byte header => split on odd boundary if (not $split_string and $space_remaining % 2 != 1) { $space_remaining--; $align = 1; } # Split section without header => split on even boundary elsif ($split_string and $space_remaining % 2 == 1) { $space_remaining--; $align = 1; } $split_string = 1; } } if ($space_remaining > $header_length) { # Write as much as possible of the string in the current block $written += $space_remaining; # Reduce the current block length by the amount written $block_length -= $continue_limit -$continue -$align; # Store the max size for this block push @block_sizes, $continue_limit -$align; # If the current string was split then the next CONTINUE block # should have the string continue flag (grbit) set unless the # split string fits exactly into the remaining space. # if ($block_length > 0) { $continue = 1; } else { $continue = 0; } } else { # Store the max size for this block push @block_sizes, $written +$continue; # Not enough space to start the string in the current block $block_length -= $continue_limit -$space_remaining -$continue; $continue = 0; } # If the string (or substr) is small enough we can write it in the # new CONTINUE block. Else, go through the loop again to write it in # one or more CONTINUE blocks # if ($block_length < $continue_limit) { $written = $block_length; } else { $written = 0; } } } # Store the max size for the last block unless it is empty push @block_sizes, $written +$continue if $written +$continue; $self->{_str_block_sizes} = [@block_sizes]; # Calculate the total length of the SST and associated CONTINUEs (if any). # The SST record will have a length even if it contains no strings. # This length is required to set the offsets in the BOUNDSHEET records since # they must be written before the SST records # my $length = 12; $length += shift @block_sizes if @block_sizes; # SST $length += 4 + shift @block_sizes while @block_sizes; # CONTINUEs return $length; } ############################################################################### # # _store_shared_strings() # # Write all of the workbooks strings into an indexed array. # # See the comments in _calculate_shared_string_sizes() for more information. # # We also use this routine to record the offsets required by the EXTSST table. # In order to do this we first identify the first string in an EXTSST bucket # and then store its global and local offset within the SST table. The offset # occurs wherever the start of the bucket string is written out via append(). # sub _store_shared_strings { my $self = shift; my @strings = @{$self->{_str_array}}; my $record = 0x00FC; # Record identifier my $length = 0x0008; # Number of bytes to follow my $total = 0x0000; # Iterate through the strings to calculate the CONTINUE block sizes my $continue_limit = 8208; my $block_length = 0; my $written = 0; my $continue = 0; # The SST and CONTINUE block sizes have been pre-calculated by # _calculate_shared_string_sizes() my @block_sizes = @{$self->{_str_block_sizes}}; # The SST record is required even if it contains no strings. Thus we will # always have a length # if (@block_sizes) { $length = 8 + shift @block_sizes; } else { # No strings $length = 8; } # Initialise variables used to track EXTSST bucket offsets. my $extsst_str_num = -1; my $sst_block_start = $self->{_datasize}; # Write the SST block header information my $header = pack("vv", $record, $length); my $data = pack("VV", $self->{_str_total}, $self->{_str_unique}); $self->_append($header, $data); # Iterate through the strings and write them out for my $string (@strings) { my $string_length = length $string; my $encoding = unpack "xx C", $string; my $split_string = 0; my $bucket_string = 0; # Used to track EXTSST bucket offsets. # Check if the string is at the start of a EXTSST bucket. if (++$extsst_str_num % $self->{_extsst_bucket_size} == 0) { $bucket_string = 1; } # Block length is the total length of the strings that will be # written out in a single SST or CONTINUE block. # $block_length += $string_length; # We can write the string if it doesn't cross a CONTINUE boundary if ($block_length < $continue_limit) { # Store location of EXTSST bucket string. if ($bucket_string) { my $global_offset = $self->{_datasize}; my $local_offset = $self->{_datasize} - $sst_block_start; push @{$self->{_extsst_offsets}}, [$global_offset, $local_offset]; $bucket_string = 0; } $self->_append($string); $written += $string_length; next; } # Deal with the cases where the next string to be written will exceed # the CONTINUE boundary. If the string is very long it may need to be # written in more than one CONTINUE record. # while ($block_length >= $continue_limit) { # We need to avoid the case where a string is continued in the first # n bytes that contain the string header information. # my $header_length = 3; # Min string + header size -1 my $space_remaining = $continue_limit -$written -$continue; # Unicode data should only be split on char (2 byte) boundaries. # Therefore, in some cases we need to reduce the amount of available # space by 1 byte to ensure the correct alignment. my $align = 0; # Only applies to Unicode strings if ($encoding == 1) { # Min string + header size -1 $header_length = 4; if ($space_remaining > $header_length) { # String contains 3 byte header => split on odd boundary if (not $split_string and $space_remaining % 2 != 1) { $space_remaining--; $align = 1; } # Split section without header => split on even boundary elsif ($split_string and $space_remaining % 2 == 1) { $space_remaining--; $align = 1; } $split_string = 1; } } if ($space_remaining > $header_length) { # Write as much as possible of the string in the current block my $tmp = substr $string, 0, $space_remaining; # Store location of EXTSST bucket string. if ($bucket_string) { my $global_offset = $self->{_datasize}; my $local_offset = $self->{_datasize} - $sst_block_start; push @{$self->{_extsst_offsets}}, [$global_offset, $local_offset]; $bucket_string = 0; } $self->_append($tmp); # The remainder will be written in the next block(s) $string = substr $string, $space_remaining; # Reduce the current block length by the amount written $block_length -= $continue_limit -$continue -$align; # If the current string was split then the next CONTINUE block # should have the string continue flag (grbit) set unless the # split string fits exactly into the remaining space. # if ($block_length > 0) { $continue = 1; } else { $continue = 0; } } else { # Not enough space to start the string in the current block $block_length -= $continue_limit -$space_remaining -$continue; $continue = 0; } # Write the CONTINUE block header if (@block_sizes) { $sst_block_start= $self->{_datasize}; # Reset EXTSST offset. $record = 0x003C; $length = shift @block_sizes; $header = pack("vv", $record, $length); $header .= pack("C", $encoding) if $continue; $self->_append($header); } # If the string (or substr) is small enough we can write it in the # new CONTINUE block. Else, go through the loop again to write it in # one or more CONTINUE blocks # if ($block_length < $continue_limit) { # Store location of EXTSST bucket string. if ($bucket_string) { my $global_offset = $self->{_datasize}; my $local_offset = $self->{_datasize} - $sst_block_start; push @{$self->{_extsst_offsets}}, [$global_offset, $local_offset]; $bucket_string = 0; } $self->_append($string); $written = $block_length; } else { $written = 0; } } } } ############################################################################### # # _calculate_extsst_size # # The number of buckets used in the EXTSST is between 0 and 128. The number of # strings per bucket (bucket size) has a minimum value of 8 and a theoretical # maximum of 2^16. For "number of strings" < 1024 there is a constant bucket # size of 8. The following algorithm generates the same size/bucket ratio # as Excel. # sub _calculate_extsst_size { my $self = shift; my $unique_strings = $self->{_str_unique}; my $bucket_size; my $buckets; if ($unique_strings < 1024) { $bucket_size = 8; } else { $bucket_size = 1 + int($unique_strings / 128); } $buckets = int(($unique_strings + $bucket_size -1) / $bucket_size); $self->{_extsst_buckets} = $buckets ; $self->{_extsst_bucket_size} = $bucket_size; return 6 + 8 * $buckets; } ############################################################################### # # _store_extsst # # Write EXTSST table using the offsets calculated in _store_shared_strings(). # sub _store_extsst { my $self = shift; my @offsets = @{$self->{_extsst_offsets}}; my $bucket_size = $self->{_extsst_bucket_size}; my $record = 0x00FF; # Record identifier my $length = 2 + 8 * @offsets; # Bytes to follow my $header = pack 'vv', $record, $length; my $data = pack 'v', $bucket_size,; for my $offset (@offsets) { $data .= pack 'Vvv', $offset->[0], $offset->[1], 0; } $self->_append($header, $data); } # # Methods related to comments and MSO objects. # ############################################################################### # # _add_mso_drawing_group() # # Write the MSODRAWINGGROUP record that keeps track of the Escher drawing # objects in the file such as images, comments and filters. # sub _add_mso_drawing_group { my $self = shift; return unless $self->{_mso_size}; my $record = 0x00EB; # Record identifier my $length = 0x0000; # Number of bytes to follow my $data = $self->_store_mso_dgg_container(); $data .= $self->_store_mso_dgg(@{$self->{_mso_clusters}}); $data .= $self->_store_mso_bstore_container(); $data .= $self->_store_mso_images(@$_) for @{$self->{_images_data}}; $data .= $self->_store_mso_opt(); $data .= $self->_store_mso_split_menu_colors(); $length = length $data; my $header = pack("vv", $record, $length); $self->_add_mso_drawing_group_continue($header . $data); return $header . $data; # For testing only. } ############################################################################### # # _add_mso_drawing_group_continue() # # See first the Spreadsheet::WriteExcel::BIFFwriter::_add_continue() method. # # Add specialised CONTINUE headers to large MSODRAWINGGROUP data block. # We use the Excel 97 max block size of 8228 - 4 bytes for the header = 8224. # # The structure depends on the size of the data block: # # Case 1: <= 8224 bytes 1 MSODRAWINGGROUP # Case 2: <= 2*8224 bytes 1 MSODRAWINGGROUP + 1 CONTINUE # Case 3: > 2*8224 bytes 2 MSODRAWINGGROUP + n CONTINUE # sub _add_mso_drawing_group_continue { my $self = shift; my $data = $_[0]; my $limit = 8228 -4; my $mso_group = 0x00EB; # Record identifier my $continue = 0x003C; # Record identifier my $block_count = 1; my $header; my $tmp; # Ignore the base class _add_continue() method. $self->{_ignore_continue} = 1; # Case 1 above. Just return the data as it is. if (length $data <= $limit) { $self->_append($data); return; } # Change length field of the first MSODRAWINGGROUP block. Case 2 and 3. $tmp = substr($data, 0, $limit +4, ""); substr($tmp, 2, 2, pack("v", $limit)); $self->_append($tmp); # Add MSODRAWINGGROUP and CONTINUE blocks for Case 3 above. while (length($data) > $limit) { if ($block_count == 1) { # Add extra MSODRAWINGGROUP block header. $header = pack("vv", $mso_group, $limit); $block_count++; } else { # Add normal CONTINUE header. $header = pack("vv", $continue, $limit); } $tmp = substr($data, 0, $limit, ""); $self->_append($header, $tmp); } # Last CONTINUE block for remaining data. Case 2 and 3 above. $header = pack("vv", $continue, length($data)); $self->_append($header, $data); # Turn the base class _add_continue() method back on. $self->{_ignore_continue} = 0; } ############################################################################### # # _store_mso_dgg_container() # # Write the Escher DggContainer record that is part of MSODRAWINGGROUP. # sub _store_mso_dgg_container { my $self = shift; my $type = 0xF000; my $version = 15; my $instance = 0; my $data = ''; my $length = $self->{_mso_size} -12; # -4 (biff header) -8 (for this). return $self->_add_mso_generic($type, $version, $instance, $data, $length); } ############################################################################### # # _store_mso_dgg() # # Write the Escher Dgg record that is part of MSODRAWINGGROUP. # sub _store_mso_dgg { my $self = shift; my $type = 0xF006; my $version = 0; my $instance = 0; my $data = ''; my $length = undef; # Calculate automatically. my $max_spid = $_[0]; my $num_clusters = $_[1]; my $shapes_saved = $_[2]; my $drawings_saved = $_[3]; my $clusters = $_[4]; $data = pack "VVVV", $max_spid, $num_clusters, $shapes_saved, $drawings_saved; for my $aref (@$clusters) { my $drawing_id = $aref->[0]; my $shape_ids_used = $aref->[1]; $data .= pack "VV", $drawing_id, $shape_ids_used; } return $self->_add_mso_generic($type, $version, $instance, $data, $length); } ############################################################################### # # _store_mso_bstore_container() # # Write the Escher BstoreContainer record that is part of MSODRAWINGGROUP. # sub _store_mso_bstore_container { my $self = shift; return '' unless $self->{_images_size}; my $type = 0xF001; my $version = 15; my $instance = @{$self->{_images_data}}; # Number of images. my $data = ''; my $length = $self->{_images_size} +8 *$instance; return $self->_add_mso_generic($type, $version, $instance, $data, $length); } ############################################################################### # # _store_mso_images() # # Write the Escher BstoreContainer record that is part of MSODRAWINGGROUP. # sub _store_mso_images { my $self = shift; my $ref_count = $_[0]; my $image_type = $_[1]; my $image = $_[2]; my $size = $_[3]; my $checksum1 = $_[4]; my $checksum2 = $_[5]; my $blip_store_entry = $self->_store_mso_blip_store_entry($ref_count, $image_type, $size, $checksum1); my $blip = $self->_store_mso_blip($image_type, $image, $size, $checksum1, $checksum2); return $blip_store_entry . $blip; } ############################################################################### # # _store_mso_blip_store_entry() # # Write the Escher BlipStoreEntry record that is part of MSODRAWINGGROUP. # sub _store_mso_blip_store_entry { my $self = shift; my $ref_count = $_[0]; my $image_type = $_[1]; my $size = $_[2]; my $checksum1 = $_[3]; my $type = 0xF007; my $version = 2; my $instance = $image_type; my $length = $size +61; my $data = pack('C', $image_type) # Win32 . pack('C', $image_type) # Mac . pack('H*', $checksum1) # Uid checksum . pack('v', 0xFF) # Tag . pack('V', $size +25) # Next Blip size . pack('V', $ref_count) # Image ref count . pack('V', 0x00000000) # File offset . pack('C', 0x00) # Usage . pack('C', 0x00) # Name length . pack('C', 0x00) # Unused . pack('C', 0x00) # Unused ; return $self->_add_mso_generic($type, $version, $instance, $data, $length); } ############################################################################### # # _store_mso_blip() # # Write the Escher Blip record that is part of MSODRAWINGGROUP. # sub _store_mso_blip { my $self = shift; my $image_type = $_[0]; my $image_data = $_[1]; my $size = $_[2]; my $checksum1 = $_[3]; my $checksum2 = $_[4]; my $instance; $instance = 0x046A if $image_type == 5; # JPG $instance = 0x06E0 if $image_type == 6; # PNG $instance = 0x07A9 if $image_type == 7; # BMP # BMPs contain an extra checksum for the stripped data. if ( $image_type == 7) { $checksum1 = $checksum2 . $checksum1; } my $type = 0xF018 + $image_type; my $version = 0x0000; my $length = $size +17; my $data = pack('H*', $checksum1) # Uid checksum . pack('C', 0xFF) # Tag . $image_data # Image ; return $self->_add_mso_generic($type, $version, $instance, $data, $length); } ############################################################################### # # _store_mso_opt() # # Write the Escher Opt record that is part of MSODRAWINGGROUP. # sub _store_mso_opt { my $self = shift; my $type = 0xF00B; my $version = 3; my $instance = 3; my $data = ''; my $length = 18; $data = pack "H*", 'BF0008000800810109000008C0014000' . '0008'; return $self->_add_mso_generic($type, $version, $instance, $data, $length); } ############################################################################### # # _store_mso_split_menu_colors() # # Write the Escher SplitMenuColors record that is part of MSODRAWINGGROUP. # sub _store_mso_split_menu_colors { my $self = shift; my $type = 0xF11E; my $version = 0; my $instance = 4; my $data = ''; my $length = 16; $data = pack "H*", '0D0000080C00000817000008F7000010'; return $self->_add_mso_generic($type, $version, $instance, $data, $length); } 1; __END__ =encoding latin1 =head1 NAME Workbook - A writer class for Excel Workbooks. =head1 SYNOPSIS See the documentation for Spreadsheet::WriteExcel =head1 DESCRIPTION This module is used in conjunction with Spreadsheet::WriteExcel. =head1 AUTHOR John McNamara jmcnamara@cpan.org =head1 COPYRIGHT Copyright MM-MMX, John McNamara. All Rights Reserved. This module is free software. It may be used, redistributed and/or modified under the same terms as Perl itself.