unxls 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/ext/extconf.rb +4 -0
- data/lib/formulas/constants.rb +886 -0
- data/lib/formulas/map.rb +5 -0
- data/lib/formulas/parsed_expressions.rb +229 -0
- data/lib/formulas/record.rb +130 -0
- data/lib/formulas/structure.rb +30 -0
- data/lib/unxls.rb +16 -0
- data/lib/unxls/biff8/browser.rb +347 -0
- data/lib/unxls/biff8/constants.rb +624 -0
- data/lib/unxls/biff8/record.rb +1267 -0
- data/lib/unxls/biff8/structure.rb +2740 -0
- data/lib/unxls/biff8/workbook_stream.rb +322 -0
- data/lib/unxls/bit_ops.rb +92 -0
- data/lib/unxls/dtyp.rb +33 -0
- data/lib/unxls/log.rb +59 -0
- data/lib/unxls/map.rb +59 -0
- data/lib/unxls/offcrypto.rb +423 -0
- data/lib/unxls/oshared.rb +210 -0
- data/lib/unxls/parser.rb +100 -0
- data/lib/unxls/version.rb +5 -0
- data/spec/bitops_spec.rb +95 -0
- data/spec/spec_helper.rb +117 -0
- data/spec/unxls_spec.rb +2651 -0
- metadata +167 -0
data/lib/formulas/map.rb
ADDED
@@ -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
|
data/lib/unxls.rb
ADDED
@@ -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
|