slayer-surpass 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.Rakefile.swp +0 -0
- data/.bnsignore +26 -0
- data/History.txt +4 -0
- data/LICENSE.txt +110 -0
- data/README.txt +26 -0
- data/Rakefile +36 -0
- data/bin/surpass +8 -0
- data/lib/surpass.rb +64 -0
- data/lib/surpass/ExcelFormula.g +393 -0
- data/lib/surpass/ExcelFormula.tokens +32 -0
- data/lib/surpass/ExcelFormulaLexer.rb +1490 -0
- data/lib/surpass/ExcelFormulaParser.rb +1822 -0
- data/lib/surpass/biff_record.rb +2173 -0
- data/lib/surpass/bitmap.rb +218 -0
- data/lib/surpass/cell.rb +187 -0
- data/lib/surpass/chart.rb +16 -0
- data/lib/surpass/column.rb +40 -0
- data/lib/surpass/document.rb +406 -0
- data/lib/surpass/excel_magic.rb +1016 -0
- data/lib/surpass/formatting.rb +605 -0
- data/lib/surpass/formula.rb +25 -0
- data/lib/surpass/row.rb +173 -0
- data/lib/surpass/style.rb +194 -0
- data/lib/surpass/tokens.txt +2 -0
- data/lib/surpass/utilities.rb +118 -0
- data/lib/surpass/workbook.rb +207 -0
- data/lib/surpass/worksheet.rb +574 -0
- data/surpass.gemspec +39 -0
- metadata +119 -0
@@ -0,0 +1,218 @@
|
|
1
|
+
class ObjBmpRecord < BiffRecord
|
2
|
+
RECORD_ID = 0x005D # Record identifier
|
3
|
+
|
4
|
+
def initialize(row, col, sheet, im_data_bmp, x, y, scale_x, scale_y)
|
5
|
+
width = im_data_bmp.width * scale_x
|
6
|
+
height = im_data_bmp.height * scale_y
|
7
|
+
|
8
|
+
col_start, x1, row_start, y1, col_end, x2, row_end, y2 = position_image(sheet, row, col, x, y, width, height)
|
9
|
+
|
10
|
+
# Store the OBJ record that precedes an IMDATA record. This could be generalise
|
11
|
+
# to support other Excel objects.
|
12
|
+
cobj = 0x0001 # count of objects in file (set to 1)
|
13
|
+
ot = 0x0008 # object type. 8 = picture
|
14
|
+
id = 0x0001 # object id
|
15
|
+
grbit = 0x0614 # option flags
|
16
|
+
coll = col_start # col containing upper left corner of object
|
17
|
+
dxl = x1 # distance from left side of cell
|
18
|
+
rwt = row_start # row containing top left corner of object
|
19
|
+
dyt = y1 # distance from top of cell
|
20
|
+
colr = col_end # col containing lower right corner of object
|
21
|
+
dxr = x2 # distance from right of cell
|
22
|
+
rwb = row_end # row containing bottom right corner of object
|
23
|
+
dyb = y2 # distance from bottom of cell
|
24
|
+
cbmacro = 0x0000 # length of fmla structure
|
25
|
+
reserved1 = 0x0000 # reserved
|
26
|
+
reserved2 = 0x0000 # reserved
|
27
|
+
icvback = 0x09 # background colour
|
28
|
+
icvfore = 0x09 # foreground colour
|
29
|
+
fls = 0x00 # fill pattern
|
30
|
+
fauto = 0x00 # automatic fill
|
31
|
+
icv = 0x08 # line colour
|
32
|
+
lns = 0xff # line style
|
33
|
+
lnw = 0x01 # line weight
|
34
|
+
fautob = 0x00 # automatic border
|
35
|
+
frs = 0x0000 # frame style
|
36
|
+
cf = 0x0009 # image format, 9 = bitmap
|
37
|
+
reserved3 = 0x0000 # reserved
|
38
|
+
cbpictfmla = 0x0000 # length of fmla structure
|
39
|
+
reserved4 = 0x0000 # reserved
|
40
|
+
grbit2 = 0x0001 # option flags
|
41
|
+
reserved5 = 0x0000 # reserved
|
42
|
+
|
43
|
+
args = [cobj, ot, id, grbit, coll, dxl, rwt, dyt, colr, dxr, rwb, dyb, cbmacro, reserved1, reserved2, icvback, icvfore, fls, fauto, icv, lns, lnw, fautob, frs, cf, reserved3, cbpictfmla, reserved4, grbit2, reserved5]
|
44
|
+
@record_data = args.pack('L v12 L v C8 v L v4 L')
|
45
|
+
end
|
46
|
+
|
47
|
+
# Calculate the vertices that define the position of the image as required by
|
48
|
+
# the OBJ record.
|
49
|
+
#
|
50
|
+
# +------------+------------+
|
51
|
+
# | A | B |
|
52
|
+
# +-----+------------+------------+
|
53
|
+
# | |(x1,y1) | |
|
54
|
+
# | 1 |(A1)._______|______ |
|
55
|
+
# | | | | |
|
56
|
+
# | | | | |
|
57
|
+
# +-----+----| BITMAP |-----+
|
58
|
+
# | | | | |
|
59
|
+
# | 2 | |______________. |
|
60
|
+
# | | | (B2)|
|
61
|
+
# | | | (x2,y2)|
|
62
|
+
# +---- +------------+------------+
|
63
|
+
#
|
64
|
+
# Example of a bitmap that covers some of the area from cell A1 to cell B2.
|
65
|
+
#
|
66
|
+
# Based on the width and height of the bitmap we need to calculate 8 vars:
|
67
|
+
# col_start, row_start, col_end, row_end, x1, y1, x2, y2.
|
68
|
+
# The width and height of the cells are also variable and have to be taken into
|
69
|
+
# account.
|
70
|
+
# The values of col_start and row_start are passed in from the calling
|
71
|
+
# function. The values of col_end and row_end are calculated by subtracting
|
72
|
+
# the width and height of the bitmap from the width and height of the
|
73
|
+
# underlying cells.
|
74
|
+
# The vertices are expressed as a percentage of the underlying cell width as
|
75
|
+
# follows (rhs values are in pixels):
|
76
|
+
#
|
77
|
+
# x1 = X / W *1024
|
78
|
+
# y1 = Y / H *256
|
79
|
+
# x2 = (X-1) / W *1024
|
80
|
+
# y2 = (Y-1) / H *256
|
81
|
+
#
|
82
|
+
# Where: X is distance from the left side of the underlying cell
|
83
|
+
# Y is distance from the top of the underlying cell
|
84
|
+
# W is the width of the cell
|
85
|
+
# H is the height of the cell
|
86
|
+
#
|
87
|
+
# Note: the SDK incorrectly states that the height should be expressed as a
|
88
|
+
# percentage of 1024.
|
89
|
+
#
|
90
|
+
# col_start - Col containing upper left corner of object
|
91
|
+
# row_start - Row containing top left corner of object
|
92
|
+
# x1 - Distance to left side of object
|
93
|
+
# y1 - Distance to top of object
|
94
|
+
# width - Width of image frame
|
95
|
+
# height - Height of image frame
|
96
|
+
def position_image(sheet, row_start, col_start, x1, y1, width, height)
|
97
|
+
while x1 >= size_col(sheet, col_start) do
|
98
|
+
x1 -= size_col(sheet, col_start)
|
99
|
+
col_start += 1
|
100
|
+
end
|
101
|
+
|
102
|
+
# Adjust start row for offsets that are greater than the row height
|
103
|
+
while y1 >= size_row(sheet, row_start) do
|
104
|
+
y1 -= size_row(sheet, row_start)
|
105
|
+
row_start += 1
|
106
|
+
end
|
107
|
+
|
108
|
+
# Initialise end cell to the same as the start cell
|
109
|
+
row_end = row_start # Row containing bottom right corner of object
|
110
|
+
col_end = col_start # Col containing lower right corner of object
|
111
|
+
width = width + x1 - 1
|
112
|
+
height = height + y1 - 1
|
113
|
+
|
114
|
+
# Subtract the underlying cell widths to find the end cell of the image
|
115
|
+
while (width >= size_col(sheet, col_end)) do
|
116
|
+
width -= size_col(sheet, col_end)
|
117
|
+
col_end += 1
|
118
|
+
end
|
119
|
+
|
120
|
+
# Subtract the underlying cell heights to find the end cell of the image
|
121
|
+
while (height >= size_row(sheet, row_end)) do
|
122
|
+
height -= size_row(sheet, row_end)
|
123
|
+
row_end += 1
|
124
|
+
end
|
125
|
+
|
126
|
+
# Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
|
127
|
+
# with zero height or width.
|
128
|
+
starts_or_ends_in_hidden_cell = ((size_col(sheet, col_start) == 0) or (size_col(sheet, col_end) == 0) or (size_row(sheet, row_start) == 0) or (size_row(sheet, row_end) == 0))
|
129
|
+
return if starts_or_ends_in_hidden_cell
|
130
|
+
|
131
|
+
# Convert the pixel values to the percentage value expected by Excel
|
132
|
+
x1 = (x1.to_f / size_col(sheet, col_start) * 1024).to_i
|
133
|
+
y1 = (y1.to_f / size_row(sheet, row_start) * 256).to_i
|
134
|
+
# Distance to right side of object
|
135
|
+
x2 = (width.to_f / size_col(sheet, col_end) * 1024).to_i
|
136
|
+
# Distance to bottom of object
|
137
|
+
y2 = (height.to_f / size_row(sheet, row_end) * 256).to_i
|
138
|
+
|
139
|
+
[col_start, x1, row_start, y1, col_end, x2, row_end, y2]
|
140
|
+
end
|
141
|
+
|
142
|
+
def size_col(sheet, col)
|
143
|
+
sheet.col_width(col)
|
144
|
+
end
|
145
|
+
|
146
|
+
def size_row(sheet, row)
|
147
|
+
sheet.row_height(row)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class ImDataBmpRecord < BiffRecord
|
152
|
+
RECORD_ID = 0x007F
|
153
|
+
|
154
|
+
attr_accessor :width
|
155
|
+
attr_accessor :height
|
156
|
+
attr_accessor :size
|
157
|
+
|
158
|
+
# Insert a 24bit bitmap image in a worksheet. The main record required is
|
159
|
+
# IMDATA but it must be proceeded by a OBJ record to define its position.
|
160
|
+
def initialize(filename)
|
161
|
+
@width, @height, @size, data = process_bitmap(filename)
|
162
|
+
|
163
|
+
cf = 0x09
|
164
|
+
env = 0x01
|
165
|
+
lcb = @size
|
166
|
+
|
167
|
+
@record_data = [cf, env, lcb].pack('v2L') + data
|
168
|
+
end
|
169
|
+
|
170
|
+
# Convert a 24 bit bitmap into the modified internal format used by Windows.
|
171
|
+
# This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the
|
172
|
+
# MSDN library.
|
173
|
+
def process_bitmap(filename)
|
174
|
+
data = nil
|
175
|
+
File.open(filename, "rb") do |f|
|
176
|
+
data = f.read
|
177
|
+
end
|
178
|
+
|
179
|
+
raise "bitmap #{filename} doesn't contain enough data" if data.length <= 0x36
|
180
|
+
raise "bitmap #{filename} is not valid" unless data[0, 2] === "BM"
|
181
|
+
|
182
|
+
# Remove bitmap data: ID.
|
183
|
+
data = data[2..-1]
|
184
|
+
|
185
|
+
# Read and remove the bitmap size. This is more reliable than reading
|
186
|
+
# the data size at offset 0x22.
|
187
|
+
size = data[0,4].unpack('L')[0]
|
188
|
+
size -= 0x36 # Subtract size of bitmap header.
|
189
|
+
size += 0x0C # Add size of BIFF header.
|
190
|
+
|
191
|
+
data = data[4..-1]
|
192
|
+
# Remove bitmap data: reserved, offset, header length.
|
193
|
+
data = data[12..-1]
|
194
|
+
# Read and remove the bitmap width and height. Verify the sizes.
|
195
|
+
width, height = data[0,8].unpack('L2')
|
196
|
+
data = data[8..-1]
|
197
|
+
raise "bitmap #{filename} largest image width supported is 65k." if (width > 0xFFFF)
|
198
|
+
raise "bitmap #{filename} largest image height supported is 65k." if (height > 0xFFFF)
|
199
|
+
|
200
|
+
# Read and remove the bitmap planes and bpp data. Verify them.
|
201
|
+
planes, bitcount = data[0,4].unpack('v2')
|
202
|
+
data = data[4..-1]
|
203
|
+
raise "bitmap #{filename} isn't a 24bit true color bitmap." if (bitcount != 24)
|
204
|
+
raise "bitmap #{filename} only 1 plane supported in bitmap image." if (planes != 1)
|
205
|
+
|
206
|
+
# Read and remove the bitmap compression. Verify compression.
|
207
|
+
compression = data[0,4].unpack('L')[0]
|
208
|
+
data = data[4..-1]
|
209
|
+
raise "bitmap #{filename} compression not supported in bitmap image." if (compression != 0)
|
210
|
+
|
211
|
+
# Remove bitmap data: data size, hres, vres, colours, imp. colours.
|
212
|
+
data = data[20..-1]
|
213
|
+
# Add the BITMAPCOREHEADER data
|
214
|
+
header = [0x000c, width, height, 0x01, 0x18].pack('Lv4')
|
215
|
+
|
216
|
+
[width, height, size, header + data]
|
217
|
+
end
|
218
|
+
end
|
data/lib/surpass/cell.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
class Cell
|
2
|
+
attr_reader :index
|
3
|
+
|
4
|
+
def set_style(style)
|
5
|
+
style = StyleFormat.new(style) if style.is_a?(Hash)
|
6
|
+
@format_index = @parent.parent_wb.styles.add(style)
|
7
|
+
end
|
8
|
+
|
9
|
+
def row
|
10
|
+
@parent
|
11
|
+
end
|
12
|
+
|
13
|
+
def col
|
14
|
+
@index
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
### @export "string-cell"
|
19
|
+
class StringCell < Cell
|
20
|
+
def initialize(parent, index, format_index, sst_index)
|
21
|
+
@parent = parent
|
22
|
+
@index = index
|
23
|
+
@format_index = format_index
|
24
|
+
@sst_index = sst_index
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_biff
|
28
|
+
LabelSSTRecord.new(@parent.index, @index, @format_index, @sst_index).to_biff
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
### @export "blank-cell"
|
33
|
+
class BlankCell < Cell
|
34
|
+
def initialize(parent, index, format_index)
|
35
|
+
@parent = parent
|
36
|
+
@index = index
|
37
|
+
@format_index = format_index
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_biff
|
41
|
+
BlankRecord.new(@parent.index, @index, @format_index).to_biff
|
42
|
+
end
|
43
|
+
end
|
44
|
+
### @end
|
45
|
+
|
46
|
+
class NumberCell < Cell
|
47
|
+
def initialize(parent, index, format_index, number)
|
48
|
+
@parent = parent
|
49
|
+
@index = index
|
50
|
+
@format_index = format_index
|
51
|
+
@number = number
|
52
|
+
end
|
53
|
+
|
54
|
+
def rk_record(rk_encoded)
|
55
|
+
RKRecord.new(@parent.index, @index, @format_index, rk_encoded).to_biff
|
56
|
+
end
|
57
|
+
|
58
|
+
# TODO test this section to be sure numbers are categorized and packed correctly.
|
59
|
+
def to_biff
|
60
|
+
# 30 bit signed int
|
61
|
+
in_range = (-0x20000000 <= @number) && (@number < 0x20000000)
|
62
|
+
is_int = (@number.to_i == @number)
|
63
|
+
if in_range && is_int
|
64
|
+
rk_encoded = 2 | (@number.to_i << 2)
|
65
|
+
return rk_record(rk_encoded)
|
66
|
+
end
|
67
|
+
|
68
|
+
# try scaling by 100 then using a 30 bit signed int
|
69
|
+
in_range = (-0x20000000 <= @number * 100) && (@number * 100 < 0x20000000)
|
70
|
+
round_trip = (@number.to_i*100) == @number*100
|
71
|
+
if in_range && round_trip
|
72
|
+
rk_encoded = (3 | (@number.to_i*100 << 2))
|
73
|
+
return rk_record(rk_encoded)
|
74
|
+
end
|
75
|
+
|
76
|
+
w0, w1, w2, w3 = [@number].pack('E').unpack('v4')
|
77
|
+
|
78
|
+
is_float_rk = (w0 == 0) && (w1 == 0) && (w2 & 0xFFFC) == w2
|
79
|
+
if is_float_rk
|
80
|
+
rk_encoded = (w3 << 16) | w2
|
81
|
+
return rk_record(rk_encoded)
|
82
|
+
end
|
83
|
+
|
84
|
+
w0, w1, w2, w3 = [@number * 100].pack('E').unpack('v4')
|
85
|
+
|
86
|
+
is_float_rk_100 = w0 == 0 && w1 == 0 && w2 & 0xFFFC == w2
|
87
|
+
if is_float_rk_100
|
88
|
+
rk_encoded = 1 | (w3 << 16) | w2
|
89
|
+
return rk_record(rk_encoded)
|
90
|
+
end
|
91
|
+
|
92
|
+
# If not an RK value, use a NumberRecord instead.
|
93
|
+
NumberRecord.new(@parent.index, @index, @format_index, @number).to_biff
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class MulNumberCell < Cell
|
98
|
+
def initialize(parent, index, format_index, sst_index)
|
99
|
+
@parent = parent
|
100
|
+
@index = index
|
101
|
+
@format_index = format_index
|
102
|
+
@sst_index = sst_index
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_biff
|
106
|
+
raise "not implemented"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class MulBlankCell < Cell
|
111
|
+
def initialize(parent, col1, col2, xf_idx)
|
112
|
+
raise unless col1 < col2
|
113
|
+
@parent = parent
|
114
|
+
@col1 = col1
|
115
|
+
@col2 = col2
|
116
|
+
@xf_idx = xf_idx
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_biff
|
120
|
+
MulBlankRecord.new(@parent.index, @col1, @col2, @xf_idx).to_biff
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
### @export "formula-cell"
|
125
|
+
class FormulaCell < Cell
|
126
|
+
def initialize(parent, index, format_index, formula, calc_flags = 0)
|
127
|
+
@parent = parent
|
128
|
+
@index = index
|
129
|
+
@format_index = format_index
|
130
|
+
@formula = formula
|
131
|
+
@calc_flags = calc_flags
|
132
|
+
end
|
133
|
+
|
134
|
+
def to_biff
|
135
|
+
args = [@parent.index, @index, @format_index, @formula.to_biff, @calc_flags]
|
136
|
+
FormulaRecord.new(*args).to_biff
|
137
|
+
end
|
138
|
+
end
|
139
|
+
### @end
|
140
|
+
|
141
|
+
class BooleanCell < Cell
|
142
|
+
def initialize(parent, index, format_index, number)
|
143
|
+
@parent = parent
|
144
|
+
@index = index
|
145
|
+
@format_index = format_index
|
146
|
+
@number = number
|
147
|
+
@is_error = 0
|
148
|
+
end
|
149
|
+
|
150
|
+
def to_biff
|
151
|
+
number = @number ? 1 : 0
|
152
|
+
BoolErrRecord.new(@parent.index, @index, @format_index, number, @is_error).to_biff
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class ErrorCell < Cell
|
157
|
+
ERROR_CODES = {
|
158
|
+
0x00 => 0, # Intersection of two cell ranges is empty
|
159
|
+
0x07 => 7, # Division by zero
|
160
|
+
0x0F => 15, # Wrong type of operand
|
161
|
+
0x17 => 23, # Illegal or deleted cell reference
|
162
|
+
0x1D => 29, # Wrong function or range name
|
163
|
+
0x24 => 36, # Value range overflow
|
164
|
+
0x2A => 42, # Argument or function not available
|
165
|
+
'#NULL!' => 0, # Intersection of two cell ranges is empty
|
166
|
+
'#DIV/0!' => 7, # Division by zero
|
167
|
+
'#VALUE!' => 36, # Wrong type of operand
|
168
|
+
'#REF!' => 23, # Illegal or deleted cell reference
|
169
|
+
'#NAME?' => 29, # Wrong function or range name
|
170
|
+
'#NUM!' => 36, # Value range overflow
|
171
|
+
'#N/A!' => 42 # Argument or function not available
|
172
|
+
}
|
173
|
+
|
174
|
+
def initialize(parent, index, format_index, error_string_or_code)
|
175
|
+
@parent = parent
|
176
|
+
@index = index
|
177
|
+
@format_index = format_index
|
178
|
+
@number = ERROR_CODES[error_string_or_code]
|
179
|
+
@is_error = 1
|
180
|
+
|
181
|
+
raise "invalid error code #{error_string_or_code}" if @number.nil?
|
182
|
+
end
|
183
|
+
|
184
|
+
def to_biff
|
185
|
+
BoolErrRecord.new(@parent.index, @index, @format_index, @number, @is_error)
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Chart
|
2
|
+
def initialize
|
3
|
+
raise "not implemented"
|
4
|
+
end
|
5
|
+
|
6
|
+
|
7
|
+
# ● OBJ Object description for the chart
|
8
|
+
# ● BOF Type = chart (➜5.8)
|
9
|
+
# Chart records
|
10
|
+
# ● EOF End of the Chart Substream of the chart object (5.37)
|
11
|
+
def to_biff
|
12
|
+
result = []
|
13
|
+
result << Biff8BOFRecord.new(Biff8BOFRecord::CHART).to_biff
|
14
|
+
result.join
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Column
|
2
|
+
attr_accessor :index
|
3
|
+
attr_accessor :width
|
4
|
+
attr_accessor :hidden
|
5
|
+
attr_accessor :level
|
6
|
+
attr_accessor :collapse
|
7
|
+
|
8
|
+
def initialize(index, parent)
|
9
|
+
is_int = index.is_a?(Integer)
|
10
|
+
in_range = (index >= 0) && (index <= 255)
|
11
|
+
raise "column index #{index} is not valid" unless is_int && in_range
|
12
|
+
|
13
|
+
@index = index
|
14
|
+
@parent = parent
|
15
|
+
@parent_wb = parent.parent
|
16
|
+
@xf_index = 0x0F
|
17
|
+
|
18
|
+
@width = 0x0B92
|
19
|
+
@hidden = 0
|
20
|
+
@level = 0
|
21
|
+
@collapse = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_biff
|
25
|
+
options = (as_numeric(@hidden) & 0x01) << 0
|
26
|
+
options |= (@level & 0x07) << 8
|
27
|
+
options |= (as_numeric(@collapse) & 0x01) << 12
|
28
|
+
|
29
|
+
ColInfoRecord.new(@index, @index, @width, @xf_index, options).to_biff
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_style(style)
|
33
|
+
@xf_index = @parent_wb.add_style(style)
|
34
|
+
end
|
35
|
+
|
36
|
+
def width_in_pixels
|
37
|
+
# *** Approximation ****
|
38
|
+
(self.width * 0.0272 + 0.446).round
|
39
|
+
end
|
40
|
+
end
|