unxls 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ map.rb
2
+
3
+ module ParsedExpressions; end # Groups together Parsed Expressions structures (2.5.198)
4
+
5
+ require_relative 'biff8/parsed_expressions'
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 2.5.198 Parsed Expressions
4
+ module Unxls::Biff8::ParsedExpressions
5
+ extend self
6
+
7
+ # 2.5.198.2 BErr
8
+ # @param id [Integer]
9
+ # @return [Symbol]
10
+ def berr(id)
11
+ {
12
+ 0x00 => :'NULL!',
13
+ 0x07 => :'DIV/0!',
14
+ 0x0F => :'VALUE!',
15
+ 0x17 => :'REF!',
16
+ 0x1D => :'NAME?',
17
+ 0x24 => :'NUM!',
18
+ 0x2A => :'N/A',
19
+ }[id]
20
+ end
21
+
22
+ # 2.5.198.3 CellParsedFormula
23
+ # @param data [String]
24
+ # @return [Hash]
25
+ def cellparsedformula(data)
26
+ io = StringIO.new(data)
27
+ cce = io.read(2).unpack('v').first
28
+
29
+ {
30
+ cce: cce,
31
+ rgce: rgce(io.read(cce)),
32
+ rgcb: rgbextra(io.read) # rest of the record
33
+ }
34
+ end
35
+
36
+ # 2.5.198.4 Cetab
37
+ # @param id [Integer]
38
+ # @return [Symbol]
39
+ def cetab(id)
40
+ Unxls::Biff8::Constants::CETAB_NAMES[id]
41
+ end
42
+
43
+ # 2.5.198.17 Ftab
44
+ # @param id [Integer]
45
+ # @return [Symbol]
46
+ def ftab(id)
47
+ Unxls::Biff8::Constants::FTAB_NAMES[id]
48
+ end
49
+
50
+ # 2.5.198.21 NameParsedFormula
51
+ # @param io [StringIO]
52
+ # @param cce [Integer]
53
+ # @return [Hash]
54
+ def nameparsedformula(io, cce)
55
+ {
56
+ rgce: rgce(io.read(cce)),
57
+ rgcb: rgbextra(io)
58
+ }
59
+ end
60
+
61
+ # @param id [Integer]
62
+ def _ptg_name(id)
63
+ Unxls::Biff8::Constants::PTG_NAMES[id]
64
+ end
65
+
66
+ # @param byte1 [String]
67
+ def _ptg_2byte_header?(byte1)
68
+ Set[0x18, 0x19].include?(byte1.unpack('C').first)
69
+ end
70
+
71
+ # 2.5.198.36 PtgAttrIf
72
+ # @param io [StringIO]
73
+ # @param id [Integer]
74
+ # @return [Hash]
75
+ def ptgattrif(io, id)
76
+ { offset: io.read(2).unpack('v').first } # specifies the byte offset
77
+ end
78
+
79
+ # 2.5.198.44 PtgDataType
80
+ # @param id [Integer]
81
+ # @return [Symbol]
82
+ def ptgdatatype(id)
83
+ {
84
+ 1 => :REFERENCE, # reference to a range
85
+ 2 => :VALUE, # single value of a simple type. The type can be a Boolean, a number, a string, or an error code
86
+ 3 => :ARRAY, # array of values
87
+ }[id]
88
+ end
89
+
90
+ # 2.5.198.58 PtgExp
91
+ # @param io [StringIO]
92
+ # @param id [Integer]
93
+ # @return [Hash]
94
+ def ptgexp(io, id)
95
+ rw, cl = io.read(4).unpack('vv')
96
+ { row: rw, col: cl }
97
+ end
98
+
99
+ # 2.5.198.62 PtgFunc
100
+ # @param io [StringIO]
101
+ # @param id [Integer]
102
+ # @return [Hash]
103
+ def ptgfunc(io, id)
104
+ type_id = Unxls::BitOps.new(id).value_at(5..6)
105
+ ftab_id = io.read(2).unpack('v').first
106
+
107
+ {
108
+ type: type_id,
109
+ type_d: ptgdatatype(type_id),
110
+ iftab: ftab(ftab_id)
111
+ }
112
+ end
113
+
114
+ # 2.5.198.63 PtgFuncVar
115
+ # @param io [StringIO]
116
+ # @param id [Integer]
117
+ # @return [Hash]
118
+ def ptgfuncvar(io, id)
119
+ type_id = Unxls::BitOps.new(id).value_at(5..6)
120
+
121
+ cparams, tab_c = io.read(3).unpack('Cv')
122
+ attrs = Unxls::BitOps.new(tab_c)
123
+ tab = attrs.value_at(0..14)
124
+ f_ce_func = attrs.set_at?(15)
125
+ tab_type = f_ce_func ? :cetab : :ftab
126
+
127
+ {
128
+ type: type_id,
129
+ type_d: ptgdatatype(type_id),
130
+ cparams: cparams,
131
+ tab: tab,
132
+ tab_d: self.send(tab_type, tab),
133
+ _tab_type: tab_type,
134
+ fCeFunc: f_ce_func,
135
+ }
136
+ end
137
+
138
+ # 2.5.198.35 PtgAttrGoto
139
+ # @param io [StringIO]
140
+ # @param id [Integer]
141
+ # @return [Hash]
142
+ def ptgattrgoto(io, id)
143
+ { offset: io.read(2).unpack('v').first } # specifies a value 1 less than the byte offset
144
+ end
145
+
146
+ # 2.5.198.66 PtgInt
147
+ # @param io [StringIO]
148
+ # @param id [Integer]
149
+ # @return [Hash]
150
+ def ptgint(io, id)
151
+ { integer: io.read(2).unpack('v').first }
152
+ end
153
+
154
+ # 2.5.198.79 PtgNum
155
+ # @param io [StringIO]
156
+ # @param id [Integer]
157
+ # @return [Hash]
158
+ def ptgnum(io, id)
159
+ { value: xnum(io.read(8)) }
160
+ end
161
+
162
+ # 2.5.198.84 PtgRef
163
+ # @param io [StringIO]
164
+ # @param id [Integer]
165
+ # @return [Hash]
166
+ def ptgref(io, id)
167
+ type_id = Unxls::BitOps.new(id).value_at(5..6)
168
+ {
169
+ type: type_id,
170
+ type_d: ptgdatatype(type_id),
171
+ loc: rgceloc(io.read(4))
172
+ }
173
+ end
174
+
175
+ # 2.5.198.89 PtgStr
176
+ # @param io [StringIO]
177
+ # @param id [Integer]
178
+ # @return [Hash]
179
+ def ptgstr(io, id)
180
+ { string: shortxlunicodestring(io) }
181
+ end
182
+
183
+ alias :ptgtbl :ptgexp # 2.5.198.92 PtgTbl
184
+
185
+ # 2.5.198.104 Rgce
186
+ # @param data [String]
187
+ # @return [Hash]
188
+ def rgce(data)
189
+ io = StringIO.new(data)
190
+ ptgs = []
191
+
192
+ until io.eof? do
193
+ id1 = io.read(1)
194
+ id = if _ptg_2byte_header?(id1)
195
+ (id1 << io.read(1)).unpack('n').first
196
+ else
197
+ id1.unpack('C').first
198
+ end
199
+
200
+ ptg_name = _ptg_name(id)
201
+ ptg_method = ptg_name.downcase
202
+ ptg = self.respond_to?(ptg_method) ? self.send(ptg_method, io, id) : {}
203
+ ptg[:ptg] = ptg_name
204
+
205
+ ptgs << ptg
206
+ end
207
+
208
+ ptgs
209
+ end
210
+
211
+ # 2.5.198.109 RgceLoc
212
+ # @param data [String]
213
+ # @return [Hash]
214
+ def rgceloc(data)
215
+ rw, cl = data.unpack('vv')
216
+ {
217
+ row: rw, # RwU that specifies the row coordinate of the cell reference
218
+ column: colrelu(cl) # ColRelU that specifies the column coordinate of the cell reference and relative reference information
219
+ }
220
+ end
221
+
222
+ # 2.5.198.103 RgbExtra
223
+ # @param io [String]
224
+ # @return [Hash]
225
+ def rgbextra(io)
226
+ # @todo
227
+ end
228
+
229
+ end
@@ -0,0 +1,130 @@
1
+ record.rb
2
+
3
+ globals
4
+
5
+ # @todo move formula parsing to a separate branch
6
+ # 2.4.150 Lbl
7
+ # @return [Hash]
8
+ def r_lbl
9
+ @multiple = true
10
+
11
+ a_j_raw, ch_key, cch, cce, _, itab = @bytes.read(14).unpack('vCCvvvCCCC')
12
+ a_j = Unxls::BitOps.new(a_j_raw)
13
+ result = {
14
+ fHidden: a_j.set_at?(0), # A - fHidden (1 bit): A bit that specifies whether the defined name is not visible in the list of defined names.
15
+ fFunc: a_j.set_at?(1), # B - fFunc (1 bit): A bit that specifies whether the defined name represents an Excel macro (XLM). If this bit is 1, fProc MUST also be 1.
16
+ fOB: a_j.set_at?(2), # C - fOB (1 bit): A bit that specifies whether the defined name represents a Visual Basic for Applications (VBA) macro. If this bit is 1, the fProc MUST also be 1.
17
+ fProc: a_j.set_at?(3), # D - fProc (1 bit): A bit that specifies whether the defined name represents a macro.
18
+ fCalcExp: a_j.set_at?(4), # E - fCalcExp (1 bit): A bit that specifies whether rgce contains a call to a function that can return an array.
19
+ fBuiltin: a_j.set_at?(5), # F - fBuiltin (1 bit): A bit that specifies whether the defined name represents a built-in name.
20
+ }
21
+
22
+ # fGrp (6 bits): An unsigned integer that specifies the function category for the defined name. MUST be less than or equal to 31. The values 17 to 31 are user-defined. User-defined values are specified in the FnGroupName record. The values 0 to 16 are defined as specified in the following table:
23
+ f_grp = a_j.value_at(6..11)
24
+ f_grp_d = {
25
+ 0 => :All,
26
+ 1 => :Financial,
27
+ 2 => :'Date Time',
28
+ 3 => :'Math Trigonometry',
29
+ 4 => :Statistical,
30
+ 5 => :Lookup,
31
+ 6 => :Database,
32
+ 7 => :Text,
33
+ 8 => :Logical,
34
+ 9 => :Info,
35
+ 10 => :Commands,
36
+ 11 => :Customize,
37
+ 12 => :'Macro Control',
38
+ 13 => :'DDE External',
39
+ 14 => :'User Defined',
40
+ 15 => :Engineering,
41
+ 16 => :Cube
42
+ }[f_grp]
43
+
44
+ result[:fGrp] = f_grp
45
+ result[:fGrp_d] = f_grp_d
46
+
47
+ # G - reserved1 (1 bit): MUST be zero, and MUST be ignored.
48
+
49
+ # H - fPublished (1 bit): A bit that specifies whether the defined name is published. This bit is
50
+ # ignored if the fPublishedBookItems field of the BookExt_Conditional12 structure is 0.
51
+ result[:fPublished] = a_j.set_at?(13)
52
+
53
+ result[:fWorkbookParam] = a_j.set_at?(14) # I - fWorkbookParam (1 bit): A bit that specifies whether the defined name is a workbook parameter.
54
+
55
+ # J - reserved2 (1 bit): MUST be zero, and MUST be ignored.
56
+
57
+ # chKey (1 byte): The unsigned integer value of the ASCII character that specifies the shortcut key for the macro represented by the defined name. MUST be 0 (no shortcut key) if fFunc is 1 or if fProc is 0. Otherwise MUST<97> be greater than or equal to 0x41 and less than or equal to 0x5A, or greater than or equal to 0x61 and less than or equal to 0x7A.
58
+ result[:chKey] = ch_key
59
+
60
+ result[:cch] = cch # cch (1 byte): An unsigned integer that specifies the number of characters in Name. MUST be greater than or equal to zero.
61
+ result[:cce] = cce # cce (2 bytes): An unsigned integer that specifies length of rgce in bytes.
62
+
63
+ # reserved3 (2 bytes): MUST be zero, and MUST be ignored.
64
+
65
+ result[:itab] = itab # itab (2 bytes): An unsigned integer that specifies if the defined name is a local name, and if so, which sheet it is on. If itab is not 0, the defined name is a local name and the value MUST be a one-based index to the collection of BoundSheet8 records as they appear in the Globals Substream.
66
+
67
+ # reserved4 (1 byte): MUST be zero, and MUST be ignored.
68
+ # reserved5 (1 byte): MUST be zero, and MUST be ignored.
69
+ # reserved6 (1 byte): MUST be zero, and MUST be ignored.
70
+ # reserved7 (1 byte): MUST be zero, and MUST be ignored.
71
+
72
+ # Name (variable): An XLUnicodeStringNoCch structure that specifies the name for the defined name. If fBuiltin is 0, this field MUST satisfy the same restrictions as the name field of the XLNameUnicodeString structure. If fBuiltin is 1, this field is for a built-in name. Each built-in name has a zero-based index value associated with it.
73
+ name_pos = @bytes.pos
74
+ result[:Name] = Unxls::Biff8::Structure.xlunicodestringnocch(@bytes, cch)
75
+
76
+ # ?? "A built-in name or its index value MUST be used for this field"
77
+ if @bytes.pos - name_pos == 1
78
+ @bytes.pos -= 1
79
+ name_index = @bytes.read(1).unpack('C').first
80
+ result[:Name_d] = {
81
+ 0x00 => :Consolidate_Area,
82
+ 0x01 => :Auto_Open,
83
+ 0x02 => :Auto_Close,
84
+ 0x03 => :Extract,
85
+ 0x04 => :Database,
86
+ 0x05 => :Criteria,
87
+ 0x06 => :Print_Area,
88
+ 0x07 => :Print_Titles,
89
+ 0x08 => :Recorder,
90
+ 0x09 => :Data_Form,
91
+ 0x0A => :Auto_Activate,
92
+ 0x0B => :Auto_Deactivate,
93
+ 0x0C => :Sheet_Title,
94
+ 0x0D => :_FilterDatabase
95
+ }[name_index]
96
+ end
97
+
98
+ # rgce (variable): A NameParsedFormula structure that specifies the formula for the defined name.
99
+ # result[:rgce] = Unxls::Biff8::ParsedExpressions.nameparsedformula(@bytes, cce) # @todo formula parsing to a separate branch
100
+
101
+ result
102
+ end
103
+
104
+ sheet
105
+
106
+ # 2.4.127 Formula
107
+ # @return [Hash]
108
+ def r_formula
109
+ @multiple = true
110
+
111
+ result = {
112
+ cell: Unxls::Biff8::Structure.cell(@bytes.read(6)),
113
+ val: Unxls::Biff8::Structure.formulavalue(@bytes.read(8))
114
+ }
115
+
116
+ a_f_raw = @bytes.read(2).unpack('v').first
117
+ a_f = Unxls::BitOps.new(a_f_raw)
118
+ result[:fAlwaysCalc] = a_f.set_at?(0) # (A) specifies whether the formula needs to be calculated during the next recalculation
119
+ result[:fFill] = a_f.set_at?(2) # (C) cell has either a fill alignment or a center-across-selection alignment
120
+ result[:fShrFmla] = a_f.set_at?(3) # (D) specifies whether the formula is part of a shared formula as defined in ShrFmla. If this formula is part of a shared formula, formula.rgce MUST begin with a PtgExp structure
121
+ result[:fClearErrors] = a_f.set_at?(5) # (F) specifies whether the formula is excluded from formula error checking
122
+
123
+ @bytes.read(4) # specifies an application-specific cache of information, exists for performance reasons only
124
+
125
+ # @todo move formula parsing to a separate branch
126
+ # result[:formula] = Unxls::Biff8::ParsedExpressions.cellparsedformula(@bytes.read) # rest of record
127
+ @bytes.read # ignore for now
128
+
129
+ result
130
+ end
@@ -0,0 +1,30 @@
1
+ structure.rb
2
+
3
+ extend Unxls::Biff8::ParsedExpressions
4
+
5
+ # 2.5.133 FormulaValue
6
+ # @param data [String]
7
+ # @return [Hash]
8
+ def formulavalue(data)
9
+ f_expr_o = data[6..7].unpack('v').first
10
+
11
+ return { _value: xnum(data), _type: :float } if f_expr_o != 0xFFFF # value is Xnum if last 2 bytes are FFFF
12
+
13
+ byte1_val = data[0].unpack('C').first
14
+ value_type = {
15
+ 0 => :string,
16
+ 1 => :boolean,
17
+ 2 => :error,
18
+ 3 => :blank_string
19
+ }[byte1_val]
20
+
21
+ value = case value_type
22
+ when :string then nil # value is stored in a String record that immediately follows Formula record
23
+ when :boolean then data[2] != "\0" # byte3 specifies Boolean value
24
+ when :error then berr(data[2].unpack('C').first) # byte 3 specifies an error
25
+ when :blank_string then ''
26
+ else raise "Unknown value type #{byte1_val} in FormulaValue structure"
27
+ end
28
+
29
+ { _value: value, _type: value_type }
30
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'unxls/map'
4
+
5
+ module Unxls
6
+ # @param path [String, Pathname]
7
+ # @param settings [Hash]
8
+ # @return [Hash]
9
+ def self.parse(path, settings = {})
10
+ file = File.open(path, 'rb')
11
+ Unxls::Parser.new(file, settings).parse
12
+ ensure
13
+ file.close if file
14
+ end
15
+
16
+ end
@@ -0,0 +1,347 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Unxls::Biff8::Browser
4
+
5
+ attr_reader :parsed
6
+
7
+ # @param file [String, Pathname, File]
8
+ # @param settings [Hash]
9
+ def initialize(file, settings = {})
10
+ @file = Pathname.new(file).to_s
11
+ @settings = settings
12
+ reload
13
+ end
14
+
15
+ # @param return_self [true, false] (true)
16
+ # @return [Unxls::Biff8::Browser, nil]
17
+ def reload(return_self = true)
18
+ @parsed = Unxls.parse(@file, @settings)
19
+ self if return_self
20
+ end
21
+
22
+ # WorkBookStream
23
+ # @return [Hash]
24
+ def wbs
25
+ @parsed[:workbook_stream]
26
+ end
27
+
28
+ # Workbook stream, globals substream
29
+ # @return [Hash]
30
+ def globals
31
+ raise unless wbs.first[:BOF][:dt_d] == :globals
32
+ wbs.first
33
+ end
34
+
35
+ # Cell address to location index
36
+ # @return [Hash]
37
+ def cell_index
38
+ wbs.last
39
+ end
40
+
41
+ # Map certain records, or their non-nil attributes, or anything non-nil that the passed block will return.
42
+ #
43
+ # @example
44
+ # # Collect all records of certain type:
45
+ # Browser.new(parsed).mapif(:Font)
46
+ # #=> [{ dyHeight: … }, …]
47
+ #
48
+ # # Collect certain attributes:
49
+ # Browser.new(parsed).mapif(:Font) { |record| record[:fontName] }
50
+ # #=> ['Arial', …]
51
+ #
52
+ # # Index is passed into the block:
53
+ # Browser.new(parsed).mapif(:Font) do |r, index|
54
+ # { index => r[:fontName] }
55
+ # end
56
+ # #=> [{ 0 => 'Arial' }, … ]
57
+ #
58
+ # # Wrap in other value to collect nils:
59
+ # Browser.new(parsed).mapif(:SomeRecord) { |r| [r[:someField]] }
60
+ # #=> [[nil], [18], [23], …]
61
+ #
62
+ # # Collect only those records that have attributes of certain value:
63
+ # Browser.new(parsed).mapif(:Blank, :LabelSst) { |r| r if r[:ixfe] == 42 }
64
+ # #=> [{ ifxe: 42, _record: { name: :Blank, … } }, …]
65
+ #
66
+ # # Join other records. Browser instance and substream index are passed into the block as well for unlimited power:
67
+ # Browser.new(parsed).mapif(:XF) do |r1, i1, browser, substream_index|
68
+ # { [substream_index, i1] => browser.mapif(:Style) { |r2| r2 if r2[:ixfe] == i1 } }
69
+ # end
70
+ # #=> [
71
+ # #=> { [0, 0] => { ixfe: 0, fBuiltIn: true, … } },
72
+ # #=> { [0, 1] => [] },
73
+ # #=> …
74
+ # #=> ]
75
+ #
76
+ # @param *record_types [Array<Symbol>] list of types of records to map
77
+ # @param first: [TrueClass, FalseClass] false Only return the first match
78
+ # @return [Array, Object, nil]
79
+ def mapif(*record_types, first: false)
80
+ result = []
81
+
82
+ wbs.each_with_index do |substream, substream_index|
83
+ record_types.each do |rt|
84
+ [substream[rt]].flatten.compact.each_with_index do |record, index|
85
+ if block_given?
86
+ temp = yield(record, index, self, substream_index)
87
+ next if temp.nil?
88
+ return(temp) if first
89
+ result << temp
90
+ else
91
+ result << record
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ first ? result.first : result
98
+ end
99
+
100
+ # @param sheet_name [String, Symbol]
101
+ # @return [Integer, nil]
102
+ def get_substream_index(sheet_name)
103
+ ref = globals[:BoundSheet8].find { |s| s[:stName] == sheet_name.to_s }
104
+ return unless ref
105
+
106
+ wbs.find_index do |s|
107
+ s[:BOF][:_record][:pos] == ref[:lbPlyPos] if s[:BOF] && s[:BOF][:dt_d] != :globals
108
+ end
109
+ end
110
+
111
+ # @param name [String, Symbol]
112
+ # @return [Hash, nil]
113
+ def get_sheet(name)
114
+ sheet_index = get_substream_index(name)
115
+ wbs[sheet_index] if sheet_index
116
+ end
117
+
118
+ # @param substream_index [Integer]
119
+ # @param row [Integer]
120
+ # @param column [Integer]
121
+ # @param :record_type [Symbol] (nil)
122
+ # @return [Hash, nil]
123
+ def get_cell(substream_index, row, column, record_type: nil)
124
+ address_key = Unxls::Biff8::WorkbookStream.make_address_key(substream_index, row, column)
125
+ return unless (access_address = cell_index[address_key])
126
+ inferred_record_type, record_index = access_address.to_s.split('_')
127
+ wbs[substream_index][record_type || inferred_record_type.to_sym][record_index.to_i]
128
+ end
129
+
130
+ # @param font_index [Integer]
131
+ # @return [Hash, nil]
132
+ def get_font(font_index)
133
+ font_index -= 1 if font_index >= 4 # See 2.5.129 FontIndex, p. 677
134
+ globals[:Font][font_index]
135
+ end
136
+
137
+ # @param substream_index [Integer]
138
+ # @param row [Integer]
139
+ # @param column [Integer]
140
+ # @return [Hash, nil]
141
+ def get_format(substream_index, row, column)
142
+ return unless (xf = get_xf(substream_index, row, column))
143
+
144
+ format_index = xf[:ifmt]
145
+ globals[:Format].find { |r| r[:ifmt] == format_index }
146
+ end
147
+
148
+ # @param substream_index [Integer]
149
+ # @param row [Integer]
150
+ # @param column [Integer]
151
+ # @return [Hash, nil]
152
+ def get_sst(substream_index, row, column)
153
+ sst_index = get_cell(substream_index, row, column)[:isst]
154
+ globals[:SST][:rgb][sst_index] if sst_index
155
+ end
156
+
157
+ # @param substream_index [Integer]
158
+ # @param row [Integer]
159
+ # @param column [Integer]
160
+ # @return [Hash, nil]
161
+ def get_xf(substream_index, row, column)
162
+ return unless (cell = get_cell(substream_index, row, column))
163
+ globals[:XF][cell_xf_index(cell, column)]
164
+ end
165
+
166
+ # @param substream_index [Integer]
167
+ # @param row [Integer]
168
+ # @param column [Integer]
169
+ # @return [Hash, nil]
170
+ def get_xfext(substream_index, row, column)
171
+ return unless (cell = get_cell(substream_index, row, column))
172
+ ixfe = cell_xf_index(cell, column)
173
+ globals[:XFExt].find { |r| r[:ixfe] == ixfe } # @todo speedup with xf_xfext index?
174
+ end
175
+
176
+ def get_style(substream_index, row, column)
177
+ return unless (xf = get_xf(substream_index, row, column))
178
+ cell_parent_xf_ixfe = xf[:ixfParent]
179
+ globals[:Style].find { |r| r[:ixfe] == cell_parent_xf_ixfe }
180
+ end
181
+
182
+ def get_styleext(substream_index, row, column)
183
+ style = get_style(substream_index, row, column)
184
+ globals[:StyleExt][style[:_record][:index]]
185
+ end
186
+
187
+ def get_tablestyle(tablestyle_name)
188
+ globals[:TableStyle].find { |r| r[:rgchName] == tablestyle_name }
189
+ end
190
+
191
+ def get_tablestyleelement(tablestyle_name, type)
192
+ index = get_tablestyle(tablestyle_name)[:_record][:index]
193
+ globals[:TableStyleElement].find { |r| r[:_tsi] == index && r[:tseType_d] == type }
194
+ end
195
+
196
+ # DXF related to TableStyleElement record
197
+ # @param tablestyle_name [String]
198
+ # @param type [Symbol]
199
+ # @return [Hash]
200
+ def get_tse_dxf(tablestyle_name, type)
201
+ globals[:DXF][ get_tablestyleelement(tablestyle_name, type)[:index] ]
202
+ end
203
+
204
+ # @param substream_index [Integer]
205
+ # @param column [Integer]
206
+ # @return [Hash, nil]
207
+ def get_colinfo(substream_index, column)
208
+ wbs[substream_index][:ColInfo].find { |r| column >= r[:colFirst] && column <= r[:colLast] }
209
+ end
210
+
211
+ # @param substream_index [Integer]
212
+ # @param row [Integer]
213
+ # @return [Hash, nil]
214
+ def get_row(substream_index, row)
215
+ wbs[substream_index][:Row].find { |r| r[:rw] == row }
216
+ end
217
+
218
+ # @param substream_index [Integer]
219
+ # @param row [Integer]
220
+ # @param column [Integer]
221
+ # @return [Hash, nil]
222
+ def get_note(substream_index, row, column)
223
+ address_key = Unxls::Biff8::WorkbookStream.make_address_key(substream_index, row, column)
224
+ return unless (record_index = cell_index[:notes][address_key])
225
+ wbs[substream_index][:Note][record_index]
226
+ end
227
+
228
+ # @param substream_index [Integer]
229
+ # @param object_id [Integer]
230
+ # @return [Hash, nil]
231
+ def get_obj(substream_index, object_id)
232
+ wbs[substream_index][:Obj].find { |r| r[:cmo][:id] == object_id }
233
+ end
234
+
235
+ # @param substream_index [Integer]
236
+ # @param row [Integer]
237
+ # @param column [Integer]
238
+ # @return [Hash, nil]
239
+ def get_hlink(substream_index, row, column)
240
+ address_key = Unxls::Biff8::WorkbookStream.make_address_key(substream_index, row, column)
241
+ return unless (record_index = cell_index[:hlinks][address_key])
242
+ wbs[substream_index][:HLink][record_index]
243
+ end
244
+
245
+ # @param substream_index [Integer]
246
+ # @param row [Integer]
247
+ # @param column [Integer]
248
+ # @return [Hash, nil]
249
+ def get_hlinktooltip(substream_index, row, column)
250
+ address_key = Unxls::Biff8::WorkbookStream.make_address_key(substream_index, row, column)
251
+ return unless (record_index = cell_index[:hlinktooltips][address_key])
252
+ wbs[substream_index][:HLinkTooltip][record_index]
253
+ end
254
+
255
+ # @todo @debug
256
+ # @param substream_index [Integer]
257
+ # @param row [Integer]
258
+ # @param column [Integer]
259
+ # @return [Hash, nil]
260
+ def get_cell_styling(substream_index, row, column) # @todo -> xf_record_chain ?
261
+ return unless (cell = get_cell(substream_index, row, column))
262
+ cell_ixfe = cell_xf_index(cell, column)
263
+
264
+ cell_xf = globals[:XF][cell_ixfe]
265
+ cell_xfext = globals[:XFExt].find { |r| r[:ixfe] == cell_ixfe } # @todo index?
266
+
267
+ cell_parent_xf_ixfe = cell_xf[:ixfParent]
268
+
269
+ style_xf = globals[:XF][cell_parent_xf_ixfe]
270
+ style_xfext = globals[:XFExt].find { |r| r[:ixfe] == cell_parent_xf_ixfe } # @todo index?
271
+
272
+ style = globals[:Style].find { |r| r[:ixfe] == cell_parent_xf_ixfe } # @todo index?
273
+ styleext = globals[:StyleExt][style[:_record][:index]]
274
+
275
+ result = {
276
+ xf: cell_xf,
277
+ xfext: cell_xfext,
278
+ style_xf: style_xf,
279
+ style_xfext: style_xfext,
280
+ style: style,
281
+ styleext: styleext,
282
+ }
283
+
284
+ cell_xf_font_index = cell_xf[:ifnt]
285
+ cell_xf_font_index -= 1 if cell_xf_font_index >= 4 # See 2.5.129 FontIndex, p. 677
286
+ result[:xf_font] = globals[:Font][cell_xf_font_index]
287
+
288
+ if (style_xf_font_index = style_xf[:ifnt]) != cell_xf[:ifnt]
289
+ style_xf_font_index -= 1 if style_xf_font_index >= 4 # See 2.5.129 FontIndex, p. 677
290
+ result[:style_xf_font] = globals[:Font][style_xf_font_index]
291
+ end
292
+
293
+ cell_xf_format_index = cell_xf[:ifmt]
294
+ result[:xf_format] = globals[:Format].find { |r| r[:ifmt] == cell_xf_format_index }
295
+
296
+ if (style_xf_format_index = style_xf[:ifmt]) != cell_xf_format_index
297
+ result[:style_xf_format] = globals[:Format].find { |r| r[:ifmt] == style_xf_format_index }
298
+ end
299
+
300
+ if (rows = wbs[substream_index][:Row])
301
+ row_record = rows.find { |r| r[:rw] == row }
302
+ result[:row] = row_record if row_record
303
+ end
304
+
305
+ if (columns = wbs[substream_index][:ColInfo])
306
+ column_record = columns.find { |r| (r[:colFirst]..r[:colLast]).include?(column) }
307
+ result[:column] = column_record if column_record
308
+ end
309
+
310
+ result
311
+ end
312
+
313
+ # @todo @debug
314
+ def xf_styles
315
+ mapif(:XF) do |r, i, s|
316
+ style = s.mapif(:Style) { |r1| r1 if r1[:ixfe] == i }.first
317
+ {
318
+ index: i,
319
+ fStyle: r[:fStyle],
320
+ _builtin: r[:_description],
321
+ ixfParent: r[:ixfParent],
322
+ Style: style
323
+ } if style
324
+ end
325
+ end
326
+
327
+ private
328
+
329
+ # @param cell [Hash]
330
+ # @param column [Integer]
331
+ # @return [Integer]
332
+ def cell_xf_index(cell, column)
333
+ case (record_type = cell[:_record][:name])
334
+ when :LabelSst, :RK, :Blank, :Formula, :Number, :BoolErr, :Label
335
+ cell[:ixfe]
336
+ when :MulRk
337
+ rgkrec_index = column - cell[:colFirst]
338
+ cell[:rgrkrec][rgkrec_index][:ixfe]
339
+ when :MulBlank
340
+ rgixfe_index = column - cell[:colFirst]
341
+ cell[:rgixfe][rgixfe_index]
342
+ else
343
+ raise "Dunno how to process record type :#{record_type}"
344
+ end
345
+ end
346
+
347
+ end