diff options
Diffstat (limited to 'lib/axlsx/workbook')
| -rw-r--r-- | lib/axlsx/workbook/shared_strings_table.rb | 2 | ||||
| -rw-r--r-- | lib/axlsx/workbook/workbook.rb | 4 | ||||
| -rw-r--r-- | lib/axlsx/workbook/worksheet/cell.rb | 47 | ||||
| -rw-r--r-- | lib/axlsx/workbook/worksheet/cell_serializer.rb | 36 | ||||
| -rw-r--r-- | lib/axlsx/workbook/worksheet/rich_text.rb | 33 | ||||
| -rw-r--r-- | lib/axlsx/workbook/worksheet/rich_text_run.rb | 251 |
6 files changed, 343 insertions, 30 deletions
diff --git a/lib/axlsx/workbook/shared_strings_table.rb b/lib/axlsx/workbook/shared_strings_table.rb index ebec6e67..7e9402b2 100644 --- a/lib/axlsx/workbook/shared_strings_table.rb +++ b/lib/axlsx/workbook/shared_strings_table.rb @@ -38,7 +38,7 @@ module Axlsx @xml_space = xml_space @unique_cells = {} @shared_xml_string = "" - shareable_cells = cells.flatten.select{ |cell| cell.plain_string? } + shareable_cells = cells.flatten.select{ |cell| cell.plain_string? || cell.contains_rich_text? } @count = shareable_cells.size resolve(shareable_cells) end diff --git a/lib/axlsx/workbook/workbook.rb b/lib/axlsx/workbook/workbook.rb index 040628d3..784ee0b4 100644 --- a/lib/axlsx/workbook/workbook.rb +++ b/lib/axlsx/workbook/workbook.rb @@ -5,6 +5,8 @@ require 'axlsx/workbook/worksheet/auto_filter/auto_filter.rb' require 'axlsx/workbook/worksheet/date_time_converter.rb' require 'axlsx/workbook/worksheet/protected_range.rb' require 'axlsx/workbook/worksheet/protected_ranges.rb' +require 'axlsx/workbook/worksheet/rich_text_run' +require 'axlsx/workbook/worksheet/rich_text' require 'axlsx/workbook/worksheet/cell_serializer.rb' require 'axlsx/workbook/worksheet/cell.rb' require 'axlsx/workbook/worksheet/page_margins.rb' @@ -292,7 +294,7 @@ require 'axlsx/workbook/worksheet/selection.rb' end r << Relationship.new(self, STYLES_R, STYLES_PN) if use_shared_strings - r << Relationship.new(self, SHARED_STRINGS_R, SHARED_STRINGS_PN) + r << Relationship.new(self, SHARED_STRINGS_R, SHARED_STRINGS_PN) end r end diff --git a/lib/axlsx/workbook/worksheet/cell.rb b/lib/axlsx/workbook/worksheet/cell.rb index 138efa34..a0607e26 100644 --- a/lib/axlsx/workbook/worksheet/cell.rb +++ b/lib/axlsx/workbook/worksheet/cell.rb @@ -47,6 +47,9 @@ module Axlsx parse_options(options) self.value = value + if value.is_a?(RichText) + value.cell = self + end end # this is the cached value for formula cells. If you want the values to render in iOS/Mac OSX preview @@ -64,7 +67,7 @@ module Axlsx :shadow, :condense, :extend, :u, :vertAlign, :sz, :color, :scheme].freeze - CELL_TYPES = [:date, :time, :float, :integer, + CELL_TYPES = [:date, :time, :float, :integer, :richtext, :string, :boolean, :iso_8601].freeze # The index of the cellXfs item to be applied to this cell. @@ -113,9 +116,13 @@ module Axlsx # Indicates that the cell has one or more of the custom cell styles applied. # @return [Boolean] def is_text_run? - defined?(@is_text_run) && @is_text_run + defined?(@is_text_run) && @is_text_run && !contains_rich_text? + end + + def contains_rich_text? + type == :richtext end - + # Indicates if the cell is good for shared string table def plain_string? type == :string && # String typed @@ -321,13 +328,6 @@ module Axlsx type == :string && @value.to_s.start_with?(?=) end - # This is still not perfect... - # - scaling is not linear as font sizes increase - def autowidth - return if is_formula? || value.nil? - (value.to_s.count(Worksheet::THIN_CHARS) + 3.0) * (font_size/10.0) - end - # returns the absolute or relative string style reference for # this cell. # @param [Boolean] absolute -when false a relative reference will be @@ -345,12 +345,36 @@ module Axlsx # returns the name of the cell attr_reader :name + + def autowidth + return if is_formula? || value.nil? + if contains_rich_text? + string_width('', font_size) + value.autowidth + elsif styles.cellXfs[style].alignment && styles.cellXfs[style].alignment.wrap_text + max_width = 0 + value.to_s.split(/\r?\n/).each do |line| + width = string_width(line, font_size) + max_width = width if width > max_width + end + max_width + else + string_width(value, font_size) + end + end private def styles row.worksheet.styles end + + # Returns the width of a string according to the current style + # This is still not perfect... + # - scaling is not linear as font sizes increase + def string_width(string, font_size) + font_scale = font_size / 10.0 + (string.to_s.count(Worksheet::THIN_CHARS) + 3.0) * (font_size/10.0) + end # we scale the font size if bold style is applied to either the style font or # the cell itself. Yes, it is a bit of a hack, but it is much better than using @@ -396,6 +420,8 @@ module Axlsx :float elsif v.to_s =~ Axlsx::ISO_8601_REGEX :iso_8601 + elsif v.is_a? RichText + :richtext else :string end @@ -407,6 +433,7 @@ module Axlsx # @see Axlsx#date1904 def cast_value(v) return nil if v.nil? + return v if v.is_a? RichText case type when :date self.style = STYLE_DATE if self.style == 0 diff --git a/lib/axlsx/workbook/worksheet/cell_serializer.rb b/lib/axlsx/workbook/worksheet/cell_serializer.rb index 77844ea6..0f66c53e 100644 --- a/lib/axlsx/workbook/worksheet/cell_serializer.rb +++ b/lib/axlsx/workbook/worksheet/cell_serializer.rb @@ -24,20 +24,12 @@ module Axlsx # @return [String] def run_xml_string(cell, str = '') if cell.is_text_run? - data = cell.instance_values.reject{ |key, value| value == nil || key == 'value' || key == 'type' } - keys = data.keys & Cell::INLINE_STYLES - str << '<r><rPr>' - keys.each do |key| - case key - when :font_name - str << ('<rFont val="' << cell.font_name << '"/>') - when :color - str << data[key].to_xml_string - else - str << ('<' << key.to_s << ' val="' << data[key].to_s << '"/>') - end - end - str << ('</rPr><t>' << cell.value.to_s << '</t></r>') + valid = RichTextRun::INLINE_STYLES - [:value, :type] + data = Hash[cell.instance_values.map{ |k, v| [k.to_sym, v] }] + data = data.select { |key, value| valid.include?(key) && !value.nil? } + RichText.new(cell.value.to_s, data).to_xml_string(str) + elsif cell.contains_rich_text? + cell.value.to_xml_string(str) else str << ('<t>' << cell.value.to_s << '</t>') end @@ -121,21 +113,29 @@ module Axlsx if cell.is_formula? formula_serialization cell, str elsif !cell.ssti.nil? - value_serialization 's', cell.ssti.to_s, str + value_serialization 's', cell.ssti, str else inline_string_serialization cell, str end end + + def richtext(cell, str) + if cell.ssti.nil? + inline_string_serialization cell, str + else + value_serialization 's', cell.ssti, str + end + end private def numeric(cell, str = '') - value_serialization 'n', cell.value.to_s, str + value_serialization 'n', cell.value, str end def value_serialization(serialization_type, serialization_value, str = '') - str << ('t="' << serialization_type << '"') if serialization_type - str << ('><v>' << serialization_value << '</v>') + str << ('t="' << serialization_type.to_s << '"') if serialization_type + str << ('><v>' << serialization_value.to_s << '</v>') end diff --git a/lib/axlsx/workbook/worksheet/rich_text.rb b/lib/axlsx/workbook/worksheet/rich_text.rb new file mode 100644 index 00000000..18c8e6b5 --- /dev/null +++ b/lib/axlsx/workbook/worksheet/rich_text.rb @@ -0,0 +1,33 @@ +module Axlsx + class RichText + def initialize(text = nil, options={}) + @runs = SimpleTypedList.new(RichTextRun) + add_run(text, options) unless text.nil? + yield self if block_given? + end + + attr_reader :runs + + attr_reader :cell + + def cell=(cell) + @cell = cell + @runs.each { |run| run.cell = cell } + end + + def autowidth + widtharray = [0] # Are arrays the best way of solving this problem? + @runs.each { |run| run.autowidth(widtharray) } + widtharray.max + end + + def add_run(text, options={}) + @runs << RichTextRun.new(text, options) + end + + def to_xml_string(str='') + runs.each{ |run| run.to_xml_string(str) } + str + end + end +end diff --git a/lib/axlsx/workbook/worksheet/rich_text_run.rb b/lib/axlsx/workbook/worksheet/rich_text_run.rb new file mode 100644 index 00000000..69063135 --- /dev/null +++ b/lib/axlsx/workbook/worksheet/rich_text_run.rb @@ -0,0 +1,251 @@ +module Axlsx + class RichTextRun + + include Axlsx::OptionsParser + + attr_accessor :value + + INLINE_STYLES = [:font_name, :charset, + :family, :b, :i, :strike, :outline, + :shadow, :condense, :extend, :u, + :vertAlign, :sz, :color, :scheme].freeze + + def initialize(value, options={}) + self.value = value + parse_options(options) + end + + attr_accessor :cell + + # The inline font_name property for the cell + # @return [String] + attr_reader :font_name + # @see font_name + def font_name=(v) set_run_style :validate_string, :font_name, v; end + + # The inline charset property for the cell + # As far as I can tell, this is pretty much ignored. However, based on the spec it should be one of the following: + # 0  ANSI_CHARSET + # 1 DEFAULT_CHARSET + # 2 SYMBOL_CHARSET + # 77 MAC_CHARSET + # 128 SHIFTJIS_CHARSET + # 129  HANGUL_CHARSET + # 130  JOHAB_CHARSET + # 134  GB2312_CHARSET + # 136  CHINESEBIG5_CHARSET + # 161  GREEK_CHARSET + # 162  TURKISH_CHARSET + # 163  VIETNAMESE_CHARSET + # 177  HEBREW_CHARSET + # 178  ARABIC_CHARSET + # 186  BALTIC_CHARSET + # 204  RUSSIAN_CHARSET + # 222  THAI_CHARSET + # 238  EASTEUROPE_CHARSET + # 255  OEM_CHARSET + # @return [String] + attr_reader :charset + # @see charset + def charset=(v) set_run_style :validate_unsigned_int, :charset, v; end + + # The inline family property for the cell + # @return [Integer] + # 1 Roman + # 2 Swiss + # 3 Modern + # 4 Script + # 5 Decorative + attr_reader :family + # @see family + def family=(v) + set_run_style :validate_family, :family, v.to_i + end + + # The inline bold property for the cell + # @return [Boolean] + attr_reader :b + # @see b + def b=(v) set_run_style :validate_boolean, :b, v; end + + # The inline italic property for the cell + # @return [Boolean] + attr_reader :i + # @see i + def i=(v) set_run_style :validate_boolean, :i, v; end + + # The inline strike property for the cell + # @return [Boolean] + attr_reader :strike + # @see strike + def strike=(v) set_run_style :validate_boolean, :strike, v; end + + # The inline outline property for the cell + # @return [Boolean] + attr_reader :outline + # @see outline + def outline=(v) set_run_style :validate_boolean, :outline, v; end + + # The inline shadow property for the cell + # @return [Boolean] + attr_reader :shadow + # @see shadow + def shadow=(v) set_run_style :validate_boolean, :shadow, v; end + + # The inline condense property for the cell + # @return [Boolean] + attr_reader :condense + # @see condense + def condense=(v) set_run_style :validate_boolean, :condense, v; end + + # The inline extend property for the cell + # @return [Boolean] + attr_reader :extend + # @see extend + def extend=(v) set_run_style :validate_boolean, :extend, v; end + + # The inline underline property for the cell. + # It must be one of :none, :single, :double, :singleAccounting, :doubleAccounting, true + # @return [Boolean] + # @return [String] + # @note true is for backwards compatability and is reassigned to :single + attr_reader :u + # @see u + def u=(v) + v = :single if (v == true || v == 1 || v == :true || v == 'true') + set_run_style :validate_cell_u, :u, v + end + + # The inline color property for the cell + # @return [Color] + attr_reader :color + # @param [String] v The 8 character representation for an rgb color #FFFFFFFF" + def color=(v) + @color = v.is_a?(Color) ? v : Color.new(:rgb=>v) + @is_text_run = true + end + + # The inline sz property for the cell + # @return [Inteter] + attr_reader :sz + # @see sz + def sz=(v) set_run_style :validate_unsigned_int, :sz, v; end + + # The inline vertical alignment property for the cell + # this must be one of [:baseline, :subscript, :superscript] + # @return [Symbol] + attr_reader :vertAlign + # @see vertAlign + def vertAlign=(v) + RestrictionValidator.validate :cell_vertAlign, [:baseline, :subscript, :superscript], v + set_run_style nil, :vertAlign, v + end + + # The inline scheme property for the cell + # this must be one of [:none, major, minor] + # @return [Symbol] + attr_reader :scheme + # @see scheme + def scheme=(v) + RestrictionValidator.validate :cell_scheme, [:none, :major, :minor], v + set_run_style nil, :scheme, v + end + + # The Shared Strings Table index for this cell + # @return [Integer] + attr_reader :ssti + + # @return [Integer] The cellXfs item index applied to this cell. + # @raise [ArgumentError] Invalid cellXfs id if the value provided is not within cellXfs items range. + def style=(v) + Axlsx::validate_unsigned_int(v) + count = styles.cellXfs.size + raise ArgumentError, "Invalid cellXfs id" unless v < count + @style = v + end + + def autowidth(widtharray) + return if value.nil? + if styles.cellXfs[style].alignment && styles.cellXfs[style].alignment.wrap_text + first = true + value.to_s.split(/\r?\n/, -1).each do |line| + if first + first = false + else + widtharray << 0 + end + widtharray[-1] += string_width(line, font_size) + end + else + widtharray[-1] += string_width(value.to_s, font_size) + end + widtharray + end + + # Utility method for setting inline style attributes + def set_run_style(validator, attr, value) + return unless INLINE_STYLES.include?(attr.to_sym) + Axlsx.send(validator, value) unless validator.nil? + self.instance_variable_set :"@#{attr.to_s}", value + @is_text_run = true + end + + def to_xml_string(str = '') + valid = RichTextRun::INLINE_STYLES + data = Hash[self.instance_values.map{ |k, v| [k.to_sym, v] }] + data = data.select { |key, value| valid.include?(key) && !value.nil? } + + str << '<r><rPr>' + data.keys.each do |key| + case key + when :font_name + str << ('<rFont val="' << font_name << '"/>') + when :color + str << data[key].to_xml_string + else + str << ('<' << key.to_s << ' val="' << xml_value(data[key]) << '"/>') + end + end + str << ('</rPr><t>' << @value.to_s << '</t></r>') + end + + private + + # Returns the width of a string according to the current style + # This is still not perfect... + # - scaling is not linear as font sizes increase + def string_width(string, font_size) + font_scale = font_size / 10.0 + string.count(Worksheet::THIN_CHARS) * font_scale + end + + # we scale the font size if bold style is applied to either the style font or + # the cell itself. Yes, it is a bit of a hack, but it is much better than using + # imagemagick and loading metrics for every character. + def font_size + return sz if sz + font = styles.fonts[styles.cellXfs[style].fontId] || styles.fonts[0] + (font.b || (defined?(@b) && @b)) ? (font.sz * 1.5) : font.sz + end + + def style + cell.style + end + + def styles + cell.row.worksheet.styles + end + + # Converts the value to the correct XML representation (fixes issues with + # Numbers) + def xml_value value + if value == true + 1 + elsif value == false + 0 + else + value + end.to_s + end + end +end |
