summaryrefslogtreecommitdiffhomepage
path: root/lib/axlsx/workbook/worksheet/rich_text_run.rb
blob: b2cd8a513f5c46a7b69942e843d81ae977a998ae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# frozen_string_literal: true

module Axlsx
  # The RichTextRun class creates and self serializing text run.
  class RichTextRun
    include Axlsx::OptionsParser

    attr_reader :value

    # A list of allowed inline style attributes used for validation
    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

    def value=(value)
      @value = value
    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)
    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

    # Tries to work out the width of the longest line in the run
    # @param [Array] widtharray this array is populated with the widths of each line in the run.
    # @return [Array]
    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?
      instance_variable_set :"@#{attr}", value
    end

    # Serializes the RichTextRun
    # @param [String] str
    # @return [String]
    def to_xml_string(str = +'')
      valid = RichTextRun::INLINE_STYLES
      data = Axlsx.instance_values_for(self).transform_keys(&:to_sym)
      data = data.select { |key, value| !value.nil? && valid.include?(key) }

      str << '<r><rPr>'
      data.each do |key, val|
        case key
        when :font_name
          str << '<rFont val="' << font_name << '"/>'
        when :color
          str << val.to_xml_string
        else
          str << '<' << key.to_s << ' val="' << xml_value(val) << '"/>'
        end
      end
      clean_value = Axlsx.trust_input ? @value.to_s : ::CGI.escapeHTML(Axlsx.sanitize(@value.to_s))
      str << '</rPr><t>' << clean_value << '</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.size * 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