module Writeexcel class Worksheet < BIFFWriter require 'writeexcel/helper' class Collection def initialize @items = {} end def <<(item) if @items[item.row] @items[item.row][item.col] = item else @items[item.row] = { item.col => item } end end def array return @array if @array @array = [] @items.keys.sort.each do |row| @items[row].keys.sort.each do |col| @array << @items[row][col] end end @array end end class Comments < Collection attr_writer :visible def initialize super @visible = false end def visible? @visible end end class Comment attr_reader :row, :col, :string, :encoding, :author, :author_encoding, :visible, :color, :vertices def initialize(worksheet, row, col, string, options = {}) @worksheet = worksheet @row, @col = row, col @params = params_with(options) @string, @params[:encoding] = string_and_encoding(string, @params[:encoding], 'comment') # Limit the string to the max number of chars (not bytes). max_len = 32767 max_len = max_len * 2 if @params[:encoding] != 0 if @string.bytesize > max_len @string = @string[0 .. max_len] end @encoding = @params[:encoding] @author = @params[:author] @author_encoding = @params[:author_encoding] @visible = @params[:visible] @color = @params[:color] @vertices = calc_vertices end def store_comment_record(i, num_objects, num_comments, spid) str_len = string.bytesize str_len = str_len / 2 if encoding != 0 # Num of chars not bytes. spid = store_comment_mso_drawing_record(i, num_objects, num_comments, spid, visible, color, vertices) store_obj_comment(num_objects + i + 1) store_mso_drawing_text_box store_txo(str_len) store_txo_continue_1(string, encoding) formats = [[0, 9], [str_len, 0]] store_txo_continue_2(formats) spid end # # Write the worksheet NOTE record that is part of cell comments. # def store_note_record(obj_id) #:nodoc: comment_author = author comment_author_enc = author_encoding ruby_19 { comment_author = [comment_author].pack('a*') if comment_author.ascii_only? } record = 0x001C # Record identifier length = 0x000C # Bytes to follow comment_author = '' unless comment_author comment_author_enc = 0 unless author_encoding # 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 visible comment_visible = visible != 0 ? 0x0002 : 0x0000 else comment_visible = @worksheet.comments_visible? ? 0x0002 : 0x0000 end # Get the number of chars in the author string (not bytes). num_chars = comment_author.bytesize num_chars = num_chars / 2 if comment_author_enc != 0 && comment_author_enc # Null terminate the author string. comment_author = ruby_18 { comment_author + "\0" } || ruby_19 { comment_author.force_encoding('BINARY') + "\0".force_encoding('BINARY') } # Pack the record. data = [@row, @col, comment_visible, obj_id, num_chars, comment_author_enc].pack("vvvvvC") length = data.bytesize + comment_author.bytesize header = [record, length].pack("vv") append(header, data, comment_author) end # # Write the Escher Opt record that is part of MSODRAWING. # def store_mso_opt_comment(spid, visible = nil, colour = 0x50) #:nodoc: type = 0xF00B version = 3 instance = 9 data = '' length = 54 # Use the visible flag if set by the user or else use the worksheet value. # Note that the value used is the opposite of Comment#note_record. # if visible visible = visible != 0 ? 0x0000 : 0x0002 else visible = @worksheet.comments_visible? ? 0x0000 : 0x0002 end data = [spid].pack('V') + ['0000BF00080008005801000000008101'].pack("H*") + [colour].pack("C") + ['000008830150000008BF011000110001'+'02000000003F0203000300BF03'].pack("H*") + [visible].pack('v') + ['0A00'].pack('H*') @worksheet.add_mso_generic(type, version, instance, data, length) end # # OBJ record that is part of cell comments. # obj_id # Object ID number. # def obj_comment_record(obj_id) #:nodoc: record = 0x005D # Record identifier length = 0x0034 # Bytes to follow obj_type = 0x0019 # Object type (comment). data = '' # Record data. sub_record = 0x0000 # Sub-record identifier. sub_length = 0x0000 # Length of sub-record. sub_data = '' # Data of sub-record. options = 0x4011 reserved = 0x0000 # Add ftCmo (common object data) subobject sub_record = 0x0015 # ftCmo sub_length = 0x0012 sub_data = [obj_type, obj_id, options, reserved, reserved, reserved].pack( "vvvVVV") data = [sub_record, sub_length].pack("vv") + sub_data # Add ftNts (note structure) subobject sub_record = 0x000D # ftNts sub_length = 0x0016 sub_data = [reserved,reserved,reserved,reserved,reserved,reserved].pack( "VVVVVv") data += [sub_record, sub_length].pack("vv") + sub_data # Add ftEnd (end of object) subobject sub_record = 0x0000 # ftNts sub_length = 0x0000 data += [sub_record, sub_length].pack("vv") # Pack the record. header = [record, length].pack("vv") header + data end private def params_with(options) params = default_params.update(options) # Ensure that a width and height have been set. params[:width] = default_width unless params[:width] && params[:width] != 0 params[:width] = params[:width] * params[:x_scale] if params[:x_scale] != 0 params[:height] = default_height unless params[:height] && params[:height] != 0 params[:height] = params[:height] * params[:y_scale] if params[:y_scale] != 0 params[:author], params[:author_encoding] = string_and_encoding(params[:author], params[:author_encoding], 'author') # Set the comment background colour. params[:color] = background_color(params[:color]) # 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. # params[:start_row] = default_start_row unless params[:start_row] params[:y_offset] = default_y_offset unless params[:y_offset] params[:start_col] = default_start_col unless params[:start_col] params[:x_offset] = default_x_offset unless params[:x_offset] params end def default_params { :author => '', :author_encoding => 0, :encoding => 0, :color => nil, :start_cell => nil, :start_col => nil, :start_row => nil, :visible => nil, :width => default_width, :height => default_height, :x_offset => nil, :x_scale => 1, :y_offset => nil, :y_scale => 1 } end def default_width 128 end def default_height 74 end def default_start_row case @row when 0 then 0 when 65533 then 65529 when 65534 then 65530 when 65535 then 65531 else @row -1 end end def default_y_offset case @row when 0 then 2 when 65533 then 4 when 65534 then 4 when 65535 then 2 else 7 end end def default_start_col case @col when 253 then 250 when 254 then 251 when 255 then 252 else @col + 1 end end def default_x_offset case @col when 253 then 49 when 254 then 49 when 255 then 49 else 15 end end def string_and_encoding(string, encoding, type) string = convert_to_ascii_if_ascii(string) if encoding != 0 raise "Uneven number of bytes in #{type} string" if string.bytesize % 2 != 0 # Change from UTF-16BE to UTF-16LE string = utf16be_to_16le(string) # Handle utf8 strings else if is_utf8?(string) string = NKF.nkf('-w16L0 -m0 -W', string) ruby_19 { string.force_encoding('UTF-16LE') } encoding = 1 end end [string, encoding] end def background_color(color) color = Colors.new.get_color(color) color = 0x50 if color == 0x7FFF # Default color. color end # Calculate the positions of comment object. def calc_vertices @worksheet.position_object( @params[:start_col], @params[:start_row], @params[:x_offset], @params[:y_offset], @params[:width], @params[:height] ) end def store_comment_mso_drawing_record(i, num_objects, num_comments, spid, visible, color, vertices) if i == 0 && num_objects == 0 # Write the parent MSODRAWIING record. dg_length = 200 + 128 * (num_comments - 1) spgr_length = 176 + 128 * (num_comments - 1) data = @worksheet.store_parent_mso_record(dg_length, spgr_length, spid) spid += 1 else data = '' end data += @worksheet.store_mso_sp_container(120) + @worksheet.store_mso_sp(202, spid, 0x0A00) spid += 1 data += store_mso_opt_comment(0x80, visible, color) + @worksheet.store_mso_client_anchor(3, *vertices) + @worksheet.store_mso_client_data record = 0x00EC # Record identifier length = data.bytesize header = [record, length].pack("vv") append(header, data) spid end def store_obj_comment(obj_id) append(obj_comment_record(obj_id)) end # # Write the MSODRAWING ClientTextbox record that is part of comments. # def store_mso_drawing_text_box #:nodoc: record = 0x00EC # Record identifier length = 0x0008 # Bytes to follow data = store_mso_client_text_box header = [record, length].pack('vv') append(header, data) end # # Write the Escher ClientTextbox record that is part of MSODRAWING. # def store_mso_client_text_box #:nodoc: type = 0xF00D version = 0 instance = 0 data = '' length = 0 @worksheet.add_mso_generic(type, version, instance, data, length) end # # Write the worksheet TXO record that is part of cell comments. # string_len # Length of the note text. # format_len # Length of the format runs. # rotation # Options # def store_txo(string_len, format_len = 16, rotation = 0) #:nodoc: record = 0x01B6 # Record identifier length = 0x0012 # Bytes to follow grbit = 0x0212 # Options reserved = 0x0000 # Options # Pack the record. header = [record, length].pack('vv') data = [grbit, rotation, reserved, reserved, string_len, format_len, reserved].pack("vvVvvvV") append(header, data) end # # Write the first CONTINUE record to follow the TXO record. It contains the # text data. # string # Comment string. # encoding # Encoding of the string. # def store_txo_continue_1(string, encoding = 0) #:nodoc: # 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. # limit = 8218 while string.bytesize > limit string[0 .. limit] = "" tmp_str = string data = [encoding].pack("C") + ruby_18 { tmp_str } || ruby_19 { tmp_str.force_encoding('ASCII-8BIT') } length = data.bytesize header = [record, length].pack('vv') append(header, data) end # Pack the record. data = ruby_18 { [encoding].pack("C") + string } || ruby_19 { [encoding].pack("C") + string.force_encoding('ASCII-8BIT') } record = 0x003C # Record identifier length = data.bytesize header = [record, length].pack('vv') append(header, data) end # # Write the second CONTINUE record to follow the TXO record. It contains the # formatting information for the string. # formats # Formatting information # def store_txo_continue_2(formats) #:nodoc: # Pack the record. data = '' formats.each do |a_ref| data += [a_ref[0], a_ref[1], 0x0].pack('vvV') end record = 0x003C # Record identifier length = data.bytesize header = [record, length].pack("vv") append(header, data) end def append(*args) @worksheet.append(*args) end end end end