[Feature]Upload Modem source code

Change-Id: Id4294f30faced84d3e6fd6d5e61e1111bf287a37
diff --git a/mcu/tools/perl/Spreadsheet/WriteExcel/Worksheet.pm b/mcu/tools/perl/Spreadsheet/WriteExcel/Worksheet.pm
new file mode 100644
index 0000000..95142f6
--- /dev/null
+++ b/mcu/tools/perl/Spreadsheet/WriteExcel/Worksheet.pm
@@ -0,0 +1,7634 @@
+package Spreadsheet::WriteExcel::Worksheet;
+
+###############################################################################
+#
+# Worksheet - A writer class for Excel Worksheets.
+#
+#
+# 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::Format;
+use Spreadsheet::WriteExcel::Formula;
+
+
+
+use vars qw($VERSION @ISA);
+@ISA = qw(Spreadsheet::WriteExcel::BIFFwriter);
+
+$VERSION = '2.37';
+
+###############################################################################
+#
+# new()
+#
+# Constructor. Creates a new Worksheet object from a BIFFwriter object
+#
+sub new {
+
+    my $class                     = shift;
+    my $self                      = Spreadsheet::WriteExcel::BIFFwriter->new();
+    my $rowmax                    = 65536;
+    my $colmax                    = 256;
+    my $strmax                    = 0;
+
+    $self->{_name}                = $_[0];
+    $self->{_index}               = $_[1];
+    $self->{_encoding}            = $_[2];
+    $self->{_activesheet}         = $_[3];
+    $self->{_firstsheet}          = $_[4];
+    $self->{_url_format}          = $_[5];
+    $self->{_parser}              = $_[6];
+    $self->{_tempdir}             = $_[7];
+
+    $self->{_str_total}           = $_[8];
+    $self->{_str_unique}          = $_[9];
+    $self->{_str_table}           = $_[10];
+    $self->{_1904}                = $_[11];
+    $self->{_compatibility}       = $_[12];
+    $self->{_palette}             = $_[13];
+
+    $self->{_sheet_type}          = 0x0000;
+    $self->{_ext_sheets}          = [];
+    $self->{_using_tmpfile}       = 1;
+    $self->{_filehandle}          = "";
+    $self->{_fileclosed}          = 0;
+    $self->{_offset}              = 0;
+    $self->{_xls_rowmax}          = $rowmax;
+    $self->{_xls_colmax}          = $colmax;
+    $self->{_xls_strmax}          = $strmax;
+    $self->{_dim_rowmin}          = undef;
+    $self->{_dim_rowmax}          = undef;
+    $self->{_dim_colmin}          = undef;
+    $self->{_dim_colmax}          = undef;
+    $self->{_colinfo}             = [];
+    $self->{_selection}           = [0, 0];
+    $self->{_panes}               = [];
+    $self->{_active_pane}         = 3;
+    $self->{_frozen}              = 0;
+    $self->{_frozen_no_split}     = 1;
+    $self->{_selected}            = 0;
+    $self->{_hidden}              = 0;
+    $self->{_active}              = 0;
+    $self->{_tab_color}           = 0;
+
+    $self->{_first_row}           = 0;
+    $self->{_first_col}           = 0;
+    $self->{_display_formulas}    = 0;
+    $self->{_display_headers}     = 1;
+    $self->{_display_zeros}       = 1;
+    $self->{_display_arabic}      = 0;
+
+    $self->{_paper_size}          = 0x0;
+    $self->{_orientation}         = 0x1;
+    $self->{_header}              = '';
+    $self->{_footer}              = '';
+    $self->{_header_encoding}     = 0;
+    $self->{_footer_encoding}     = 0;
+    $self->{_hcenter}             = 0;
+    $self->{_vcenter}             = 0;
+    $self->{_margin_header}       = 0.50;
+    $self->{_margin_footer}       = 0.50;
+    $self->{_margin_left}         = 0.75;
+    $self->{_margin_right}        = 0.75;
+    $self->{_margin_top}          = 1.00;
+    $self->{_margin_bottom}       = 1.00;
+
+    $self->{_title_rowmin}        = undef;
+    $self->{_title_rowmax}        = undef;
+    $self->{_title_colmin}        = undef;
+    $self->{_title_colmax}        = undef;
+    $self->{_print_rowmin}        = undef;
+    $self->{_print_rowmax}        = undef;
+    $self->{_print_colmin}        = undef;
+    $self->{_print_colmax}        = undef;
+
+    $self->{_print_gridlines}     = 1;
+    $self->{_screen_gridlines}    = 1;
+    $self->{_print_headers}       = 0;
+
+    $self->{_page_order}          = 0;
+    $self->{_black_white}         = 0;
+    $self->{_draft_quality}       = 0;
+    $self->{_print_comments}      = 0;
+    $self->{_page_start}          = 1;
+    $self->{_custom_start}        = 0;
+
+    $self->{_fit_page}            = 0;
+    $self->{_fit_width}           = 0;
+    $self->{_fit_height}          = 0;
+
+    $self->{_hbreaks}             = [];
+    $self->{_vbreaks}             = [];
+
+    $self->{_protect}             = 0;
+    $self->{_password}            = undef;
+
+    $self->{_col_sizes}           = {};
+    $self->{_row_sizes}           = {};
+
+    $self->{_col_formats}         = {};
+    $self->{_row_formats}         = {};
+
+    $self->{_zoom}                = 100;
+    $self->{_print_scale}         = 100;
+    $self->{_page_view}           = 0;
+
+    $self->{_leading_zeros}       = 0;
+
+    $self->{_outline_row_level}   = 0;
+    $self->{_outline_style}       = 0;
+    $self->{_outline_below}       = 1;
+    $self->{_outline_right}       = 1;
+    $self->{_outline_on}          = 1;
+
+    $self->{_write_match}         = [];
+
+    $self->{_object_ids}          = [];
+    $self->{_images}              = {};
+    $self->{_images_array}        = [];
+    $self->{_charts}              = {};
+    $self->{_charts_array}        = [];
+    $self->{_comments}            = {};
+    $self->{_comments_array}      = [];
+    $self->{_comments_author}     = '';
+    $self->{_comments_author_enc} = 0;
+    $self->{_comments_visible}    = 0;
+
+    $self->{_filter_area}         = [];
+    $self->{_filter_count}        = 0;
+    $self->{_filter_on}           = 0;
+
+    $self->{_writing_url}         = 0;
+
+    $self->{_db_indices}          = [];
+
+    $self->{_validations}         = [];
+
+    bless $self, $class;
+    $self->_initialize();
+    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.
+#
+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 ($self->{_index} == 0 && $^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" ;
+        }
+    }
+}
+
+
+###############################################################################
+#
+# _close()
+#
+# Add data to the beginning of the workbook (note the reverse order)
+# and to the end of the workbook.
+#
+sub _close {
+
+    my $self = shift;
+
+    ################################################
+    # Prepend in reverse order!!
+    #
+
+    # Prepend the sheet dimensions
+    $self->_store_dimensions();
+
+    # Prepend the autofilter filters.
+    $self->_store_autofilters;
+
+    # Prepend the sheet autofilter info.
+    $self->_store_autofilterinfo();
+
+    # Prepend the sheet filtermode record.
+    $self->_store_filtermode();
+
+    # Prepend the COLINFO records if they exist
+    if (@{$self->{_colinfo}}){
+        my @colinfo = @{$self->{_colinfo}};
+        while (@colinfo) {
+            my $arrayref = pop @colinfo;
+            $self->_store_colinfo(@$arrayref);
+        }
+    }
+
+    # Prepend the DEFCOLWIDTH record
+    $self->_store_defcol();
+
+    # Prepend the sheet password
+    $self->_store_password();
+
+    # Prepend the sheet protection
+    $self->_store_protect();
+    $self->_store_obj_protect();
+
+    # Prepend the page setup
+    $self->_store_setup();
+
+    # Prepend the bottom margin
+    $self->_store_margin_bottom();
+
+    # Prepend the top margin
+    $self->_store_margin_top();
+
+    # Prepend the right margin
+    $self->_store_margin_right();
+
+    # Prepend the left margin
+    $self->_store_margin_left();
+
+    # Prepend the page vertical centering
+    $self->_store_vcenter();
+
+    # Prepend the page horizontal centering
+    $self->_store_hcenter();
+
+    # Prepend the page footer
+    $self->_store_footer();
+
+    # Prepend the page header
+    $self->_store_header();
+
+    # Prepend the vertical page breaks
+    $self->_store_vbreak();
+
+    # Prepend the horizontal page breaks
+    $self->_store_hbreak();
+
+    # Prepend WSBOOL
+    $self->_store_wsbool();
+
+    # Prepend the default row height.
+    $self->_store_defrow();
+
+    # Prepend GUTS
+    $self->_store_guts();
+
+    # Prepend GRIDSET
+    $self->_store_gridset();
+
+    # Prepend PRINTGRIDLINES
+    $self->_store_print_gridlines();
+
+    # Prepend PRINTHEADERS
+    $self->_store_print_headers();
+
+    #
+    # End of prepend. Read upwards from here.
+    ################################################
+
+    # Append
+    $self->_store_table();
+    $self->_store_images();
+    $self->_store_charts();
+    $self->_store_filters();
+    $self->_store_comments();
+    $self->_store_window2();
+    $self->_store_page_view();
+    $self->_store_zoom();
+    $self->_store_panes(@{$self->{_panes}}) if @{$self->{_panes}};
+    $self->_store_selection(@{$self->{_selection}});
+    $self->_store_validation_count();
+    $self->_store_validations();
+    $self->_store_tab_color();
+    $self->_store_eof();
+
+    # Prepend the BOF and INDEX records
+    $self->_store_index();
+    $self->_store_bof(0x0010);
+}
+
+
+###############################################################################
+#
+# _compatibility_mode()
+#
+# Set the compatibility mode.
+#
+# See the explanation in Workbook::compatibility_mode(). This private method
+# is mainly used for test purposes.
+#
+sub _compatibility_mode {
+
+    my $self      = shift;
+
+    if (defined($_[0])) {
+        $self->{_compatibility} = $_[0];
+    }
+    else {
+        $self->{_compatibility} = 1;
+    }
+}
+
+
+###############################################################################
+#
+# get_name().
+#
+# Retrieve the worksheet name.
+#
+# Note, there is no set_name() method because names are used in formulas and
+# converted to internal indices. Allowing the user to change sheet names
+# after they have been set in add_worksheet() is asking for trouble.
+#
+sub get_name {
+
+    my $self    = shift;
+
+    return $self->{_name};
+}
+
+
+###############################################################################
+#
+# 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;
+}
+
+
+###############################################################################
+#
+# select()
+#
+# Set this worksheet as a selected worksheet, i.e. the worksheet has its tab
+# highlighted.
+#
+sub select {
+
+    my $self = shift;
+
+    $self->{_hidden}         = 0; # Selected worksheet can't be hidden.
+    $self->{_selected}       = 1;
+}
+
+
+###############################################################################
+#
+# activate()
+#
+# Set this worksheet as the active worksheet, i.e. the worksheet that is
+# displayed when the workbook is opened. Also set it as selected.
+#
+sub activate {
+
+    my $self = shift;
+
+    $self->{_hidden}         = 0; # Active worksheet can't be hidden.
+    $self->{_selected}       = 1;
+    ${$self->{_activesheet}} = $self->{_index};
+}
+
+
+###############################################################################
+#
+# hide()
+#
+# Hide this worksheet.
+#
+sub hide {
+
+    my $self = shift;
+
+    $self->{_hidden}         = 1;
+
+    # A hidden worksheet shouldn't be active or selected.
+    $self->{_selected}       = 0;
+    ${$self->{_activesheet}} = 0;
+    ${$self->{_firstsheet}}  = 0;
+}
+
+
+###############################################################################
+#
+# set_first_sheet()
+#
+# Set this worksheet as the first visible sheet. This is necessary
+# when there are a large number of worksheets and the activated
+# worksheet is not visible on the screen.
+#
+sub set_first_sheet {
+
+    my $self = shift;
+
+    $self->{_hidden}         = 0; # Active worksheet can't be hidden.
+    ${$self->{_firstsheet}}  = $self->{_index};
+}
+
+
+###############################################################################
+#
+# protect($password)
+#
+# Set the worksheet protection flag to prevent accidental modification and to
+# hide formulas if the locked and hidden format properties have been set.
+#
+sub protect {
+
+    my $self = shift;
+
+    $self->{_protect}   = 1;
+    $self->{_password}  = $self->_encode_password($_[0]) if defined $_[0];
+
+}
+
+
+###############################################################################
+#
+# set_column($firstcol, $lastcol, $width, $format, $hidden, $level)
+#
+# Set the width of a single column or a range of columns.
+# See also: _store_colinfo
+#
+sub set_column {
+
+    my $self = shift;
+    my @data = @_;
+    my $cell = $data[0];
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($cell =~ /^\D/) {
+        @data = $self->_substitute_cellref(@_);
+
+        # Returned values $row1 and $row2 aren't required here. Remove them.
+        shift  @data;       # $row1
+        splice @data, 1, 1; # $row2
+    }
+
+    return if @data < 3; # Ensure at least $firstcol, $lastcol and $width
+    return if not defined $data[0]; # Columns must be defined.
+    return if not defined $data[1];
+
+    # Assume second column is the same as first if 0. Avoids KB918419 bug.
+    $data[1] = $data[0] if $data[1] == 0;
+
+    # Ensure 2nd col is larger than first. Also for KB918419 bug.
+    ($data[0], $data[1]) = ($data[1], $data[0]) if $data[0] > $data[1];
+
+    # Limit columns to Excel max of 255.
+    $data[0] = 255 if $data[0] > 255;
+    $data[1] = 255 if $data[1] > 255;
+
+    push @{$self->{_colinfo}}, [ @data ];
+
+
+    # Store the col sizes for use when calculating image vertices taking
+    # hidden columns into account. Also store the column formats.
+    #
+    my $width  = $data[4] ? 0 : $data[2]; # Set width to zero if col is hidden
+       $width  ||= 0;                     # Ensure width isn't undef.
+    my $format = $data[3];
+
+    my ($firstcol, $lastcol) = @data;
+
+    foreach my $col ($firstcol .. $lastcol) {
+        $self->{_col_sizes}->{$col}   = $width;
+        $self->{_col_formats}->{$col} = $format if defined $format;
+    }
+}
+
+
+###############################################################################
+#
+# set_selection()
+#
+# Set which cell or cells are selected in a worksheet: see also the
+# sub _store_selection
+#
+sub set_selection {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    $self->{_selection} = [ @_ ];
+}
+
+
+###############################################################################
+#
+# freeze_panes()
+#
+# Set panes and mark them as frozen. See also _store_panes().
+#
+sub freeze_panes {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    # Extra flag indicated a split and freeze.
+    $self->{_frozen_no_split} = 0 if $_[4];
+
+    $self->{_frozen} = 1;
+    $self->{_panes}  = [ @_ ];
+}
+
+
+###############################################################################
+#
+# split_panes()
+#
+# Set panes and mark them as split. See also _store_panes().
+#
+sub split_panes {
+
+    my $self = shift;
+
+    $self->{_frozen}            = 0;
+    $self->{_frozen_no_split}   = 0;
+    $self->{_panes}             = [ @_ ];
+}
+
+# Older method name for backwards compatibility.
+*thaw_panes = *split_panes;
+
+
+###############################################################################
+#
+# set_portrait()
+#
+# Set the page orientation as portrait.
+#
+sub set_portrait {
+
+    my $self = shift;
+
+    $self->{_orientation} = 1;
+}
+
+
+###############################################################################
+#
+# set_landscape()
+#
+# Set the page orientation as landscape.
+#
+sub set_landscape {
+
+    my $self = shift;
+
+    $self->{_orientation} = 0;
+}
+
+
+###############################################################################
+#
+# set_page_view()
+#
+# Set the page view mode for Mac Excel.
+#
+sub set_page_view {
+
+    my $self = shift;
+
+    $self->{_page_view} = defined $_[0] ? $_[0] : 1;
+}
+
+
+###############################################################################
+#
+# set_tab_color()
+#
+# Set the colour of the worksheet colour.
+#
+sub set_tab_color {
+
+    my $self  = shift;
+
+    my $color = &Spreadsheet::WriteExcel::Format::_get_color($_[0]);
+       $color = 0 if $color == 0x7FFF; # Default color.
+
+    $self->{_tab_color} = $color;
+}
+
+
+###############################################################################
+#
+# set_paper()
+#
+# Set the paper type. Ex. 1 = US Letter, 9 = A4
+#
+sub set_paper {
+
+    my $self = shift;
+
+    $self->{_paper_size} = $_[0] || 0;
+}
+
+
+###############################################################################
+#
+# set_header()
+#
+# Set the page header caption and optional margin.
+#
+sub set_header {
+
+    my $self     = shift;
+    my $string   = $_[0] || '';
+    my $margin   = $_[1] || 0.50;
+    my $encoding = $_[2] || 0;
+
+    # Handle utf8 strings in perl 5.8.
+    if ($] >= 5.008) {
+        require Encode;
+
+        if (Encode::is_utf8($string)) {
+            $string = Encode::encode("UTF-16BE", $string);
+            $encoding = 1;
+        }
+    }
+
+    my $limit    = $encoding ? 255 *2 : 255;
+
+    if (length $string >= $limit) {
+        carp 'Header string must be less than 255 characters';
+        return;
+    }
+
+    $self->{_header}          = $string;
+    $self->{_margin_header}   = $margin;
+    $self->{_header_encoding} = $encoding;
+}
+
+
+###############################################################################
+#
+# set_footer()
+#
+# Set the page footer caption and optional margin.
+#
+sub set_footer {
+
+    my $self     = shift;
+    my $string   = $_[0] || '';
+    my $margin   = $_[1] || 0.50;
+    my $encoding = $_[2] || 0;
+
+    # Handle utf8 strings in perl 5.8.
+    if ($] >= 5.008) {
+        require Encode;
+
+        if (Encode::is_utf8($string)) {
+            $string = Encode::encode("UTF-16BE", $string);
+            $encoding = 1;
+        }
+    }
+
+    my $limit    = $encoding ? 255 *2 : 255;
+
+
+    if (length $string >= $limit) {
+        carp 'Footer string must be less than 255 characters';
+        return;
+    }
+
+    $self->{_footer}          = $string;
+    $self->{_margin_footer}   = $margin;
+    $self->{_footer_encoding} = $encoding;
+}
+
+
+###############################################################################
+#
+# center_horizontally()
+#
+# Center the page horizontally.
+#
+sub center_horizontally {
+
+    my $self = shift;
+
+    if (defined $_[0]) {
+        $self->{_hcenter} = $_[0];
+    }
+    else {
+        $self->{_hcenter} = 1;
+    }
+}
+
+
+###############################################################################
+#
+# center_vertically()
+#
+# Center the page horizontally.
+#
+sub center_vertically {
+
+    my $self = shift;
+
+    if (defined $_[0]) {
+        $self->{_vcenter} = $_[0];
+    }
+    else {
+        $self->{_vcenter} = 1;
+    }
+}
+
+
+###############################################################################
+#
+# set_margins()
+#
+# Set all the page margins to the same value in inches.
+#
+sub set_margins {
+
+    my $self = shift;
+
+    $self->set_margin_left($_[0]);
+    $self->set_margin_right($_[0]);
+    $self->set_margin_top($_[0]);
+    $self->set_margin_bottom($_[0]);
+}
+
+
+###############################################################################
+#
+# set_margins_LR()
+#
+# Set the left and right margins to the same value in inches.
+#
+sub set_margins_LR {
+
+    my $self = shift;
+
+    $self->set_margin_left($_[0]);
+    $self->set_margin_right($_[0]);
+}
+
+
+###############################################################################
+#
+# set_margins_TB()
+#
+# Set the top and bottom margins to the same value in inches.
+#
+sub set_margins_TB {
+
+    my $self = shift;
+
+    $self->set_margin_top($_[0]);
+    $self->set_margin_bottom($_[0]);
+}
+
+
+###############################################################################
+#
+# set_margin_left()
+#
+# Set the left margin in inches.
+#
+sub set_margin_left {
+
+    my $self = shift;
+
+    $self->{_margin_left} = defined $_[0] ? $_[0] : 0.75;
+}
+
+
+###############################################################################
+#
+# set_margin_right()
+#
+# Set the right margin in inches.
+#
+sub set_margin_right {
+
+    my $self = shift;
+
+    $self->{_margin_right} = defined $_[0] ? $_[0] : 0.75;
+}
+
+
+###############################################################################
+#
+# set_margin_top()
+#
+# Set the top margin in inches.
+#
+sub set_margin_top {
+
+    my $self = shift;
+
+    $self->{_margin_top} = defined $_[0] ? $_[0] : 1.00;
+}
+
+
+###############################################################################
+#
+# set_margin_bottom()
+#
+# Set the bottom margin in inches.
+#
+sub set_margin_bottom {
+
+    my $self = shift;
+
+    $self->{_margin_bottom} = defined $_[0] ? $_[0] : 1.00;
+}
+
+
+###############################################################################
+#
+# repeat_rows($first_row, $last_row)
+#
+# Set the rows to repeat at the top of each printed page. See also the
+# _store_name_xxxx() methods in Workbook.pm.
+#
+sub repeat_rows {
+
+    my $self = shift;
+
+    $self->{_title_rowmin}  = $_[0];
+    $self->{_title_rowmax}  = $_[1] || $_[0]; # Second row is optional
+}
+
+
+###############################################################################
+#
+# repeat_columns($first_col, $last_col)
+#
+# Set the columns to repeat at the left hand side of each printed page.
+# See also the _store_names() methods in Workbook.pm.
+#
+sub repeat_columns {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+
+        # Returned values $row1 and $row2 aren't required here. Remove them.
+        shift  @_;       # $row1
+        splice @_, 1, 1; # $row2
+    }
+
+    $self->{_title_colmin}  = $_[0];
+    $self->{_title_colmax}  = $_[1] || $_[0]; # Second col is optional
+}
+
+
+###############################################################################
+#
+# print_area($first_row, $first_col, $last_row, $last_col)
+#
+# Set the area of each worksheet that will be printed. See also the
+# _store_names() methods in Workbook.pm.
+#
+sub print_area {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    return if @_ != 4; # Require 4 parameters
+
+    $self->{_print_rowmin} = $_[0];
+    $self->{_print_colmin} = $_[1];
+    $self->{_print_rowmax} = $_[2];
+    $self->{_print_colmax} = $_[3];
+}
+
+
+###############################################################################
+#
+# autofilter($first_row, $first_col, $last_row, $last_col)
+#
+# Set the autofilter area in the worksheet.
+#
+sub autofilter {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    return if @_ != 4; # Require 4 parameters
+
+    my ($row1, $col1, $row2, $col2) = @_;
+
+    # Reverse max and min values if necessary.
+    ($row1, $row2) = ($row2, $row1) if $row2 < $row1;
+    ($col1, $col2) = ($col2, $col1) if $col2 < $col1;
+
+    # Store the Autofilter information
+    $self->{_filter_area}  = [$row1, $row2, $col1, $col2];
+    $self->{_filter_count} = 1+ $col2 -$col1;
+}
+
+
+###############################################################################
+#
+# filter_column($column, $criteria, ...)
+#
+# Set the column filter criteria.
+#
+sub filter_column {
+
+    my $self        = shift;
+    my $col         = $_[0];
+    my $expression  = $_[1];
+
+
+    croak "Must call autofilter() before filter_column()"
+                                                 unless $self->{_filter_count};
+    croak "Incorrect number of arguments to filter_column()" unless @_ == 2;
+
+
+    # Check for a column reference in A1 notation and substitute.
+    if ($col =~ /^\D/) {
+        # Convert col ref to a cell ref and then to a col number.
+        (undef, $col) = $self->_substitute_cellref($col . '1');
+    }
+
+    my (undef, undef, $col_first, $col_last) = @{$self->{_filter_area}};
+
+    # Reject column if it is outside filter range.
+    if ($col < $col_first or $col > $col_last) {
+        croak "Column '$col' outside autofilter() column range " .
+              "($col_first .. $col_last)";
+    }
+
+
+    my @tokens = $self->_extract_filter_tokens($expression);
+
+    croak "Incorrect number of tokens in expression '$expression'"
+          unless (@tokens == 3 or @tokens == 7);
+
+
+    @tokens = $self->_parse_filter_expression($expression, @tokens);
+
+    $self->{_filter_cols}->{$col} = [@tokens];
+    $self->{_filter_on}           = 1;
+}
+
+
+###############################################################################
+#
+# _extract_filter_tokens($expression)
+#
+# Extract the tokens from the filter expression. The tokens are mainly non-
+# whitespace groups. The only tricky part is to extract string tokens that
+# contain whitespace and/or quoted double quotes (Excel's escaped quotes).
+#
+# Examples: 'x <  2000'
+#           'x >  2000 and x <  5000'
+#           'x = "foo"'
+#           'x = "foo bar"'
+#           'x = "foo "" bar"'
+#
+sub _extract_filter_tokens {
+
+    my $self        = shift;
+    my $expression  = $_[0];
+
+    return unless $expression;
+
+    my @tokens = ($expression  =~ /"(?:[^"]|"")*"|\S+/g); #"
+
+    # Remove leading and trailing quotes and unescape other quotes
+    for (@tokens) {
+        s/^"//;     #"
+        s/"$//;     #"
+        s/""/"/g;   #"
+    }
+
+    return @tokens;
+}
+
+
+###############################################################################
+#
+# _parse_filter_expression(@token)
+#
+# Converts the tokens of a possibly conditional expression into 1 or 2
+# sub expressions for further parsing.
+#
+# Examples:
+#          ('x', '==', 2000) -> exp1
+#          ('x', '>',  2000, 'and', 'x', '<', 5000) -> exp1 and exp2
+#
+sub _parse_filter_expression {
+
+    my $self        = shift;
+    my $expression  = shift;
+    my @tokens      = @_;
+
+    # The number of tokens will be either 3 (for 1 expression)
+    # or 7 (for 2  expressions).
+    #
+    if (@tokens == 7) {
+
+        my $conditional = $tokens[3];
+
+        if    ($conditional =~ /^(and|&&)$/) {
+            $conditional = 0;
+        }
+        elsif ($conditional =~ /^(or|\|\|)$/) {
+            $conditional = 1;
+        }
+        else {
+            croak "Token '$conditional' is not a valid conditional " .
+                  "in filter expression '$expression'";
+        }
+
+        my @expression_1 = $self->_parse_filter_tokens($expression,
+                                                       @tokens[0, 1, 2]);
+        my @expression_2 = $self->_parse_filter_tokens($expression,
+                                                       @tokens[4, 5, 6]);
+
+        return (@expression_1, $conditional, @expression_2);
+    }
+    else {
+        return $self->_parse_filter_tokens($expression, @tokens);
+    }
+}
+
+
+###############################################################################
+#
+# _parse_filter_tokens(@token)
+#
+# Parse the 3 tokens of a filter expression and return the operator and token.
+#
+sub _parse_filter_tokens {
+
+    my $self        = shift;
+    my $expression  = shift;
+    my @tokens      = @_;
+
+    my %operators = (
+                        '==' => 2,
+                        '='  => 2,
+                        '=~' => 2,
+                        'eq' => 2,
+
+                        '!=' => 5,
+                        '!~' => 5,
+                        'ne' => 5,
+                        '<>' => 5,
+
+                        '<'  => 1,
+                        '<=' => 3,
+                        '>'  => 4,
+                        '>=' => 6,
+                    );
+
+    my $operator = $operators{$tokens[1]};
+    my $token    = $tokens[2];
+
+
+    # Special handling of "Top" filter expressions.
+    if ($tokens[0] =~ /^top|bottom$/i) {
+
+        my $value = $tokens[1];
+
+        if ($value =~ /\D/ or
+            $value < 1     or
+            $value > 500)
+        {
+            croak "The value '$value' in expression '$expression' " .
+                   "must be in the range 1 to 500";
+        }
+
+        $token = lc $token;
+
+        if ($token ne 'items' and $token ne '%') {
+            croak "The type '$token' in expression '$expression' " .
+                   "must be either 'items' or '%'";
+        }
+
+        if ($tokens[0] =~ /^top$/i) {
+            $operator = 30;
+        }
+        else {
+            $operator = 32;
+        }
+
+        if ($tokens[2] eq '%') {
+            $operator++;
+        }
+
+        $token    = $value;
+    }
+
+
+    if (not $operator and $tokens[0]) {
+        croak "Token '$tokens[1]' is not a valid operator " .
+              "in filter expression '$expression'";
+    }
+
+
+    # Special handling for Blanks/NonBlanks.
+    if ($token =~ /^blanks|nonblanks$/i) {
+
+        # Only allow Equals or NotEqual in this context.
+        if ($operator != 2 and $operator != 5) {
+            croak "The operator '$tokens[1]' in expression '$expression' " .
+                   "is not valid in relation to Blanks/NonBlanks'";
+        }
+
+        $token = lc $token;
+
+        # The operator should always be 2 (=) to flag a "simple" equality in
+        # the binary record. Therefore we convert <> to =.
+        if ($token eq 'blanks') {
+            if ($operator == 5) {
+                $operator = 2;
+                $token    = 'nonblanks';
+            }
+        }
+        else {
+            if ($operator == 5) {
+                $operator = 2;
+                $token    = 'blanks';
+            }
+        }
+    }
+
+
+    # if the string token contains an Excel match character then change the
+    # operator type to indicate a non "simple" equality.
+    if ($operator == 2 and $token =~ /[*?]/) {
+        $operator = 22;
+    }
+
+
+    return ($operator, $token);
+}
+
+
+###############################################################################
+#
+# hide_gridlines()
+#
+# Set the option to hide gridlines on the screen and the printed page.
+# There are two ways of doing this in the Excel BIFF format: The first is by
+# setting the DspGrid field of the WINDOW2 record, this turns off the screen
+# and subsequently the print gridline. The second method is to via the
+# PRINTGRIDLINES and GRIDSET records, this turns off the printed gridlines
+# only. The first method is probably sufficient for most cases. The second
+# method is supported for backwards compatibility. Porters take note.
+#
+sub hide_gridlines {
+
+    my $self   = shift;
+    my $option = $_[0];
+
+    $option = 1 unless defined $option; # Default to hiding printed gridlines
+
+    if ($option == 0) {
+        $self->{_print_gridlines}  = 1; # 1 = display, 0 = hide
+        $self->{_screen_gridlines} = 1;
+    }
+    elsif ($option == 1) {
+        $self->{_print_gridlines}  = 0;
+        $self->{_screen_gridlines} = 1;
+    }
+    else {
+        $self->{_print_gridlines}  = 0;
+        $self->{_screen_gridlines} = 0;
+    }
+}
+
+
+###############################################################################
+#
+# print_row_col_headers()
+#
+# Set the option to print the row and column headers on the printed page.
+# See also the _store_print_headers() method below.
+#
+sub print_row_col_headers {
+
+    my $self = shift;
+
+    if (defined $_[0]) {
+        $self->{_print_headers} = $_[0];
+    }
+    else {
+        $self->{_print_headers} = 1;
+    }
+}
+
+
+###############################################################################
+#
+# fit_to_pages($width, $height)
+#
+# Store the vertical and horizontal number of pages that will define the
+# maximum area printed. See also _store_setup() and _store_wsbool() below.
+#
+sub fit_to_pages {
+
+    my $self = shift;
+
+    $self->{_fit_page}      = 1;
+    $self->{_fit_width}     = $_[0] || 0;
+    $self->{_fit_height}    = $_[1] || 0;
+}
+
+
+###############################################################################
+#
+# set_h_pagebreaks(@breaks)
+#
+# Store the horizontal page breaks on a worksheet.
+#
+sub set_h_pagebreaks {
+
+    my $self = shift;
+
+    push @{$self->{_hbreaks}}, @_;
+}
+
+
+###############################################################################
+#
+# set_v_pagebreaks(@breaks)
+#
+# Store the vertical page breaks on a worksheet.
+#
+sub set_v_pagebreaks {
+
+    my $self = shift;
+
+    push @{$self->{_vbreaks}}, @_;
+}
+
+
+###############################################################################
+#
+# set_zoom($scale)
+#
+# Set the worksheet zoom factor.
+#
+sub set_zoom {
+
+    my $self  = shift;
+    my $scale = $_[0] || 100;
+
+    # Confine the scale to Excel's range
+    if ($scale < 10 or $scale > 400) {
+        carp "Zoom factor $scale outside range: 10 <= zoom <= 400";
+        $scale = 100;
+    }
+
+    $self->{_zoom} = int $scale;
+}
+
+
+###############################################################################
+#
+# set_print_scale($scale)
+#
+# Set the scale factor for the printed page.
+#
+sub set_print_scale {
+
+    my $self  = shift;
+    my $scale = $_[0] || 100;
+
+    # Confine the scale to Excel's range
+    if ($scale < 10 or $scale > 400) {
+        carp "Print scale $scale outside range: 10 <= zoom <= 400";
+        $scale = 100;
+    }
+
+    # Turn off "fit to page" option
+    $self->{_fit_page}    = 0;
+
+    $self->{_print_scale} = int $scale;
+}
+
+
+###############################################################################
+#
+# keep_leading_zeros()
+#
+# Causes the write() method to treat integers with a leading zero as a string.
+# This ensures that any leading zeros such, as in zip codes, are maintained.
+#
+sub keep_leading_zeros {
+
+    my $self = shift;
+
+    if (defined $_[0]) {
+        $self->{_leading_zeros} = $_[0];
+    }
+    else {
+        $self->{_leading_zeros} = 1;
+    }
+}
+
+
+###############################################################################
+#
+# show_comments()
+#
+# Make any comments in the worksheet visible.
+#
+sub show_comments {
+
+    my $self = shift;
+
+    $self->{_comments_visible} = defined $_[0] ? $_[0] : 1;
+}
+
+
+###############################################################################
+#
+# set_comments_author()
+#
+# Set the default author of the cell comments.
+#
+sub set_comments_author {
+
+    my $self = shift;
+
+    $self->{_comments_author}     = defined $_[0] ? $_[0] : '';
+    $self->{_comments_author_enc} =         $_[1] ? 1     : 0;
+}
+
+
+###############################################################################
+#
+# right_to_left()
+#
+# Display the worksheet right to left for some eastern versions of Excel.
+#
+sub right_to_left {
+
+    my $self = shift;
+
+    $self->{_display_arabic} = defined $_[0] ? $_[0] : 1;
+}
+
+
+###############################################################################
+#
+# hide_zero()
+#
+# Hide cell zero values.
+#
+sub hide_zero {
+
+    my $self = shift;
+
+    $self->{_display_zeros} = defined $_[0] ? not $_[0] : 0;
+}
+
+
+###############################################################################
+#
+# print_across()
+#
+# Set the order in which pages are printed.
+#
+sub print_across {
+
+    my $self = shift;
+
+    $self->{_page_order} = defined $_[0] ? $_[0] : 1;
+}
+
+
+###############################################################################
+#
+# set_start_page()
+#
+# Set the start page number.
+#
+sub set_start_page {
+
+    my $self = shift;
+    return unless defined $_[0];
+
+    $self->{_page_start}    = $_[0];
+    $self->{_custom_start}  = 1;
+}
+
+
+###############################################################################
+#
+# set_first_row_column()
+#
+# Set the topmost and leftmost visible row and column.
+# TODO: Document this when tested fully for interaction with panes.
+#
+sub set_first_row_column {
+
+    my $self = shift;
+
+    my $row  = $_[0] || 0;
+    my $col  = $_[1] || 0;
+
+    $row = 65535 if $row > 65535;
+    $col = 255   if $col > 255;
+
+    $self->{_first_row} = $row;
+    $self->{_first_col} = $col;
+}
+
+
+###############################################################################
+#
+# add_write_handler($re, $code_ref)
+#
+# Allow the user to add their own matches and handlers to the write() method.
+#
+sub add_write_handler {
+
+    my $self = shift;
+
+    return unless @_ == 2;
+    return unless ref $_[1] eq 'CODE';
+
+    push @{$self->{_write_match}}, [ @_ ];
+}
+
+
+
+###############################################################################
+#
+# write($row, $col, $token, $format)
+#
+# Parse $token and call appropriate write method. $row and $column are zero
+# indexed. $format is optional.
+#
+# The write_url() methods have a flag to prevent recursion when writing a
+# string that looks like a url.
+#
+# Returns: return value of called subroutine
+#
+sub write {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    my $token = $_[2];
+
+    # Handle undefs as blanks
+    $token = '' unless defined $token;
+
+
+    # First try user defined matches.
+    for my $aref (@{$self->{_write_match}}) {
+        my $re  = $aref->[0];
+        my $sub = $aref->[1];
+
+        if ($token =~ /$re/) {
+            my $match = &$sub($self, @_);
+            return $match if defined $match;
+        }
+    }
+
+
+    # Match an array ref.
+    if (ref $token eq "ARRAY") {
+        return $self->write_row(@_);
+    }
+    # Match integer with leading zero(s)
+    elsif ($self->{_leading_zeros} and $token =~ /^0\d+$/) {
+        return $self->write_string(@_);
+    }
+    # Match number
+    elsif ($token =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/) {
+        return $self->write_number(@_);
+    }
+    # Match http, https or ftp URL
+    elsif ($token =~ m|^[fh]tt?ps?://|    and not $self->{_writing_url}) {
+        return $self->write_url(@_);
+    }
+    # Match mailto:
+    elsif ($token =~ m/^mailto:/          and not $self->{_writing_url}) {
+        return $self->write_url(@_);
+    }
+    # Match internal or external sheet link
+    elsif ($token =~ m[^(?:in|ex)ternal:] and not $self->{_writing_url}) {
+        return $self->write_url(@_);
+    }
+    # Match formula
+    elsif ($token =~ /^=/) {
+        return $self->write_formula(@_);
+    }
+    # Match blank
+    elsif ($token eq '') {
+        splice @_, 2, 1; # remove the empty string from the parameter list
+        return $self->write_blank(@_);
+    }
+    else {
+        return $self->write_string(@_);
+    }
+}
+
+
+###############################################################################
+#
+# write_row($row, $col, $array_ref, $format)
+#
+# Write a row of data starting from ($row, $col). Call write_col() if any of
+# the elements of the array ref are in turn array refs. This allows the writing
+# of 1D or 2D arrays of data in one go.
+#
+# Returns: the first encountered error value or zero for no errors
+#
+sub write_row {
+
+    my $self = shift;
+
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    # Catch non array refs passed by user.
+    if (ref $_[2] ne 'ARRAY') {
+        croak "Not an array ref in call to write_row()$!";
+    }
+
+    my $row     = shift;
+    my $col     = shift;
+    my $tokens  = shift;
+    my @options = @_;
+    my $error   = 0;
+    my $ret;
+
+    foreach my $token (@$tokens) {
+
+        # Check for nested arrays
+        if (ref $token eq "ARRAY") {
+            $ret = $self->write_col($row, $col, $token, @options);
+        } else {
+            $ret = $self->write    ($row, $col, $token, @options);
+        }
+
+        # Return only the first error encountered, if any.
+        $error ||= $ret;
+        $col++;
+    }
+
+    return $error;
+}
+
+
+###############################################################################
+#
+# write_col($row, $col, $array_ref, $format)
+#
+# Write a column of data starting from ($row, $col). Call write_row() if any of
+# the elements of the array ref are in turn array refs. This allows the writing
+# of 1D or 2D arrays of data in one go.
+#
+# Returns: the first encountered error value or zero for no errors
+#
+sub write_col {
+
+    my $self = shift;
+
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    # Catch non array refs passed by user.
+    if (ref $_[2] ne 'ARRAY') {
+        croak "Not an array ref in call to write_row()$!";
+    }
+
+    my $row     = shift;
+    my $col     = shift;
+    my $tokens  = shift;
+    my @options = @_;
+    my $error   = 0;
+    my $ret;
+
+    foreach my $token (@$tokens) {
+
+        # write() will deal with any nested arrays
+        $ret = $self->write($row, $col, $token, @options);
+
+        # Return only the first error encountered, if any.
+        $error ||= $ret;
+        $row++;
+    }
+
+    return $error;
+}
+
+
+###############################################################################
+#
+# write_comment($row, $col, $comment)
+#
+# Write a comment to the specified row and column (zero indexed).
+#
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#
+sub write_comment {
+
+    my $self = shift;
+
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    if (@_ < 3) { return -1 } # Check the number of args
+
+
+    my $row = $_[0];
+    my $col = $_[1];
+
+    # Check for pairs of optional arguments, i.e. an odd number of args.
+    croak "Uneven number of additional arguments" unless @_ % 2;
+
+
+    # Check that row and col are valid and store max and min values
+    return -2 if $self->_check_dimensions($row, $col);
+
+
+    # We have to avoid duplicate comments in cells or else Excel will complain.
+    $self->{_comments}->{$row}->{$col} = [ $self->_comment_params(@_) ];
+
+}
+
+
+###############################################################################
+#
+# _XF()
+#
+# Returns an index to the XF record in the workbook.
+#
+# Note: this is a function, not a method.
+#
+sub _XF {
+
+    my $self   = $_[0];
+    my $row    = $_[1];
+    my $col    = $_[2];
+    my $format = $_[3];
+
+    my $error = "Error: refer to merge_range() in the documentation. " .
+                 "Can't use previously merged format in non-merged cell";
+
+    if (ref($format)) {
+        # Temp code to prevent merged formats in non-merged cells.
+        croak $error if $format->{_used_merge} == 1;
+        $format->{_used_merge} = -1;
+
+        return $format->get_xf_index();
+    }
+    elsif (exists $self->{_row_formats}->{$row}) {
+        # Temp code to prevent merged formats in non-merged cells.
+        croak $error if $self->{_row_formats}->{$row}->{_used_merge} == 1;
+        $self->{_row_formats}->{$row}->{_used_merge} = -1;
+
+        return $self->{_row_formats}->{$row}->get_xf_index();
+    }
+    elsif (exists $self->{_col_formats}->{$col}) {
+        # Temp code to prevent merged formats in non-merged cells.
+        croak $error if $self->{_col_formats}->{$col}->{_used_merge} == 1;
+        $self->{_col_formats}->{$col}->{_used_merge} = -1;
+
+        return $self->{_col_formats}->{$col}->get_xf_index();
+    }
+    else {
+        return 0x0F;
+    }
+}
+
+
+###############################################################################
+###############################################################################
+#
+# Internal methods
+#
+
+
+###############################################################################
+#
+# _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;
+}
+
+
+###############################################################################
+#
+# _substitute_cellref()
+#
+# Substitute an Excel cell reference in A1 notation for  zero based row and
+# column values in an argument list.
+#
+# Ex: ("A4", "Hello") is converted to (3, 0, "Hello").
+#
+sub _substitute_cellref {
+
+    my $self = shift;
+    my $cell = uc(shift);
+
+    # Convert a column range: 'A:A' or 'B:G'.
+    # A range such as A:A is equivalent to A1:65536, so add rows as required
+    if ($cell =~ /\$?([A-I]?[A-Z]):\$?([A-I]?[A-Z])/) {
+        my ($row1, $col1) =  $self->_cell_to_rowcol($1 .'1');
+        my ($row2, $col2) =  $self->_cell_to_rowcol($2 .'65536');
+        return $row1, $col1, $row2, $col2, @_;
+    }
+
+    # Convert a cell range: 'A1:B7'
+    if ($cell =~ /\$?([A-I]?[A-Z]\$?\d+):\$?([A-I]?[A-Z]\$?\d+)/) {
+        my ($row1, $col1) =  $self->_cell_to_rowcol($1);
+        my ($row2, $col2) =  $self->_cell_to_rowcol($2);
+        return $row1, $col1, $row2, $col2, @_;
+    }
+
+    # Convert a cell reference: 'A1' or 'AD2000'
+    if ($cell =~ /\$?([A-I]?[A-Z]\$?\d+)/) {
+        my ($row1, $col1) =  $self->_cell_to_rowcol($1);
+        return $row1, $col1, @_;
+
+    }
+
+    croak("Unknown cell reference $cell");
+}
+
+
+###############################################################################
+#
+# _cell_to_rowcol($cell_ref)
+#
+# Convert an Excel cell reference in A1 notation to a zero based row and column
+# reference; converts C1 to (0, 2).
+#
+# Returns: row, column
+#
+sub _cell_to_rowcol {
+
+    my $self = shift;
+    my $cell = shift;
+
+    $cell =~ /\$?([A-I]?[A-Z])\$?(\d+)/;
+
+    my $col     = $1;
+    my $row     = $2;
+
+    # Convert base26 column string to number
+    # All your Base are belong to us.
+    my @chars = split //, $col;
+    my $expn  = 0;
+    $col      = 0;
+
+    while (@chars) {
+        my $char = pop(@chars); # LS char first
+        $col += (ord($char) -ord('A') +1) * (26**$expn);
+        $expn++;
+    }
+
+    # Convert 1-index to zero-index
+    $row--;
+    $col--;
+
+    return $row, $col;
+}
+
+
+###############################################################################
+#
+# _sort_pagebreaks()
+#
+#
+# This is an internal method that is used to filter elements of the array of
+# pagebreaks used in the _store_hbreak() and _store_vbreak() methods. It:
+#   1. Removes duplicate entries from the list.
+#   2. Sorts the list.
+#   3. Removes 0 from the list if present.
+#
+sub _sort_pagebreaks {
+
+    my $self= shift;
+
+    my %hash;
+    my @array;
+
+    @hash{@_} = undef;                       # Hash slice to remove duplicates
+    @array    = sort {$a <=> $b} keys %hash; # Numerical sort
+    shift @array if $array[0] == 0;          # Remove zero
+
+    # 1000 vertical pagebreaks appears to be an internal Excel 5 limit.
+    # It is slightly higher in Excel 97/200, approx. 1026
+    splice(@array, 1000) if (@array > 1000);
+
+    return @array
+}
+
+
+###############################################################################
+#
+# _encode_password($password)
+#
+# Based on the algorithm provided by Daniel Rentz of OpenOffice.
+#
+#
+sub _encode_password {
+
+    use integer;
+
+    my $self      = shift;
+    my $plaintext = $_[0];
+    my $password;
+    my $count;
+    my @chars;
+    my $i = 0;
+
+    $count = @chars = split //, $plaintext;
+
+    foreach my $char (@chars) {
+        my $low_15;
+        my $high_15;
+        $char     = ord($char) << ++$i;
+        $low_15   = $char & 0x7fff;
+        $high_15  = $char & 0x7fff << 15;
+        $high_15  = $high_15 >> 15;
+        $char     = $low_15 | $high_15;
+    }
+
+    $password  = 0x0000;
+    $password ^= $_ for @chars;
+    $password ^= $count;
+    $password ^= 0xCE4B;
+
+    return $password;
+}
+
+
+###############################################################################
+#
+# outline_settings($visible, $symbols_below, $symbols_right, $auto_style)
+#
+# This method sets the properties for outlining and grouping. The defaults
+# correspond to Excel's defaults.
+#
+sub outline_settings {
+
+    my $self                = shift;
+
+    $self->{_outline_on}    = defined $_[0] ? $_[0] : 1;
+    $self->{_outline_below} = defined $_[1] ? $_[1] : 1;
+    $self->{_outline_right} = defined $_[2] ? $_[2] : 1;
+    $self->{_outline_style} =         $_[3] || 0;
+
+    # Ensure this is a boolean vale for Window2
+    $self->{_outline_on}    = 1 if $self->{_outline_on};
+}
+
+
+
+
+###############################################################################
+###############################################################################
+#
+# BIFF RECORDS
+#
+
+
+###############################################################################
+#
+# write_number($row, $col, $num, $format)
+#
+# Write a double to the specified row and column (zero indexed).
+# An integer can be written as a double. Excel will display an
+# integer. $format is optional.
+#
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#
+sub write_number {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    if (@_ < 3) { return -1 }                    # Check the number of args
+
+    my $record  = 0x0203;                        # Record identifier
+    my $length  = 0x000E;                        # Number of bytes to follow
+
+    my $row     = $_[0];                         # Zero indexed row
+    my $col     = $_[1];                         # Zero indexed column
+    my $num     = $_[2];
+    my $xf      = _XF($self, $row, $col, $_[3]); # The cell format
+
+    # Check that row and col are valid and store max and min values
+    return -2 if $self->_check_dimensions($row, $col);
+
+    my $header    = pack("vv",  $record, $length);
+    my $data      = pack("vvv", $row, $col, $xf);
+    my $xl_double = pack("d",   $num);
+
+    if ($self->{_byte_order}) { $xl_double = reverse $xl_double }
+
+    # Store the data or write immediately depending on the compatibility mode.
+    if ($self->{_compatibility}) {
+        $self->{_table}->[$row]->[$col] = $header . $data . $xl_double;
+    }
+    else {
+        $self->_append($header, $data, $xl_double);
+    }
+
+    return 0;
+}
+
+
+###############################################################################
+#
+# write_string ($row, $col, $string, $format)
+#
+# Write a string to the specified row and column (zero indexed).
+# NOTE: there is an Excel 5 defined limit of 255 characters.
+# $format is optional.
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#         -3 : long string truncated to 255 chars
+#
+sub write_string {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    if (@_ < 3) { return -1 }                        # Check the number of args
+
+    my $record      = 0x00FD;                        # Record identifier
+    my $length      = 0x000A;                        # Bytes to follow
+
+    my $row         = $_[0];                         # Zero indexed row
+    my $col         = $_[1];                         # Zero indexed column
+    my $strlen      = length($_[2]);
+    my $str         = $_[2];
+    my $xf          = _XF($self, $row, $col, $_[3]); # The cell format
+    my $encoding    = 0x0;
+    my $str_error   = 0;
+
+
+    # Handle utf8 strings in perl 5.8.
+    if ($] >= 5.008) {
+        require Encode;
+
+        if (Encode::is_utf8($str)) {
+            my $tmp = Encode::encode("UTF-16LE", $str);
+            return $self->write_utf16le_string($row, $col, $tmp, $_[3]);
+        }
+    }
+
+
+    # Check that row and col are valid and store max and min values
+    return -2 if $self->_check_dimensions($row, $col);
+
+    # Limit the string to the max number of chars.
+    if ($strlen > 32767) {
+        $str       = substr($str, 0, 32767);
+        $str_error = -3;
+    }
+
+
+    # Prepend the string with the type.
+    my $str_header  = pack("vC", length($str), $encoding);
+    $str            = $str_header . $str;
+
+
+    if (not exists ${$self->{_str_table}}->{$str}) {
+        ${$self->{_str_table}}->{$str} = ${$self->{_str_unique}}++;
+    }
+
+
+    ${$self->{_str_total}}++;
+
+
+    my $header = pack("vv",   $record, $length);
+    my $data   = pack("vvvV", $row, $col, $xf, ${$self->{_str_table}}->{$str});
+
+
+    # Store the data or write immediately depending on the compatibility mode.
+    if ($self->{_compatibility}) {
+        $self->{_table}->[$row]->[$col] = $header . $data;
+    }
+    else {
+        $self->_append($header, $data);
+    }
+
+    return $str_error;
+}
+
+
+###############################################################################
+#
+# write_blank($row, $col, $format)
+#
+# Write a blank cell to the specified row and column (zero indexed).
+# A blank cell is used to specify formatting without adding a string
+# or a number.
+#
+# A blank cell without a format serves no purpose. Therefore, we don't write
+# a BLANK record unless a format is specified. This is mainly an optimisation
+# for the write_row() and write_col() methods.
+#
+# Returns  0 : normal termination (including no format)
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#
+sub write_blank {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    # Check the number of args
+    return -1 if @_ < 2;
+
+    # Don't write a blank cell unless it has a format
+    return 0 if not defined $_[2];
+
+
+    my $record  = 0x0201;                        # Record identifier
+    my $length  = 0x0006;                        # Number of bytes to follow
+
+    my $row     = $_[0];                         # Zero indexed row
+    my $col     = $_[1];                         # Zero indexed column
+    my $xf      = _XF($self, $row, $col, $_[2]); # The cell format
+
+    # Check that row and col are valid and store max and min values
+    return -2 if $self->_check_dimensions($row, $col);
+
+    my $header    = pack("vv",  $record, $length);
+    my $data      = pack("vvv", $row, $col, $xf);
+
+    # Store the data or write immediately depending on the compatibility mode.
+    if ($self->{_compatibility}) {
+        $self->{_table}->[$row]->[$col] = $header . $data;
+    }
+    else {
+        $self->_append($header, $data);
+    }
+
+    return 0;
+}
+
+
+###############################################################################
+#
+# write_formula($row, $col, $formula, $format, $value)
+#
+# Write a formula to the specified row and column (zero indexed).
+# The textual representation of the formula is passed to the parser in
+# Formula.pm which returns a packed binary string.
+#
+# $format is optional.
+#
+# $value is an optional result of the formula that can be supplied by the user.
+#
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#
+sub write_formula {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    if (@_ < 3) { return -1 }   # Check the number of args
+
+    my $record    = 0x0006;     # Record identifier
+    my $length;                 # Bytes to follow
+
+    my $row       = $_[0];      # Zero indexed row
+    my $col       = $_[1];      # Zero indexed column
+    my $formula   = $_[2];      # The formula text string
+    my $value     = $_[4];      # The formula text string
+
+
+    my $xf        = _XF($self, $row, $col, $_[3]);  # The cell format
+    my $chn       = 0x0000;                         # Must be zero
+    my $is_string = 0;                              # Formula evaluates to str
+    my $num;                                        # Current value of formula
+    my $grbit;                                      # Option flags
+
+
+    # Excel normally stores the last calculated value of the formula in $num.
+    # Clearly we are not in a position to calculate this "a priori". Instead
+    # we set $num to zero and set the option flags in $grbit to ensure
+    # automatic calculation of the formula when the file is opened.
+    # As a workaround for some non-Excel apps we also allow the user to
+    # specify the result of the formula.
+    #
+    ($num, $grbit, $is_string) = $self->_encode_formula_result($value);
+
+
+    # Check that row and col are valid and store max and min values
+    return -2 if $self->_check_dimensions($row, $col);
+
+    # Strip the = sign at the beginning of the formula string
+    $formula    =~ s(^=)();
+
+    my $tmp     = $formula;
+
+    # 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 { $formula = $parser->parse_formula($formula) };
+
+    if ($@) {
+        $@ =~ s/\n$//;  # Strip the \n used in the Formula.pm die()
+        croak $@;       # Re-raise the error
+    }
+
+
+    my $formlen = length($formula); # Length of the binary string
+       $length  = 0x16 + $formlen;  # Length of the record data
+
+    my $header  = pack("vv",    $record, $length);
+    my $data    = pack("vvv",   $row, $col, $xf);
+       $data   .= $num;
+       $data   .= pack("vVv",   $grbit, $chn, $formlen);
+
+    # The STRING record if the formula evaluates to a string.
+    my $string  = '';
+       $string  = $self->_get_formula_string($value) if $is_string;
+
+
+    # Store the data or write immediately depending on the compatibility mode.
+    if ($self->{_compatibility}) {
+        $self->{_table}->[$row]->[$col] = $header . $data . $formula . $string;
+    }
+    else {
+        $self->_append($header, $data, $formula, $string);
+    }
+
+    return 0;
+}
+
+
+###############################################################################
+#
+# _encode_formula_result()
+#
+# Encode the user supplied result for a formula.
+#
+sub _encode_formula_result {
+
+    my $self = shift;
+
+    my $value     = $_[0];      # Result to be encoded.
+    my $is_string = 0;          # Formula evaluates to str.
+    my $num;                    # Current value of formula.
+    my $grbit;                  # Option flags.
+
+    if (not defined $value) {
+        $grbit  = 0x03;
+        $num    = pack "d", 0;
+    }
+    else {
+        # The user specified the result of the formula. We turn off the recalc
+        # flag and check the result type.
+        $grbit  = 0x00;
+
+        if ($value =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/) {
+            # Value is a number.
+            $num = pack "d", $value;
+        }
+        else {
+
+            my %bools = (
+                            'TRUE'    => [1,  1],
+                            'FALSE'   => [1,  0],
+                            '#NULL!'  => [2,  0],
+                            '#DIV/0!' => [2,  7],
+                            '#VALUE!' => [2, 15],
+                            '#REF!'   => [2, 23],
+                            '#NAME?'  => [2, 29],
+                            '#NUM!'   => [2, 36],
+                            '#N/A'    => [2, 42],
+                        );
+
+            if (exists $bools{$value}) {
+                # Value is a boolean.
+                $num = pack "vvvv", $bools{$value}->[0],
+                                    $bools{$value}->[1],
+                                    0,
+                                    0xFFFF;
+            }
+            else {
+                # Value is a string.
+                $num = pack "vvvv", 0,
+                                    0,
+                                    0,
+                                    0xFFFF;
+                $is_string = 1;
+            }
+        }
+    }
+
+    return ($num, $grbit, $is_string);
+}
+
+
+###############################################################################
+#
+# _get_formula_string()
+#
+# Pack the string value when a formula evaluates to a string. The value cannot
+# be calculated by the module and thus must be supplied by the user.
+#
+sub _get_formula_string {
+
+    my $self = shift;
+
+    my $record    = 0x0207;         # Record identifier
+    my $length    = 0x00;           # Bytes to follow
+    my $string    = $_[0];          # Formula string.
+    my $strlen    = length $_[0];   # Length of the formula string (chars).
+    my $encoding  = 0;              # String encoding.
+
+
+    # Handle utf8 strings in perl 5.8.
+    if ($] >= 5.008) {
+        require Encode;
+
+        if (Encode::is_utf8($string)) {
+            $string = Encode::encode("UTF-16BE", $string);
+            $encoding = 1;
+        }
+    }
+
+
+    $length       = 0x03 + length $string;  # Length of the record data
+
+    my $header    = pack("vv", $record, $length);
+    my $data      = pack("vC", $strlen, $encoding);
+
+    return $header . $data . $string;
+}
+
+
+###############################################################################
+#
+# store_formula($formula)
+#
+# Pre-parse a formula. This is used in conjunction with repeat_formula()
+# to repetitively rewrite a formula without re-parsing it.
+#
+sub store_formula {
+
+    my $self    = shift;
+    my $formula = $_[0];      # The formula text string
+
+    # 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.
+    #
+    my @tokens;
+    eval { @tokens = $parser->parse_formula($formula) };
+
+    if ($@) {
+        $@ =~ s/\n$//;  # Strip the \n used in the Formula.pm die()
+        croak $@;       # Re-raise the error
+    }
+
+
+    # Return the parsed tokens in an anonymous array
+    return [@tokens];
+}
+
+
+###############################################################################
+#
+# repeat_formula($row, $col, $formula, $format, ($pattern => $replacement,...))
+#
+# Write a formula to the specified row and column (zero indexed) by
+# substituting $pattern $replacement pairs in the $formula created via
+# store_formula(). This allows the user to repetitively rewrite a formula
+# without the significant overhead of parsing.
+#
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#
+sub repeat_formula {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    if (@_ < 2) { return -1 }   # Check the number of args
+
+    my $record      = 0x0006;   # Record identifier
+    my $length;                 # Bytes to follow
+
+    my $row         = shift;    # Zero indexed row
+    my $col         = shift;    # Zero indexed column
+    my $formula_ref = shift;    # Array ref with formula tokens
+    my $format      = shift;    # XF format
+    my @pairs       = @_;       # Pattern/replacement pairs
+
+
+    # Enforce an even number of arguments in the pattern/replacement list
+    croak "Odd number of elements in pattern/replacement list" if @pairs %2;
+
+    # Check that $formula is an array ref
+    croak "Not a valid formula" if ref $formula_ref ne 'ARRAY';
+
+    my @tokens  = @$formula_ref;
+
+    # Ensure that there are tokens to substitute
+    croak "No tokens in formula" unless @tokens;
+
+
+    # As a temporary and undocumented measure we allow the user to specify the
+    # result of the formula by appending a result => $value pair to the end
+    # of the arguments.
+    my $value = undef;
+    if ($pairs[-2] eq 'result') {
+        $value = pop @pairs;
+                 pop @pairs;
+    }
+
+
+    while (@pairs) {
+        my $pattern = shift @pairs;
+        my $replace = shift @pairs;
+
+        foreach my $token (@tokens) {
+            last if $token =~ s/$pattern/$replace/;
+        }
+    }
+
+
+    # Change the parameters in the formula cached by the Formula.pm object
+    my $parser    = $self->{_parser};
+    my $formula   = $parser->parse_tokens(@tokens);
+
+    croak "Unrecognised token in formula" unless defined $formula;
+
+
+    my $xf        = _XF($self, $row, $col, $format); # The cell format
+    my $chn       = 0x0000;                          # Must be zero
+    my $is_string = 0;                               # Formula evaluates to str
+    my $num;                                         # Current value of formula
+    my $grbit;                                       # Option flags
+
+    # Excel normally stores the last calculated value of the formula in $num.
+    # Clearly we are not in a position to calculate this "a priori". Instead
+    # we set $num to zero and set the option flags in $grbit to ensure
+    # automatic calculation of the formula when the file is opened.
+    # As a workaround for some non-Excel apps we also allow the user to
+    # specify the result of the formula.
+    #
+    ($num, $grbit, $is_string) = $self->_encode_formula_result($value);
+
+    # Check that row and col are valid and store max and min values
+    return -2 if $self->_check_dimensions($row, $col);
+
+
+    my $formlen   = length($formula); # Length of the binary string
+    $length       = 0x16 + $formlen;  # Length of the record data
+
+    my $header    = pack("vv",    $record, $length);
+    my $data      = pack("vvv",   $row, $col, $xf);
+       $data     .= $num;
+       $data     .= pack("vVv",   $grbit, $chn, $formlen);
+
+
+    # The STRING record if the formula evaluates to a string.
+    my $string  = '';
+       $string  = $self->_get_formula_string($value) if $is_string;
+
+
+    # Store the data or write immediately depending on the compatibility mode.
+    if ($self->{_compatibility}) {
+        $self->{_table}->[$row]->[$col] = $header . $data . $formula . $string;
+    }
+    else {
+        $self->_append($header, $data, $formula, $string);
+    }
+
+    return 0;
+}
+
+
+###############################################################################
+#
+# write_url($row, $col, $url, $string, $format)
+#
+# Write a hyperlink. This is comprised of two elements: the visible label and
+# the invisible link. The visible label is the same as the link unless an
+# alternative string is specified.
+#
+# The parameters $string and $format are optional and their order is
+# interchangeable for backward compatibility reasons.
+#
+# The hyperlink can be to a http, ftp, mail, internal sheet, or external
+# directory url.
+#
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#         -3 : long string truncated to 255 chars
+#
+sub write_url {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    # Check the number of args
+    return -1 if @_ < 3;
+
+    # Add start row and col to arg list
+    return $self->write_url_range($_[0], $_[1], @_);
+}
+
+
+###############################################################################
+#
+# write_url_range($row1, $col1, $row2, $col2, $url, $string, $format)
+#
+# This is the more general form of write_url(). It allows a hyperlink to be
+# written to a range of cells. This function also decides the type of hyperlink
+# to be written. These are either, Web (http, ftp, mailto), Internal
+# (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1').
+#
+# See also write_url() above for a general description and return values.
+#
+sub write_url_range {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    # Check the number of args
+    return -1 if @_ < 5;
+
+
+    # Reverse the order of $string and $format if necessary. We work on a copy
+    # in order to protect the callers args. We don't use "local @_" in case of
+    # perl50005 threads.
+    #
+    my @args = @_;
+
+    ($args[5], $args[6]) = ($args[6], $args[5]) if ref $args[5];
+
+    my $url = $args[4];
+
+
+    # Check for internal/external sheet links or default to web link
+    return $self->_write_url_internal(@args) if $url =~ m[^internal:];
+    return $self->_write_url_external(@args) if $url =~ m[^external:];
+    return $self->_write_url_web(@args);
+}
+
+
+###############################################################################
+#
+# _write_url_web($row1, $col1, $row2, $col2, $url, $string, $format)
+#
+# Used to write http, ftp and mailto hyperlinks.
+# The link type ($options) is 0x03 is the same as absolute dir ref without
+# sheet. However it is differentiated by the $unknown2 data stream.
+#
+# See also write_url() above for a general description and return values.
+#
+sub _write_url_web {
+
+    my $self    = shift;
+
+    my $record      = 0x01B8;                       # Record identifier
+    my $length      = 0x00000;                      # Bytes to follow
+
+    my $row1        = $_[0];                        # Start row
+    my $col1        = $_[1];                        # Start column
+    my $row2        = $_[2];                        # End row
+    my $col2        = $_[3];                        # End column
+    my $url         = $_[4];                        # URL string
+    my $str         = $_[5];                        # Alternative label
+    my $xf          = $_[6] || $self->{_url_format};# The cell format
+
+
+    # Write the visible label but protect against url recursion in write().
+    $str                  = $url unless defined $str;
+    $self->{_writing_url} = 1;
+    my $error             = $self->write($row1, $col1, $str, $xf);
+    $self->{_writing_url} = 0;
+    return $error         if $error == -2;
+
+
+    # Pack the undocumented parts of the hyperlink stream
+    my $unknown1    = pack("H*", "D0C9EA79F9BACE118C8200AA004BA90B02000000");
+    my $unknown2    = pack("H*", "E0C9EA79F9BACE118C8200AA004BA90B");
+
+
+    # Pack the option flags
+    my $options     = pack("V", 0x03);
+
+
+    # URL encoding.
+    my $encoding    = 0;
+
+    # Convert an Utf8 URL type and to a null terminated wchar string.
+    if ($] >= 5.008) {
+        require Encode;
+
+        if (Encode::is_utf8($url)) {
+            $url      = Encode::encode("UTF-16LE", $url);
+            $url     .= "\0\0"; # URL is null terminated.
+            $encoding = 1;
+        }
+    }
+
+    # Convert an Ascii URL type and to a null terminated wchar string.
+    if ($encoding == 0) {
+        $url       .= "\0";
+        $url        = pack 'v*', unpack 'c*', $url;
+    }
+
+
+    # Pack the length of the URL
+    my $url_len     = pack("V", length($url));
+
+
+    # Calculate the data length
+    $length         = 0x34 + length($url);
+
+
+    # Pack the header data
+    my $header      = pack("vv",   $record, $length);
+    my $data        = pack("vvvv", $row1, $row2, $col1, $col2);
+
+
+    # Write the packed data
+    $self->_append( $header,
+                    $data,
+                    $unknown1,
+                    $options,
+                    $unknown2,
+                    $url_len,
+                    $url);
+
+    return $error;
+}
+
+
+###############################################################################
+#
+# _write_url_internal($row1, $col1, $row2, $col2, $url, $string, $format)
+#
+# Used to write internal reference hyperlinks such as "Sheet1!A1".
+#
+# See also write_url() above for a general description and return values.
+#
+sub _write_url_internal {
+
+    my $self    = shift;
+
+    my $record      = 0x01B8;                       # Record identifier
+    my $length      = 0x00000;                      # Bytes to follow
+
+    my $row1        = $_[0];                        # Start row
+    my $col1        = $_[1];                        # Start column
+    my $row2        = $_[2];                        # End row
+    my $col2        = $_[3];                        # End column
+    my $url         = $_[4];                        # URL string
+    my $str         = $_[5];                        # Alternative label
+    my $xf          = $_[6] || $self->{_url_format};# The cell format
+
+    # Strip URL type
+    $url            =~ s[^internal:][];
+
+
+    # Write the visible label but protect against url recursion in write().
+    $str                  = $url unless defined $str;
+    $self->{_writing_url} = 1;
+    my $error             = $self->write($row1, $col1, $str, $xf);
+    $self->{_writing_url} = 0;
+    return $error         if $error == -2;
+
+
+    # Pack the undocumented parts of the hyperlink stream
+    my $unknown1    = pack("H*", "D0C9EA79F9BACE118C8200AA004BA90B02000000");
+
+
+    # Pack the option flags
+    my $options     = pack("V", 0x08);
+
+
+    # URL encoding.
+    my $encoding    = 0;
+
+
+    # Convert an Utf8 URL type and to a null terminated wchar string.
+    if ($] >= 5.008) {
+        require Encode;
+
+        if (Encode::is_utf8($url)) {
+            # Quote sheet name if not already, i.e., Sheet!A1 to 'Sheet!A1'.
+            $url      =~ s/^(.+)!/'$1'!/ if not $url =~ /^'/;
+
+            $url      = Encode::encode("UTF-16LE", $url);
+            $url     .= "\0\0"; # URL is null terminated.
+            $encoding = 1;
+        }
+    }
+
+
+    # Convert an Ascii URL type and to a null terminated wchar string.
+    if ($encoding == 0) {
+        $url       .= "\0";
+        $url        = pack 'v*', unpack 'c*', $url;
+    }
+
+
+    # Pack the length of the URL as chars (not wchars)
+    my $url_len     = pack("V", int(length($url)/2));
+
+
+    # Calculate the data length
+    $length         = 0x24 + length($url);
+
+
+    # Pack the header data
+    my $header      = pack("vv",   $record, $length);
+    my $data        = pack("vvvv", $row1, $row2, $col1, $col2);
+
+
+    # Write the packed data
+    $self->_append( $header,
+                    $data,
+                    $unknown1,
+                    $options,
+                    $url_len,
+                    $url);
+
+    return $error;
+}
+
+
+###############################################################################
+#
+# _write_url_external($row1, $col1, $row2, $col2, $url, $string, $format)
+#
+# Write links to external directory names such as 'c:\foo.xls',
+# c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'.
+#
+# Note: Excel writes some relative links with the $dir_long string. We ignore
+# these cases for the sake of simpler code.
+#
+# See also write_url() above for a general description and return values.
+#
+sub _write_url_external {
+
+    my $self    = shift;
+
+    # Network drives are different. We will handle them separately
+    # MS/Novell network drives and shares start with \\
+    return $self->_write_url_external_net(@_) if $_[4] =~ m[^external:\\\\];
+
+
+    my $record      = 0x01B8;                       # Record identifier
+    my $length      = 0x00000;                      # Bytes to follow
+
+    my $row1        = $_[0];                        # Start row
+    my $col1        = $_[1];                        # Start column
+    my $row2        = $_[2];                        # End row
+    my $col2        = $_[3];                        # End column
+    my $url         = $_[4];                        # URL string
+    my $str         = $_[5];                        # Alternative label
+    my $xf          = $_[6] || $self->{_url_format};# The cell format
+
+
+    # Strip URL type and change Unix dir separator to Dos style (if needed)
+    #
+    $url            =~ s[^external:][];
+    $url            =~ s[/][\\]g;
+
+
+    # Write the visible label but protect against url recursion in write().
+    ($str = $url)         =~ s[\#][ - ] unless defined $str;
+    $self->{_writing_url} = 1;
+    my $error             = $self->write($row1, $col1, $str, $xf);
+    $self->{_writing_url} = 0;
+    return $error         if $error == -2;
+
+
+    # Determine if the link is relative or absolute:
+    # Absolute if link starts with DOS drive specifier like C:
+    # Otherwise default to 0x00 for relative link.
+    #
+    my $absolute    = 0x00;
+       $absolute    = 0x02  if $url =~ m/^[A-Za-z]:/;
+
+
+    # Determine if the link contains a sheet reference and change some of the
+    # parameters accordingly.
+    # Split the dir name and sheet name (if it exists)
+    #
+    my ($dir_long , $sheet) = split /\#/, $url;
+    my $link_type           = 0x01 | $absolute;
+    my $sheet_len;
+
+    if (defined $sheet) {
+        $link_type |= 0x08;
+        $sheet_len  = pack("V", length($sheet) + 0x01);
+        $sheet      = join("\0", split('', $sheet));
+        $sheet     .= "\0\0\0";
+    }
+    else {
+        $sheet_len  = '';
+        $sheet      = '';
+    }
+
+
+    # Pack the link type
+    $link_type      = pack("V", $link_type);
+
+
+    # Calculate the up-level dir count e.g. (..\..\..\ == 3)
+    my $up_count    = 0;
+    $up_count++       while $dir_long =~ s[^\.\.\\][];
+    $up_count       = pack("v", $up_count);
+
+
+    # Store the short dos dir name (null terminated)
+    my $dir_short   = $dir_long . "\0";
+
+
+    # Store the long dir name as a wchar string (non-null terminated)
+    $dir_long       = join("\0", split('', $dir_long));
+    $dir_long       = $dir_long . "\0";
+
+
+    # Pack the lengths of the dir strings
+    my $dir_short_len = pack("V", length $dir_short      );
+    my $dir_long_len  = pack("V", length $dir_long       );
+    my $stream_len    = pack("V", length($dir_long) + 0x06);
+
+
+    # Pack the undocumented parts of the hyperlink stream
+    my $unknown1 =pack("H*",'D0C9EA79F9BACE118C8200AA004BA90B02000000'       );
+    my $unknown2 =pack("H*",'0303000000000000C000000000000046'               );
+    my $unknown3 =pack("H*",'FFFFADDE000000000000000000000000000000000000000');
+    my $unknown4 =pack("v",  0x03                                            );
+
+
+    # Pack the main data stream
+    my $data        = pack("vvvv", $row1, $row2, $col1, $col2) .
+                      $unknown1     .
+                      $link_type    .
+                      $unknown2     .
+                      $up_count     .
+                      $dir_short_len.
+                      $dir_short    .
+                      $unknown3     .
+                      $stream_len   .
+                      $dir_long_len .
+                      $unknown4     .
+                      $dir_long     .
+                      $sheet_len    .
+                      $sheet        ;
+
+
+    # Pack the header data
+    $length         = length $data;
+    my $header      = pack("vv",   $record, $length);
+
+
+    # Write the packed data
+    $self->_append($header, $data);
+
+    return $error;
+}
+
+
+
+
+###############################################################################
+#
+# _write_url_external_net($row1, $col1, $row2, $col2, $url, $string, $format)
+#
+# Write links to external MS/Novell network drives and shares such as
+# '//NETWORK/share/foo.xls' and '//NETWORK/share/foo.xls#Sheet1!A1'.
+#
+# See also write_url() above for a general description and return values.
+#
+sub _write_url_external_net {
+
+    my $self    = shift;
+
+    my $record      = 0x01B8;                       # Record identifier
+    my $length      = 0x00000;                      # Bytes to follow
+
+    my $row1        = $_[0];                        # Start row
+    my $col1        = $_[1];                        # Start column
+    my $row2        = $_[2];                        # End row
+    my $col2        = $_[3];                        # End column
+    my $url         = $_[4];                        # URL string
+    my $str         = $_[5];                        # Alternative label
+    my $xf          = $_[6] || $self->{_url_format};# The cell format
+
+
+    # Strip URL type and change Unix dir separator to Dos style (if needed)
+    #
+    $url            =~ s[^external:][];
+    $url            =~ s[/][\\]g;
+
+
+    # Write the visible label but protect against url recursion in write().
+    ($str = $url)         =~ s[\#][ - ] unless defined $str;
+    $self->{_writing_url} = 1;
+    my $error             = $self->write($row1, $col1, $str, $xf);
+    $self->{_writing_url} = 0;
+    return $error         if $error == -2;
+
+
+    # Determine if the link contains a sheet reference and change some of the
+    # parameters accordingly.
+    # Split the dir name and sheet name (if it exists)
+    #
+    my ($dir_long , $sheet) = split /\#/, $url;
+    my $link_type           = 0x0103; # Always absolute
+    my $sheet_len;
+
+    if (defined $sheet) {
+        $link_type |= 0x08;
+        $sheet_len  = pack("V", length($sheet) + 0x01);
+        $sheet      = join("\0", split('', $sheet));
+        $sheet     .= "\0\0\0";
+    }
+    else {
+        $sheet_len   = '';
+        $sheet       = '';
+    }
+
+    # Pack the link type
+    $link_type      = pack("V", $link_type);
+
+
+    # Make the string null terminated
+    $dir_long       = $dir_long . "\0";
+
+
+    # Pack the lengths of the dir string
+    my $dir_long_len  = pack("V", length $dir_long);
+
+
+    # Store the long dir name as a wchar string (non-null terminated)
+    $dir_long       = join("\0", split('', $dir_long));
+    $dir_long       = $dir_long . "\0";
+
+
+    # Pack the undocumented part of the hyperlink stream
+    my $unknown1    = pack("H*",'D0C9EA79F9BACE118C8200AA004BA90B02000000');
+
+
+    # Pack the main data stream
+    my $data        = pack("vvvv", $row1, $row2, $col1, $col2) .
+                      $unknown1     .
+                      $link_type    .
+                      $dir_long_len .
+                      $dir_long     .
+                      $sheet_len    .
+                      $sheet        ;
+
+
+    # Pack the header data
+    $length         = length $data;
+    my $header      = pack("vv",   $record, $length);
+
+
+    # Write the packed data
+    $self->_append($header, $data);
+
+    return $error;
+}
+
+
+###############################################################################
+#
+# write_date_time ($row, $col, $string, $format)
+#
+# Write a datetime string in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format as a
+# number representing an Excel date. $format is optional.
+#
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#         -3 : Invalid date_time, written as string
+#
+sub write_date_time {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    if (@_ < 3) { return -1 }                        # Check the number of args
+
+    my $row       = $_[0];                           # Zero indexed row
+    my $col       = $_[1];                           # Zero indexed column
+    my $str       = $_[2];
+
+
+    # Check that row and col are valid and store max and min values
+    return -2 if $self->_check_dimensions($row, $col);
+
+    my $error     = 0;
+    my $date_time = $self->convert_date_time($str);
+
+    if (defined $date_time) {
+        $error = $self->write_number($row, $col, $date_time, $_[3]);
+    }
+    else {
+        # The date isn't valid so write it as a string.
+        $self->write_string($row, $col, $str, $_[3]);
+        $error = -3;
+    }
+    return $error;
+}
+
+
+
+###############################################################################
+#
+# convert_date_time($date_time_string)
+#
+# The function takes a date and time in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format
+# and converts it to a decimal number representing a valid Excel date.
+#
+# Dates and times in Excel are represented by real numbers. The integer part of
+# the number stores the number of days since the epoch and the fractional part
+# stores the percentage of the day in seconds. The epoch can be either 1900 or
+# 1904.
+#
+# Parameter: Date and time string in one of the following formats:
+#               yyyy-mm-ddThh:mm:ss.ss  # Standard
+#               yyyy-mm-ddT             # Date only
+#                         Thh:mm:ss.ss  # Time only
+#
+# Returns:
+#            A decimal number representing a valid Excel date, or
+#            undef if the date is invalid.
+#
+sub convert_date_time {
+
+    my $self      = shift;
+    my $date_time = $_[0];
+
+    my $days      = 0; # Number of days since epoch
+    my $seconds   = 0; # Time expressed as fraction of 24h hours in seconds
+
+    my ($year, $month, $day);
+    my ($hour, $min, $sec);
+
+
+    # Strip leading and trailing whitespace.
+    $date_time =~ s/^\s+//;
+    $date_time =~ s/\s+$//;
+
+    # Check for invalid date char.
+    return if     $date_time =~ /[^0-9T:\-\.Z]/;
+
+    # Check for "T" after date or before time.
+    return unless $date_time =~ /\dT|T\d/;
+
+    # Strip trailing Z in ISO8601 date.
+    $date_time =~ s/Z$//;
+
+
+    # Split into date and time.
+    my ($date, $time) = split /T/, $date_time;
+
+
+    # We allow the time portion of the input DateTime to be optional.
+    if ($time ne '') {
+        # Match hh:mm:ss.sss+ where the seconds are optional
+        if ($time =~ /^(\d\d):(\d\d)(:(\d\d(\.\d+)?))?/) {
+            $hour   = $1;
+            $min    = $2;
+            $sec    = $4 || 0;
+        }
+        else {
+            return undef; # Not a valid time format.
+        }
+
+        # Some boundary checks
+        return if $hour >= 24;
+        return if $min  >= 60;
+        return if $sec  >= 60;
+
+        # Excel expresses seconds as a fraction of the number in 24 hours.
+        $seconds = ($hour *60*60 + $min *60 + $sec) / (24 *60 *60);
+    }
+
+
+    # We allow the date portion of the input DateTime to be optional.
+    return $seconds if $date eq '';
+
+
+    # Match date as yyyy-mm-dd.
+    if ($date =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/) {
+        $year   = $1;
+        $month  = $2;
+        $day    = $3;
+    }
+    else {
+        return undef; # Not a valid date format.
+    }
+
+    # Set the epoch as 1900 or 1904. Defaults to 1900.
+    my $date_1904 = $self->{_1904};
+
+
+    # Special cases for Excel.
+    if (not $date_1904) {
+        return      $seconds if $date eq '1899-12-31'; # Excel 1900 epoch
+        return      $seconds if $date eq '1900-01-00'; # Excel 1900 epoch
+        return 60 + $seconds if $date eq '1900-02-29'; # Excel false leapday
+    }
+
+
+    # We calculate the date by calculating the number of days since the epoch
+    # and adjust for the number of leap days. We calculate the number of leap
+    # days by normalising the year in relation to the epoch. Thus the year 2000
+    # becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
+    #
+    my $epoch   = $date_1904 ? 1904 : 1900;
+    my $offset  = $date_1904 ?    4 :    0;
+    my $norm    = 300;
+    my $range   = $year -$epoch;
+
+
+    # Set month days and check for leap year.
+    my @mdays   = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
+    my $leap    = 0;
+       $leap    = 1  if $year % 4 == 0 and $year % 100 or $year % 400 == 0;
+    $mdays[1]   = 29 if $leap;
+
+
+    # Some boundary checks
+    return if $year  < $epoch or $year  > 9999;
+    return if $month < 1      or $month > 12;
+    return if $day   < 1      or $day   > $mdays[$month -1];
+
+    # Accumulate the number of days since the epoch.
+    $days  = $day;                              # Add days for current month
+    $days += $mdays[$_] for 0 .. $month -2;     # Add days for past months
+    $days += $range *365;                       # Add days for past years
+    $days += int(($range)                /  4); # Add leapdays
+    $days -= int(($range +$offset)       /100); # Subtract 100 year leapdays
+    $days += int(($range +$offset +$norm)/400); # Add 400 year leapdays
+    $days -= $leap;                             # Already counted above
+
+
+    # Adjust for Excel erroneously treating 1900 as a leap year.
+    $days++ if $date_1904 == 0 and $days > 59;
+
+    return $days + $seconds;
+}
+
+
+
+
+
+###############################################################################
+#
+# set_row($row, $height, $XF, $hidden, $level)
+#
+# This method is used to set the height and XF format for a row.
+# Writes the  BIFF record ROW.
+#
+sub set_row {
+
+    my $self        = shift;
+    my $record      = 0x0208;               # Record identifier
+    my $length      = 0x0010;               # Number of bytes to follow
+
+    my $row         = $_[0];                # Row Number
+    my $colMic      = 0x0000;               # First defined column
+    my $colMac      = 0x0000;               # Last defined column
+    my $miyRw;                              # Row height
+    my $irwMac      = 0x0000;               # Used by Excel to optimise loading
+    my $reserved    = 0x0000;               # Reserved
+    my $grbit       = 0x0000;               # Option flags
+    my $ixfe;                               # XF index
+    my $height      = $_[1];                # Format object
+    my $format      = $_[2];                # Format object
+    my $hidden      = $_[3] || 0;           # Hidden flag
+    my $level       = $_[4] || 0;           # Outline level
+    my $collapsed   = $_[5] || 0;           # Collapsed row
+
+
+    return unless defined $row;  # Ensure at least $row is specified.
+
+    # Check that row and col are valid and store max and min values
+    return -2 if $self->_check_dimensions($row, 0, 0, 1);
+
+    # Check for a format object
+    if (ref $format) {
+        $ixfe = $format->get_xf_index();
+    }
+    else {
+        $ixfe = 0x0F;
+    }
+
+
+    # Set the row height in units of 1/20 of a point. Note, some heights may
+    # not be obtained exactly due to rounding in Excel.
+    #
+    if (defined $height) {
+        $miyRw = $height *20;
+    }
+    else {
+        $miyRw = 0xff; # The default row height
+        $height = 0;
+    }
+
+
+    # Set the limits for the outline levels (0 <= x <= 7).
+    $level = 0 if $level < 0;
+    $level = 7 if $level > 7;
+
+    $self->{_outline_row_level} = $level if $level >$self->{_outline_row_level};
+
+
+    # Set the options flags.
+    # 0x10: The fCollapsed flag indicates that the row contains the "+"
+    #       when an outline group is collapsed.
+    # 0x20: The fDyZero height flag indicates a collapsed or hidden row.
+    # 0x40: The fUnsynced flag is used to show that the font and row heights
+    #       are not compatible. This is usually the case for WriteExcel.
+    # 0x80: The fGhostDirty flag indicates that the row has been formatted.
+    #
+    $grbit |= $level;
+    $grbit |= 0x0010 if $collapsed;
+    $grbit |= 0x0020 if $hidden;
+    $grbit |= 0x0040;
+    $grbit |= 0x0080 if $format;
+    $grbit |= 0x0100;
+
+
+    my $header   = pack("vv",       $record, $length);
+    my $data     = pack("vvvvvvvv", $row, $colMic, $colMac, $miyRw,
+                                    $irwMac,$reserved, $grbit, $ixfe);
+
+
+    # Store the data or write immediately depending on the compatibility mode.
+    if ($self->{_compatibility}) {
+        $self->{_row_data}->{$_[0]} = $header . $data;
+    }
+    else {
+        $self->_append($header, $data);
+    }
+
+
+    # Store the row sizes for use when calculating image vertices.
+    # Also store the column formats.
+    $self->{_row_sizes}->{$_[0]}   = $height;
+    $self->{_row_formats}->{$_[0]} = $format if defined $format;
+}
+
+
+
+###############################################################################
+#
+# _write_row_default()
+#
+# Write a default row record, in compatibility mode, for rows that don't have
+# user specified values..
+#
+sub _write_row_default {
+
+    my $self        = shift;
+    my $record      = 0x0208;               # Record identifier
+    my $length      = 0x0010;               # Number of bytes to follow
+
+    my $row         = $_[0];                # Row Number
+    my $colMic      = $_[1];                # First defined column
+    my $colMac      = $_[2];                # Last defined column
+    my $miyRw       = 0xFF;                 # Row height
+    my $irwMac      = 0x0000;               # Used by Excel to optimise loading
+    my $reserved    = 0x0000;               # Reserved
+    my $grbit       = 0x0100;               # Option flags
+    my $ixfe        = 0x0F;                 # XF index
+
+    my $header   = pack("vv",       $record, $length);
+    my $data     = pack("vvvvvvvv", $row, $colMic, $colMac, $miyRw,
+                                    $irwMac,$reserved, $grbit, $ixfe);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _check_dimensions($row, $col, $ignore_row, $ignore_col)
+#
+# Check that $row and $col are valid and store max and min values for use in
+# DIMENSIONS record. See, _store_dimensions().
+#
+# The $ignore_row/$ignore_col flags is used to indicate that we wish to
+# perform the dimension check without storing the value.
+#
+# The ignore flags are use by set_row() and data_validate.
+#
+sub _check_dimensions {
+
+    my $self        = shift;
+    my $row         = $_[0];
+    my $col         = $_[1];
+    my $ignore_row  = $_[2];
+    my $ignore_col  = $_[3];
+
+
+    return -2 if not defined $row;
+    return -2 if $row >= $self->{_xls_rowmax};
+
+    return -2 if not defined $col;
+    return -2 if $col >= $self->{_xls_colmax};
+
+
+    if (not $ignore_row) {
+
+        if (not defined $self->{_dim_rowmin} or $row < $self->{_dim_rowmin}) {
+            $self->{_dim_rowmin} = $row;
+        }
+
+        if (not defined $self->{_dim_rowmax} or $row > $self->{_dim_rowmax}) {
+            $self->{_dim_rowmax} = $row;
+        }
+    }
+
+    if (not $ignore_col) {
+
+        if (not defined $self->{_dim_colmin} or $col < $self->{_dim_colmin}) {
+            $self->{_dim_colmin} = $col;
+        }
+
+        if (not defined $self->{_dim_colmax} or $col > $self->{_dim_colmax}) {
+            $self->{_dim_colmax} = $col;
+        }
+    }
+
+    return 0;
+}
+
+
+###############################################################################
+#
+# _store_dimensions()
+#
+# Writes Excel DIMENSIONS to define the area in which there is cell data.
+#
+# Notes:
+#   Excel stores the max row/col as row/col +1.
+#   Max and min values of 0 are used to indicate that no cell data.
+#   We set the undef member data to 0 since it is used by _store_table().
+#   Inserting images or charts doesn't change the DIMENSION data.
+#
+sub _store_dimensions {
+
+    my $self      = shift;
+    my $record    = 0x0200;         # Record identifier
+    my $length    = 0x000E;         # Number of bytes to follow
+    my $row_min;                    # First row
+    my $row_max;                    # Last row plus 1
+    my $col_min;                    # First column
+    my $col_max;                    # Last column plus 1
+    my $reserved  = 0x0000;         # Reserved by Excel
+
+    if (defined $self->{_dim_rowmin}) {$row_min = $self->{_dim_rowmin}    }
+    else                              {$row_min = 0                       }
+
+    if (defined $self->{_dim_rowmax}) {$row_max = $self->{_dim_rowmax} + 1}
+    else                              {$row_max = 0                       }
+
+    if (defined $self->{_dim_colmin}) {$col_min = $self->{_dim_colmin}    }
+    else                              {$col_min = 0                       }
+
+    if (defined $self->{_dim_colmax}) {$col_max = $self->{_dim_colmax} + 1}
+    else                              {$col_max = 0                       }
+
+
+    # Set member data to the new max/min value for use by _store_table().
+    $self->{_dim_rowmin} = $row_min;
+    $self->{_dim_rowmax} = $row_max;
+    $self->{_dim_colmin} = $col_min;
+    $self->{_dim_colmax} = $col_max;
+
+
+    my $header    = pack("vv",    $record, $length);
+    my $data      = pack("VVvvv", $row_min, $row_max,
+                                  $col_min, $col_max, $reserved);
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_window2()
+#
+# Write BIFF record Window2.
+#
+sub _store_window2 {
+
+    use integer;    # Avoid << shift bug in Perl 5.6.0 on HP-UX
+
+    my $self           = shift;
+    my $record         = 0x023E;     # Record identifier
+    my $length         = 0x0012;     # Number of bytes to follow
+
+    my $grbit          = 0x00B6;     # Option flags
+    my $rwTop          = $self->{_first_row};   # Top visible row
+    my $colLeft        = $self->{_first_col};   # Leftmost visible column
+    my $rgbHdr         = 0x00000040;            # Row/col heading, grid color
+
+    my $wScaleSLV      = 0x0000;                # Zoom in page break preview
+    my $wScaleNormal   = 0x0000;                # Zoom in normal view
+    my $reserved       = 0x00000000;
+
+
+    # The options flags that comprise $grbit
+    my $fDspFmla       = $self->{_display_formulas}; # 0 - bit
+    my $fDspGrid       = $self->{_screen_gridlines}; # 1
+    my $fDspRwCol      = $self->{_display_headers};  # 2
+    my $fFrozen        = $self->{_frozen};           # 3
+    my $fDspZeros      = $self->{_display_zeros};    # 4
+    my $fDefaultHdr    = 1;                          # 5
+    my $fArabic        = $self->{_display_arabic};   # 6
+    my $fDspGuts       = $self->{_outline_on};       # 7
+    my $fFrozenNoSplit = $self->{_frozen_no_split};  # 0 - bit
+    my $fSelected      = $self->{_selected};         # 1
+    my $fPaged         = $self->{_active};           # 2
+    my $fBreakPreview  = 0;                          # 3
+
+    $grbit             = $fDspFmla;
+    $grbit            |= $fDspGrid       << 1;
+    $grbit            |= $fDspRwCol      << 2;
+    $grbit            |= $fFrozen        << 3;
+    $grbit            |= $fDspZeros      << 4;
+    $grbit            |= $fDefaultHdr    << 5;
+    $grbit            |= $fArabic        << 6;
+    $grbit            |= $fDspGuts       << 7;
+    $grbit            |= $fFrozenNoSplit << 8;
+    $grbit            |= $fSelected      << 9;
+    $grbit            |= $fPaged         << 10;
+    $grbit            |= $fBreakPreview  << 11;
+
+    my $header  = pack("vv",      $record, $length);
+    my $data    = pack("vvvVvvV", $grbit, $rwTop, $colLeft, $rgbHdr,
+                                  $wScaleSLV, $wScaleNormal, $reserved );
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_page_view()
+#
+# Set page view mode. Only applicable to Mac Excel.
+#
+sub _store_page_view {
+
+    my $self    = shift;
+
+    return unless $self->{_page_view};
+
+    my $data    = pack "H*", 'C8081100C808000000000040000000000900000000';
+
+    $self->_append($data);
+}
+
+
+###############################################################################
+#
+# _store_tab_color()
+#
+# Write the Tab Color BIFF record.
+#
+sub _store_tab_color {
+
+    my $self    = shift;
+    my $color   = $self->{_tab_color};
+
+    return unless $color;
+
+    my $record  = 0x0862;      # Record identifier
+    my $length  = 0x0014;      # Number of bytes to follow
+
+    my $zero    = 0x0000;
+    my $unknown = 0x0014;
+
+    my $header  = pack("vv", $record, $length);
+    my $data    = pack("vvvvvvvvvv", $record, $zero, $zero, $zero, $zero,
+                                     $zero, $unknown, $zero, $color, $zero);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_defrow()
+#
+# Write BIFF record DEFROWHEIGHT.
+#
+sub _store_defrow {
+
+    my $self     = shift;
+    my $record   = 0x0225;      # Record identifier
+    my $length   = 0x0004;      # Number of bytes to follow
+
+    my $grbit    = 0x0000;      # Options.
+    my $height   = 0x00FF;      # Default row height
+
+    my $header   = pack("vv", $record, $length);
+    my $data     = pack("vv", $grbit,  $height);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_defcol()
+#
+# Write BIFF record DEFCOLWIDTH.
+#
+sub _store_defcol {
+
+    my $self     = shift;
+    my $record   = 0x0055;      # Record identifier
+    my $length   = 0x0002;      # Number of bytes to follow
+
+    my $colwidth = 0x0008;      # Default column width
+
+    my $header   = pack("vv", $record, $length);
+    my $data     = pack("v",  $colwidth);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_colinfo($firstcol, $lastcol, $width, $format, $hidden)
+#
+# Write BIFF record COLINFO to define column widths
+#
+# Note: The SDK says the record length is 0x0B but Excel writes a 0x0C
+# length record.
+#
+sub _store_colinfo {
+
+    my $self     = shift;
+    my $record   = 0x007D;          # Record identifier
+    my $length   = 0x000B;          # Number of bytes to follow
+
+    my $colFirst = $_[0] || 0;      # First formatted column
+    my $colLast  = $_[1] || 0;      # Last formatted column
+    my $width    = $_[2] || 8.43;   # Col width in user units, 8.43 is default
+    my $coldx;                      # Col width in internal units
+    my $pixels;                     # Col width in pixels
+
+    # Excel rounds the column width to the nearest pixel. Therefore we first
+    # convert to pixels and then to the internal units. The pixel to users-units
+    # relationship is different for values less than 1.
+    #
+    if ($width < 1) {
+        $pixels = int($width *12);
+    }
+    else {
+        $pixels = int($width *7 ) +5;
+    }
+
+    $coldx = int($pixels *256/7);
+
+
+    my $ixfe;                          # XF index
+    my $grbit       = 0x0000;          # Option flags
+    my $reserved    = 0x00;            # Reserved
+    my $format      = $_[3];           # Format object
+    my $hidden      = $_[4] || 0;      # Hidden flag
+    my $level       = $_[5] || 0;      # Outline level
+    my $collapsed   = $_[6] || 0;      # Outline level
+
+
+    # Check for a format object
+    if (ref $format) {
+        $ixfe = $format->get_xf_index();
+    }
+    else {
+        $ixfe = 0x0F;
+    }
+
+
+    # Set the limits for the outline levels (0 <= x <= 7).
+    $level = 0 if $level < 0;
+    $level = 7 if $level > 7;
+
+
+    # Set the options flags. (See set_row() for more details).
+    $grbit |= 0x0001 if $hidden;
+    $grbit |= $level << 8;
+    $grbit |= 0x1000 if $collapsed;
+
+
+    my $header   = pack("vv",     $record, $length);
+    my $data     = pack("vvvvvC", $colFirst, $colLast, $coldx,
+                                  $ixfe, $grbit, $reserved);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_filtermode()
+#
+# Write BIFF record FILTERMODE to indicate that the worksheet contains
+# AUTOFILTER record, ie. autofilters with a filter set.
+#
+sub _store_filtermode {
+
+    my $self        = shift;
+
+    my $record      = 0x009B;      # Record identifier
+    my $length      = 0x0000;      # Number of bytes to follow
+
+    # Only write the record if the worksheet contains a filtered autofilter.
+    return unless $self->{_filter_on};
+
+    my $header   = pack("vv", $record, $length);
+
+    $self->_prepend($header);
+}
+
+
+###############################################################################
+#
+# _store_autofilterinfo()
+#
+# Write BIFF record AUTOFILTERINFO.
+#
+sub _store_autofilterinfo {
+
+    my $self        = shift;
+
+    my $record      = 0x009D;      # Record identifier
+    my $length      = 0x0002;      # Number of bytes to follow
+    my $num_filters = $self->{_filter_count};
+
+    # Only write the record if the worksheet contains an autofilter.
+    return unless $self->{_filter_count};
+
+    my $header   = pack("vv", $record, $length);
+    my $data     = pack("v",  $num_filters);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_selection($first_row, $first_col, $last_row, $last_col)
+#
+# Write BIFF record SELECTION.
+#
+sub _store_selection {
+
+    my $self     = shift;
+    my $record   = 0x001D;                  # Record identifier
+    my $length   = 0x000F;                  # Number of bytes to follow
+
+    my $pnn      = $self->{_active_pane};   # Pane position
+    my $rwAct    = $_[0];                   # Active row
+    my $colAct   = $_[1];                   # Active column
+    my $irefAct  = 0;                       # Active cell ref
+    my $cref     = 1;                       # Number of refs
+
+    my $rwFirst  = $_[0];                   # First row in reference
+    my $colFirst = $_[1];                   # First col in reference
+    my $rwLast   = $_[2] || $rwFirst;       # Last  row in reference
+    my $colLast  = $_[3] || $colFirst;      # Last  col in reference
+
+    # Swap last row/col for first row/col as necessary
+    if ($rwFirst > $rwLast) {
+        ($rwFirst, $rwLast) = ($rwLast, $rwFirst);
+    }
+
+    if ($colFirst > $colLast) {
+        ($colFirst, $colLast) = ($colLast, $colFirst);
+    }
+
+
+    my $header   = pack("vv",           $record, $length);
+    my $data     = pack("CvvvvvvCC",    $pnn, $rwAct, $colAct,
+                                        $irefAct, $cref,
+                                        $rwFirst, $rwLast,
+                                        $colFirst, $colLast);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_externcount($count)
+#
+# Write BIFF record EXTERNCOUNT to indicate the number of external sheet
+# references in a worksheet.
+#
+# Excel only stores references to external sheets that are used in formulas.
+# For simplicity we store references to all the sheets in the workbook
+# regardless of whether they are used or not. This reduces the overall
+# complexity and eliminates the need for a two way dialogue between the formula
+# parser the worksheet objects.
+#
+sub _store_externcount {
+
+    my $self     = shift;
+    my $record   = 0x0016;          # Record identifier
+    my $length   = 0x0002;          # Number of bytes to follow
+
+    my $cxals    = $_[0];           # Number of external references
+
+    my $header   = pack("vv", $record, $length);
+    my $data     = pack("v",  $cxals);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_externsheet($sheetname)
+#
+#
+# Writes the Excel BIFF EXTERNSHEET record. These references are used by
+# formulas. A formula references a sheet name via an index. Since we store a
+# reference to all of the external worksheets the EXTERNSHEET index is the same
+# as the worksheet index.
+#
+sub _store_externsheet {
+
+    my $self      = shift;
+
+    my $record    = 0x0017;         # Record identifier
+    my $length;                     # Number of bytes to follow
+
+    my $sheetname = $_[0];          # Worksheet name
+    my $cch;                        # Length of sheet name
+    my $rgch;                       # Filename encoding
+
+    # References to the current sheet are encoded differently to references to
+    # external sheets.
+    #
+    if ($self->{_name} eq $sheetname) {
+        $sheetname = '';
+        $length    = 0x02;  # The following 2 bytes
+        $cch       = 1;     # The following byte
+        $rgch      = 0x02;  # Self reference
+    }
+    else {
+        $length    = 0x02 + length($_[0]);
+        $cch       = length($sheetname);
+        $rgch      = 0x03;  # Reference to a sheet in the current workbook
+    }
+
+    my $header     = pack("vv",  $record, $length);
+    my $data       = pack("CC", $cch, $rgch);
+
+    $self->_prepend($header, $data, $sheetname);
+}
+
+
+###############################################################################
+#
+# _store_panes()
+#
+#
+# Writes the Excel BIFF PANE record.
+# The panes can either be frozen or thawed (unfrozen).
+# Frozen panes are specified in terms of a integer number of rows and columns.
+# Thawed panes are specified in terms of Excel's units for rows and columns.
+#
+sub _store_panes {
+
+    my $self        = shift;
+    my $record      = 0x0041;       # Record identifier
+    my $length      = 0x000A;       # Number of bytes to follow
+
+    my $y           = $_[0] || 0;   # Vertical split position
+    my $x           = $_[1] || 0;   # Horizontal split position
+    my $rwTop       = $_[2];        # Top row visible
+    my $colLeft     = $_[3];        # Leftmost column visible
+    my $no_split    = $_[4];        # No used here.
+    my $pnnAct      = $_[5];        # Active pane
+
+
+    # Code specific to frozen or thawed panes.
+    if ($self->{_frozen}) {
+        # Set default values for $rwTop and $colLeft
+        $rwTop   = $y unless defined $rwTop;
+        $colLeft = $x unless defined $colLeft;
+    }
+    else {
+        # Set default values for $rwTop and $colLeft
+        $rwTop   = 0  unless defined $rwTop;
+        $colLeft = 0  unless defined $colLeft;
+
+        # Convert Excel's row and column units to the internal units.
+        # The default row height is 12.75
+        # The default column width is 8.43
+        # The following slope and intersection values were interpolated.
+        #
+        $y = 20*$y      + 255;
+        $x = 113.879*$x + 390;
+    }
+
+
+    # Determine which pane should be active. There is also the undocumented
+    # option to override this should it be necessary: may be removed later.
+    #
+    if (not defined $pnnAct) {
+        $pnnAct = 0 if ($x != 0 && $y != 0); # Bottom right
+        $pnnAct = 1 if ($x != 0 && $y == 0); # Top right
+        $pnnAct = 2 if ($x == 0 && $y != 0); # Bottom left
+        $pnnAct = 3 if ($x == 0 && $y == 0); # Top left
+    }
+
+    $self->{_active_pane} = $pnnAct; # Used in _store_selection
+
+    my $header     = pack("vv",    $record, $length);
+    my $data       = pack("vvvvv", $x, $y, $rwTop, $colLeft, $pnnAct);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_setup()
+#
+# Store the page setup SETUP BIFF record.
+#
+sub _store_setup {
+
+    use integer;    # Avoid << shift bug in Perl 5.6.0 on HP-UX
+
+    my $self         = shift;
+    my $record       = 0x00A1;                  # Record identifier
+    my $length       = 0x0022;                  # Number of bytes to follow
+
+
+    my $iPaperSize   = $self->{_paper_size};    # Paper size
+    my $iScale       = $self->{_print_scale};   # Print scaling factor
+    my $iPageStart   = $self->{_page_start};    # Starting page number
+    my $iFitWidth    = $self->{_fit_width};     # Fit to number of pages wide
+    my $iFitHeight   = $self->{_fit_height};    # Fit to number of pages high
+    my $grbit        = 0x00;                    # Option flags
+    my $iRes         = 0x0258;                  # Print resolution
+    my $iVRes        = 0x0258;                  # Vertical print resolution
+    my $numHdr       = $self->{_margin_header}; # Header Margin
+    my $numFtr       = $self->{_margin_footer}; # Footer Margin
+    my $iCopies      = 0x01;                    # Number of copies
+
+
+    my $fLeftToRight = $self->{_page_order};    # Print over then down
+    my $fLandscape   = $self->{_orientation};   # Page orientation
+    my $fNoPls       = 0x0;                     # Setup not read from printer
+    my $fNoColor     = $self->{_black_white};   # Print black and white
+    my $fDraft       = $self->{_draft_quality}; # Print draft quality
+    my $fNotes       = $self->{_print_comments};# Print notes
+    my $fNoOrient    = 0x0;                     # Orientation not set
+    my $fUsePage     = $self->{_custom_start};  # Use custom starting page
+
+
+    $grbit           = $fLeftToRight;
+    $grbit          |= $fLandscape    << 1;
+    $grbit          |= $fNoPls        << 2;
+    $grbit          |= $fNoColor      << 3;
+    $grbit          |= $fDraft        << 4;
+    $grbit          |= $fNotes        << 5;
+    $grbit          |= $fNoOrient     << 6;
+    $grbit          |= $fUsePage      << 7;
+
+
+    $numHdr = pack("d", $numHdr);
+    $numFtr = pack("d", $numFtr);
+
+    if ($self->{_byte_order}) {
+        $numHdr = reverse $numHdr;
+        $numFtr = reverse $numFtr;
+    }
+
+    my $header = pack("vv",         $record, $length);
+    my $data1  = pack("vvvvvvvv",   $iPaperSize,
+                                    $iScale,
+                                    $iPageStart,
+                                    $iFitWidth,
+                                    $iFitHeight,
+                                    $grbit,
+                                    $iRes,
+                                    $iVRes);
+    my $data2  = $numHdr .$numFtr;
+    my $data3  = pack("v", $iCopies);
+
+    $self->_prepend($header, $data1, $data2, $data3);
+
+}
+
+###############################################################################
+#
+# _store_header()
+#
+# Store the header caption BIFF record.
+#
+sub _store_header {
+
+    my $self        = shift;
+
+    my $record      = 0x0014;                       # Record identifier
+    my $length;                                     # Bytes to follow
+
+    my $str         = $self->{_header};             # header string
+    my $cch         = length($str);                 # Length of header string
+    my $encoding    = $self->{_header_encoding};    # Character encoding
+
+
+    # Character length is num of chars not num of bytes
+    $cch           /= 2 if $encoding;
+
+    # Change the UTF-16 name from BE to LE
+    $str            = pack 'n*', unpack 'v*', $str if $encoding;
+
+    $length         = 3 + length($str);
+
+    my $header      = pack("vv",  $record, $length);
+    my $data        = pack("vC",  $cch, $encoding);
+
+    $self->_prepend($header, $data, $str);
+}
+
+
+###############################################################################
+#
+# _store_footer()
+#
+# Store the footer caption BIFF record.
+#
+sub _store_footer {
+
+    my $self        = shift;
+
+    my $record      = 0x0015;                       # Record identifier
+    my $length;                                     # Bytes to follow
+
+    my $str         = $self->{_footer};             # footer string
+    my $cch         = length($str);                 # Length of footer string
+    my $encoding    = $self->{_footer_encoding};    # Character encoding
+
+
+    # Character length is num of chars not num of bytes
+    $cch           /= 2 if $encoding;
+
+    # Change the UTF-16 name from BE to LE
+    $str            = pack 'n*', unpack 'v*', $str if $encoding;
+
+    $length         = 3 + length($str);
+
+    my $header      = pack("vv",  $record, $length);
+    my $data        = pack("vC",  $cch, $encoding);
+
+    $self->_prepend($header, $data, $str);
+}
+
+
+###############################################################################
+#
+# _store_hcenter()
+#
+# Store the horizontal centering HCENTER BIFF record.
+#
+sub _store_hcenter {
+
+    my $self     = shift;
+
+    my $record   = 0x0083;              # Record identifier
+    my $length   = 0x0002;              # Bytes to follow
+
+    my $fHCenter = $self->{_hcenter};   # Horizontal centering
+
+    my $header    = pack("vv",  $record, $length);
+    my $data      = pack("v",   $fHCenter);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_vcenter()
+#
+# Store the vertical centering VCENTER BIFF record.
+#
+sub _store_vcenter {
+
+    my $self     = shift;
+
+    my $record   = 0x0084;              # Record identifier
+    my $length   = 0x0002;              # Bytes to follow
+
+    my $fVCenter = $self->{_vcenter};   # Horizontal centering
+
+    my $header    = pack("vv",  $record, $length);
+    my $data      = pack("v",   $fVCenter);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_margin_left()
+#
+# Store the LEFTMARGIN BIFF record.
+#
+sub _store_margin_left {
+
+    my $self    = shift;
+
+    my $record  = 0x0026;                   # Record identifier
+    my $length  = 0x0008;                   # Bytes to follow
+
+    my $margin  = $self->{_margin_left};    # Margin in inches
+
+    my $header    = pack("vv",  $record, $length);
+    my $data      = pack("d",   $margin);
+
+    if ($self->{_byte_order}) { $data = reverse $data }
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_margin_right()
+#
+# Store the RIGHTMARGIN BIFF record.
+#
+sub _store_margin_right {
+
+    my $self    = shift;
+
+    my $record  = 0x0027;                   # Record identifier
+    my $length  = 0x0008;                   # Bytes to follow
+
+    my $margin  = $self->{_margin_right};   # Margin in inches
+
+    my $header    = pack("vv",  $record, $length);
+    my $data      = pack("d",   $margin);
+
+    if ($self->{_byte_order}) { $data = reverse $data }
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_margin_top()
+#
+# Store the TOPMARGIN BIFF record.
+#
+sub _store_margin_top {
+
+    my $self    = shift;
+
+    my $record  = 0x0028;                   # Record identifier
+    my $length  = 0x0008;                   # Bytes to follow
+
+    my $margin  = $self->{_margin_top};     # Margin in inches
+
+    my $header    = pack("vv",  $record, $length);
+    my $data      = pack("d",   $margin);
+
+    if ($self->{_byte_order}) { $data = reverse $data }
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_margin_bottom()
+#
+# Store the BOTTOMMARGIN BIFF record.
+#
+sub _store_margin_bottom {
+
+    my $self    = shift;
+
+    my $record  = 0x0029;                   # Record identifier
+    my $length  = 0x0008;                   # Bytes to follow
+
+    my $margin  = $self->{_margin_bottom};  # Margin in inches
+
+    my $header    = pack("vv",  $record, $length);
+    my $data      = pack("d",   $margin);
+
+    if ($self->{_byte_order}) { $data = reverse $data }
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# merge_cells($first_row, $first_col, $last_row, $last_col)
+#
+# This is an Excel97/2000 method. It is required to perform more complicated
+# merging than the normal align merge in Format.pm
+#
+sub merge_cells {
+
+    my $self    = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    my $record  = 0x00E5;                   # Record identifier
+    my $length  = 0x000A;                   # Bytes to follow
+
+    my $cref     = 1;                       # Number of refs
+    my $rwFirst  = $_[0];                   # First row in reference
+    my $colFirst = $_[1];                   # First col in reference
+    my $rwLast   = $_[2] || $rwFirst;       # Last  row in reference
+    my $colLast  = $_[3] || $colFirst;      # Last  col in reference
+
+
+    # Excel doesn't allow a single cell to be merged
+    return if $rwFirst == $rwLast and $colFirst == $colLast;
+
+    # Swap last row/col with first row/col as necessary
+    ($rwFirst,  $rwLast ) = ($rwLast,  $rwFirst ) if $rwFirst  > $rwLast;
+    ($colFirst, $colLast) = ($colLast, $colFirst) if $colFirst > $colLast;
+
+    my $header   = pack("vv",       $record, $length);
+    my $data     = pack("vvvvv",    $cref,
+                                    $rwFirst, $rwLast,
+                                    $colFirst, $colLast);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# merge_range($row1, $col1, $row2, $col2, $string, $format, $encoding)
+#
+# This is a wrapper to ensure correct use of the merge_cells method, i.e., write
+# the first cell of the range, write the formatted blank cells in the range and
+# then call the merge_cells record. Failing to do the steps in this order will
+# cause Excel 97 to crash.
+#
+sub merge_range {
+
+    my $self    = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+    croak "Incorrect number of arguments" if @_ != 6 and @_ != 7;
+    croak "Format argument is not a format object" unless ref $_[5];
+
+    my $rwFirst  = $_[0];
+    my $colFirst = $_[1];
+    my $rwLast   = $_[2];
+    my $colLast  = $_[3];
+    my $string   = $_[4];
+    my $format   = $_[5];
+    my $encoding = $_[6] ? 1 : 0;
+
+
+    # Temp code to prevent merged formats in non-merged cells.
+    my $error = "Error: refer to merge_range() in the documentation. " .
+                "Can't use previously non-merged format in merged cells";
+
+    croak $error if $format->{_used_merge} == -1;
+    $format->{_used_merge} = 0; # Until the end of this function.
+
+
+    # Set the merge_range property of the format object. For BIFF8+.
+    $format->set_merge_range();
+
+    # Excel doesn't allow a single cell to be merged
+    croak "Can't merge single cell" if $rwFirst  == $rwLast and
+                                       $colFirst == $colLast;
+
+    # Swap last row/col with first row/col as necessary
+    ($rwFirst,  $rwLast ) = ($rwLast,  $rwFirst ) if $rwFirst  > $rwLast;
+    ($colFirst, $colLast) = ($colLast, $colFirst) if $colFirst > $colLast;
+
+    # Write the first cell
+    if ($encoding) {
+        $self->write_utf16be_string($rwFirst, $colFirst, $string, $format);
+    }
+    else {
+        $self->write               ($rwFirst, $colFirst, $string, $format);
+    }
+
+    # Pad out the rest of the area with formatted blank cells.
+    for my $row ($rwFirst .. $rwLast) {
+        for my $col ($colFirst .. $colLast) {
+            next if $row == $rwFirst and $col == $colFirst;
+            $self->write_blank($row, $col, $format);
+        }
+    }
+
+    $self->merge_cells($rwFirst, $colFirst, $rwLast, $colLast);
+
+    # Temp code to prevent merged formats in non-merged cells.
+    $format->{_used_merge} = 1;
+
+}
+
+
+###############################################################################
+#
+# _store_print_headers()
+#
+# Write the PRINTHEADERS BIFF record.
+#
+sub _store_print_headers {
+
+    my $self        = shift;
+
+    my $record      = 0x002a;                   # Record identifier
+    my $length      = 0x0002;                   # Bytes to follow
+
+    my $fPrintRwCol = $self->{_print_headers};  # Boolean flag
+
+    my $header      = pack("vv",  $record, $length);
+    my $data        = pack("v",   $fPrintRwCol);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_print_gridlines()
+#
+# Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the
+# GRIDSET record.
+#
+sub _store_print_gridlines {
+
+    my $self        = shift;
+
+    my $record      = 0x002b;                    # Record identifier
+    my $length      = 0x0002;                    # Bytes to follow
+
+    my $fPrintGrid  = $self->{_print_gridlines}; # Boolean flag
+
+    my $header      = pack("vv",  $record, $length);
+    my $data        = pack("v",   $fPrintGrid);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_gridset()
+#
+# Write the GRIDSET BIFF record. Must be used in conjunction with the
+# PRINTGRIDLINES record.
+#
+sub _store_gridset {
+
+    my $self        = shift;
+
+    my $record      = 0x0082;                        # Record identifier
+    my $length      = 0x0002;                        # Bytes to follow
+
+    my $fGridSet    = not $self->{_print_gridlines}; # Boolean flag
+
+    my $header      = pack("vv",  $record, $length);
+    my $data        = pack("v",   $fGridSet);
+
+    $self->_prepend($header, $data);
+
+}
+
+
+###############################################################################
+#
+# _store_guts()
+#
+# Write the GUTS BIFF record. This is used to configure the gutter margins
+# where Excel outline symbols are displayed. The visibility of the gutters is
+# controlled by a flag in WSBOOL. See also _store_wsbool().
+#
+# We are all in the gutter but some of us are looking at the stars.
+#
+sub _store_guts {
+
+    my $self        = shift;
+
+    my $record      = 0x0080;   # Record identifier
+    my $length      = 0x0008;   # Bytes to follow
+
+    my $dxRwGut     = 0x0000;   # Size of row gutter
+    my $dxColGut    = 0x0000;   # Size of col gutter
+
+    my $row_level   = $self->{_outline_row_level};
+    my $col_level   = 0;
+
+
+    # Calculate the maximum column outline level. The equivalent calculation
+    # for the row outline level is carried out in set_row().
+    #
+    foreach my $colinfo (@{$self->{_colinfo}}) {
+        # Skip cols without outline level info.
+        next if @{$colinfo} < 6;
+        $col_level = @{$colinfo}[5] if @{$colinfo}[5] > $col_level;
+    }
+
+
+    # Set the limits for the outline levels (0 <= x <= 7).
+    $col_level = 0 if $col_level < 0;
+    $col_level = 7 if $col_level > 7;
+
+
+    # The displayed level is one greater than the max outline levels
+    $row_level++ if $row_level > 0;
+    $col_level++ if $col_level > 0;
+
+    my $header      = pack("vv",   $record, $length);
+    my $data        = pack("vvvv", $dxRwGut, $dxColGut, $row_level, $col_level);
+
+    $self->_prepend($header, $data);
+
+}
+
+
+###############################################################################
+#
+# _store_wsbool()
+#
+# Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction
+# with the SETUP record.
+#
+sub _store_wsbool {
+
+    my $self        = shift;
+
+    my $record      = 0x0081;   # Record identifier
+    my $length      = 0x0002;   # Bytes to follow
+
+    my $grbit       = 0x0000;   # Option flags
+
+    # Set the option flags
+    $grbit |= 0x0001;                            # Auto page breaks visible
+    $grbit |= 0x0020 if $self->{_outline_style}; # Auto outline styles
+    $grbit |= 0x0040 if $self->{_outline_below}; # Outline summary below
+    $grbit |= 0x0080 if $self->{_outline_right}; # Outline summary right
+    $grbit |= 0x0100 if $self->{_fit_page};      # Page setup fit to page
+    $grbit |= 0x0400 if $self->{_outline_on};    # Outline symbols displayed
+
+
+    my $header      = pack("vv",  $record, $length);
+    my $data        = pack("v",   $grbit);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_hbreak()
+#
+# Write the HORIZONTALPAGEBREAKS BIFF record.
+#
+sub _store_hbreak {
+
+    my $self    = shift;
+
+    # Return if the user hasn't specified pagebreaks
+    return unless @{$self->{_hbreaks}};
+
+    # Sort and filter array of page breaks
+    my @breaks  = $self->_sort_pagebreaks(@{$self->{_hbreaks}});
+
+    my $record  = 0x001b;               # Record identifier
+    my $cbrk    = scalar @breaks;       # Number of page breaks
+    my $length  = 2 + 6*$cbrk;          # Bytes to follow
+
+
+    my $header  = pack("vv",  $record, $length);
+    my $data    = pack("v",   $cbrk);
+
+    # Append each page break
+    foreach my $break (@breaks) {
+        $data .= pack("vvv", $break, 0x0000, 0x00ff);
+    }
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_vbreak()
+#
+# Write the VERTICALPAGEBREAKS BIFF record.
+#
+sub _store_vbreak {
+
+    my $self    = shift;
+
+    # Return if the user hasn't specified pagebreaks
+    return unless @{$self->{_vbreaks}};
+
+    # Sort and filter array of page breaks
+    my @breaks  = $self->_sort_pagebreaks(@{$self->{_vbreaks}});
+
+    my $record  = 0x001a;               # Record identifier
+    my $cbrk    = scalar @breaks;       # Number of page breaks
+    my $length  = 2 + 6*$cbrk;          # Bytes to follow
+
+
+    my $header  = pack("vv",  $record, $length);
+    my $data    = pack("v",   $cbrk);
+
+    # Append each page break
+    foreach my $break (@breaks) {
+        $data .= pack("vvv", $break, 0x0000, 0xffff);
+    }
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_protect()
+#
+# Set the Biff PROTECT record to indicate that the worksheet is protected.
+#
+sub _store_protect {
+
+    my $self        = shift;
+
+    # Exit unless sheet protection has been specified
+    return unless $self->{_protect};
+
+    my $record      = 0x0012;               # Record identifier
+    my $length      = 0x0002;               # Bytes to follow
+
+    my $fLock       = $self->{_protect};    # Worksheet is protected
+
+    my $header      = pack("vv", $record, $length);
+    my $data        = pack("v",  $fLock);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_obj_protect()
+#
+# Set the Biff OBJPROTECT record to indicate that objects are protected.
+#
+sub _store_obj_protect {
+
+    my $self        = shift;
+
+    # Exit unless sheet protection has been specified
+    return unless $self->{_protect};
+
+    my $record      = 0x0063;               # Record identifier
+    my $length      = 0x0002;               # Bytes to follow
+
+    my $fLock       = $self->{_protect};    # Worksheet is protected
+
+    my $header      = pack("vv", $record, $length);
+    my $data        = pack("v",  $fLock);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_password()
+#
+# Write the worksheet PASSWORD record.
+#
+sub _store_password {
+
+    my $self        = shift;
+
+    # Exit unless sheet protection and password have been specified
+    return unless $self->{_protect} and defined $self->{_password};
+
+    my $record      = 0x0013;               # Record identifier
+    my $length      = 0x0002;               # Bytes to follow
+
+    my $wPassword   = $self->{_password};   # Encoded password
+
+    my $header      = pack("vv", $record, $length);
+    my $data        = pack("v",  $wPassword);
+
+    $self->_prepend($header, $data);
+}
+
+
+#
+# Note about 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.
+#
+
+
+
+###############################################################################
+#
+# _store_table()
+#
+# Write cell data stored in the worksheet row/col table.
+#
+# This is only used when compatibity_mode() is in operation.
+#
+# This method writes ROW data, then cell data (NUMBER, LABELSST, etc) and then
+# DBCELL records in blocks of 32 rows. This is explained in detail (for a
+# change) in the Excel SDK and in the OOo Excel file format doc.
+#
+sub _store_table {
+
+    my $self = shift;
+
+    return unless $self->{_compatibility};
+
+    # Offset from the DBCELL record back to the first ROW of the 32 row block.
+    my $row_offset = 0;
+
+    # Track rows that have cell data or modified by set_row().
+    my @written_rows;
+
+
+    # Write the ROW records with updated max/min col fields.
+    #
+    for my $row (0 .. $self->{_dim_rowmax} -1) {
+        # Skip unless there is cell data in row or the row has been modified.
+        next unless $self->{_table}->[$row] or $self->{_row_data}->{$row};
+
+        # Store the rows with data.
+        push @written_rows, $row;
+
+        # Increase the row offset by the length of a ROW record;
+        $row_offset += 20;
+
+        # The max/min cols in the ROW records are the same as in DIMENSIONS.
+        my $col_min = $self->{_dim_colmin};
+        my $col_max = $self->{_dim_colmax};
+
+        # Write a user specified ROW record (modified by set_row()).
+        if ($self->{_row_data}->{$row}) {
+            # Rewrite the min and max cols for user defined row record.
+            my $packed_row = $self->{_row_data}->{$row};
+            substr $packed_row, 6, 4, pack('vv', $col_min, $col_max);
+            $self->_append($packed_row);
+        }
+        else {
+            # Write a default Row record if there isn't a  user defined ROW.
+            $self->_write_row_default($row, $col_min, $col_max);
+        }
+
+
+
+        # If 32 rows have been written or we are at the last row in the
+        # worksheet then write the cell data and the DBCELL record.
+        #
+        if (@written_rows == 32 or $row == $self->{_dim_rowmax} -1) {
+
+            # Offsets to the first cell of each row.
+            my @cell_offsets;
+            push @cell_offsets, $row_offset - 20;
+
+            # Write the cell data in each row and sum their lengths for the
+            # cell offsets.
+            #
+            for my $row (@written_rows) {
+                my $cell_offset = 0;
+
+                for my $col (@{$self->{_table}->[$row]}) {
+                    next unless $col;
+                    $self->_append($col);
+                    my $length = length $col;
+                    $row_offset  += $length;
+                    $cell_offset += $length;
+                }
+                push @cell_offsets, $cell_offset;
+            }
+
+            # The last offset isn't required.
+            pop @cell_offsets;
+
+            # Stores the DBCELL offset for use in the INDEX record.
+            push @{$self->{_db_indices}}, $self->{_datasize};
+
+            # Write the DBCELL record.
+            $self->_store_dbcell($row_offset, @cell_offsets);
+
+            # Clear the variable for the next block of rows.
+            @written_rows   = ();
+            @cell_offsets   = ();
+            $row_offset     = 0;
+        }
+    }
+}
+
+
+###############################################################################
+#
+# _store_dbcell()
+#
+# Store the DBCELL record using the offset calculated in _store_table().
+#
+# This is only used when compatibity_mode() is in operation.
+#
+sub _store_dbcell {
+
+    my $self            = shift;
+    my $row_offset      = shift;
+    my @cell_offsets    = @_;
+
+
+    my $record          = 0x00D7;                 # Record identifier
+    my $length          = 4 + 2 * @cell_offsets;  # Bytes to follow
+
+
+    my $header          = pack 'vv', $record, $length;
+    my $data            = pack 'V',  $row_offset;
+       $data           .= pack 'v', $_ for @cell_offsets;
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_index()
+#
+# Store the INDEX record using the DBCELL offsets calculated in _store_table().
+#
+# This is only used when compatibity_mode() is in operation.
+#
+sub _store_index {
+
+    my $self = shift;
+
+    return unless $self->{_compatibility};
+
+    my @indices     = @{$self->{_db_indices}};
+    my $reserved    = 0x00000000;
+    my $row_min     = $self->{_dim_rowmin};
+    my $row_max     = $self->{_dim_rowmax};
+
+    my $record      = 0x020B;             # Record identifier
+    my $length      = 16 + 4 * @indices;  # Bytes to follow
+
+    my $header      = pack 'vv',   $record, $length;
+    my $data        = pack 'VVVV', $reserved,
+                                   $row_min,
+                                   $row_max,
+                                   $reserved;
+
+    for my $index (@indices) {
+       $data .= pack 'V', $index + $self->{_offset} + 20 + $length +4;
+    }
+
+    $self->_prepend($header, $data);
+
+}
+
+
+###############################################################################
+#
+# insert_chart($row, $col, $chart, $x, $y, $scale_x, $scale_y)
+#
+# Insert a chart into a worksheet. The $chart argument should be a Chart
+# object or else it is assumed to be a filename of an external binary file.
+# The latter is for backwards compatibility.
+#
+sub insert_chart {
+
+    my $self        = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    my $row         = $_[0];
+    my $col         = $_[1];
+    my $chart       = $_[2];
+    my $x_offset    = $_[3] || 0;
+    my $y_offset    = $_[4] || 0;
+    my $scale_x     = $_[5] || 1;
+    my $scale_y     = $_[6] || 1;
+
+    croak "Insufficient arguments in insert_chart()" unless @_ >= 3;
+
+    if ( ref $chart ) {
+        # Check for a Chart object.
+        croak "Not a Chart object in insert_chart()"
+          unless $chart->isa( 'Spreadsheet::WriteExcel::Chart' );
+
+        # Check that the chart is an embedded style chart.
+        croak "Not a embedded style Chart object in insert_chart()"
+          unless $chart->{_embedded};
+
+    }
+    else {
+
+        # Assume an external bin filename.
+        croak "Couldn't locate $chart in insert_chart(): $!" unless -e $chart;
+    }
+
+    $self->{_charts}->{$row}->{$col} =  [
+                                           $row,
+                                           $col,
+                                           $chart,
+                                           $x_offset,
+                                           $y_offset,
+                                           $scale_x,
+                                           $scale_y,
+                                        ];
+
+}
+
+# Older method name for backwards compatibility.
+*embed_chart = *insert_chart;
+
+###############################################################################
+#
+# insert_image($row, $col, $filename, $x, $y, $scale_x, $scale_y)
+#
+# Insert an image into the worksheet.
+#
+sub insert_image {
+
+    my $self        = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    my $row         = $_[0];
+    my $col         = $_[1];
+    my $image       = $_[2];
+    my $x_offset    = $_[3] || 0;
+    my $y_offset    = $_[4] || 0;
+    my $scale_x     = $_[5] || 1;
+    my $scale_y     = $_[6] || 1;
+
+    croak "Insufficient arguments in insert_image()" unless @_ >= 3;
+    croak "Couldn't locate $image: $!"               unless -e $image;
+
+    $self->{_images}->{$row}->{$col} = [
+                                           $row,
+                                           $col,
+                                           $image,
+                                           $x_offset,
+                                           $y_offset,
+                                           $scale_x,
+                                           $scale_y,
+                                        ];
+
+}
+
+# Older method name for backwards compatibility.
+*insert_bitmap = *insert_image;
+
+
+###############################################################################
+#
+#  _position_object()
+#
+# Calculate the vertices that define the position of a graphical object within
+# the worksheet.
+#
+#         +------------+------------+
+#         |     A      |      B     |
+#   +-----+------------+------------+
+#   |     |(x1,y1)     |            |
+#   |  1  |(A1)._______|______      |
+#   |     |    |              |     |
+#   |     |    |              |     |
+#   +-----+----|    BITMAP    |-----+
+#   |     |    |              |     |
+#   |  2  |    |______________.     |
+#   |     |            |        (B2)|
+#   |     |            |     (x2,y2)|
+#   +---- +------------+------------+
+#
+# Example of a bitmap that covers some of the area from cell A1 to cell B2.
+#
+# Based on the width and height of the bitmap we need to calculate 8 vars:
+#     $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2.
+# The width and height of the cells are also variable and have to be taken into
+# account.
+# The values of $col_start and $row_start are passed in from the calling
+# function. The values of $col_end and $row_end are calculated by subtracting
+# the width and height of the bitmap from the width and height of the
+# underlying cells.
+# The vertices are expressed as a percentage of the underlying cell width as
+# follows (rhs values are in pixels):
+#
+#       x1 = X / W *1024
+#       y1 = Y / H *256
+#       x2 = (X-1) / W *1024
+#       y2 = (Y-1) / H *256
+#
+#       Where:  X is distance from the left side of the underlying cell
+#               Y is distance from the top of the underlying cell
+#               W is the width of the cell
+#               H is the height of the cell
+#
+# Note: the SDK incorrectly states that the height should be expressed as a
+# percentage of 1024.
+#
+sub _position_object {
+
+    my $self = shift;
+
+    my $col_start;  # Col containing upper left corner of object
+    my $x1;         # Distance to left side of object
+
+    my $row_start;  # Row containing top left corner of object
+    my $y1;         # Distance to top of object
+
+    my $col_end;    # Col containing lower right corner of object
+    my $x2;         # Distance to right side of object
+
+    my $row_end;    # Row containing bottom right corner of object
+    my $y2;         # Distance to bottom of object
+
+    my $width;      # Width of image frame
+    my $height;     # Height of image frame
+
+    ($col_start, $row_start, $x1, $y1, $width, $height) = @_;
+
+
+    # Adjust start column for offsets that are greater than the col width
+    while ($x1 >= $self->_size_col($col_start)) {
+        $x1 -= $self->_size_col($col_start);
+        $col_start++;
+    }
+
+    # Adjust start row for offsets that are greater than the row height
+    while ($y1 >= $self->_size_row($row_start)) {
+        $y1 -= $self->_size_row($row_start);
+        $row_start++;
+    }
+
+
+    # Initialise end cell to the same as the start cell
+    $col_end    = $col_start;
+    $row_end    = $row_start;
+
+    $width      = $width  + $x1;
+    $height     = $height + $y1;
+
+
+    # Subtract the underlying cell widths to find the end cell of the image
+    while ($width >= $self->_size_col($col_end)) {
+        $width -= $self->_size_col($col_end);
+        $col_end++;
+    }
+
+    # Subtract the underlying cell heights to find the end cell of the image
+    while ($height >= $self->_size_row($row_end)) {
+        $height -= $self->_size_row($row_end);
+        $row_end++;
+    }
+
+    # Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
+    # with zero eight or width.
+    #
+    return if $self->_size_col($col_start) == 0;
+    return if $self->_size_col($col_end)   == 0;
+    return if $self->_size_row($row_start) == 0;
+    return if $self->_size_row($row_end)   == 0;
+
+    # Convert the pixel values to the percentage value expected by Excel
+    $x1 = $x1     / $self->_size_col($col_start)   * 1024;
+    $y1 = $y1     / $self->_size_row($row_start)   *  256;
+    $x2 = $width  / $self->_size_col($col_end)     * 1024;
+    $y2 = $height / $self->_size_row($row_end)     *  256;
+
+    # Simulate ceil() without calling POSIX::ceil().
+    $x1 = int($x1 +0.5);
+    $y1 = int($y1 +0.5);
+    $x2 = int($x2 +0.5);
+    $y2 = int($y2 +0.5);
+
+    return( $col_start, $x1,
+            $row_start, $y1,
+            $col_end,   $x2,
+            $row_end,   $y2
+          );
+}
+
+
+###############################################################################
+#
+# _size_col($col)
+#
+# Convert the width of a cell from user's units to pixels. Excel rounds the
+# column width to the nearest pixel. If the width hasn't been set by the user
+# we use the default value. If the column is hidden we use a value of zero.
+#
+sub _size_col {
+
+    my $self = shift;
+    my $col  = $_[0];
+
+    # Look up the cell value to see if it has been changed
+    if (exists $self->{_col_sizes}->{$col}) {
+        my $width = $self->{_col_sizes}->{$col};
+
+        # The relationship is different for user units less than 1.
+        if ($width < 1) {
+            return int($width *12);
+        }
+        else {
+            return int($width *7 ) +5;
+        }
+    }
+    else {
+        return 64;
+    }
+}
+
+
+###############################################################################
+#
+# _size_row($row)
+#
+# Convert the height of a cell from user's units to pixels. By interpolation
+# the relationship is: y = 4/3x. If the height hasn't been set by the user we
+# use the default value. If the row is hidden we use a value of zero. (Not
+# possible to hide row yet).
+#
+sub _size_row {
+
+    my $self = shift;
+    my $row  = $_[0];
+
+    # Look up the cell value to see if it has been changed
+    if (exists $self->{_row_sizes}->{$row}) {
+        if ($self->{_row_sizes}->{$row} == 0) {
+            return 0;
+        }
+        else {
+            return int (4/3 * $self->{_row_sizes}->{$row});
+        }
+    }
+    else {
+        return 17;
+    }
+}
+
+
+###############################################################################
+#
+# _store_zoom($zoom)
+#
+#
+# Store the window zoom factor. This should be a reduced fraction but for
+# simplicity we will store all fractions with a numerator of 100.
+#
+sub _store_zoom {
+
+    my $self        = shift;
+
+    # If scale is 100 we don't need to write a record
+    return if $self->{_zoom} == 100;
+
+    my $record      = 0x00A0;               # Record identifier
+    my $length      = 0x0004;               # Bytes to follow
+
+    my $header      = pack("vv", $record, $length   );
+    my $data        = pack("vv", $self->{_zoom}, 100);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# write_utf16be_string($row, $col, $string, $format)
+#
+# Write a Unicode string to the specified row and column (zero indexed).
+# $format is optional.
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#         -3 : long string truncated to 255 chars
+#
+sub write_utf16be_string {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    if (@_ < 3) { return -1 }                        # Check the number of args
+
+    my $record      = 0x00FD;                        # Record identifier
+    my $length      = 0x000A;                        # Bytes to follow
+
+    my $row         = $_[0];                         # Zero indexed row
+    my $col         = $_[1];                         # Zero indexed column
+    my $strlen      = length($_[2]);
+    my $str         = $_[2];
+    my $xf          = _XF($self, $row, $col, $_[3]); # The cell format
+    my $encoding    = 0x1;
+    my $str_error   = 0;
+
+    # Check that row and col are valid and store max and min values
+    return -2 if $self->_check_dimensions($row, $col);
+
+    # Limit the utf16 string to the max number of chars (not bytes).
+    if ($strlen > 32767* 2) {
+        $str       = substr($str, 0, 32767*2);
+        $str_error = -3;
+    }
+
+
+    my $num_bytes = length $str;
+    my $num_chars = int($num_bytes / 2);
+
+
+    # Check for a valid 2-byte char string.
+    croak "Uneven number of bytes in Unicode string" if $num_bytes % 2;
+
+
+    # Change from UTF16 big-endian to little endian
+    $str = pack "v*", unpack "n*", $str;
+
+
+    # Add the encoding and length header to the string.
+    my $str_header  = pack("vC", $num_chars, $encoding);
+    $str            = $str_header . $str;
+
+
+    if (not exists ${$self->{_str_table}}->{$str}) {
+        ${$self->{_str_table}}->{$str} = ${$self->{_str_unique}}++;
+    }
+
+
+    ${$self->{_str_total}}++;
+
+
+    my $header = pack("vv",   $record, $length);
+    my $data   = pack("vvvV", $row, $col, $xf, ${$self->{_str_table}}->{$str});
+
+    # Store the data or write immediately depending on the compatibility mode.
+    if ($self->{_compatibility}) {
+        $self->{_table}->[$row]->[$col] = $header . $data;
+    }
+    else {
+        $self->_append($header, $data);
+    }
+
+    return $str_error;
+}
+
+
+###############################################################################
+#
+# write_utf16le_string($row, $col, $string, $format)
+#
+# Write a UTF-16LE string to the specified row and column (zero indexed).
+# $format is optional.
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#         -3 : long string truncated to 255 chars
+#
+sub write_utf16le_string {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    if (@_ < 3) { return -1 }                        # Check the number of args
+
+    my $record      = 0x00FD;                        # Record identifier
+    my $length      = 0x000A;                        # Bytes to follow
+
+    my $row         = $_[0];                         # Zero indexed row
+    my $col         = $_[1];                         # Zero indexed column
+    my $str         = $_[2];
+    my $format      = $_[3];                         # The cell format
+
+
+    # Change from UTF16 big-endian to little endian
+    $str = pack "v*", unpack "n*", $str;
+
+
+    return $self->write_utf16be_string($row, $col, $str, $format);
+}
+
+
+# Older method name for backwards compatibility.
+*write_unicode    = *write_utf16be_string;
+*write_unicode_le = *write_utf16le_string;
+
+
+
+###############################################################################
+#
+# _store_autofilters()
+#
+# Function to iterate through the columns that form part of an autofilter
+# range and write Biff AUTOFILTER records if a filter expression has been set.
+#
+sub _store_autofilters {
+
+    my $self = shift;
+
+    # Skip all columns if no filter have been set.
+    return unless $self->{_filter_on};
+
+    my (undef, undef, $col1, $col2) = @{$self->{_filter_area}};
+
+    for my $i ($col1 .. $col2) {
+        # Reverse order since records are being pre-pended.
+        my $col = $col2 -$i;
+
+        # Skip if column doesn't have an active filter.
+        next unless $self->{_filter_cols}->{$col};
+
+        # Retrieve the filter tokens and write the autofilter records.
+        my @tokens =  @{$self->{_filter_cols}->{$col}};
+        $self->_store_autofilter($col, @tokens);
+    }
+}
+
+
+###############################################################################
+#
+# _store_autofilter()
+#
+# Function to write worksheet AUTOFILTER records. These contain 2 Biff Doper
+# structures to represent the 2 possible filter conditions.
+#
+sub _store_autofilter {
+
+    my $self            = shift;
+
+    my $record          = 0x009E;
+    my $length          = 0x0000;
+
+    my $index           = $_[0];
+    my $operator_1      = $_[1];
+    my $token_1         = $_[2];
+    my $join            = $_[3]; # And/Or
+    my $operator_2      = $_[4];
+    my $token_2         = $_[5];
+
+    my $top10_active    = 0;
+    my $top10_direction = 0;
+    my $top10_percent   = 0;
+    my $top10_value     = 101;
+
+    my $grbit       = $join;
+    my $optimised_1 = 0;
+    my $optimised_2 = 0;
+    my $doper_1     = '';
+    my $doper_2     = '';
+    my $string_1    = '';
+    my $string_2    = '';
+
+    # Excel used an optimisation in the case of a simple equality.
+    $optimised_1 = 1 if                         $operator_1 == 2;
+    $optimised_2 = 1 if defined $operator_2 and $operator_2 == 2;
+
+
+    # Convert non-simple equalities back to type 2. See  _parse_filter_tokens().
+    $operator_1 = 2 if                         $operator_1 == 22;
+    $operator_2 = 2 if defined $operator_2 and $operator_2 == 22;
+
+
+    # Handle a "Top" style expression.
+    if ($operator_1 >= 30) {
+        # Remove the second expression if present.
+        $operator_2 = undef;
+        $token_2    = undef;
+
+        # Set the active flag.
+        $top10_active    = 1;
+
+        if ($operator_1 == 30 or $operator_1 == 31) {
+            $top10_direction = 1;
+        }
+
+        if ($operator_1 == 31 or $operator_1 == 33) {
+            $top10_percent = 1;
+        }
+
+        if ($top10_direction == 1) {
+            $operator_1 = 6
+        }
+        else {
+            $operator_1 = 3
+        }
+
+        $top10_value     = $token_1;
+        $token_1         = 0;
+    }
+
+
+    $grbit     |= $optimised_1      << 2;
+    $grbit     |= $optimised_2      << 3;
+    $grbit     |= $top10_active     << 4;
+    $grbit     |= $top10_direction  << 5;
+    $grbit     |= $top10_percent    << 6;
+    $grbit     |= $top10_value      << 7;
+
+    ($doper_1, $string_1) = $self->_pack_doper($operator_1, $token_1);
+    ($doper_2, $string_2) = $self->_pack_doper($operator_2, $token_2);
+
+    my $data    = pack 'v', $index;
+       $data   .= pack 'v', $grbit;
+       $data   .= $doper_1;
+       $data   .= $doper_2;
+       $data   .= $string_1;
+       $data   .= $string_2;
+
+       $length  = length $data;
+    my $header  = pack('vv',  $record, $length);
+
+    $self->_prepend($header, $data);
+}
+
+
+###############################################################################
+#
+# _pack_doper()
+#
+# Create a Biff Doper structure that represents a filter expression. Depending
+# on the type of the token we pack an Empty, String or Number doper.
+#
+sub _pack_doper {
+
+    my $self        = shift;
+
+    my $operator    = $_[0];
+    my $token       = $_[1];
+
+    my $doper       = '';
+    my $string      = '';
+
+
+    # Return default doper for non-defined filters.
+    if (not defined $operator) {
+        return ($self->_pack_unused_doper, $string);
+    }
+
+
+    if ($token =~ /^blanks|nonblanks$/i) {
+        $doper  = $self->_pack_blanks_doper($operator, $token);
+    }
+    elsif ($operator == 2 or
+        $token    !~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/)
+    {
+        # Excel treats all tokens as strings if the operator is equality, =.
+
+        $string = $token;
+
+        my $encoding = 0;
+        my $length   = length $string;
+
+        # Handle utf8 strings in perl 5.8.
+        if ($] >= 5.008) {
+            require Encode;
+
+            if (Encode::is_utf8($string)) {
+                $string = Encode::encode("UTF-16BE", $string);
+                $encoding = 1;
+            }
+        }
+
+        $string = pack('C', $encoding) . $string;
+        $doper  = $self->_pack_string_doper($operator, $length);
+    }
+    else {
+        $string = '';
+        $doper  = $self->_pack_number_doper($operator, $token);
+    }
+
+    return ($doper, $string);
+}
+
+
+###############################################################################
+#
+# _pack_unused_doper()
+#
+# Pack an empty Doper structure.
+#
+sub _pack_unused_doper {
+
+    my $self        = shift;
+
+    return pack 'C10', (0x0) x 10;
+}
+
+
+###############################################################################
+#
+# _pack_blanks_doper()
+#
+# Pack an Blanks/NonBlanks Doper structure.
+#
+sub _pack_blanks_doper {
+
+    my $self        = shift;
+
+    my $operator    = $_[0];
+    my $token       = $_[1];
+    my $type;
+
+    if ($token eq 'blanks') {
+        $type     = 0x0C;
+        $operator = 2;
+
+    }
+    else {
+        $type     = 0x0E;
+        $operator = 5;
+    }
+
+
+    my $doper       = pack 'CCVV',    $type,         # Data type
+                                      $operator,     #
+                                      0x0000,        # Reserved
+                                      0x0000;        # Reserved
+    return $doper;
+}
+
+
+###############################################################################
+#
+# _pack_string_doper()
+#
+# Pack an string Doper structure.
+#
+sub _pack_string_doper {
+
+    my $self        = shift;
+
+    my $operator    = $_[0];
+    my $length      = $_[1];
+    my $doper       = pack 'CCVCCCC', 0x06,          # Data type
+                                      $operator,     #
+                                      0x0000,        # Reserved
+                                      $length,       # String char length.
+                                      0x0, 0x0, 0x0; # Reserved
+    return $doper;
+}
+
+
+###############################################################################
+#
+# _pack_number_doper()
+#
+# Pack an IEEE double number Doper structure.
+#
+sub _pack_number_doper {
+
+    my $self        = shift;
+
+    my $operator    = $_[0];
+    my $number      = $_[1];
+       $number      = pack 'd', $number;
+       $number      = reverse $number if $self->{_byte_order};
+
+    my $doper       = pack 'CC', 0x04, $operator;
+       $doper      .= $number;
+
+    return $doper;
+}
+
+
+#
+# Methods related to comments and MSO objects.
+#
+
+
+###############################################################################
+#
+# _prepare_images()
+#
+# Turn the HoH that stores the images into an array for easier handling.
+#
+sub _prepare_images {
+
+    my $self    = shift;
+
+    my $count   = 0;
+    my @images;
+
+
+    # We sort the images by row and column but that isn't strictly required.
+    #
+    my @rows = sort {$a <=> $b} keys %{$self->{_images}};
+
+    for my $row (@rows) {
+        my @cols = sort {$a <=> $b} keys %{$self->{_images}->{$row}};
+
+        for my $col (@cols) {
+            push @images, $self->{_images}->{$row}->{$col};
+            $count++;
+        }
+    }
+
+    $self->{_images}       = {};
+    $self->{_images_array} = \@images;
+
+    return $count;
+}
+
+
+###############################################################################
+#
+# _prepare_comments()
+#
+# Turn the HoH that stores the comments into an array for easier handling.
+#
+sub _prepare_comments {
+
+    my $self    = shift;
+
+    my $count   = 0;
+    my @comments;
+
+
+    # We sort the comments by row and column but that isn't strictly required.
+    #
+    my @rows = sort {$a <=> $b} keys %{$self->{_comments}};
+
+    for my $row (@rows) {
+        my @cols = sort {$a <=> $b} keys %{$self->{_comments}->{$row}};
+
+        for my $col (@cols) {
+            push @comments, $self->{_comments}->{$row}->{$col};
+            $count++;
+        }
+    }
+
+    $self->{_comments}       = {};
+    $self->{_comments_array} = \@comments;
+
+    return $count;
+}
+
+
+###############################################################################
+#
+# _prepare_charts()
+#
+# Turn the HoH that stores the charts into an array for easier handling.
+#
+sub _prepare_charts {
+
+    my $self    = shift;
+
+    my $count   = 0;
+    my @charts;
+
+
+    # We sort the charts by row and column but that isn't strictly required.
+    #
+    my @rows = sort {$a <=> $b} keys %{$self->{_charts}};
+
+    for my $row (@rows) {
+        my @cols = sort {$a <=> $b} keys %{$self->{_charts}->{$row}};
+
+        for my $col (@cols) {
+            push @charts, $self->{_charts}->{$row}->{$col};
+            $count++;
+        }
+    }
+
+    $self->{_charts}       = {};
+    $self->{_charts_array} = \@charts;
+
+    return $count;
+}
+
+
+###############################################################################
+#
+# _store_images()
+#
+# Store the collections of records that make up images.
+#
+sub _store_images {
+
+    my $self            = shift;
+
+    my $record          = 0x00EC;           # Record identifier
+    my $length          = 0x0000;           # Bytes to follow
+
+    my @ids             = @{$self->{_object_ids  }};
+    my $spid            = shift @ids;
+
+    my @images          = @{$self->{_images_array}};
+    my $num_images      = scalar @images;
+
+    my $num_filters     = $self->{_filter_count};
+    my $num_comments    = @{$self->{_comments_array}};
+    my $num_charts      = @{$self->{_charts_array  }};
+
+    # Skip this if there aren't any images.
+    return unless $num_images;
+
+    for my $i (0 .. $num_images-1) {
+        my $row         =   $images[$i]->[0];
+        my $col         =   $images[$i]->[1];
+        my $name        =   $images[$i]->[2];
+        my $x_offset    =   $images[$i]->[3];
+        my $y_offset    =   $images[$i]->[4];
+        my $scale_x     =   $images[$i]->[5];
+        my $scale_y     =   $images[$i]->[6];
+        my $image_id    =   $images[$i]->[7];
+        my $type        =   $images[$i]->[8];
+        my $width       =   $images[$i]->[9];
+        my $height      =   $images[$i]->[10];
+
+        $width  *= $scale_x if $scale_x;
+        $height *= $scale_y if $scale_y;
+
+
+        # Calculate the positions of image object.
+        my @vertices = $self->_position_object( $col,
+                                                $row,
+                                                $x_offset,
+                                                $y_offset,
+                                                $width,
+                                                $height
+                                              );
+
+        if ($i == 0) {
+            # Write the parent MSODRAWIING record.
+            my $dg_length    = 156 + 84*($num_images -1);
+            my $spgr_length  = 132 + 84*($num_images -1);
+
+               $dg_length   += 120 *$num_charts;
+               $spgr_length += 120 *$num_charts;
+
+               $dg_length   +=  96 *$num_filters;
+               $spgr_length +=  96 *$num_filters;
+
+               $dg_length   += 128 *$num_comments;
+               $spgr_length += 128 *$num_comments;
+
+
+
+            my $data        = $self->_store_mso_dg_container($dg_length);
+               $data       .= $self->_store_mso_dg(@ids);
+               $data       .= $self->_store_mso_spgr_container($spgr_length);
+               $data       .= $self->_store_mso_sp_container(40);
+               $data       .= $self->_store_mso_spgr();
+               $data       .= $self->_store_mso_sp(0x0, $spid++, 0x0005);
+               $data       .= $self->_store_mso_sp_container(76);
+               $data       .= $self->_store_mso_sp(75, $spid++, 0x0A00);
+               $data       .= $self->_store_mso_opt_image($image_id);
+               $data       .= $self->_store_mso_client_anchor(2, @vertices);
+               $data       .= $self->_store_mso_client_data();
+
+            $length         = length $data;
+            my $header      = pack("vv", $record, $length);
+            $self->_append($header, $data);
+
+        }
+        else {
+            # Write the child MSODRAWIING record.
+            my $data        = $self->_store_mso_sp_container(76);
+               $data       .= $self->_store_mso_sp(75, $spid++, 0x0A00);
+               $data       .= $self->_store_mso_opt_image($image_id);
+               $data       .= $self->_store_mso_client_anchor(2, @vertices);
+               $data       .= $self->_store_mso_client_data();
+
+            $length         = length $data;
+            my $header      = pack("vv", $record, $length);
+            $self->_append($header, $data);
+
+
+        }
+
+        $self->_store_obj_image($i+1);
+    }
+
+    $self->{_object_ids}->[0] = $spid;
+}
+
+
+
+###############################################################################
+#
+# _store_charts()
+#
+# Store the collections of records that make up charts.
+#
+sub _store_charts {
+
+    my $self            = shift;
+
+    my $record          = 0x00EC;           # Record identifier
+    my $length          = 0x0000;           # Bytes to follow
+
+    my @ids             = @{$self->{_object_ids}};
+    my $spid            = shift @ids;
+
+    my @charts          = @{$self->{_charts_array}};
+    my $num_charts      = scalar @charts;
+
+    my $num_filters     = $self->{_filter_count};
+    my $num_comments    = @{$self->{_comments_array}};
+
+    # Number of objects written so far.
+    my $num_objects     = @{$self->{_images_array}};
+
+    # Skip this if there aren't any charts.
+    return unless $num_charts;
+
+    for my $i (0 .. $num_charts-1 ) {
+        my $row         =   $charts[$i]->[0];
+        my $col         =   $charts[$i]->[1];
+        my $chart        =   $charts[$i]->[2];
+        my $x_offset    =   $charts[$i]->[3];
+        my $y_offset    =   $charts[$i]->[4];
+        my $scale_x     =   $charts[$i]->[5];
+        my $scale_y     =   $charts[$i]->[6];
+        my $width       =   526;
+        my $height      =   319;
+
+        $width  *= $scale_x if $scale_x;
+        $height *= $scale_y if $scale_y;
+
+        # Calculate the positions of chart object.
+        my @vertices = $self->_position_object( $col,
+                                                $row,
+                                                $x_offset,
+                                                $y_offset,
+                                                $width,
+                                                $height
+                                              );
+
+
+        if ($i == 0 and not $num_objects) {
+            # Write the parent MSODRAWIING record.
+            my $dg_length    = 192 + 120*($num_charts -1);
+            my $spgr_length  = 168 + 120*($num_charts -1);
+
+               $dg_length   +=  96 *$num_filters;
+               $spgr_length +=  96 *$num_filters;
+
+               $dg_length   += 128 *$num_comments;
+               $spgr_length += 128 *$num_comments;
+
+
+            my $data        = $self->_store_mso_dg_container($dg_length);
+               $data       .= $self->_store_mso_dg(@ids);
+               $data       .= $self->_store_mso_spgr_container($spgr_length);
+               $data       .= $self->_store_mso_sp_container(40);
+               $data       .= $self->_store_mso_spgr();
+               $data       .= $self->_store_mso_sp(0x0, $spid++, 0x0005);
+               $data       .= $self->_store_mso_sp_container(112);
+               $data       .= $self->_store_mso_sp(201, $spid++, 0x0A00);
+               $data       .= $self->_store_mso_opt_chart();
+               $data       .= $self->_store_mso_client_anchor(0, @vertices);
+               $data       .= $self->_store_mso_client_data();
+
+            $length         = length $data;
+            my $header      = pack("vv", $record, $length);
+            $self->_append($header, $data);
+
+        }
+        else {
+            # Write the child MSODRAWIING record.
+            my $data        = $self->_store_mso_sp_container(112);
+               $data       .= $self->_store_mso_sp(201, $spid++, 0x0A00);
+               $data       .= $self->_store_mso_opt_chart();
+               $data       .= $self->_store_mso_client_anchor(0, @vertices);
+               $data       .= $self->_store_mso_client_data();
+
+            $length         = length $data;
+            my $header      = pack("vv", $record, $length);
+            $self->_append($header, $data);
+
+
+        }
+
+        $self->_store_obj_chart($num_objects+$i+1);
+        $self->_store_chart_binary($chart);
+    }
+
+
+    # Simulate the EXTERNSHEET link between the chart and data using a formula
+    # such as '=Sheet1!A1'.
+    # TODO. Won't work for external data refs. Also should use a more direct
+    #       method.
+    #
+    my $formula = "='$self->{_name}'!A1";
+    $self->store_formula($formula);
+
+    $self->{_object_ids}->[0] = $spid;
+}
+
+
+###############################################################################
+#
+# _store_chart_binary
+#
+# Add the binary data for a chart. This could either be from a Chart object
+# or from an external binary file (for backwards compatibility).
+#
+sub _store_chart_binary {
+
+    my $self  = shift;
+    my $chart = $_[0];
+    my $tmp;
+
+
+    if ( ref $chart ) {
+        $chart->_close();
+        my $tmp = $chart->get_data();
+        $self->_append( $tmp );
+    }
+    else {
+
+        my $filehandle = FileHandle->new( $chart )
+          or die "Couldn't open $chart in insert_chart(): $!.\n";
+
+        binmode( $filehandle );
+
+        while ( read( $filehandle, $tmp, 4096 ) ) {
+            $self->_append( $tmp );
+        }
+    }
+}
+
+
+###############################################################################
+#
+# _store_filters()
+#
+# Store the collections of records that make up filters.
+#
+sub _store_filters {
+
+    my $self            = shift;
+
+    my $record          = 0x00EC;           # Record identifier
+    my $length          = 0x0000;           # Bytes to follow
+
+    my @ids             = @{$self->{_object_ids}};
+    my $spid            = shift @ids;
+
+    my $filter_area     = $self->{_filter_area};
+    my $num_filters     = $self->{_filter_count};
+
+    my $num_comments    = @{$self->{_comments_array}};
+
+    # Number of objects written so far.
+    my $num_objects     = @{$self->{_images_array}}
+                        + @{$self->{_charts_array}};
+
+    # Skip this if there aren't any filters.
+    return unless $num_filters;
+
+
+    my ($row1, $row2, $col1, $col2) = @$filter_area;
+
+    for my $i (0 .. $num_filters-1 ) {
+
+        my @vertices = ( $col1 +$i,
+                         0,
+                         $row1,
+                         0,
+                         $col1 +$i +1,
+                         0,
+                         $row1 +1,
+                         0);
+
+        if ($i == 0 and not $num_objects) {
+            # Write the parent MSODRAWIING record.
+            my $dg_length    = 168 + 96*($num_filters -1);
+            my $spgr_length  = 144 + 96*($num_filters -1);
+
+               $dg_length   += 128 *$num_comments;
+               $spgr_length += 128 *$num_comments;
+
+
+            my $data        = $self->_store_mso_dg_container($dg_length);
+               $data       .= $self->_store_mso_dg(@ids);
+               $data       .= $self->_store_mso_spgr_container($spgr_length);
+               $data       .= $self->_store_mso_sp_container(40);
+               $data       .= $self->_store_mso_spgr();
+               $data       .= $self->_store_mso_sp(0x0, $spid++, 0x0005);
+               $data       .= $self->_store_mso_sp_container(88);
+               $data       .= $self->_store_mso_sp(201, $spid++, 0x0A00);
+               $data       .= $self->_store_mso_opt_filter();
+               $data       .= $self->_store_mso_client_anchor(1, @vertices);
+               $data       .= $self->_store_mso_client_data();
+
+            $length         = length $data;
+            my $header      = pack("vv", $record, $length);
+            $self->_append($header, $data);
+
+        }
+        else {
+            # Write the child MSODRAWIING record.
+            my $data        = $self->_store_mso_sp_container(88);
+               $data       .= $self->_store_mso_sp(201, $spid++, 0x0A00);
+               $data       .= $self->_store_mso_opt_filter();
+               $data       .= $self->_store_mso_client_anchor(1, @vertices);
+               $data       .= $self->_store_mso_client_data();
+
+            $length         = length $data;
+            my $header      = pack("vv", $record, $length);
+            $self->_append($header, $data);
+
+
+        }
+
+        $self->_store_obj_filter($num_objects+$i+1, $col1 +$i);
+    }
+
+
+    # Simulate the EXTERNSHEET link between the filter and data using a formula
+    # such as '=Sheet1!A1'.
+    # TODO. Won't work for external data refs. Also should use a more direct
+    #       method.
+    #
+    my $formula = "='$self->{_name}'!A1";
+    $self->store_formula($formula);
+
+    $self->{_object_ids}->[0] = $spid;
+}
+
+
+###############################################################################
+#
+# _store_comments()
+#
+# Store the collections of records that make up cell comments.
+#
+# NOTE: We write the comment objects last since that makes it a little easier
+# to write the NOTE records directly after the MSODRAWIING records.
+#
+sub _store_comments {
+
+    my $self            = shift;
+
+    my $record          = 0x00EC;           # Record identifier
+    my $length          = 0x0000;           # Bytes to follow
+
+    my @ids             = @{$self->{_object_ids}};
+    my $spid            = shift @ids;
+
+    my @comments        = @{$self->{_comments_array}};
+    my $num_comments    = scalar @comments;
+
+    # Number of objects written so far.
+    my $num_objects     = @{$self->{_images_array}}
+                        +   $self->{_filter_count}
+                        + @{$self->{_charts_array}};
+
+    # Skip this if there aren't any comments.
+    return unless $num_comments;
+
+    for my $i (0 .. $num_comments-1) {
+
+        my $row         =   $comments[$i]->[0];
+        my $col         =   $comments[$i]->[1];
+        my $str         =   $comments[$i]->[2];
+        my $encoding    =   $comments[$i]->[3];
+        my $visible     =   $comments[$i]->[6];
+        my $color       =   $comments[$i]->[7];
+        my @vertices    = @{$comments[$i]->[8]};
+        my $str_len     = length $str;
+           $str_len    /= 2 if $encoding; # Num of chars not bytes.
+        my $formats     = [[0, 9], [$str_len, 0]];
+
+
+        if ($i == 0 and not $num_objects) {
+            # Write the parent MSODRAWIING record.
+            my $dg_length   = 200 + 128*($num_comments -1);
+            my $spgr_length = 176 + 128*($num_comments -1);
+
+            my $data        = $self->_store_mso_dg_container($dg_length);
+               $data       .= $self->_store_mso_dg(@ids);
+               $data       .= $self->_store_mso_spgr_container($spgr_length);
+               $data       .= $self->_store_mso_sp_container(40);
+               $data       .= $self->_store_mso_spgr();
+               $data       .= $self->_store_mso_sp(0x0, $spid++, 0x0005);
+               $data       .= $self->_store_mso_sp_container(120);
+               $data       .= $self->_store_mso_sp(202, $spid++, 0x0A00);
+               $data       .= $self->_store_mso_opt_comment(0x80, $visible, $color);
+               $data       .= $self->_store_mso_client_anchor(3, @vertices);
+               $data       .= $self->_store_mso_client_data();
+
+            $length         = length $data;
+            my $header      = pack("vv", $record, $length);
+            $self->_append($header, $data);
+
+        }
+        else {
+            # Write the child MSODRAWIING record.
+            my $data        = $self->_store_mso_sp_container(120);
+               $data       .= $self->_store_mso_sp(202, $spid++, 0x0A00);
+               $data       .= $self->_store_mso_opt_comment(0x80, $visible, $color);
+               $data       .= $self->_store_mso_client_anchor(3, @vertices);
+               $data       .= $self->_store_mso_client_data();
+
+            $length         = length $data;
+            my $header      = pack("vv", $record, $length);
+            $self->_append($header, $data);
+
+
+        }
+
+        $self->_store_obj_comment($num_objects+$i+1);
+        $self->_store_mso_drawing_text_box();
+        $self->_store_txo($str_len);
+        $self->_store_txo_continue_1($str, $encoding);
+        $self->_store_txo_continue_2($formats);
+    }
+
+
+    # Write the NOTE records after MSODRAWIING records.
+    for my $i (0 .. $num_comments-1) {
+
+        my $row         = $comments[$i]->[0];
+        my $col         = $comments[$i]->[1];
+        my $author      = $comments[$i]->[4];
+        my $author_enc  = $comments[$i]->[5];
+        my $visible     = $comments[$i]->[6];
+
+        $self->_store_note($row, $col, $num_objects+$i+1,
+                           $author, $author_enc, $visible);
+    }
+}
+
+
+###############################################################################
+#
+# _store_mso_dg_container()
+#
+# Write the Escher DgContainer record that is part of MSODRAWING.
+#
+sub _store_mso_dg_container {
+
+    my $self        = shift;
+
+    my $type        = 0xF002;
+    my $version     = 15;
+    my $instance    = 0;
+    my $data        = '';
+    my $length      = $_[0];
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_dg()
+#
+# Write the Escher Dg record that is part of MSODRAWING.
+#
+sub _store_mso_dg {
+
+    my $self        = shift;
+
+    my $type        = 0xF008;
+    my $version     = 0;
+    my $instance    = $_[0];
+    my $data        = '';
+    my $length      = 8;
+
+    my $num_shapes  = $_[1];
+    my $max_spid    = $_[2];
+
+    $data           = pack "VV", $num_shapes, $max_spid;
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_spgr_container()
+#
+# Write the Escher SpgrContainer record that is part of MSODRAWING.
+#
+sub _store_mso_spgr_container {
+
+    my $self        = shift;
+
+    my $type        = 0xF003;
+    my $version     = 15;
+    my $instance    = 0;
+    my $data        = '';
+    my $length      = $_[0];
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_sp_container()
+#
+# Write the Escher SpContainer record that is part of MSODRAWING.
+#
+sub _store_mso_sp_container {
+
+    my $self        = shift;
+
+    my $type        = 0xF004;
+    my $version     = 15;
+    my $instance    = 0;
+    my $data        = '';
+    my $length      = $_[0];
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_spgr()
+#
+# Write the Escher Spgr record that is part of MSODRAWING.
+#
+sub _store_mso_spgr {
+
+    my $self        = shift;
+
+    my $type        = 0xF009;
+    my $version     = 1;
+    my $instance    = 0;
+    my $data        = pack "VVVV", 0, 0, 0, 0;
+    my $length      = 16;
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_sp()
+#
+# Write the Escher Sp record that is part of MSODRAWING.
+#
+sub _store_mso_sp {
+
+    my $self        = shift;
+
+    my $type        = 0xF00A;
+    my $version     = 2;
+    my $instance    = $_[0];
+    my $data        = '';
+    my $length      = 8;
+
+    my $spid        = $_[1];
+    my $options     = $_[2];
+
+    $data           = pack "VV", $spid, $options;
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_opt_comment()
+#
+# Write the Escher Opt record that is part of MSODRAWING.
+#
+sub _store_mso_opt_comment {
+
+    my $self        = shift;
+
+    my $type        = 0xF00B;
+    my $version     = 3;
+    my $instance    = 9;
+    my $data        = '';
+    my $length      = 54;
+
+    my $spid        = $_[0];
+    my $visible     = $_[1];
+    my $colour      = $_[2] || 0x50;
+
+
+    # Use the visible flag if set by the user or else use the worksheet value.
+    # Note that the value used is the opposite of _store_note().
+    #
+    if (defined $visible) {
+        $visible = $visible                   ? 0x0000 : 0x0002;
+    }
+    else {
+        $visible = $self->{_comments_visible} ? 0x0000 : 0x0002;
+    }
+
+
+    $data    = pack "V",  $spid;
+    $data   .= pack "H*", '0000BF00080008005801000000008101' ;
+    $data   .= pack "C",  $colour;
+    $data   .= pack "H*", '000008830150000008BF011000110001' .
+                          '02000000003F0203000300BF03';
+    $data   .= pack "v",  $visible;
+    $data   .= pack "H*", '0A00';
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_opt_image()
+#
+# Write the Escher Opt record that is part of MSODRAWING.
+#
+sub _store_mso_opt_image {
+
+    my $self        = shift;
+
+    my $type        = 0xF00B;
+    my $version     = 3;
+    my $instance    = 3;
+    my $data        = '';
+    my $length      = undef;
+    my $spid        = $_[0];
+
+    $data    = pack 'v', 0x4104;        # Blip -> pib
+    $data   .= pack 'V', $spid;
+    $data   .= pack 'v', 0x01BF;        # Fill Style -> fNoFillHitTest
+    $data   .= pack 'V', 0x00010000;
+    $data   .= pack 'v', 0x03BF;        # Group Shape -> fPrint
+    $data   .= pack 'V', 0x00080000;
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_opt_chart()
+#
+# Write the Escher Opt record that is part of MSODRAWING.
+#
+sub _store_mso_opt_chart {
+
+    my $self        = shift;
+
+    my $type        = 0xF00B;
+    my $version     = 3;
+    my $instance    = 9;
+    my $data        = '';
+    my $length      = undef;
+
+    $data    = pack 'v', 0x007F;        # Protection -> fLockAgainstGrouping
+    $data   .= pack 'V', 0x01040104;
+
+    $data   .= pack 'v', 0x00BF;        # Text -> fFitTextToShape
+    $data   .= pack 'V', 0x00080008;
+
+    $data   .= pack 'v', 0x0181;        # Fill Style -> fillColor
+    $data   .= pack 'V', 0x0800004E ;
+
+    $data   .= pack 'v', 0x0183;        # Fill Style -> fillBackColor
+    $data   .= pack 'V', 0x0800004D;
+
+    $data   .= pack 'v', 0x01BF;        # Fill Style -> fNoFillHitTest
+    $data   .= pack 'V', 0x00110010;
+
+    $data   .= pack 'v', 0x01C0;        # Line Style -> lineColor
+    $data   .= pack 'V', 0x0800004D;
+
+    $data   .= pack 'v', 0x01FF;        # Line Style -> fNoLineDrawDash
+    $data   .= pack 'V', 0x00080008;
+
+    $data   .= pack 'v', 0x023F;        # Shadow Style -> fshadowObscured
+    $data   .= pack 'V', 0x00020000;
+
+    $data   .= pack 'v', 0x03BF;        # Group Shape -> fPrint
+    $data   .= pack 'V', 0x00080000;
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_opt_filter()
+#
+# Write the Escher Opt record that is part of MSODRAWING.
+#
+sub _store_mso_opt_filter {
+
+    my $self        = shift;
+
+    my $type        = 0xF00B;
+    my $version     = 3;
+    my $instance    = 5;
+    my $data        = '';
+    my $length      = undef;
+
+
+
+    $data    = pack 'v', 0x007F;        # Protection -> fLockAgainstGrouping
+    $data   .= pack 'V', 0x01040104;
+
+    $data   .= pack 'v', 0x00BF;        # Text -> fFitTextToShape
+    $data   .= pack 'V', 0x00080008;
+
+    $data   .= pack 'v', 0x01BF;        # Fill Style -> fNoFillHitTest
+    $data   .= pack 'V', 0x00010000;
+
+    $data   .= pack 'v', 0x01FF;        # Line Style -> fNoLineDrawDash
+    $data   .= pack 'V', 0x00080000;
+
+    $data   .= pack 'v', 0x03BF;        # Group Shape -> fPrint
+    $data   .= pack 'V', 0x000A0000;
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_client_anchor()
+#
+# Write the Escher ClientAnchor record that is part of MSODRAWING.
+#
+sub _store_mso_client_anchor {
+
+    my $self        = shift;
+
+    my $type        = 0xF010;
+    my $version     = 0;
+    my $instance    = 0;
+    my $data        = '';
+    my $length      = 18;
+
+    my $flag        = shift;
+
+    my $col_start   = $_[0];    # Col containing upper left corner of object
+    my $x1          = $_[1];    # Distance to left side of object
+
+    my $row_start   = $_[2];    # Row containing top left corner of object
+    my $y1          = $_[3];    # Distance to top of object
+
+    my $col_end     = $_[4];    # Col containing lower right corner of object
+    my $x2          = $_[5];    # Distance to right side of object
+
+    my $row_end     = $_[6];    # Row containing bottom right corner of object
+    my $y2          = $_[7];    # Distance to bottom of object
+
+    $data   = pack "v9",    $flag,
+                            $col_start, $x1,
+                            $row_start, $y1,
+                            $col_end,   $x2,
+                            $row_end,   $y2;
+
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_mso_client_data()
+#
+# Write the Escher ClientData record that is part of MSODRAWING.
+#
+sub _store_mso_client_data {
+
+    my $self        = shift;
+
+    my $type        = 0xF011;
+    my $version     = 0;
+    my $instance    = 0;
+    my $data        = '';
+    my $length      = 0;
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_obj_comment()
+#
+# Write the OBJ record that is part of cell comments.
+#
+sub _store_obj_comment {
+
+    my $self        = shift;
+
+    my $record      = 0x005D;   # Record identifier
+    my $length      = 0x0034;   # Bytes to follow
+
+    my $obj_id      = $_[0];    # Object ID number.
+    my $obj_type    = 0x0019;   # Object type (comment).
+    my $data        = '';       # Record data.
+
+    my $sub_record  = 0x0000;   # Sub-record identifier.
+    my $sub_length  = 0x0000;   # Length of sub-record.
+    my $sub_data    = '';       # Data of sub-record.
+    my $options     = 0x4011;
+    my $reserved    = 0x0000;
+
+    # Add ftCmo (common object data) subobject
+    $sub_record     = 0x0015;   # ftCmo
+    $sub_length     = 0x0012;
+    $sub_data       = pack "vvvVVV", $obj_type, $obj_id,   $options,
+                                     $reserved, $reserved, $reserved;
+    $data           = pack("vv",     $sub_record, $sub_length);
+    $data          .= $sub_data;
+
+
+    # Add ftNts (note structure) subobject
+    $sub_record     = 0x000D;   # ftNts
+    $sub_length     = 0x0016;
+    $sub_data       = pack "VVVVVv", ($reserved) x 6;
+    $data          .= pack("vv",     $sub_record, $sub_length);
+    $data          .= $sub_data;
+
+
+    # Add ftEnd (end of object) subobject
+    $sub_record     = 0x0000;   # ftNts
+    $sub_length     = 0x0000;
+    $data          .= pack("vv",     $sub_record, $sub_length);
+
+
+    # Pack the record.
+    my $header  = pack("vv",        $record, $length);
+
+    $self->_append($header, $data);
+
+}
+
+
+###############################################################################
+#
+# _store_obj_image()
+#
+# Write the OBJ record that is part of image records.
+#
+sub _store_obj_image {
+
+    my $self        = shift;
+
+    my $record      = 0x005D;   # Record identifier
+    my $length      = 0x0026;   # Bytes to follow
+
+    my $obj_id      = $_[0];    # Object ID number.
+    my $obj_type    = 0x0008;   # Object type (Picture).
+    my $data        = '';       # Record data.
+
+    my $sub_record  = 0x0000;   # Sub-record identifier.
+    my $sub_length  = 0x0000;   # Length of sub-record.
+    my $sub_data    = '';       # Data of sub-record.
+    my $options     = 0x6011;
+    my $reserved    = 0x0000;
+
+    # Add ftCmo (common object data) subobject
+    $sub_record     = 0x0015;   # ftCmo
+    $sub_length     = 0x0012;
+    $sub_data       = pack 'vvvVVV', $obj_type, $obj_id,   $options,
+                                     $reserved, $reserved, $reserved;
+    $data           = pack 'vv',     $sub_record, $sub_length;
+    $data          .= $sub_data;
+
+
+    # Add ftCf (Clipboard format) subobject
+    $sub_record     = 0x0007;   # ftCf
+    $sub_length     = 0x0002;
+    $sub_data       = pack 'v',      0xFFFF;
+    $data          .= pack 'vv',     $sub_record, $sub_length;
+    $data          .= $sub_data;
+
+    # Add ftPioGrbit (Picture option flags) subobject
+    $sub_record     = 0x0008;   # ftPioGrbit
+    $sub_length     = 0x0002;
+    $sub_data       = pack 'v',      0x0001;
+    $data          .= pack 'vv',     $sub_record, $sub_length;
+    $data          .= $sub_data;
+
+
+    # Add ftEnd (end of object) subobject
+    $sub_record     = 0x0000;   # ftNts
+    $sub_length     = 0x0000;
+    $data          .= pack 'vv',     $sub_record, $sub_length;
+
+
+    # Pack the record.
+    my $header  = pack('vv',        $record, $length);
+
+    $self->_append($header, $data);
+
+}
+
+
+###############################################################################
+#
+# _store_obj_chart()
+#
+# Write the OBJ record that is part of chart records.
+#
+sub _store_obj_chart {
+
+    my $self        = shift;
+
+    my $record      = 0x005D;   # Record identifier
+    my $length      = 0x001A;   # Bytes to follow
+
+    my $obj_id      = $_[0];    # Object ID number.
+    my $obj_type    = 0x0005;   # Object type (chart).
+    my $data        = '';       # Record data.
+
+    my $sub_record  = 0x0000;   # Sub-record identifier.
+    my $sub_length  = 0x0000;   # Length of sub-record.
+    my $sub_data    = '';       # Data of sub-record.
+    my $options     = 0x6011;
+    my $reserved    = 0x0000;
+
+    # Add ftCmo (common object data) subobject
+    $sub_record     = 0x0015;   # ftCmo
+    $sub_length     = 0x0012;
+    $sub_data       = pack 'vvvVVV', $obj_type, $obj_id,   $options,
+                                     $reserved, $reserved, $reserved;
+    $data           = pack 'vv',     $sub_record, $sub_length;
+    $data          .= $sub_data;
+
+    # Add ftEnd (end of object) subobject
+    $sub_record     = 0x0000;   # ftNts
+    $sub_length     = 0x0000;
+    $data          .= pack 'vv',     $sub_record, $sub_length;
+
+
+    # Pack the record.
+    my $header  = pack('vv',        $record, $length);
+
+    $self->_append($header, $data);
+
+}
+
+
+
+
+###############################################################################
+#
+# _store_obj_filter()
+#
+# Write the OBJ record that is part of filter records.
+#
+sub _store_obj_filter {
+
+    my $self        = shift;
+
+    my $record      = 0x005D;   # Record identifier
+    my $length      = 0x0046;   # Bytes to follow
+
+    my $obj_id      = $_[0];    # Object ID number.
+    my $obj_type    = 0x0014;   # Object type (combo box).
+    my $data        = '';       # Record data.
+
+    my $sub_record  = 0x0000;   # Sub-record identifier.
+    my $sub_length  = 0x0000;   # Length of sub-record.
+    my $sub_data    = '';       # Data of sub-record.
+    my $options     = 0x2101;
+    my $reserved    = 0x0000;
+
+    # Add ftCmo (common object data) subobject
+    $sub_record     = 0x0015;   # ftCmo
+    $sub_length     = 0x0012;
+    $sub_data       = pack 'vvvVVV', $obj_type, $obj_id,   $options,
+                                     $reserved, $reserved, $reserved;
+    $data           = pack 'vv',     $sub_record, $sub_length;
+    $data          .= $sub_data;
+
+    # Add ftSbs Scroll bar subobject
+    $sub_record     = 0x000C;   # ftSbs
+    $sub_length     = 0x0014;
+    $sub_data       = pack 'H*', '0000000000000000640001000A00000010000100';
+    $data          .= pack 'vv',     $sub_record, $sub_length;
+    $data          .= $sub_data;
+
+
+    # Add ftLbsData (List box data) subobject
+    $sub_record     = 0x0013;   # ftLbsData
+    $sub_length     = 0x1FEE;   # Special case (undocumented).
+
+
+    # If the filter is active we set one of the undocumented flags.
+    my $col         = $_[1];
+
+    if ($self->{_filter_cols}->{$col}) {
+        $sub_data       = pack 'H*', '000000000100010300000A0008005700';
+    }
+    else {
+        $sub_data       = pack 'H*', '00000000010001030000020008005700';
+    }
+
+    $data          .= pack 'vv',     $sub_record, $sub_length;
+    $data          .= $sub_data;
+
+
+    # Add ftEnd (end of object) subobject
+    $sub_record     = 0x0000;   # ftNts
+    $sub_length     = 0x0000;
+    $data          .= pack 'vv', $sub_record, $sub_length;
+
+    # Pack the record.
+    my $header  = pack('vv',        $record, $length);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_mso_drawing_text_box()
+#
+# Write the MSODRAWING ClientTextbox record that is part of comments.
+#
+sub _store_mso_drawing_text_box {
+
+    my $self        = shift;
+
+    my $record      = 0x00EC;           # Record identifier
+    my $length      = 0x0008;           # Bytes to follow
+
+
+    my $data        = $self->_store_mso_client_text_box();
+    my $header      = pack("vv", $record, $length);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_mso_client_text_box()
+#
+# Write the Escher ClientTextbox record that is part of MSODRAWING.
+#
+sub _store_mso_client_text_box {
+
+    my $self        = shift;
+
+    my $type        = 0xF00D;
+    my $version     = 0;
+    my $instance    = 0;
+    my $data        = '';
+    my $length      = 0;
+
+
+    return $self->_add_mso_generic($type, $version, $instance, $data, $length);
+}
+
+
+###############################################################################
+#
+# _store_txo()
+#
+# Write the worksheet TXO record that is part of cell comments.
+#
+sub _store_txo {
+
+    my $self        = shift;
+
+    my $record      = 0x01B6;               # Record identifier
+    my $length      = 0x0012;               # Bytes to follow
+
+    my $string_len  = $_[0];                # Length of the note text.
+    my $format_len  = $_[1] || 16;          # Length of the format runs.
+    my $rotation    = $_[2] || 0;           # Options
+    my $grbit       = 0x0212;               # Options
+    my $reserved    = 0x0000;               # Options
+
+    # Pack the record.
+    my $header  = pack("vv",        $record, $length);
+    my $data    = pack("vvVvvvV",   $grbit, $rotation, $reserved, $reserved,
+                                    $string_len, $format_len, $reserved);
+
+    $self->_append($header, $data);
+
+}
+
+
+###############################################################################
+#
+# _store_txo_continue_1()
+#
+# Write the first CONTINUE record to follow the TXO record. It contains the
+# text data.
+#
+sub _store_txo_continue_1 {
+
+    my $self        = shift;
+
+    my $record      = 0x003C;               # Record identifier
+    my $string      = $_[0];                # Comment string.
+    my $encoding    = $_[1] || 0;           # Encoding of the string.
+
+
+    # Split long comment strings into smaller continue blocks if necessary.
+    # We can't let BIFFwriter::_add_continue() handled this since an extra
+    # encoding byte has to be added similar to the SST block.
+    #
+    # We make the limit size smaller than the _add_continue() size and even
+    # so that UTF16 chars occur in the same block.
+    #
+    my $limit = 8218;
+    while (length($string) > $limit) {
+        my $tmp_str = substr($string, 0, $limit, "");
+
+        my $data    = pack("C", $encoding) . $tmp_str;
+        my $length  = length $data;
+        my $header  = pack("vv", $record, $length);
+
+        $self->_append($header, $data);
+    }
+
+    # Pack the record.
+    my $data    = pack("C", $encoding) . $string;
+    my $length  = length $data;
+    my $header  = pack("vv", $record, $length);
+
+    $self->_append($header, $data);
+
+}
+
+
+###############################################################################
+#
+# _store_txo_continue_2()
+#
+# Write the second CONTINUE record to follow the TXO record. It contains the
+# formatting information for the string.
+#
+sub _store_txo_continue_2 {
+
+    my $self        = shift;
+
+    my $record      = 0x003C;               # Record identifier
+    my $length      = 0x0000;               # Bytes to follow
+    my $formats     = $_[0];                # Formatting information
+
+
+    # Pack the record.
+    my $data = '';
+
+    for my $a_ref (@$formats) {
+        $data .= pack "vvV", $a_ref->[0], $a_ref->[1], 0x0;
+    }
+
+    $length     = length $data;
+    my $header  = pack("vv", $record, $length);
+
+
+    $self->_append($header, $data);
+
+}
+
+
+###############################################################################
+#
+# _store_note()
+#
+# Write the worksheet NOTE record that is part of cell comments.
+#
+sub _store_note {
+
+    my $self        = shift;
+
+    my $record      = 0x001C;               # Record identifier
+    my $length      = 0x000C;               # Bytes to follow
+
+    my $row         = $_[0];
+    my $col         = $_[1];
+    my $obj_id      = $_[2];
+    my $author      = $_[3] || $self->{_comments_author};
+    my $author_enc  = $_[4] || $self->{_comments_author_enc};
+    my $visible     = $_[5];
+
+
+    # Use the visible flag if set by the user or else use the worksheet value.
+    # The flag is also set in _store_mso_opt_comment() but with the opposite
+    # value.
+    if (defined $visible) {
+        $visible = $visible                   ? 0x0002 : 0x0000;
+    }
+    else {
+        $visible = $self->{_comments_visible} ? 0x0002 : 0x0000;
+    }
+
+
+    # Get the number of chars in the author string (not bytes).
+    my $num_chars  = length $author;
+       $num_chars /= 2 if $author_enc;
+
+
+    # Null terminate the author string.
+    $author .= "\0";
+
+
+    # Pack the record.
+    my $data    = pack("vvvvvC", $row, $col, $visible, $obj_id,
+                                 $num_chars, $author_enc);
+
+    $length     = length($data) + length($author);
+    my $header  = pack("vv", $record, $length);
+
+    $self->_append($header, $data, $author);
+}
+
+
+###############################################################################
+#
+# _comment_params()
+#
+# This method handles the additional optional parameters to write_comment() as
+# well as calculating the comment object position and vertices.
+#
+sub _comment_params {
+
+    my $self            = shift;
+
+    my $row             = shift;
+    my $col             = shift;
+    my $string          = shift;
+
+    my $default_width   = 128;
+    my $default_height  = 74;
+
+    my %params  = (
+                    author          => '',
+                    author_encoding => 0,
+                    encoding        => 0,
+                    color           => undef,
+                    start_cell      => undef,
+                    start_col       => undef,
+                    start_row       => undef,
+                    visible         => undef,
+                    width           => $default_width,
+                    height          => $default_height,
+                    x_offset        => undef,
+                    x_scale         => 1,
+                    y_offset        => undef,
+                    y_scale         => 1,
+                  );
+
+
+    # Overwrite the defaults with any user supplied values. Incorrect or
+    # misspelled parameters are silently ignored.
+    %params     = (%params, @_);
+
+
+    # Ensure that a width and height have been set.
+    $params{width}  = $default_width  if not $params{width};
+    $params{height} = $default_height if not $params{height};
+
+
+    # Check that utf16 strings have an even number of bytes.
+    if ($params{encoding}) {
+        croak "Uneven number of bytes in comment string"
+               if length($string) % 2;
+
+        # Change from UTF-16BE to UTF-16LE
+        $string = pack 'v*', unpack 'n*', $string;
+    }
+
+    if ($params{author_encoding}) {
+        croak "Uneven number of bytes in author string"
+                if length($params{author}) % 2;
+
+        # Change from UTF-16BE to UTF-16LE
+        $params{author} = pack 'v*', unpack 'n*', $params{author};
+    }
+
+
+    # Handle utf8 strings in perl 5.8.
+    if ($] >= 5.008) {
+        require Encode;
+
+        if (Encode::is_utf8($string)) {
+            $string = Encode::encode("UTF-16LE", $string);
+            $params{encoding} = 1;
+        }
+
+        if (Encode::is_utf8($params{author})) {
+            $params{author} = Encode::encode("UTF-16LE", $params{author});
+            $params{author_encoding} = 1;
+        }
+    }
+
+
+    # Limit the string to the max number of chars (not bytes).
+    my $max_len  = 32767;
+       $max_len *= 2 if $params{encoding};
+
+    if (length($string) > $max_len) {
+        $string       = substr($string, 0, $max_len);
+    }
+
+
+    # Set the comment background colour.
+    my $color       = $params{color};
+       $color       = &Spreadsheet::WriteExcel::Format::_get_color($color);
+       $color       = 0x50 if $color == 0x7FFF; # Default color.
+    $params{color}  = $color;
+
+
+    # Convert a cell reference to a row and column.
+    if (defined $params{start_cell}) {
+        my ($row, $col)    = $self->_substitute_cellref($params{start_cell});
+        $params{start_row} = $row;
+        $params{start_col} = $col;
+    }
+
+
+    # Set the default start cell and offsets for the comment. These are
+    # generally fixed in relation to the parent cell. However there are
+    # some edge cases for cells at the, er, edges.
+    #
+    if (not defined $params{start_row}) {
+
+        if    ($row == 0    ) {$params{start_row} = 0      }
+        elsif ($row == 65533) {$params{start_row} = 65529  }
+        elsif ($row == 65534) {$params{start_row} = 65530  }
+        elsif ($row == 65535) {$params{start_row} = 65531  }
+        else                  {$params{start_row} = $row -1}
+    }
+
+    if (not defined $params{y_offset}) {
+
+        if    ($row == 0    ) {$params{y_offset}  = 2      }
+        elsif ($row == 65533) {$params{y_offset}  = 4      }
+        elsif ($row == 65534) {$params{y_offset}  = 4      }
+        elsif ($row == 65535) {$params{y_offset}  = 2      }
+        else                  {$params{y_offset}  = 7      }
+    }
+
+    if (not defined $params{start_col}) {
+
+        if    ($col == 253  ) {$params{start_col} = 250    }
+        elsif ($col == 254  ) {$params{start_col} = 251    }
+        elsif ($col == 255  ) {$params{start_col} = 252    }
+        else                  {$params{start_col} = $col +1}
+    }
+
+    if (not defined $params{x_offset}) {
+
+        if    ($col == 253  ) {$params{x_offset}  = 49     }
+        elsif ($col == 254  ) {$params{x_offset}  = 49     }
+        elsif ($col == 255  ) {$params{x_offset}  = 49     }
+        else                  {$params{x_offset}  = 15     }
+    }
+
+
+    # Scale the size of the comment box if required.
+    if ($params{x_scale}) {
+        $params{width}  = $params{width}  * $params{x_scale};
+    }
+
+    if ($params{y_scale}) {
+        $params{height} = $params{height} * $params{y_scale};
+    }
+
+
+    # Calculate the positions of comment object.
+    my @vertices = $self->_position_object( $params{start_col},
+                                            $params{start_row},
+                                            $params{x_offset},
+                                            $params{y_offset},
+                                            $params{width},
+                                            $params{height}
+                                          );
+
+    return(
+           $row,
+           $col,
+           $string,
+           $params{encoding},
+           $params{author},
+           $params{author_encoding},
+           $params{visible},
+           $params{color},
+           [@vertices]
+          );
+}
+
+
+
+#
+# DATA VALIDATION
+#
+
+###############################################################################
+#
+# data_validation($row, $col, {...})
+#
+# This method handles the interface to Excel data validation.
+# Somewhat ironically the this requires a lot of validation code since the
+# interface is flexible and covers a several types of data validation.
+#
+# We allow data validation to be called on one cell or a range of cells. The
+# hashref contains the validation parameters and must be the last param:
+#    data_validation($row, $col, {...})
+#    data_validation($first_row, $first_col, $last_row, $last_col, {...})
+#
+# Returns  0 : normal termination
+#         -1 : insufficient number of arguments
+#         -2 : row or column out of range
+#         -3 : incorrect parameter.
+#
+sub data_validation {
+
+    my $self = shift;
+
+    # Check for a cell reference in A1 notation and substitute row and column
+    if ($_[0] =~ /^\D/) {
+        @_ = $self->_substitute_cellref(@_);
+    }
+
+    # Check for a valid number of args.
+    if (@_ != 5 && @_ != 3) { return -1 }
+
+    # The final hashref contains the validation parameters.
+    my $param = pop;
+
+    # Make the last row/col the same as the first if not defined.
+    my ($row1, $col1, $row2, $col2) = @_;
+    if (!defined $row2) {
+        $row2 = $row1;
+        $col2 = $col1;
+    }
+
+    # Check that row and col are valid without storing the values.
+    return -2 if $self->_check_dimensions($row1, $col1, 1, 1);
+    return -2 if $self->_check_dimensions($row2, $col2, 1, 1);
+
+
+    # Check that the last parameter is a hash list.
+    if (ref $param ne 'HASH') {
+        carp "Last parameter '$param' in data_validation() must be a hash ref";
+        return -3;
+    }
+
+    # List of valid input parameters.
+    my %valid_parameter = (
+                              validate          => 1,
+                              criteria          => 1,
+                              value             => 1,
+                              source            => 1,
+                              minimum           => 1,
+                              maximum           => 1,
+                              ignore_blank      => 1,
+                              dropdown          => 1,
+                              show_input        => 1,
+                              input_title       => 1,
+                              input_message     => 1,
+                              show_error        => 1,
+                              error_title       => 1,
+                              error_message     => 1,
+                              error_type        => 1,
+                              other_cells       => 1,
+                          );
+
+    # Check for valid input parameters.
+    for my $param_key (keys %$param) {
+        if (not exists $valid_parameter{$param_key}) {
+            carp "Unknown parameter '$param_key' in data_validation()";
+            return -3;
+        }
+    }
+
+    # Map alternative parameter names 'source' or 'minimum' to 'value'.
+    $param->{value} = $param->{source}  if defined $param->{source};
+    $param->{value} = $param->{minimum} if defined $param->{minimum};
+
+    # 'validate' is a required parameter.
+    if (not exists $param->{validate}) {
+        carp "Parameter 'validate' is required in data_validation()";
+        return -3;
+    }
+
+
+    # List of  valid validation types.
+    my %valid_type = (
+                              'any'             => 0,
+                              'any value'       => 0,
+                              'whole number'    => 1,
+                              'whole'           => 1,
+                              'integer'         => 1,
+                              'decimal'         => 2,
+                              'list'            => 3,
+                              'date'            => 4,
+                              'time'            => 5,
+                              'text length'     => 6,
+                              'length'          => 6,
+                              'custom'          => 7,
+                      );
+
+
+    # Check for valid validation types.
+    if (not exists $valid_type{lc($param->{validate})}) {
+        carp "Unknown validation type '$param->{validate}' for parameter " .
+             "'validate' in data_validation()";
+        return -3;
+    }
+    else {
+        $param->{validate} = $valid_type{lc($param->{validate})};
+    }
+
+
+    # No action is required for validation type 'any'.
+    # TODO: we should perhaps store 'any' for message only validations.
+    return 0 if $param->{validate} == 0;
+
+
+    # The list and custom validations don't have a criteria so we use a default
+    # of 'between'.
+    if ($param->{validate} == 3 || $param->{validate} == 7) {
+        $param->{criteria}  = 'between';
+        $param->{maximum}   = undef;
+    }
+
+    # 'criteria' is a required parameter.
+    if (not exists $param->{criteria}) {
+        carp "Parameter 'criteria' is required in data_validation()";
+        return -3;
+    }
+
+
+    # List of valid criteria types.
+    my %criteria_type = (
+                              'between'                     => 0,
+                              'not between'                 => 1,
+                              'equal to'                    => 2,
+                              '='                           => 2,
+                              '=='                          => 2,
+                              'not equal to'                => 3,
+                              '!='                          => 3,
+                              '<>'                          => 3,
+                              'greater than'                => 4,
+                              '>'                           => 4,
+                              'less than'                   => 5,
+                              '<'                           => 5,
+                              'greater than or equal to'    => 6,
+                              '>='                          => 6,
+                              'less than or equal to'       => 7,
+                              '<='                          => 7,
+                      );
+
+    # Check for valid criteria types.
+    if (not exists $criteria_type{lc($param->{criteria})}) {
+        carp "Unknown criteria type '$param->{criteria}' for parameter " .
+             "'criteria' in data_validation()";
+        return -3;
+    }
+    else {
+        $param->{criteria} = $criteria_type{lc($param->{criteria})};
+    }
+
+
+    # 'Between' and 'Not between' criteria require 2 values.
+    if ($param->{criteria} == 0 || $param->{criteria} == 1) {
+        if (not exists $param->{maximum}) {
+            carp "Parameter 'maximum' is required in data_validation() " .
+                 "when using 'between' or 'not between' criteria";
+            return -3;
+        }
+    }
+    else {
+        $param->{maximum} = undef;
+    }
+
+
+
+    # List of valid error dialog types.
+    my %error_type = (
+                              'stop'        => 0,
+                              'warning'     => 1,
+                              'information' => 2,
+                     );
+
+    # Check for valid error dialog types.
+    if (not exists $param->{error_type}) {
+        $param->{error_type} = 0;
+    }
+    elsif (not exists $error_type{lc($param->{error_type})}) {
+        carp "Unknown criteria type '$param->{error_type}' for parameter " .
+             "'error_type' in data_validation()";
+        return -3;
+    }
+    else {
+        $param->{error_type} = $error_type{lc($param->{error_type})};
+    }
+
+
+    # Convert date/times value if required.
+    if ($param->{validate} == 4 || $param->{validate} == 5) {
+        if ($param->{value} =~ /T/) {
+            my $date_time = $self->convert_date_time($param->{value});
+
+            if (!defined $date_time) {
+                carp "Invalid date/time value '$param->{value}' " .
+                     "in data_validation()";
+                return -3;
+            }
+            else {
+                $param->{value} = $date_time;
+            }
+        }
+        if (defined $param->{maximum} && $param->{maximum} =~ /T/) {
+            my $date_time = $self->convert_date_time($param->{maximum});
+
+            if (!defined $date_time) {
+                carp "Invalid date/time value '$param->{maximum}' " .
+                     "in data_validation()";
+                return -3;
+            }
+            else {
+                $param->{maximum} = $date_time;
+            }
+        }
+    }
+
+
+    # Set some defaults if they haven't been defined by the user.
+    $param->{ignore_blank}  = 1 if !defined $param->{ignore_blank};
+    $param->{dropdown}      = 1 if !defined $param->{dropdown};
+    $param->{show_input}    = 1 if !defined $param->{show_input};
+    $param->{show_error}    = 1 if !defined $param->{show_error};
+
+
+    # These are the cells to which the validation is applied.
+    $param->{cells} = [[$row1, $col1, $row2, $col2]];
+
+    # A (for now) undocumented parameter to pass additional cell ranges.
+    if (exists $param->{other_cells}) {
+
+        push @{$param->{cells}}, @{$param->{other_cells}};
+    }
+
+    # Store the validation information until we close the worksheet.
+    push @{$self->{_validations}}, $param;
+}
+
+
+###############################################################################
+#
+# _store_validation_count()
+#
+# Store the count of the DV records to follow.
+#
+# Note, this could be wrapped into _store_dv() but we may require separate
+# handling of the object id at a later stage.
+#
+sub _store_validation_count {
+
+    my $self = shift;
+
+    my $dv_count = @{$self->{_validations}};
+    my $obj_id   = -1;
+
+    return unless $dv_count;
+
+    $self->_store_dval($obj_id , $dv_count);
+}
+
+
+###############################################################################
+#
+# _store_validations()
+#
+# Store the data_validation records.
+#
+sub _store_validations {
+
+    my $self = shift;
+
+    return unless scalar @{$self->{_validations}};
+
+    for my $param (@{$self->{_validations}}) {
+        $self->_store_dv(   $param->{cells},
+                            $param->{validate},
+                            $param->{criteria},
+                            $param->{value},
+                            $param->{maximum},
+                            $param->{input_title},
+                            $param->{input_message},
+                            $param->{error_title},
+                            $param->{error_message},
+                            $param->{error_type},
+                            $param->{ignore_blank},
+                            $param->{dropdown},
+                            $param->{show_input},
+                            $param->{show_error},
+                            );
+    }
+}
+
+
+###############################################################################
+#
+# _store_dval()
+#
+# Store the DV record which contains the number of and information common to
+# all DV structures.
+#
+sub _store_dval {
+
+    my $self        = shift;
+
+    my $record      = 0x01B2;       # Record identifier
+    my $length      = 0x0012;       # Bytes to follow
+
+    my $obj_id      = $_[0];        # Object ID number.
+    my $dv_count    = $_[1];        # Count of DV structs to follow.
+
+    my $flags       = 0x0004;       # Option flags.
+    my $x_coord     = 0x00000000;   # X coord of input box.
+    my $y_coord     = 0x00000000;   # Y coord of input box.
+
+
+    # Pack the record.
+    my $header = pack('vv', $record, $length);
+    my $data   = pack('vVVVV', $flags, $x_coord, $y_coord, $obj_id, $dv_count);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _store_dv()
+#
+# Store the DV record that specifies the data validation criteria and options
+# for a range of cells..
+#
+sub _store_dv {
+
+    my $self            = shift;
+
+    my $record          = 0x01BE;       # Record identifier
+    my $length          = 0x0000;       # Bytes to follow
+
+    my $flags           = 0x00000000;   # DV option flags.
+
+    my $cells           = $_[0];        # Aref of cells to which DV applies.
+    my $validation_type = $_[1];        # Type of data validation.
+    my $criteria_type   = $_[2];        # Validation criteria.
+    my $formula_1       = $_[3];        # Value/Source/Minimum formula.
+    my $formula_2       = $_[4];        # Maximum formula.
+    my $input_title     = $_[5];        # Title of input message.
+    my $input_message   = $_[6];        # Text of input message.
+    my $error_title     = $_[7];        # Title of error message.
+    my $error_message   = $_[8];        # Text of input message.
+    my $error_type      = $_[9];        # Error dialog type.
+    my $ignore_blank    = $_[10];       # Ignore blank cells.
+    my $dropdown        = $_[11];       # Display dropdown with list.
+    my $input_box       = $_[12];       # Display input box.
+    my $error_box       = $_[13];       # Display error box.
+    my $ime_mode        = 0;            # IME input mode for far east fonts.
+    my $str_lookup      = 0;            # See below.
+
+    # Set the string lookup flag for 'list' validations with a string array.
+    if ($validation_type == 3 && ref $formula_1 eq 'ARRAY')  {
+        $str_lookup = 1;
+    }
+
+    # The dropdown flag is stored as a negated value.
+    my $no_dropdown = not $dropdown;
+
+    # Set the required flags.
+    $flags |= $validation_type;
+    $flags |= $error_type       << 4;
+    $flags |= $str_lookup       << 7;
+    $flags |= $ignore_blank     << 8;
+    $flags |= $no_dropdown      << 9;
+    $flags |= $ime_mode         << 10;
+    $flags |= $input_box        << 18;
+    $flags |= $error_box        << 19;
+    $flags |= $criteria_type    << 20;
+
+    # Pack the validation formulas.
+    $formula_1 = $self->_pack_dv_formula($formula_1);
+    $formula_2 = $self->_pack_dv_formula($formula_2);
+
+    # Pack the input and error dialog strings.
+    $input_title   = $self->_pack_dv_string($input_title,   32 );
+    $error_title   = $self->_pack_dv_string($error_title,   32 );
+    $input_message = $self->_pack_dv_string($input_message, 255);
+    $error_message = $self->_pack_dv_string($error_message, 255);
+
+    # Pack the DV cell data.
+    my $dv_count = scalar @$cells;
+    my $dv_data  = pack 'v', $dv_count;
+    for my $range (@$cells) {
+        $dv_data .= pack 'vvvv', $range->[0],
+                                 $range->[2],
+                                 $range->[1],
+                                 $range->[3];
+    }
+
+    # Pack the record.
+    my $data   = pack 'V', $flags;
+       $data  .= $input_title;
+       $data  .= $error_title;
+       $data  .= $input_message;
+       $data  .= $error_message;
+       $data  .= $formula_1;
+       $data  .= $formula_2;
+       $data  .= $dv_data;
+
+    my $header = pack('vv', $record, length $data);
+
+    $self->_append($header, $data);
+}
+
+
+###############################################################################
+#
+# _pack_dv_string()
+#
+# Pack the strings used in the input and error dialog captions and messages.
+# Captions are limited to 32 characters. Messages are limited to 255 chars.
+#
+sub _pack_dv_string {
+
+    my $self        = shift;
+
+    my $string      = $_[0];
+    my $max_length  = $_[1];
+
+    my $str_length  = 0;
+    my $encoding    = 0;
+
+    # The default empty string is "\0".
+    if (!defined $string || $string eq '') {
+        $string = "\0";
+    }
+
+    # Excel limits DV captions to 32 chars and messages to 255.
+    if (length $string > $max_length) {
+        $string = substr($string, 0, $max_length);
+    }
+
+    $str_length = length $string;
+
+    # Handle utf8 strings in perl 5.8.
+    if ($] >= 5.008) {
+        require Encode;
+
+        if (Encode::is_utf8($string)) {
+            $string = Encode::encode("UTF-16LE", $string);
+            $encoding = 1;
+        }
+    }
+
+    return pack('vC', $str_length, $encoding) . $string;
+}
+
+
+###############################################################################
+#
+# _pack_dv_formula()
+#
+# Pack the formula used in the DV record. This is the same as an cell formula
+# with some additional header information. Note, DV formulas in Excel use
+# relative addressing (R1C1 and ptgXxxN) however we use the Formula.pm's
+# default absolute addressing (A1 and ptgXxx).
+#
+sub _pack_dv_formula {
+
+    my $self        = shift;
+
+    my $formula     = $_[0];
+    my $encoding    = 0;
+    my $length      = 0;
+    my $unused      = 0x0000;
+    my @tokens;
+
+    # Return a default structure for unused formulas.
+    if (!defined $formula || $formula eq '') {
+        return pack('vv', 0, $unused);
+    }
+
+    # Pack a list array ref as a null separated string.
+    if (ref $formula eq 'ARRAY') {
+        $formula   = join "\0", @$formula;
+        $formula   = qq("$formula");
+    }
+
+    # 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
+    }
+    else {
+        # TODO test for non valid ptgs such as Sheet2!A1
+    }
+    # Force 2d ranges to be a reference class.
+    s/_range2d/_range2dR/ for @tokens;
+    s/_name/_nameR/       for @tokens;
+
+    # Parse the tokens into a formula string.
+    $formula = $parser->parse_tokens(@tokens);
+
+
+    return pack('vv', length $formula, $unused) . $formula;
+}
+
+
+
+
+
+1;
+
+
+__END__
+
+
+=head1 NAME
+
+Worksheet - A writer class for Excel Worksheets.
+
+=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
+
+© 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.
+