############################################################################### # # Workbook - A writer class for Excel Workbooks. # # # Used in conjunction with WriteExcel # # Copyright 2000-2010, John McNamara, jmcnamara@cpan.org # # original written in Perl by John McNamara # converted to Ruby by Hideo Nakamura, cxn03651@msj.biglobe.ne.jp # require 'writeexcel/biffwriter' require 'writeexcel/olewriter' require 'writeexcel/formula' require 'writeexcel/format' require 'writeexcel/worksheet' require 'writeexcel/chart' require 'writeexcel/charts/area' require 'writeexcel/charts/bar' require 'writeexcel/charts/column' require 'writeexcel/charts/external' require 'writeexcel/charts/line' require 'writeexcel/charts/pie' require 'writeexcel/charts/scatter' require 'writeexcel/charts/stock' require 'writeexcel/properties' require 'digest/md5' require 'writeexcel/storage_lite' class Workbook < BIFFWriter BOF = 11 # :nodoc: EOF = 4 # :nodoc: SheetName = "Sheet" # :nodoc: NonAscii = /[^!"#\$%&'\(\)\*\+,\-\.\/\:\;<=>\?@0-9A-Za-z_\[\\\]^` ~\0\n]/ # :nodoc: # # file is a filename (as string) or io object where to out spreadsheet data. # you can set default format of workbook using default_formats. # # A new Excel workbook is created using the new() constructor which accepts # either a filename or a filehandle as a parameter. The following example # creates a new Excel file based on a filename: # # workbook = WriteExcel.new('filename.xls') # worksheet = workbook.add_worksheet # worksheet.write(0, 0, 'Hi Excel!') # # Here are some other examples of using new() with filenames: # # workbook1 = WriteExcel.new(filename) # workbook2 = WriteExcel.new('/tmp/filename.xls') # workbook3 = WriteExcel.new("c:\\tmp\\filename.xls") # workbook4 = WriteExcel.new('c:\tmp\filename.xls') # # The last two examples demonstrates how to create a file on DOS or # Windows where it is necessary to either escape the directory # separator \ or to use single quotes to ensure that it isn't interpolated. # # The new() constructor returns a WriteExcel object that you can use to add # worksheets and store data. # # If the file cannot be created, due to file permissions or some other reason, # new will return undef. Therefore, it is good practice to check the return # value of new before proceeding. # # workbook = WriteExcel.new('protected.xls') # die "Problems creating new Excel file:" if workbook.nil? # # You can also pass a valid IO object to the new() constructor. # def initialize(file, default_formats = {}) super() @file = file @default_formats = default_formats @parser = Formula.new(@byte_order) @tempdir = nil @date_1904 = false @sheet = @selected = 0 @xf_index = 0 @fileclosed = false @biffsize = 0 @sheet_name = "Sheet" @chart_name = "Chart" @sheet_count = 0 @chart_count = 0 @url_format = '' @codepage = 0x04E4 @country = 1 @worksheets = [] @sheetnames = [] @formats = [] @palette = [] @biff_only = 0 @internal_fh = 0 @fh_out = "" @sinfo = { :activesheet => 0, :firstsheet => 0, :str_total => 0, :str_unique => 0, :str_table => {} } @str_array = [] @str_block_sizes = [] @extsst_offsets = [] @extsst_buckets = 0 @extsst_bucket_size = 0 @ext_ref_count = 0 @ext_refs = {} @mso_clusters = [] @mso_size = 0 @hideobj = 0 @compatibility = 0 @add_doc_properties = 0 @summary = '' @doc_summary = '' @localtime = Time.now @defined_names = [] # Add the in-built style formats and the default cell format. add_format(:type => 1) # 0 Normal add_format(:type => 1) # 1 RowLevel 1 add_format(:type => 1) # 2 RowLevel 2 add_format(:type => 1) # 3 RowLevel 3 add_format(:type => 1) # 4 RowLevel 4 add_format(:type => 1) # 5 RowLevel 5 add_format(:type => 1) # 6 RowLevel 6 add_format(:type => 1) # 7 RowLevel 7 add_format(:type => 1) # 8 ColLevel 1 add_format(:type => 1) # 9 ColLevel 2 add_format(:type => 1) # 10 ColLevel 3 add_format(:type => 1) # 11 ColLevel 4 add_format(:type => 1) # 12 ColLevel 5 add_format(:type => 1) # 13 ColLevel 6 add_format(:type => 1) # 14 ColLevel 7 add_format(default_formats) # 15 Cell XF add_format(:type => 1, :num_format => 0x2B) # 16 Comma add_format(:type => 1, :num_format => 0x29) # 17 Comma[0] add_format(:type => 1, :num_format => 0x2C) # 18 Currency add_format(:type => 1, :num_format => 0x2A) # 19 Currency[0] add_format(:type => 1, :num_format => 0x09) # 20 Percent # Add the default format for hyperlinks @url_format = add_format(:color => 'blue', :underline => 1) if file.kind_of?(String) && file != '' @fh_out = open(file, "wb") @internal_fh = 1 else @fh_out = file end # Set colour palette. set_palette_xl97 get_checksum_method end ############################################################################### # # _get_checksum_method. # # Check for modules available to calculate image checksum. Excel uses MD4 but # MD5 will also work. # # ------- cxn03651 add ------- # md5 can use in ruby. so, @checksum_method is always 3. def get_checksum_method #:nodoc: @checksum_method = 3 end private :get_checksum_method # # Calls finalization methods and explicitly close the OLEwriter files # handle. # # In general your Excel file will be closed automatically when your program # ends or when the Workbook object goes out of scope, however the close method # can be used to explicitly close an Excel file. # # workbook.close # # An explicit close() is required if the file must be closed prior to performing # some external action on it such as copying it, reading its size or attaching # it to an email. # # In general, if you create a file with a size of 0 bytes or you fail to create # a file you need to call close(). # # The return value of close() is the same as that returned by perl when it # closes the file created by new(). This allows you to handle error conditions # in the usual way: # # $workbook.close() or die "Error closing file: $!"; # def close return if @fileclosed # Prevent close() from being called twice. @fileclosed = true return store_workbook end # get array of Worksheet objects # # :call-seq: # sheets -> array of all Wordsheet object # sheets(1, 3, 4) -> array of spcified Worksheet object. # # The sheets() method returns a array, or a sliced array, of the worksheets # in a workbook. # # If no arguments are passed the method returns a list of all the worksheets # in the workbook. This is useful if you want to repeat an operation on each # worksheet: # # workbook.sheets.each do |worksheet| # print worksheet.get_name # end # # You can also specify a slice list to return one or more worksheet objects: # # worksheet = workbook.sheets(0) # worksheet.write('A1', 'Hello') # # you can write the above example as: # # workbook.sheets(0).write('A1', 'Hello') # # The following example returns the first and last worksheet in a workbook: # # workbook.sheets(0, -1).each do |sheet| # # Do something # end # def sheets(*args) if args.empty? @worksheets else ary = [] args.each do |i| ary << @worksheets[i] end ary end end # # Add a new worksheet to the Excel workbook. # # if _sheetname_ is UTF-16BE format, pass 1 as _encoding_. # # At least one worksheet should be added to a new workbook. A worksheet is # used to write data into cells: # # worksheet1 = workbook.add_worksheet # Sheet1 # worksheet2 = workbook.add_worksheet('Foglio2') # Foglio2 # worksheet3 = workbook.add_worksheet('Data') # Data # worksheet4 = workbook.add_worksheet # Sheet4 # # If sheetname is not specified the default Excel convention will be followed, # i.e. Sheet1, Sheet2, etc. The utf_16_be parameter is optional, see below. # # The worksheet name must be a valid Excel worksheet name, i.e. it cannot # contain any of the following characters, [ ] : * ? / \ # and it must be less than 32 characters. In addition, you cannot use the same, # case insensitive, _sheetname_ for more than one worksheet. # # This method will also handle strings in UTF-8 format. # # worksheet = workbook.add_worksheet("シート名") # # UTF-16BE worksheet names using an additional optional parameter: # # name = [0x263a].pack('n') # worksheet = workbook.add_worksheet(name, 1) # Smiley # def add_worksheet(sheetname = '', encoding = 0) index = @worksheets.size name, encoding = check_sheetname(sheetname, encoding) # Porters take note, the following scheme of passing references to Workbook # data (in the \$self->{_foo} cases) instead of a reference to the Workbook # itself is a workaround to avoid circular references between Workbook and # Worksheet objects. Feel free to implement this in any way the suits your # language. # init_data = [ name, index, encoding, @url_format, @parser, @tempdir, @date_1904, @compatibility, nil, # Palette. Not used yet. See add_chart(). @sinfo, ] worksheet = Worksheet.new(*init_data) @worksheets[index] = worksheet # Store ref for iterator @sheetnames[index] = name # Store EXTERNSHEET names @parser.set_ext_sheets(name, index) # Store names in Formula.rb worksheet end ############################################################################### # # add_chart(params) # # Create a chart for embedding or as as new sheet. # # This method is use to create a new chart either as a standalone worksheet # (the default) or as an embeddable object that can be inserted into a # worksheet via the insert_chart() Worksheet method. # # chart = workbook.add_chart(:type => Chart::Column) # # The properties that can be set are: # # :type (required) # :name (optional) # :embedded (optional) # # * type # # This is a required parameter. It defines the type of chart that will be created. # # chart = workbook.add_chart(:type => Chart::Line) # # The available types are: # # Chart::Column # Chart::Bar # Chart::Line # Chart::Area # Chart::Pie # Chart::Scatter # Chart::Stock # # * :name # # Set the name for the chart sheet. The name property is optional and # if it isn't supplied will default to Chart1 .. n. The name must be # a valid Excel worksheet name. See add_worksheet() for more details # on valid sheet names. The name property can be omitted for embedded # charts. # # chart = workbook.add_chart( # :type => Chart::Line, # :name => 'Results Chart' # ) # # * :embedded # # Specifies that the Chart object will be inserted in a worksheet via # the insert_chart() Worksheet method. It is an error to try insert a # Chart that doesn't have this flag set. # # chart = workbook.add_chart(:type => Chart::Line, :embedded => 1) # # # Configure the chart. # ... # # # Insert the chart into the a worksheet. # worksheet.insert_chart('E2', chart) # # See Spreadsheet::WriteExcel::Chart for details on how to configure the # chart object once it is created. See also the chart_*.pl programs in the # examples directory of the distro. # def add_chart(params) name = '' encoding = 0 index = @worksheets.size # Type must be specified so we can create the required chart instance. type = params[:type] print "Must define chart type in add_chart()" if type.nil? # Ensure that the chart defaults to non embedded. embedded = params[:embedded] # Check the worksheet name for non-embedded charts. unless embedded name, encoding = check_sheetname(params[:name], params[:name_encoding], 1) end init_data = [ name, index, encoding, @url_format, @parser, @tempdir, @date_1904 ? 1 : 0, @compatibility, @palette, @sinfo ] chart = Chart.factory(type, *init_data) # If the chart isn't embedded let the workbook control it. if !embedded @worksheets[index] = chart # Store ref for iterator @sheetnames[index] = name # Store EXTERNSHEET names else # Set index to 0 so that the activate() and set_first_sheet() methods # point back to the first worksheet if used for embedded charts. chart.index = 0 chart.set_embedded_config_data end chart end ############################################################################### # # add_chart_ext($filename, $name) # # Add an externally created chart. # # This method is use to include externally generated charts in a WriteExcel # file. # # chart = workbook.add_chart_ext('chart01.bin', 'Chart1') # # This feature is semi-deprecated in favour of the "native" charts created # using add_chart(). Read external_charts.txt in the external_charts # directory of the distro for a full explanation. # def add_chart_ext(filename, name, encoding = 0) index = @worksheets.size type = 'extarnal' name, encoding = check_sheetname(name, encoding) init_data = [ filename, name, index, encoding, @sinfo ] chart = Chart.factory(self, type, init_data) @worksheets[index] = chart # Store ref for iterator @sheetnames[index] = name # Store EXTERNSHEET names chart end ############################################################################### # # _check_sheetname($name, $encoding) # # Check for valid worksheet names. We check the length, if it contains any # invalid characters and if the name is unique in the workbook. # def check_sheetname(name, encoding = 0, chart = 0) #:nodoc: encoding ||= 0 limit = encoding != 0 ? 62 : 31 invalid_char = %r![\[\]:*?/\\]! # Increment the Sheet/Chart number used for default sheet names below. if chart != 0 @chart_count += 1 else @sheet_count += 1 end # Supply default Sheet/Chart name if none has been defined. if name.nil? || name == "" encoding = 0 if chart != 0 name = @chart_name + @chart_count.to_s else name = @sheet_name + @sheet_count.to_s end end # Check that sheetname is <= 31 (1 or 2 byte chars). Excel limit. raise "Sheetname $name must be <= 31 chars" if name.length > limit # Check that Unicode sheetname has an even number of bytes if encoding == 1 && (name.length % 2 != 0) raise "Odd number of bytes in Unicode worksheet name: #{name}" end # Check that sheetname doesn't contain any invalid characters if encoding != 1 && name =~ invalid_char # Check ASCII names raise "Invalid character []:*?/\\ in worksheet name: #{name}" else # Extract any 8bit clean chars from the UTF16 name and validate them. str = name.dup while str =~ /../m hi, lo = $~[0].unpack('aa') if hi == "\0" and lo =~ invalid_char raise 'Invalid character []:*?/\\ in worksheet name: ' + name end str = $~.post_match end end # Handle utf8 strings if name =~ NonAscii name = NKF.nkf('-w16B0 -m0 -W', name) encoding = 1 end # Check that the worksheet name doesn't already exist since this is a fatal # error in Excel 97. The check must also exclude case insensitive matches # since the names 'Sheet1' and 'sheet1' are equivalent. The tests also have # to take the encoding into account. # @worksheets.each do |worksheet| name_a = name encd_a = encoding name_b = worksheet.name encd_b = worksheet.encoding error = 0; if encd_a == 0 and encd_b == 0 error = 1 if name_a.downcase == name_b.downcase elsif encd_a == 0 and encd_b == 1 name_a = name_a.unpack("C*").pack("n*") error = 1 if name_a.downcase == name_b.downcase elsif encd_a == 1 and encd_b == 0 name_b = name_b.unpack("C*").pack("n*") error = 1 if name_a.downcase == name_b.downcase elsif encd_a == 1 and encd_b == 1 # # We can do a true case insensitive test with Perl 5.8 and utf8. # if ($] >= 5.008) { # $name_a = Encode::decode("UTF-16BE", $name_a); # $name_b = Encode::decode("UTF-16BE", $name_b); # $error = 1 if lc($name_a) eq lc($name_b); # } # else { # # We can't easily do a case insensitive test of the UTF16 names. # # As a special case we check if all of the high bytes are nulls and # # then do an ASCII style case insensitive test. # # # Strip out the high bytes (funkily). # my $hi_a = grep {ord} $name_a =~ /(.)./sg; # my $hi_b = grep {ord} $name_b =~ /(.)./sg; # # if ($hi_a or $hi_b) { # $error = 1 if $name_a eq $name_b; # } # else { # $error = 1 if lc($name_a) eq lc($name_b); # } # } # } # # If any of the cases failed we throw the error here. end if error != 0 raise "Worksheet name '#{name}', with case ignored, " + "is already in use" end end [name, encoding] end private :check_sheetname # # The add_format method can be used to create new Format objects which are # used to apply formatting to a cell. You can either define the properties # at creation time via a hash of property values or later via method calls. # # format1 = workbook.add_format(props) # Set properties at creation # format2 = workbook.add_format # Set properties later # # See the "CELL FORMATTING" section for more details about Format properties and how to set them. # def add_format(formats = {}) format = Format.new(@xf_index, @default_formats.merge(formats)) @xf_index += 1 @formats.push format # Store format reference format end # # Set the compatibility mode. # # This method is used to improve compatibility with third party # applications that read Excel files. # # workbook.compatibility_mode # # An Excel file is comprised of binary records that describe properties of # a spreadsheet. Excel is reasonably liberal about this and, outside of a # core subset, it doesn't require every possible record to be present when # it reads a file. This is also true of Gnumeric and OpenOffice.Org Calc. # # WriteExcel takes advantage of this fact to omit some records in order to # minimise the amount of data stored in memory and to simplify and speed up # the writing of files. However, some third party applications that read # Excel files often expect certain records to be present. In # "compatibility mode" WriteExcel writes these records and tries to be as # close to an Excel generated file as possible. # # Applications that require compatibility_mode() are Apache POI, # Apple Numbers, and Quickoffice on Nokia, Palm and other devices. You should # also use compatibility_mode() if your Excel file will be used as an external # data source by another Excel file. # # If you encounter other situations that require compatibility_mode(), # please let me know. # # It should be noted that compatibility_mode() requires additional data to be # stored in memory and additional processing. This incurs a memory and speed # penalty and may not be suitable for very large files (>20MB). # # You must call compatibility_mode() before calling add_worksheet(). # # # 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. # def compatibility_mode(mode = 1) unless sheets.empty? raise "compatibility_mode() must be called before add_worksheet()" end @compatibility = mode end # # Set the date system: false = 1900 (the default), true = 1904 # # Excel stores dates as real numbers where the integer part stores the # number of days since the epoch and the fractional part stores the # percentage of the day. The epoch can be either 1900 or 1904. Excel for # Windows uses 1900 and Excel for Macintosh uses 1904. However, Excel on # either platform will convert automatically between one system and # the other. # # WriteExcel stores dates in the 1900 format by default. If you wish to # change this you can call the set_1904() workbook method. You can query # the current value by calling the get_1904() workbook method. This returns # 0 for 1900 and 1 for 1904. # # See also "DATES AND TIME IN EXCEL" for more information about working # with Excel's date system. # # In general you probably won't need to use set_1904(). # def set_1904(mode = true) unless sheets.empty? raise "set_1904() must be called before add_worksheet()" end @date_1904 = mode end # # Change the RGB components of the elements in the colour palette. # # The set_custom_color() method can be used to override one of the built-in # palette values with a more suitable colour. # # The value for _index_ should be in the range 8..63, see "COLOURS IN EXCEL". # # The default named colours use the following indices: # # 8 => black # 9 => white # 10 => red # 11 => lime # 12 => blue # 13 => yellow # 14 => magenta # 15 => cyan # 16 => brown # 17 => green # 18 => navy # 20 => purple # 22 => silver # 23 => gray # 33 => pink # 53 => orange # # A new colour is set using its RGB (red green blue) components. The red, # green and blue values must be in the range 0..255. You can determine the # required values in Excel using the Tools->Options->Colors->Modify dialog. # # The set_custom_color() workbook method can also be used with a HTML style # #rrggbb hex value: # # workbook.set_custom_color(40, 255, 102, 0 ) # Orange # workbook.set_custom_color(40, 0xFF, 0x66, 0x00) # Same thing # workbook.set_custom_color(40, '#FF6600' ) # Same thing # # font = workbook.add_format(:color => 40) # Use the modified colour # # The return value from set_custom_color() is the index of the colour that # was changed: # # ferrari = workbook.set_custom_color(40, 216, 12, 12) # # format = workbook.add_format( # :bg_color => $ferrari, # :pattern => 1, # :border => 1 # ) # def set_custom_color(index = nil, red = nil, green = nil, blue = nil) # Match a HTML #xxyyzz style parameter if !red.nil? && red =~ /^#(\w\w)(\w\w)(\w\w)/ red = $1.hex green = $2.hex blue = $3.hex end # Check that the colour index is the right range if index < 8 || index > 64 raise "Color index #{index} outside range: 8 <= index <= 64"; end # Check that the colour components are in the right range if (red < 0 || red > 255) || (green < 0 || green > 255) || (blue < 0 || blue > 255) raise "Color component outside range: 0 <= color <= 255"; end index -=8 # Adjust colour index (wingless dragonfly) # Set the RGB value @palette[index] = [red, green, blue, 0] return index + 8 end ############################################################################### # # set_palette_xl97() # # Sets the colour palette to the Excel 97+ default. # def set_palette_xl97 #:nodoc: @palette = [ [0x00, 0x00, 0x00, 0x00], # 8 [0xff, 0xff, 0xff, 0x00], # 9 [0xff, 0x00, 0x00, 0x00], # 10 [0x00, 0xff, 0x00, 0x00], # 11 [0x00, 0x00, 0xff, 0x00], # 12 [0xff, 0xff, 0x00, 0x00], # 13 [0xff, 0x00, 0xff, 0x00], # 14 [0x00, 0xff, 0xff, 0x00], # 15 [0x80, 0x00, 0x00, 0x00], # 16 [0x00, 0x80, 0x00, 0x00], # 17 [0x00, 0x00, 0x80, 0x00], # 18 [0x80, 0x80, 0x00, 0x00], # 19 [0x80, 0x00, 0x80, 0x00], # 20 [0x00, 0x80, 0x80, 0x00], # 21 [0xc0, 0xc0, 0xc0, 0x00], # 22 [0x80, 0x80, 0x80, 0x00], # 23 [0x99, 0x99, 0xff, 0x00], # 24 [0x99, 0x33, 0x66, 0x00], # 25 [0xff, 0xff, 0xcc, 0x00], # 26 [0xcc, 0xff, 0xff, 0x00], # 27 [0x66, 0x00, 0x66, 0x00], # 28 [0xff, 0x80, 0x80, 0x00], # 29 [0x00, 0x66, 0xcc, 0x00], # 30 [0xcc, 0xcc, 0xff, 0x00], # 31 [0x00, 0x00, 0x80, 0x00], # 32 [0xff, 0x00, 0xff, 0x00], # 33 [0xff, 0xff, 0x00, 0x00], # 34 [0x00, 0xff, 0xff, 0x00], # 35 [0x80, 0x00, 0x80, 0x00], # 36 [0x80, 0x00, 0x00, 0x00], # 37 [0x00, 0x80, 0x80, 0x00], # 38 [0x00, 0x00, 0xff, 0x00], # 39 [0x00, 0xcc, 0xff, 0x00], # 40 [0xcc, 0xff, 0xff, 0x00], # 41 [0xcc, 0xff, 0xcc, 0x00], # 42 [0xff, 0xff, 0x99, 0x00], # 43 [0x99, 0xcc, 0xff, 0x00], # 44 [0xff, 0x99, 0xcc, 0x00], # 45 [0xcc, 0x99, 0xff, 0x00], # 46 [0xff, 0xcc, 0x99, 0x00], # 47 [0x33, 0x66, 0xff, 0x00], # 48 [0x33, 0xcc, 0xcc, 0x00], # 49 [0x99, 0xcc, 0x00, 0x00], # 50 [0xff, 0xcc, 0x00, 0x00], # 51 [0xff, 0x99, 0x00, 0x00], # 52 [0xff, 0x66, 0x00, 0x00], # 53 [0x66, 0x66, 0x99, 0x00], # 54 [0x96, 0x96, 0x96, 0x00], # 55 [0x00, 0x33, 0x66, 0x00], # 56 [0x33, 0x99, 0x66, 0x00], # 57 [0x00, 0x33, 0x00, 0x00], # 58 [0x33, 0x33, 0x00, 0x00], # 59 [0x99, 0x33, 0x00, 0x00], # 60 [0x99, 0x33, 0x66, 0x00], # 61 [0x33, 0x33, 0x99, 0x00], # 62 [0x33, 0x33, 0x33, 0x00] # 63 ] return 0 end private :set_palette_xl97 # # Change the default temp directory # # # For speed and efficiency WriteExcel stores worksheet data in temporary # files prior to assembling the final workbook. # # If WriteExcel is unable to create these temporary files it will store # the required data in memory. This can be slow for large files. # # The problem occurs mainly with IIS on Windows although it could feasibly # occur on Unix systems as well. The problem generally occurs because the # default temp file directory is defined as C:/ or some other directory that # IIS doesn't provide write access to. # # To check if this might be a problem on a particular system you can run a # simple test program with -w or use warnings. This will generate a warning # if the module cannot create the required temporary files: # # #!/usr/bin/ruby -w # # require 'WriteExcel' # # workbook = WriteExcel.new('test.xls') # worksheet = workbook.add_worksheet # workbook.close # # To avoid this problem the set_tempdir() method can be used to specify a # directory that is accessible for the creation of temporary files. # # Even if the default temporary file directory is accessible you may wish # to specify an alternative location for security or maintenance reasons: # # workbook.set_tempdir('/tmp/writeexcel') # workbook.set_tempdir('c:\windows\temp\writeexcel') # # The directory for the temporary file must exist, set_tempdir() will not # create a new directory. # # One disadvantage of using the set_tempdir() method is that on some Windows # systems it will limit you to approximately 800 concurrent tempfiles. This # means that a single program running on one of these systems will be limited # to creating a total of 800 workbook and worksheet objects. You can run # multiple, non-concurrent programs to work around this if necessary. # def set_tempdir(dir = '') raise "#{dir} is not a valid directory" if dir != '' && !FileTest.directory?(dir) raise "set_tempdir must be called before add_worksheet" unless sheets.empty? @tempdir = dir end # # The default code page or character set used by WriteExcel is ANSI. This is # also the default used by Excel for Windows. Occasionally however it may be # necessary to change the code page via the set_codepage() method. # # Changing the code page may be required if your are using WriteExcel on the # Macintosh and you are using characters outside the ASCII 128 character set: # # workbook.set_codepage(1) # ANSI, MS Windows # workbook.set_codepage(2) # Apple Macintosh # # The set_codepage() method is rarely required. # def set_codepage(type = 1) if type == 2 @codepage = 0x8000 else @codepage = 0x04E4 end end # # store the country code. # # Some non-english versions of Excel may need this set to some value other # than 1 = "United States". In general the country code is equal to the # international dialling code. # def set_country(code = 1) @country = code end # # This method is used to defined a name that can be used to represent a # value, a single cell or a range of cells in a workbook. # # workbook.define_name('Exchange_rate', '=0.96') # workbook.define_name('Sales', '=Sheet1!$G$1:$H$10') # workbook.define_name('Sheet2!Sales', '=Sheet2!$G$1:$G$10') # # See the defined_name.rb program in the examples dir of the distro. # # Note: This currently a beta feature. More documentation and examples # will be added. # def define_name(name, formula, encoding = 0) sheet_index = 0 full_name = name.downcase if name =~ /^(.*)!(.*)$/ sheetname = $1 name = $2; sheet_index = 1 + @parser.get_sheet_index(sheetname) end # Strip the = sign at the beginning of the formula string formula = formula.sub(/^=/, '') # Parse the formula using the parser in Formula.pm parser = @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. # tokens = parser.parse_formula(formula) # Force 2d ranges to be a reference class. tokens.collect! { |t| t.gsub(/_ref3d/, '_ref3dR') } tokens.collect! { |t| t.gsub(/_range3d/, '_range3dR') } # Parse the tokens into a formula string. formula = parser.parse_tokens(tokens) @defined_names.push( { :name => name, :encoding => encoding, :sheet_index => sheet_index, :formula => formula } ) index = @defined_names.length parser.set_ext_name(name, index) end # # Set the document properties such as Title, Author etc. These are written to # property sets in the OLE container. # # The set_properties method can be used to set the document properties of # the Excel file created by WriteExcel. These properties are visible when you # use the File->Properties menu option in Excel and are also available to # external applications that read or index windows files. # # The properties should be passed as a hash of values as follows: # # workbook.set_properties( # :title => 'This is an example spreadsheet', # :author => 'cxn03651', # :comments => 'Created with Ruby and WriteExcel', # ) # # The properties that can be set are: # # * title # * subject # * author # * manager # * company # * category # * keywords # * comments # # User defined properties are not supported due to effort required. # # You can also pass UTF-8 strings as properties. # # $workbook->set_properties( # :subject => "住所録", # ); # # Usually WriteExcel allows you to use UTF-16. However, document properties # don't support UTF-16 for these type of strings. # # In order to promote the usefulness of Ruby and the WriteExcel module # consider adding a comment such as the following when using document # properties: # # workbook.set_properties( # ..., # :comments => 'Created with Ruby and WriteExcel', # ..., # ) # # See also the properties.rb program in the examples directory of the distro. # def set_properties(params) # Ignore if no args were passed. return -1 if !params.kind_of?(Hash) || params.empty? # List of valid input parameters. properties = { :codepage => [0x0001, 'VT_I2' ], :title => [0x0002, 'VT_LPSTR' ], :subject => [0x0003, 'VT_LPSTR' ], :author => [0x0004, 'VT_LPSTR' ], :keywords => [0x0005, 'VT_LPSTR' ], :comments => [0x0006, 'VT_LPSTR' ], :last_author => [0x0008, 'VT_LPSTR' ], :created => [0x000C, 'VT_FILETIME'], :category => [0x0002, 'VT_LPSTR' ], :manager => [0x000E, 'VT_LPSTR' ], :company => [0x000F, 'VT_LPSTR' ], :utf8 => 1 } # Check for valid input parameters. params.each_key do |k| unless properties.has_key?(k) raise "Unknown parameter '#{k}' in set_properties()"; end end # Set the creation time unless specified by the user. unless params.has_key?(:created) params[:created] = @localtime end # # Create the SummaryInformation property set. # # Get the codepage of the strings in the property set. strings = ["title", "subject", "author", "keywords", "comments", "last_author"] params[:codepage] = get_property_set_codepage(params, strings) # Create an array of property set values. property_sets = [] strings.unshift("codepage") strings.push("created") strings.each do |property| if params.has_key?(property.to_sym) && !params[property.to_sym].nil? property_sets.push( [ properties[property.to_sym][0], properties[property.to_sym][1], params[property.to_sym] ] ) end end # Pack the property sets. @summary = create_summary_property_set(property_sets) # # Create the DocSummaryInformation property set. # # Get the codepage of the strings in the property set. strings = ["category", "manager", "company"] params[:codepage] = get_property_set_codepage(params, strings) # Create an array of property set values. property_sets = [] ["codepage", "category", "manager", "company"].each do |property| if params.has_key?(property.to_sym) && !params[property.to_sym].nil? property_sets.push( [ properties[property.to_sym][0], properties[property.to_sym][1], params[property.to_sym] ] ) end end # Pack the property sets. @doc_summary = create_doc_summary_property_set(property_sets) # Set a flag for when the files is written. @add_doc_properties = 1 end ############################################################################### # # _get_property_set_codepage() # # Get the character codepage used by the strings in a property set. If one of # the strings used is utf8 then the codepage is marked as utf8. Otherwise # Latin 1 is used (although in our case this is limited to 7bit ASCII). # def get_property_set_codepage(params, strings) #:nodoc: # Allow for manually marked utf8 strings. unless params[:utf8].nil? return 0xFDE9 else strings.each do |string| next unless params.has_key?(string.to_sym) return 0xFDE9 if params[string.to_sym] =~ NonAscii end return 0x04E4; # Default codepage, Latin 1. end end private :get_property_set_codepage ############################################################################### # # _store_workbook() # # Assemble worksheets into a workbook and send the BIFF data to an OLE # storage. # def store_workbook #:nodoc: # Add a default worksheet if non have been added. add_worksheet if @worksheets.empty? # Calculate size required for MSO records and update worksheets. calc_mso_sizes # Ensure that at least one worksheet has been selected. @worksheets[0].select if @sinfo[:activesheet] == 0 # Calculate the number of selected sheet tabs and set the active sheet. @worksheets.each do |sheet| @selected += 1 if sheet.selected != 0 sheet.active = 1 if sheet.index == @sinfo[:activesheet] end # Add Workbook globals store_bof(0x0005) store_codepage store_window1 store_hideobj store_1904 store_all_fonts store_all_num_formats store_all_xfs store_all_styles store_palette # Calculate the offsets required by the BOUNDSHEET records calc_sheet_offsets # Add BOUNDSHEET records. @worksheets.each do |sheet| store_boundsheet( sheet.name, sheet.offset, sheet.sheet_type, sheet.hidden, sheet.encoding ) end # NOTE: If any records are added between here and EOF the # _calc_sheet_offsets() should be updated to include the new length. store_country if @ext_ref_count != 0 store_supbook store_externsheet store_names end add_mso_drawing_group store_shared_strings store_extsst # End Workbook globals store_eof # Store the workbook in an OLE container store_OLE_file end private :store_workbook def str_unique=(val) # :nodoc: @sinfo[:str_unique] = val end def extsst_buckets # :nodoc: @extsst_buckets end def extsst_bucket_size # :nodoc: @extsst_bucket_size end def biff_only=(val) # :nodoc: @biff_only = val end def summary # :nodoc: @summary end def localtime=(val) # :nodoc: @localtime = val end ############################################################################### # # _store_OLE_file() # # Store the workbook in an OLE container using the default handler or using # OLE::Storage_Lite if the workbook data is > ~ 7MB. # def store_OLE_file #:nodoc: maxsize = 7_087_104 # maxsize = 1 if @add_doc_properties == 0 && @biffsize <= maxsize # Write the OLE file using OLEwriter if data <= 7MB ole = OLEWriter.new(@fh_out) # Write the BIFF data without the OLE container for testing. ole.biff_only = @biff_only # Indicate that we created the filehandle and want to close it. ole.internal_fh = @internal_fh ole.set_size(@biffsize) ole.write_header while tmp = get_data ole.write(tmp) end @worksheets.each do |worksheet| while tmp = worksheet.get_data ole.write(tmp) end end return ole.close else # Write the OLE file using ruby-ole if data > 7MB # Create the Workbook stream. stream = 'Workbook'.unpack('C*').pack('v*') workbook = OLEStorageLitePPSFile.new(stream) workbook.set_file # use tempfile while tmp = get_data print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) workbook.append(tmp) end @worksheets.each do |worksheet| while tmp = worksheet.get_data print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) workbook.append(tmp) end end streams = [] streams << workbook # Create the properties streams, if any. if @add_doc_properties != 0 stream = "\5SummaryInformation".unpack('C*').pack('v*') summary = OLEStorageLitePPSFile.new(stream, @summary) streams << summary stream = "\5DocumentSummaryInformation".unpack('C*').pack('v*') summary = OLEStorageLitePPSFile.new(stream, @doc_summary) streams << summary end # Create the OLE root document and add the substreams. localtime = @localtime.to_a[0..5] localtime[4] -= 1 # month localtime[5] -= 1900 ole_root = OLEStorageLitePPSRoot.new( localtime, localtime, streams ) ole_root.save(@file) # Close the filehandle if it was created internally. return @fh_out.close if @internal_fh != 0 end end private :store_OLE_file ############################################################################### # # _calc_sheet_offsets() # # Calculate Worksheet BOF offsets records for use in the BOUNDSHEET records. # def calc_sheet_offsets #:nodoc: _bof = 12 _eof = 4 offset = @datasize # Add the length of the COUNTRY record offset += 8 # Add the length of the SST and associated CONTINUEs offset += calculate_shared_string_sizes # Add the length of the EXTSST record. offset += calculate_extsst_size # Add the length of the SUPBOOK, EXTERNSHEET and NAME records offset += calculate_extern_sizes # Add the length of the MSODRAWINGGROUP records including an extra 4 bytes # for any CONTINUE headers. See _add_mso_drawing_group_continue(). mso_size = @mso_size mso_size += 4 * Integer((mso_size -1) / Float(@limit)) offset += mso_size @worksheets.each do |sheet| offset += _bof + sheet.name.length end offset += _eof @worksheets.each do |sheet| sheet.offset = offset sheet.close offset += sheet.datasize end @biffsize = offset end private :calc_sheet_offsets ############################################################################### # # _calc_mso_sizes() # # Calculate the MSODRAWINGGROUP sizes and the indexes of the Worksheet # MSODRAWING records. # # In the following SPID is shape id, according to Escher nomenclature. # def calc_mso_sizes #:nodoc: mso_size = 0 # Size of the MSODRAWINGGROUP record start_spid = 1024 # Initial spid for each sheet max_spid = 1024 # spidMax num_clusters = 1 # cidcl shapes_saved = 0 # cspSaved drawings_saved = 0 # cdgSaved clusters = [] process_images # Add Bstore container size if there are images. mso_size += 8 unless @images_data.empty? # Iterate through the worksheets, calculate the MSODRAWINGGROUP parameters # and space required to store the record and the MSODRAWING parameters # required by each worksheet. # @worksheets.each do |sheet| next unless sheet.sheet_type == 0x0000 num_images = sheet.num_images image_mso_size = sheet.image_mso_size num_comments = sheet.prepare_comments num_charts = sheet.prepare_charts num_filters = sheet.filter_count next if num_images + num_comments + num_charts + num_filters == 0 # Include 1 parent MSODRAWING shape, per sheet, in the shape count. num_shapes = 1 + num_images + num_comments + num_charts + num_filters shapes_saved += num_shapes mso_size += image_mso_size # Add a drawing object for each sheet with comments. drawings_saved += 1 # For each sheet start the spids at the next 1024 interval. max_spid = 1024 * (1 + Integer((max_spid -1)/1024.0)) start_spid = max_spid # Max spid for each sheet and eventually for the workbook. max_spid += num_shapes # Store the cluster ids i = num_shapes while i > 0 num_clusters += 1 mso_size += 8 size = i > 1024 ? 1024 : i clusters.push([drawings_saved, size]) i -= 1024 end # Pass calculated values back to the worksheet sheet.object_ids = [start_spid, drawings_saved, num_shapes, max_spid -1] end # Calculate the MSODRAWINGGROUP size if we have stored some shapes. mso_size += 86 if mso_size != 0 # Smallest size is 86+8=94 @mso_size = mso_size @mso_clusters = [ max_spid, num_clusters, shapes_saved, drawings_saved, clusters ] end private :calc_mso_sizes ############################################################################### # # _process_images() # # We need to process each image in each worksheet and extract information. # Some of this information is stored and used in the Workbook and some is # passed back into each Worksheet. The overall size for the image related # BIFF structures in the Workbook is calculated here. # # MSO size = 8 bytes for bstore_container + # 44 bytes for blip_store_entry + # 25 bytes for blip # = 77 + image size. # def process_images #:nodoc: images_seen = {} image_data = [] previous_images = [] image_id = 1; images_size = 0; @worksheets.each do |sheet| next unless sheet.sheet_type == 0x0000 next if sheet.prepare_images == 0 num_images = 0 image_mso_size = 0 sheet.images_array.each do |image| filename = image[2] num_images += 1 # # For each Worksheet image we get a structure like this # [ # $row, # $col, # $name, # $x_offset, # $y_offset, # $scale_x, # $scale_y, # ] # # And we add additional information: # # $image_id, # $type, # $width, # $height; if images_seen[filename].nil? # TODO should also match seen images based on checksum. # Open the image file and import the data. fh = open(filename, "rb") raise "Couldn't import #{filename}: #{$!}" unless fh # Slurp the file into a string and do some size calcs. # my $data = do {local $/; <$fh>}; data = fh.read size = data.length checksum1 = image_checksum(data, image_id) checksum2 = checksum1 ref_count = 1 # Process the image and extract dimensions. # Test for PNGs... if data.unpack('x A3')[0] == 'PNG' type, width, height = process_png(data) # Test for JFIF and Exif JPEGs... elsif ( data.unpack('n')[0] == 0xFFD8 && (data.unpack('x6 A4')[0] == 'JFIF' || data.unpack('x6 A4')[0] == 'Exif') ) type, width, height = process_jpg(data, filename) # Test for BMPs... elsif data.unpack('A2')[0] == 'BM' type, width, height = process_bmp(data, filename) # The 14 byte header of the BMP is stripped off. data[0, 13] = '' # A checksum of the new image data is also required. checksum2 = image_checksum(data, image_id, image_id) # Adjust size -14 (header) + 16 (extra checksum). size += 2 else raise "Unsupported image format for file: #{filename}\n" end # Push the new data back into the Worksheet array; image.push(image_id, type, width, height) # Also store new data for use in duplicate images. previous_images.push([image_id, type, width, height]) # Store information required by the Workbook. image_data.push([ref_count, type, data, size, checksum1, checksum2]) # Keep track of overall data size. images_size += size +61; # Size for bstore container. image_mso_size += size +69; # Size for dgg container. images_seen[filename] = image_id image_id += 1 fh.close else # We've processed this file already. index = images_seen[filename] -1 # Increase image reference count. image_data[index][0] += 1 # Add previously calculated data back onto the Worksheet array. # $image_id, $type, $width, $height a_ref = sheet.images_array[index] image.concat(previous_images[index]) end end # Store information required by the Worksheet. sheet.num_images = num_images sheet.image_mso_size = image_mso_size end # Store information required by the Workbook. @images_size = images_size @images_data = image_data # Store the data for MSODRAWINGGROUP. end private :process_images ############################################################################### # # _image_checksum() # # Generate a checksum for the image using whichever module is available..The # available modules are checked in _get_checksum_method(). Excel uses an MD4 # checksum but any other will do. In the event of no checksum module being # available we simulate a checksum using the image index. # def image_checksum(data, index1, index2 = 0) #:nodoc: if @checksum_method == 1 # Digest::MD4 # return Digest::MD4::md4_hex($data); elsif @checksum_method == 2 # Digest::Perl::MD4 # return Digest::Perl::MD4::md4_hex($data); elsif @checksum_method == 3 # Digest::MD5 return Digest::MD5.hexdigest(data) else # Default return sprintf('%016X%016X', index2, index1) end end private :image_checksum ############################################################################### # # _process_png() # # Extract width and height information from a PNG file. # def process_png(data) #:nodoc: type = 6 # Excel Blip type (MSOBLIPTYPE). width = data[16, 4].unpack("N")[0] height = data[20, 4].unpack("N")[0] return [type, width, height] end private :process_png ############################################################################### # # _process_bmp() # # Extract width and height information from a BMP file. # # Most of these checks came from the old Worksheet::_process_bitmap() method. # def process_bmp(data, filename) #:nodoc: type = 7 # Excel Blip type (MSOBLIPTYPE). # Check that the file is big enough to be a bitmap. if data.length <= 0x36 raise "#{filename} doesn't contain enough data." end # Read the bitmap width and height. Verify the sizes. width, height = data.unpack("x18 V2") if width > 0xFFFF raise "#{filename}: largest image width #{width} supported is 65k." end if height > 0xFFFF raise "#{filename}: largest image height supported is 65k." end # Read the bitmap planes and bpp data. Verify them. planes, bitcount = data.unpack("x26 v2") if bitcount != 24 raise "#{filename} isn't a 24bit true color bitmap." end if planes != 1 raise "#{filename}: only 1 plane supported in bitmap image." end # Read the bitmap compression. Verify compression. compression = data.unpack("x30 V") if compression != 0 raise "#{filename}: compression not supported in bitmap image." end return [type, width, height] end private :process_bmp ############################################################################### # # _process_jpg() # # Extract width and height information from a JPEG file. # def process_jpg(data, filename) # :nodoc: type = 5 # Excel Blip type (MSOBLIPTYPE). offset = 2; data_length = data.length # Search through the image data to find the 0xFFC0 marker. The height and # width are contained in the data for that sub element. while offset < data_length marker = data[offset, 2].unpack("n") marker = marker[0] length = data[offset+2, 2].unpack("n") length = length[0] if marker == 0xFFC0 || marker == 0xFFC2 height = data[offset+5, 2].unpack("n") height = height[0] width = data[offset+7, 2].unpack("n") width = width[0] break end offset = offset + length + 2 break if marker == 0xFFDA end if height.nil? raise "#{filename}: no size data found in jpeg image.\n" end [type, width, height] end ############################################################################### # # _store_all_fonts() # # Store the Excel FONT records. # def store_all_fonts #:nodoc: format = @formats[15] # The default cell format. font = format.get_font # Fonts are 0-indexed. According to the SDK there is no index 4, (0..3).each do print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(font) end # Add the default fonts for charts and comments. This aren't connected # to XF formats. Note, the font size, and some other properties of # chart fonts are set in the FBI record of the chart. # Index 5. Axis numbers. tmp_format = Format.new( nil, :font_only => 1 ) print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(tmp_format.get_font) # Index 6. Series names. tmp_format = Format.new( nil, :font_only => 1 ) print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(tmp_format.get_font) # Index 7. Title. tmp_format = Format.new( nil, :font_only => 1, :bold => 1 ) print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(tmp_format.get_font) # Index 8. Axes. tmp_format = Format.new( nil, :font_only => 1, :bold => 1 ) print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(tmp_format.get_font) # Index 9. Comments. tmp_format = Format.new( nil, :font_only => 1, :font => 'Tahoma', :size => 8 ) print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(tmp_format.get_font) # Iterate through the XF objects and write a FONT record if it isn't the # same as the default FONT and if it hasn't already been used. # fonts = {} index = 10 # The first user defined FONT key = format.get_font_key # The default font for cell formats. fonts[key] = 0 # Index of the default font # Fonts that are marked as '_font_only' are always stored. These are used # mainly for charts and may not have an associated XF record. @formats.each do |format| key = format.get_font_key if format.font_only == 0 and !fonts[key].nil? # FONT has already been used format.font_index = fonts[key] else # Add a new FONT record if format.font_only == 0 fonts[key] = index end format.font_index = index index += 1 font = format.get_font print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(font) end end end private :store_all_fonts ############################################################################### # # _store_all_num_formats() # # Store user defined numerical formats i.e. FORMAT records # def store_all_num_formats #:nodoc: num_formats = {} index = 164 # User defined FORMAT records start from 0xA4 # Iterate through the XF objects and write a FORMAT record if it isn't a # built-in format type and if the FORMAT string hasn't already been used. # @formats.each do |format| num_format = format.num_format encoding = format.num_format_enc # Check if $num_format is an index to a built-in format. # Also check for a string of zeros, which is a valid format string # but would evaluate to zero. # unless num_format.to_s =~ /^0+\d/ next if num_format.to_s =~ /^\d+$/ # built-in end if num_formats[num_format] # FORMAT has already been used format.num_format = num_formats[num_format] else # Add a new FORMAT num_formats[num_format] = index format.num_format = index store_num_format(num_format, index, encoding) index += 1 end end end private :store_all_num_formats ############################################################################### # # _store_all_xfs() # # Write all XF records. # def store_all_xfs #:nodoc: @formats.each do |format| xf = format.get_xf print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(xf) end end private :store_all_xfs ############################################################################### # # _store_all_styles() # # Write all STYLE records. # def store_all_styles #:nodoc: # Excel adds the built-in styles in alphabetical order. built_ins = [ [0x03, 16], # Comma [0x06, 17], # Comma[0] [0x04, 18], # Currency [0x07, 19], # Currency[0] [0x00, 0], # Normal [0x05, 20] # Percent # We don't deal with these styles yet. #[0x08, 21], # Hyperlink #[0x02, 8], # ColLevel_n #[0x01, 1], # RowLevel_n ] built_ins.each do |aref| type = aref[0] xf_index = aref[1] store_style(type, xf_index) end end private :store_all_styles ############################################################################### # # _store_names() # # Write the NAME record to define the print area and the repeat rows and cols. # def store_names # :nodoc: # Create the user defined names. @defined_names.each do |defined_name| store_name( defined_name[:name], defined_name[:encoding], defined_name[:sheet_index], defined_name[:formula] ) end # Sort the worksheets into alphabetical order by name. This is a # requirement for some non-English language Excel patch levels. worksheets = @worksheets.sort_by{ |x| x.name } # Create the autofilter NAME records worksheets.each do |worksheet| index = worksheet.index key = "#{index}:#{index}" ref = @ext_refs[key] # Write a Name record if Autofilter has been defined if worksheet.filter_count != 0 store_name_short( worksheet.index, 0x0D, # NAME type = Filter Database ref, worksheet.filter_area[0], worksheet.filter_area[1], worksheet.filter_area[2], worksheet.filter_area[3], 1 # Hidden ) end end # Create the print area NAME records worksheets.each do |worksheet| index = worksheet.index key = "#{index}:#{index}" ref = @ext_refs[key] # Write a Name record if the print area has been defined if !worksheet.print_rowmin.nil? store_name_short( worksheet.index, 0x06, # NAME type = Print_Area ref, worksheet.print_rowmin, worksheet.print_rowmax, worksheet.print_colmin, worksheet.print_colmax ) end end # Create the print title NAME records worksheets.each do |worksheet| index = worksheet.index rowmin = worksheet.title_rowmin rowmax = worksheet.title_rowmax colmin = worksheet.title_colmin colmax = worksheet.title_colmax key = "#{index}:#{index}" ref = @ext_refs[key] # Determine if row + col, row, col or nothing has been defined # and write the appropriate record # if rowmin && colmin # Row and column titles have been defined. # Row title has been defined. store_name_long( worksheet.index, 0x07, # NAME type = Print_Titles ref, rowmin, rowmax, colmin, colmax ) elsif rowmin # Row title has been defined. store_name_short( worksheet.index, 0x07, # NAME type = Print_Titles ref, rowmin, rowmax, 0x00, 0xff ) elsif colmin # Column title has been defined. store_name_short( worksheet.index, 0x07, # NAME type = Print_Titles ref, 0x0000, 0xffff, colmin, colmax ) else # Nothing left to do end end end ############################################################################### ############################################################################### # # BIFF RECORDS # ############################################################################### # # _store_window1() # # Write Excel BIFF WINDOW1 record. # def store_window1 #:nodoc: record = 0x003D # Record identifier length = 0x0012 # Number of bytes to follow xWn = 0x0000 # Horizontal position of window yWn = 0x0000 # Vertical position of window dxWn = 0x355C # Width of window dyWn = 0x30ED # Height of window grbit = 0x0038 # Option flags ctabsel = @selected # Number of workbook tabs selected wTabRatio = 0x0258 # Tab to scrollbar ratio itabFirst = @sinfo[:firstsheet] # 1st displayed worksheet itabCur = @sinfo[:activesheet] # Active worksheet header = [record, length].pack("vv") data = [ xWn, yWn, dxWn, dyWn, grbit, itabCur, itabFirst, ctabsel, wTabRatio ].pack("vvvvvvvvv") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_window1 ############################################################################### # # _store_boundsheet() # my $sheetname = $_[0]; # Worksheet name # my $offset = $_[1]; # Location of worksheet BOF # my $type = $_[2]; # Worksheet type # my $hidden = $_[3]; # Worksheet hidden flag # my $encoding = $_[4]; # Sheet name encoding # # Writes Excel BIFF BOUNDSHEET record. # def store_boundsheet(sheetname, offset, type, hidden, encoding) #:nodoc: record = 0x0085 # Record identifier length = 0x08 + sheetname.length # Number of bytes to follow cch = sheetname.length # Length of sheet name grbit = type | hidden # Character length is num of chars not num of bytes cch /= 2 if encoding != 0 # Change the UTF-16 name from BE to LE sheetname = sheetname.unpack('v*').pack('n*') if encoding != 0 header = [record, length].pack("vv") data = [offset, grbit, cch, encoding].pack("VvCC") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data, sheetname) end private :store_boundsheet ############################################################################### # # _store_style() # type = $_[0] # Built-in style # xf_index = $_[1] # Index to style XF # # Write Excel BIFF STYLE records. # def store_style(type, xf_index) #:nodoc: record = 0x0293 # Record identifier length = 0x0004 # Bytes to follow level = 0xff # Outline style level xf_index |= 0x8000 # Add flag to indicate built-in style. header = [record, length].pack("vv") data = [xf_index, type, level].pack("vCC") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_style ############################################################################### # # _store_num_format() # my $format = $_[0]; # Custom format string # my $ifmt = $_[1]; # Format index code # my $encoding = $_[2]; # Char encoding for format string # # Writes Excel FORMAT record for non "built-in" numerical formats. # def store_num_format(format, ifmt, encoding) #:nodoc: format = format.to_s unless format.kind_of?(String) record = 0x041E # Record identifier # length # Number of bytes to follow # Char length of format string cch = format.length # Handle utf8 strings if format =~ NonAscii format = NKF.nkf('-w16B0 -m0 -W', format) encoding = 1 end # Handle Unicode format strings. if encoding == 1 raise "Uneven number of bytes in Unicode font name" if cch % 2 != 0 cch /= 2 if encoding != 0 format = format.unpack('n*').pack('v*') end # Special case to handle Euro symbol, 0x80, in non-Unicode strings. if encoding == 0 and format =~ /\x80/ format = format.unpack('C*').pack('v*') format.gsub!(/\x80\x00/, "\xAC\x20") encoding = 1 end length = 0x05 + format.length header = [record, length].pack("vv") data = [ifmt, cch, encoding].pack("vvC") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data, format) end private :store_num_format ############################################################################### # # _store_1904() # # Write Excel 1904 record to indicate the date system in use. # def store_1904 #:nodoc: record = 0x0022 # Record identifier length = 0x0002 # Bytes to follow f1904 = @date_1904 ? 1 : 0 # Flag for 1904 date system header = [record, length].pack("vv") data = [f1904].pack("v") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_1904 ############################################################################### # # _store_supbook() # # Write BIFF record SUPBOOK to indicate that the workbook contains external # references, in our case, formula, print area and print title refs. # def store_supbook #:nodoc: record = 0x01AE # Record identifier length = 0x0004 # Number of bytes to follow ctabs = @worksheets.size # Number of worksheets stVirtPath = 0x0401 # Encoded workbook filename header = [record, length].pack("vv") data = [ctabs, stVirtPath].pack("vv") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_supbook ############################################################################### # # _store_externsheet() # # Writes the Excel BIFF EXTERNSHEET record. These references are used by # formulas. TODO NAME record is required to define the print area and the # repeat rows and columns. # def store_externsheet # :nodoc: record = 0x0017 # Record identifier # Get the external refs ext_refs = @ext_refs ext = ext_refs.keys.sort # Change the external refs from stringified "1:1" to [1, 1] ext.map! {|e| e.split(/:/).map! {|e| e.to_i} } cxti = ext.size # Number of Excel XTI structures rgxti = '' # Array of XTI structures # Write the XTI structs ext.each do |e| rgxti = rgxti + [0, e[0], e[1]].pack("vvv") end data = [cxti].pack("v") + rgxti header = [record, data.length].pack("vv") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end # # Store the NAME record used for storing the print area, repeat rows, repeat # columns, autofilters and defined names. # # TODO. This is a more generic version that will replace _store_name_short() # and _store_name_long(). # def store_name(name, encoding, sheet_index, formula) # :nodoc: record = 0x0018 # Record identifier text_length = name.length formula_length = formula.length # UTF-16 string length is in characters not bytes. text_length /= 2 if encoding != 0 grbit = 0x0000 # Option flags shortcut = 0x00 # Keyboard shortcut ixals = 0x0000 # Unused index. menu_length = 0x00 # Length of cust menu text desc_length = 0x00 # Length of description text help_length = 0x00 # Length of help topic text status_length = 0x00 # Length of status bar text # Set grbit built-in flag and the hidden flag for autofilters. if text_length == 1 grbit = 0x0020 if name[0] == 0x06 # Print area grbit = 0x0020 if name[0] == 0x07 # Print titles grbit = 0x0021 if name[0] == 0x0D # Autofilter end data = [grbit].pack("v") data += [shortcut].pack("C") data += [text_length].pack("C") data += [formula_length].pack("v") data += [ixals].pack("v") data += [sheet_index].pack("v") data += [menu_length].pack("C") data += [desc_length].pack("C") data += [help_length].pack("C") data += [status_length].pack("C") data += [encoding].pack("C") data += name data += formula header = [record, data.length].pack("vv") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end ############################################################################### # # _store_name_short() # index = shift # Sheet index # type = shift # ext_ref = shift # TODO # rowmin = $_[0] # Start row # rowmax = $_[1] # End row # colmin = $_[2] # Start column # colmax = $_[3] # end column # hidden = $_[4] # Name is hidden # # # Store the NAME record in the short format that is used for storing the print # area, repeat rows only and repeat columns only. # def store_name_short(index, type, ext_ref, rowmin, rowmax, colmin, colmax, hidden = nil) #:nodoc: record = 0x0018 # Record identifier length = 0x001b # Number of bytes to follow grbit = 0x0020 # Option flags chKey = 0x00 # Keyboard shortcut cch = 0x01 # Length of text name cce = 0x000b # Length of text definition unknown01 = 0x0000 # ixals = index +1 # Sheet index unknown02 = 0x00 # cchCustMenu = 0x00 # Length of cust menu text cchDescription = 0x00 # Length of description text cchHelptopic = 0x00 # Length of help topic text cchStatustext = 0x00 # Length of status bar text rgch = type # Built-in name type unknown03 = 0x3b # grbit = 0x0021 if hidden header = [record, length].pack("vv") data = [grbit].pack("v") data += [chKey].pack("C") data += [cch].pack("C") data += [cce].pack("v") data += [unknown01].pack("v") data += [ixals].pack("v") data += [unknown02].pack("C") data += [cchCustMenu].pack("C") data += [cchDescription].pack("C") data += [cchHelptopic].pack("C") data += [cchStatustext].pack("C") data += [rgch].pack("C") data += [unknown03].pack("C") data += [ext_ref].pack("v") data += [rowmin].pack("v") data += [rowmax].pack("v") data += [colmin].pack("v") data += [colmax].pack("v") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_name_short ############################################################################### # # _store_name_long() # my $index = shift; # Sheet index # my $type = shift; # my $ext_ref = shift; # TODO # my $rowmin = $_[0]; # Start row # my $rowmax = $_[1]; # End row # my $colmin = $_[2]; # Start column # my $colmax = $_[3]; # end column # # # Store the NAME record in the long format that is used for storing the repeat # rows and columns when both are specified. This share a lot of code with # _store_name_short() but we use a separate method to keep the code clean. # Code abstraction for reuse can be carried too far, and I should know. ;-) # def store_name_long(index, type, ext_ref, rowmin, rowmax, colmin, colmax) #:nodoc: record = 0x0018 # Record identifier length = 0x002a # Number of bytes to follow grbit = 0x0020 # Option flags chKey = 0x00 # Keyboard shortcut cch = 0x01 # Length of text name cce = 0x001a # Length of text definition unknown01 = 0x0000 # ixals = index +1 # Sheet index unknown02 = 0x00 # cchCustMenu = 0x00 # Length of cust menu text cchDescription = 0x00 # Length of description text cchHelptopic = 0x00 # Length of help topic text cchStatustext = 0x00 # Length of status bar text rgch = type # Built-in name type unknown03 = 0x29 unknown04 = 0x0017 unknown05 = 0x3b header = [record, length].pack("vv") data = [grbit].pack("v") data += [chKey].pack("C") data += [cch].pack("C") data += [cce].pack("v") data += [unknown01].pack("v") data += [ixals].pack("v") data += [unknown02].pack("C") data += [cchCustMenu].pack("C") data += [cchDescription].pack("C") data += [cchHelptopic].pack("C") data += [cchStatustext].pack("C") data += [rgch].pack("C") # Column definition data += [unknown03].pack("C") data += [unknown04].pack("v") data += [unknown05].pack("C") data += [ext_ref].pack("v") data += [0x0000].pack("v") data += [0xffff].pack("v") data += [colmin].pack("v") data += [colmax].pack("v") # Row definition data += [unknown05].pack("C") data += [ext_ref].pack("v") data += [rowmin].pack("v") data += [rowmax].pack("v") data += [0x00].pack("v") data += [0xff].pack("v") # End of data data += [0x10].pack("C") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_name_long ############################################################################### # # _store_palette() # # Stores the PALETTE biff record. # def store_palette #:nodoc: record = 0x0092 # Record identifier length = 2 + 4 * @palette.size # Number of bytes to follow ccv = @palette.size # Number of RGB values to follow data = '' # The RGB data # Pack the RGB data @palette.each do |p| data = data + p.pack('CCCC') end header = [record, length, ccv].pack("vvv") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_palette ############################################################################### # # _store_codepage() # # Stores the CODEPAGE biff record. # def store_codepage #:nodoc: record = 0x0042 # Record identifier length = 0x0002 # Number of bytes to follow cv = @codepage # The code page header = [record, length].pack("vv") data = [cv].pack("v") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_codepage ############################################################################### # # _store_country() # # Stores the COUNTRY biff record. # def store_country #:nodoc: record = 0x008C # Record identifier length = 0x0004 # Number of bytes to follow country_default = @country country_win_ini = @country header = [record, length].pack("vv") data = [country_default, country_win_ini].pack("vv") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_country ############################################################################### # # _store_hideobj() # # Stores the HIDEOBJ biff record. # def store_hideobj #:nodoc: record = 0x008D # Record identifier length = 0x0002 # Number of bytes to follow hide = @hideobj # Option to hide objects header = [record, length].pack("vv") data = [hide].pack("v") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_hideobj ############################################################################### ############################################################################### ############################################################################### ############################################################################### # # _calculate_extern_sizes() # # We need to calculate the space required by the SUPBOOK, EXTERNSHEET and NAME # records so that it can be added to the BOUNDSHEET offsets. # def calculate_extern_sizes # :nodoc: ext_refs = @parser.get_ext_sheets ext_ref_count = ext_refs.keys.size length = 0 index = 0 unless @defined_names.empty? index = 0 key = "#{index}:#{index}" unless ext_refs.has_key?(key) ext_refs[key] = ext_ref_count ext_ref_count += 1 end end @defined_names.each do |defined_name| length += 19 + defined_name[:name].length + defined_name[:formula].length end @worksheets.each do |worksheet| rowmin = worksheet.title_rowmin colmin = worksheet.title_colmin filter = worksheet.filter_count key = "#{index}:#{index}" index += 1 # Add area NAME records # if !worksheet.print_rowmin.nil? if ext_refs[key].nil? ext_refs[key] = ext_ref_count ext_ref_count += 1 end length += 31 end # Add title NAME records # if rowmin and colmin if ext_refs[key].nil? ext_refs[key] = ext_ref_count ext_ref_count += 1 end length += 46 elsif rowmin or colmin if ext_refs[key].nil? ext_refs[key] = ext_ref_count ext_ref_count += 1 end length += 31 else # TODO, may need this later. end # Add Autofilter NAME records # if filter != 0 if ext_refs[key].nil? ext_refs[key] = ext_ref_count ext_ref_count += 1 end length += 31 end end # Update the ref counts. @ext_ref_count = ext_ref_count @ext_refs = ext_refs # If there are no external refs then we don't write, SUPBOOK, EXTERNSHEET # and NAME. Therefore the length is 0. return length = 0 if ext_ref_count == 0 # The SUPBOOK record is 8 bytes length += 8 # The EXTERNSHEET record is 6 bytes + 6 bytes for each external ref length += 6 * (1 + ext_ref_count) return length end ############################################################################### # # _calculate_shared_string_sizes() # # Handling of the SST continue blocks is complicated by the need to include an # additional continuation byte depending on whether the string is split between # blocks or whether it starts at the beginning of the block. (There are also # additional complications that will arise later when/if Rich Strings are # supported). As such we cannot use the simple CONTINUE mechanism provided by # the _add_continue() method in BIFFwriter.pm. Thus we have to make two passes # through the strings data. The first is to calculate the required block sizes # and the second, in _store_shared_strings(), is to write the actual strings. # The first pass through the data is also used to calculate the size of the SST # and CONTINUE records for use in setting the BOUNDSHEET record offsets. The # downside of this is that the same algorithm repeated in _store_shared_strings. # def calculate_shared_string_sizes #:nodoc: strings = Array.new(@sinfo[:str_unique]) @sinfo[:str_table].each_key do |key| strings[@sinfo[:str_table][key]] = key end # The SST data could be very large, free some memory (maybe). @sinfo[:str_table] = nil @str_array = strings # Iterate through the strings to calculate the CONTINUE block sizes. # # The SST blocks requires a specialised CONTINUE block, so we have to # ensure that the maximum data block size is less than the limit used by # _add_continue() in BIFFwriter.pm. For simplicity we use the same size # for the SST and CONTINUE records: # 8228 : Maximum Excel97 block size # -4 : Length of block header # -8 : Length of additional SST header information # -8 : Arbitrary number to keep within _add_continue() limit # = 8208 # continue_limit = 8208 block_length = 0 written = 0 block_sizes = [] continue = 0 strings.each do |string| string_length = string.length encoding = string.unpack("xx C")[0] split_string = 0 # Block length is the total length of the strings that will be # written out in a single SST or CONTINUE block. # block_length += string_length # We can write the string if it doesn't cross a CONTINUE boundary if block_length < continue_limit written += string_length next end # Deal with the cases where the next string to be written will exceed # the CONTINUE boundary. If the string is very long it may need to be # written in more than one CONTINUE record. # while block_length >= continue_limit # We need to avoid the case where a string is continued in the first # n bytes that contain the string header information. # header_length = 3 # Min string + header size -1 space_remaining = continue_limit -written -continue # Unicode data should only be split on char (2 byte) boundaries. # Therefore, in some cases we need to reduce the amount of available # space by 1 byte to ensure the correct alignment. align = 0 # Only applies to Unicode strings if encoding == 1 # Min string + header size -1 header_length = 4 if space_remaining > header_length # String contains 3 byte header => split on odd boundary if split_string == 0 and space_remaining % 2 != 1 space_remaining -= 1 align = 1 # Split section without header => split on even boundary elsif split_string != 0 and space_remaining % 2 == 1 space_remaining -= 1 align = 1 end split_string = 1 end end if space_remaining > header_length # Write as much as possible of the string in the current block written += space_remaining # Reduce the current block length by the amount written block_length -= continue_limit -continue -align # Store the max size for this block block_sizes.push(continue_limit -align) # If the current string was split then the next CONTINUE block # should have the string continue flag (grbit) set unless the # split string fits exactly into the remaining space. # if block_length > 0 continue = 1 else continue = 0 end else # Store the max size for this block block_sizes.push(written +continue) # Not enough space to start the string in the current block block_length -= continue_limit -space_remaining -continue continue = 0 end # If the string (or substr) is small enough we can write it in the # new CONTINUE block. Else, go through the loop again to write it in # one or more CONTINUE blocks # if block_length < continue_limit written = block_length else written = 0 end end end # Store the max size for the last block unless it is empty block_sizes.push(written +continue) if written +continue != 0 @str_block_sizes = block_sizes.dup # Calculate the total length of the SST and associated CONTINUEs (if any). # The SST record will have a length even if it contains no strings. # This length is required to set the offsets in the BOUNDSHEET records since # they must be written before the SST records # length = 12 length += block_sizes.shift unless block_sizes.empty? # SST while !block_sizes.empty? do length += 4 + block_sizes.shift # CONTINUEs end return length end private :calculate_shared_string_sizes ############################################################################### # # _store_shared_strings() # # Write all of the workbooks strings into an indexed array. # # See the comments in _calculate_shared_string_sizes() for more information. # # We also use this routine to record the offsets required by the EXTSST table. # In order to do this we first identify the first string in an EXTSST bucket # and then store its global and local offset within the SST table. The offset # occurs wherever the start of the bucket string is written out via append(). # def store_shared_strings #:nodoc: strings = @str_array record = 0x00FC # Record identifier length = 0x0008 # Number of bytes to follow total = 0x0000 # Iterate through the strings to calculate the CONTINUE block sizes continue_limit = 8208 block_length = 0 written = 0 continue = 0 # The SST and CONTINUE block sizes have been pre-calculated by # _calculate_shared_string_sizes() block_sizes = @str_block_sizes # The SST record is required even if it contains no strings. Thus we will # always have a length # if block_sizes.size != 0 length = 8 + block_sizes.shift else # No strings length = 8 end # Initialise variables used to track EXTSST bucket offsets. extsst_str_num = -1 sst_block_start = @datasize # Write the SST block header information header = [record, length].pack("vv") data = [@sinfo[:str_total], @sinfo[:str_unique]].pack("VV") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) # Iterate through the strings and write them out return if strings.empty? strings.each do |string| string_length = string.length encoding = string.unpack("xx C")[0] split_string = 0 bucket_string = 0 # Used to track EXTSST bucket offsets. # Check if the string is at the start of a EXTSST bucket. extsst_str_num += 1 if extsst_str_num % @extsst_bucket_size == 0 bucket_string = 1 end # Block length is the total length of the strings that will be # written out in a single SST or CONTINUE block. # block_length += string_length # We can write the string if it doesn't cross a CONTINUE boundary if block_length < continue_limit # Store location of EXTSST bucket string. if bucket_string != 0 global_offset = @datasize local_offset = @datasize - sst_block_start @extsst_offsets.push([global_offset, local_offset]) bucket_string = 0 end print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(string) written += string_length next end # Deal with the cases where the next string to be written will exceed # the CONTINUE boundary. If the string is very long it may need to be # written in more than one CONTINUE record. # while block_length >= continue_limit # We need to avoid the case where a string is continued in the first # n bytes that contain the string header information. # header_length = 3 # Min string + header size -1 space_remaining = continue_limit -written -continue # Unicode data should only be split on char (2 byte) boundaries. # Therefore, in some cases we need to reduce the amount of available # space by 1 byte to ensure the correct alignment. align = 0 # Only applies to Unicode strings if encoding == 1 # Min string + header size -1 header_length = 4 if space_remaining > header_length # String contains 3 byte header => split on odd boundary if split_string == 0 and space_remaining % 2 != 1 space_remaining -= 1 align = 1 # Split section without header => split on even boundary elsif split_string != 0 and space_remaining % 2 == 1 space_remaining -= 1 align = 1 end split_string = 1 end end if space_remaining > header_length # Write as much as possible of the string in the current block tmp = string[0, space_remaining] # Store location of EXTSST bucket string. if bucket_string != 0 global_offset = @datasize local_offset = @datasize - sst_block_start @extsst_offsets.push([global_offset, local_offset]) bucket_string = 0 end print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(tmp) # The remainder will be written in the next block(s) string = string[space_remaining .. string.length-1] # Reduce the current block length by the amount written block_length -= continue_limit -continue -align # If the current string was split then the next CONTINUE block # should have the string continue flag (grbit) set unless the # split string fits exactly into the remaining space. # if block_length > 0 continue = 1 else continue = 0 end else # Not enough space to start the string in the current block block_length -= continue_limit -space_remaining -continue continue = 0 end # Write the CONTINUE block header if block_sizes.size != 0 sst_block_start= @datasize # Reset EXTSST offset. record = 0x003C length = block_sizes.shift header = [record, length].pack("vv") header = header + [encoding].pack("C") if continue != 0 print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header) end # If the string (or substr) is small enough we can write it in the # new CONTINUE block. Else, go through the loop again to write it in # one or more CONTINUE blocks # if block_length < continue_limit # Store location of EXTSST bucket string. if bucket_string != 0 global_offset = @datasize local_offset = @datasize - sst_block_start @extsst_offsets.push([global_offset, local_offset]) bucket_string = 0 end print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(string) written = block_length else written = 0 end end end end private :store_shared_strings ############################################################################### # # _calculate_extsst_size # # The number of buckets used in the EXTSST is between 0 and 128. The number of # strings per bucket (bucket size) has a minimum value of 8 and a theoretical # maximum of 2^16. For "number of strings" < 1024 there is a constant bucket # size of 8. The following algorithm generates the same size/bucket ratio # as Excel. # def calculate_extsst_size #:nodoc: unique_strings = @sinfo[:str_unique] if unique_strings < 1024 bucket_size = 8 else bucket_size = 1 + Integer(unique_strings / 128.0) end buckets = Integer((unique_strings + bucket_size -1) / Float(bucket_size)) @extsst_buckets = buckets @extsst_bucket_size = bucket_size return 6 + 8 * buckets end ############################################################################### # # _store_extsst # # Write EXTSST table using the offsets calculated in _store_shared_strings(). # def store_extsst #:nodoc: offsets = @extsst_offsets bucket_size = @extsst_bucket_size record = 0x00FF # Record identifier length = 2 + 8 * offsets.size # Bytes to follow header = [record, length].pack('vv') data = [bucket_size].pack('v') offsets.each do |offset| data = data + [offset[0], offset[1], 0].pack('Vvv') end print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) end private :store_extsst # # Methods related to comments and MSO objects. # ############################################################################### # # _add_mso_drawing_group() # # Write the MSODRAWINGGROUP record that keeps track of the Escher drawing # objects in the file such as images, comments and filters. # def add_mso_drawing_group #:nodoc: return unless @mso_size != 0 record = 0x00EB # Record identifier length = 0x0000 # Number of bytes to follow data = store_mso_dgg_container data = data + store_mso_dgg(*@mso_clusters) data = data + store_mso_bstore_container @images_data.each do |image| data = data + store_mso_images(*image) end data = data + store_mso_opt data = data + store_mso_split_menu_colors length = data.length header = [record, length].pack("vv") add_mso_drawing_group_continue(header + data) return header + data # For testing only. end private :add_mso_drawing_group ############################################################################### # # _add_mso_drawing_group_continue() # # See first the WriteExcel::BIFFwriter::_add_continue() method. # # Add specialised CONTINUE headers to large MSODRAWINGGROUP data block. # We use the Excel 97 max block size of 8228 - 4 bytes for the header = 8224. # # The structure depends on the size of the data block: # # Case 1: <= 8224 bytes 1 MSODRAWINGGROUP # Case 2: <= 2*8224 bytes 1 MSODRAWINGGROUP + 1 CONTINUE # Case 3: > 2*8224 bytes 2 MSODRAWINGGROUP + n CONTINUE # def add_mso_drawing_group_continue(data) #:nodoc: limit = 8228 -4 mso_group = 0x00EB # Record identifier continue = 0x003C # Record identifier block_count = 1 # Ignore the base class _add_continue() method. @ignore_continue = 1 # Case 1 above. Just return the data as it is. if data.length <= limit print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(data) return end # Change length field of the first MSODRAWINGGROUP block. Case 2 and 3. tmp = data.dup tmp[0, limit + 4] = "" tmp[2, 2] = [limit].pack('v') print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(tmp) # Add MSODRAWINGGROUP and CONTINUE blocks for Case 3 above. while data.length > limit if block_count == 1 # Add extra MSODRAWINGGROUP block header. header = [mso_group, limit].pack("vv") block_count += 1 else # Add normal CONTINUE header. header = [continue, limit].pack("vv") end tmp = data.dup tmp[0, limit] = '' print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, tmp) end # Last CONTINUE block for remaining data. Case 2 and 3 above. header = [continue, data.length].pack("vv") print "#{__FILE__}(#{__LINE__}) \n" if defined?($debug) append(header, data) # Turn the base class _add_continue() method back on. @ignore_continue = 0 end private :add_mso_drawing_group_continue ############################################################################### # # _store_mso_dgg_container() # # Write the Escher DggContainer record that is part of MSODRAWINGGROUP. # def store_mso_dgg_container #:nodoc: type = 0xF000 version = 15 instance = 0 data = '' length = @mso_size -12 # -4 (biff header) -8 (for this). return add_mso_generic(type, version, instance, data, length) end private :store_mso_dgg_container ############################################################################### # # _store_mso_dgg() # my $max_spid = $_[0]; # my $num_clusters = $_[1]; # my $shapes_saved = $_[2]; # my $drawings_saved = $_[3]; # my $clusters = $_[4]; # # Write the Escher Dgg record that is part of MSODRAWINGGROUP. # def store_mso_dgg(max_spid, num_clusters, shapes_saved, drawings_saved, clusters) #:nodoc: type = 0xF006 version = 0 instance = 0 data = '' length = nil # Calculate automatically. data = [max_spid, num_clusters, shapes_saved, drawings_saved].pack("VVVV") clusters.each do |aref| drawing_id = aref[0] shape_ids_used = aref[1] data = data + [drawing_id, shape_ids_used].pack("VV") end return add_mso_generic(type, version, instance, data, length) end private :store_mso_dgg ############################################################################### # # _store_mso_bstore_container() # # Write the Escher BstoreContainer record that is part of MSODRAWINGGROUP. # def store_mso_bstore_container #:nodoc: return '' if @images_size == 0 type = 0xF001 version = 15 instance = @images_data.size # Number of images. data = '' length = @images_size +8 *instance return add_mso_generic(type, version, instance, data, length) end private :store_mso_bstore_container ############################################################################### # # _store_mso_images() # ref_count = $_[0] # image_type = $_[1] # image = $_[2] # size = $_[3] # checksum1 = $_[4] # checksum2 = $_[5] # # Write the Escher BstoreContainer record that is part of MSODRAWINGGROUP. # def store_mso_images(ref_count, image_type, image, size, checksum1, checksum2) #:nodoc: blip_store_entry = store_mso_blip_store_entry( ref_count, image_type, size, checksum1 ) blip = store_mso_blip( image_type, image, size, checksum1, checksum2 ) return blip_store_entry + blip end private :store_mso_images ############################################################################### # # _store_mso_blip_store_entry() # ref_count = $_[0] # image_type = $_[1] # size = $_[2] # checksum1 = $_[3] # # Write the Escher BlipStoreEntry record that is part of MSODRAWINGGROUP. # def store_mso_blip_store_entry(ref_count, image_type, size, checksum1) #:nodoc: type = 0xF007 version = 2 instance = image_type length = size +61 data = [image_type].pack('C') + # Win32 [image_type].pack('C') + # Mac [checksum1].pack('H*') + # Uid checksum [0xFF].pack('v') + # Tag [size +25].pack('V') + # Next Blip size [ref_count].pack('V') + # Image ref count [0x00000000].pack('V') + # File offset [0x00].pack('C') + # Usage [0x00].pack('C') + # Name length [0x00].pack('C') + # Unused [0x00].pack('C') # Unused return add_mso_generic(type, version, instance, data, length) end private :store_mso_blip_store_entry ############################################################################### # # _store_mso_blip() # image_type = $_[0] # image_data = $_[1] # size = $_[2] # checksum1 = $_[3] # checksum2 = $_[4] # # Write the Escher Blip record that is part of MSODRAWINGGROUP. # def store_mso_blip(image_type, image_data, size, checksum1, checksum2) #:nodoc: instance = 0x046A if image_type == 5 # JPG instance = 0x06E0 if image_type == 6 # PNG instance = 0x07A9 if image_type == 7 # BMP # BMPs contain an extra checksum for the stripped data. if image_type == 7 checksum1 = checksum2 + checksum1 end type = 0xF018 + image_type version = 0x0000 length = size +17 data = [checksum1].pack('H*') + # Uid checksum [0xFF].pack('C') + # Tag image_data # Image return add_mso_generic(type, version, instance, data, length) end private :store_mso_blip ############################################################################### # # _store_mso_opt() # # Write the Escher Opt record that is part of MSODRAWINGGROUP. # def store_mso_opt #:nodoc: type = 0xF00B version = 3 instance = 3 data = '' length = 18 data = ['BF0008000800810109000008C0014000'+'0008'].pack("H*") return add_mso_generic(type, version, instance, data, length) end private :store_mso_opt ############################################################################### # # _store_mso_split_menu_colors() # # Write the Escher SplitMenuColors record that is part of MSODRAWINGGROUP. # def store_mso_split_menu_colors #:nodoc: type = 0xF11E version = 0 instance = 4 data = '' length = 16 data = ['0D0000080C00000817000008F7000010'].pack("H*") return add_mso_generic(type, version, instance, data, length) end private :store_mso_split_menu_colors end