rmasalov-surpass 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/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 +607 -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/surpass_cell.rb +187 -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/rmasalov-surpass +0 -0
- data/surpass.gemspec +39 -0
- metadata +120 -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
|
@@ -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
|
@@ -0,0 +1,406 @@
|
|
1
|
+
class Reader
|
2
|
+
DIR_ENTRY_SIZE = 128
|
3
|
+
|
4
|
+
attr_accessor :header
|
5
|
+
attr_accessor :data
|
6
|
+
|
7
|
+
attr_accessor :doc_magic
|
8
|
+
attr_accessor :file_uid
|
9
|
+
attr_accessor :rev_num
|
10
|
+
attr_accessor :ver_num
|
11
|
+
attr_accessor :byte_order
|
12
|
+
attr_accessor :sect_size
|
13
|
+
attr_accessor :short_sect_size
|
14
|
+
|
15
|
+
attr_accessor :total_sat_sectors
|
16
|
+
attr_accessor :dir_start_sid
|
17
|
+
attr_accessor :min_stream_size
|
18
|
+
attr_accessor :ssat_start_sid
|
19
|
+
attr_accessor :total_ssat_sectors
|
20
|
+
attr_accessor :msat_start_sid
|
21
|
+
attr_accessor :total_msat_sectors
|
22
|
+
|
23
|
+
attr_accessor :msat
|
24
|
+
attr_accessor :sat
|
25
|
+
attr_accessor :ssat
|
26
|
+
attr_accessor :dir_entry_list
|
27
|
+
|
28
|
+
def initialize(file)
|
29
|
+
@streams = {}
|
30
|
+
|
31
|
+
file = File.open(file, 'rb') unless file.respond_to?(:read)
|
32
|
+
doc = file.read
|
33
|
+
@header, @data = doc[0...512], doc[512..-1]
|
34
|
+
|
35
|
+
build_header
|
36
|
+
build_msat
|
37
|
+
build_sat
|
38
|
+
build_directory
|
39
|
+
build_short_sectors_data
|
40
|
+
|
41
|
+
if @short_sectors_data.length > 0
|
42
|
+
build_ssat
|
43
|
+
else
|
44
|
+
@ssat_start_sid = -2
|
45
|
+
@total_ssat_sectors = 0
|
46
|
+
@ssat = [-2]
|
47
|
+
end
|
48
|
+
|
49
|
+
@dir_entry_list[1..-1].each do |d|
|
50
|
+
did, sz, name, t, c, did_left, did_right, did_root, dentry_start_sid, stream_size = d
|
51
|
+
stream_data = ''
|
52
|
+
if stream_size > 0
|
53
|
+
if stream_size > @min_stream_size
|
54
|
+
args = [@data, @sat, dentry_start_sid, @sect_size]
|
55
|
+
else
|
56
|
+
args = [@short_sectors_data, @ssat, dentry_start_sid, @short_sect_size]
|
57
|
+
end
|
58
|
+
stream_data = stream_data(*args)
|
59
|
+
end
|
60
|
+
# BAD IDEA: names may be equal. NEED use full paths...
|
61
|
+
@streams[name] = stream_data if !name.length == 0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_header
|
66
|
+
@doc_magic = @header[0...8]
|
67
|
+
raise 'Not an OLE file.' unless @doc_magic === "\320\317\021\340\241\261\032\341"
|
68
|
+
|
69
|
+
@file_uid = @header[8...24]
|
70
|
+
@rev_num = @header[24...26]
|
71
|
+
@ver_num = @header[26...28]
|
72
|
+
@byte_order = @header[28...30]
|
73
|
+
@log2_sect_size = @header[30...32].unpack('v')[0]
|
74
|
+
@log2_short_sect_size = @header[32...34].unpack('v')[0]
|
75
|
+
@total_sat_sectors = @header[44...48].unpack('V')[0]
|
76
|
+
@dir_start_sid = @header[48...52].unpack('V')[0]
|
77
|
+
@min_stream_size = @header[56...60].unpack('V')[0]
|
78
|
+
@ssat_start_sid = @header[60...64].unpack('V')[0]
|
79
|
+
@total_ssat_sectors = @header[64...68].unpack('V')[0]
|
80
|
+
@msat_start_sid = @header[68...72].unpack('V')[0]
|
81
|
+
@total_msat_sectors = @header[72...76].unpack('V')[0]
|
82
|
+
|
83
|
+
@sect_size = 1 << @log2_sect_size
|
84
|
+
@short_sect_size = 1 << @log2_short_sect_size
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_msat
|
88
|
+
@msat = @header[76..-1].unpack('V109')
|
89
|
+
next_sector = @msat_start_sid
|
90
|
+
while next_sector > 0 do
|
91
|
+
raise "not implemented"
|
92
|
+
start = next_sector * @sect_size
|
93
|
+
finish = (next_sector + 1) * @sect_size
|
94
|
+
msat_sector = @data[start...finish]
|
95
|
+
@msat << msat_sector
|
96
|
+
next_sector = msat_sector[-1]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def build_sat
|
101
|
+
sat_stream = @msat.collect {|i| i >= 0 ? @data[(i*@sect_size)...((i+1)*@sect_size)] : '' }.join
|
102
|
+
@sat = sat_stream.unpack('V*')
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_ssat
|
106
|
+
ssat_stream = stream_data(@data, @sat, @ssat_start_sid, @sect_size)
|
107
|
+
@ssat = ssat_stream.unpack('V*')
|
108
|
+
end
|
109
|
+
|
110
|
+
def build_directory
|
111
|
+
dir_stream = stream_data(@data, @sat, @dir_start_sid, @sect_size)
|
112
|
+
@dir_entry_list = []
|
113
|
+
i = 0
|
114
|
+
while i < dir_stream.length do
|
115
|
+
dentry = dir_stream[i...(i+DIR_ENTRY_SIZE)]
|
116
|
+
i += DIR_ENTRY_SIZE
|
117
|
+
|
118
|
+
did = @dir_entry_list.length
|
119
|
+
sz = dentry[64...66].unpack('C')[0]
|
120
|
+
|
121
|
+
if sz > 0
|
122
|
+
name = dentry[0...(sz-2)] # TODO unicode
|
123
|
+
else
|
124
|
+
name = ''
|
125
|
+
end
|
126
|
+
|
127
|
+
t = dentry[66...67].unpack('C')[0]
|
128
|
+
c = dentry[67...68].unpack('C')[0]
|
129
|
+
did_left = dentry[68...72].unpack('V')[0]
|
130
|
+
did_right = dentry[72...76].unpack('V')[0]
|
131
|
+
did_root = dentry[76...80].unpack('V')[0]
|
132
|
+
dentry_start_sid = dentry[116...120].unpack('V')[0]
|
133
|
+
stream_size = dentry[120...124].unpack('V')[0]
|
134
|
+
|
135
|
+
@dir_entry_list << [did, sz, name, t, c, did_left, did_right, did_root, dentry_start_sid, stream_size]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_short_sectors_data
|
140
|
+
did, sz, name, t, c, did_left, did_right, did_root, dentry_start_sid, stream_size = @dir_entry_list[0]
|
141
|
+
raise unless t == 0x05 # Short-Stream Container Stream (SSCS) resides in Root Storage
|
142
|
+
|
143
|
+
if stream_size == 0
|
144
|
+
@short_sectors_data = ''
|
145
|
+
else
|
146
|
+
@short_sectors_data = stream_data(@data, @sat, dentry_start_sid, @sect_size)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def stream_data(data, sat, start_sid, sect_size)
|
151
|
+
sid = start_sid
|
152
|
+
chunks = [[sid, sid]]
|
153
|
+
stream_data = ''
|
154
|
+
while sat[sid] >= 0 do
|
155
|
+
next_in_chain = sat[sid]
|
156
|
+
last_chunk_start, last_chunk_finish = chunks[-1]
|
157
|
+
if next_in_chain == last_chunk_finish + 1
|
158
|
+
chunks[-1] = last_chunk_start, next_in_chain
|
159
|
+
else
|
160
|
+
chunks << [next_in_chain, next_in_chain]
|
161
|
+
end
|
162
|
+
sid = next_in_chain
|
163
|
+
end
|
164
|
+
|
165
|
+
chunks.each do |s, f|
|
166
|
+
stream_data += data[s*sect_size...(f+1)*sect_size]
|
167
|
+
end
|
168
|
+
stream_data
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
# This implementation writes only 'Root Entry', 'Workbook' streams
|
174
|
+
# and 2 empty streams for aligning directory stream on sector boundary
|
175
|
+
#
|
176
|
+
# LAYOUT:
|
177
|
+
# 0 header
|
178
|
+
# 76 MSAT (1st part: 109 SID)
|
179
|
+
# 512 workbook stream
|
180
|
+
# ... additional MSAT sectors if streams' size > about 7 Mb == (109*512 * 128)
|
181
|
+
# ... SAT
|
182
|
+
# ... directory stream
|
183
|
+
#
|
184
|
+
# NOTE: this layout is "ad hoc". It can be more general. RTFM
|
185
|
+
class ExcelDocument
|
186
|
+
SECTOR_SIZE = 0x0200
|
187
|
+
MIN_LIMIT = 0x1000
|
188
|
+
|
189
|
+
SID_FREE_SECTOR = -1
|
190
|
+
SID_END_OF_CHAIN = -2
|
191
|
+
SID_USED_BY_SAT = -3
|
192
|
+
SID_USED_BY_MSAT = -4
|
193
|
+
|
194
|
+
def initialize
|
195
|
+
@book_stream_sect = []
|
196
|
+
@dir_stream_sect = []
|
197
|
+
|
198
|
+
@packed_sat = ''
|
199
|
+
@sat_sect = []
|
200
|
+
|
201
|
+
@packed_msat_1st = ''
|
202
|
+
@packed_msat_2nd = ''
|
203
|
+
@msat_sect_2nd = []
|
204
|
+
@header = ''
|
205
|
+
end
|
206
|
+
|
207
|
+
def build_directory
|
208
|
+
@dir_stream = ''
|
209
|
+
|
210
|
+
name = 'Root Entry'
|
211
|
+
type = 0x05 # root storage
|
212
|
+
colour = 0x01 # black
|
213
|
+
did_left = -1
|
214
|
+
did_right = -1
|
215
|
+
did_root = 1
|
216
|
+
start_sid = -2
|
217
|
+
stream_sz = 0
|
218
|
+
@dir_stream += pack_directory(name, type, colour, did_left, did_right, did_root, start_sid, stream_sz)
|
219
|
+
|
220
|
+
name = 'Workbook'
|
221
|
+
type = 0x02 # user stream
|
222
|
+
colour = 0x01 # black
|
223
|
+
did_left = -1
|
224
|
+
did_right = -1
|
225
|
+
did_root = -1
|
226
|
+
start_sid = 0
|
227
|
+
stream_sz = @book_stream_len
|
228
|
+
@dir_stream += pack_directory(name, type, colour, did_left, did_right, did_root, start_sid, stream_sz)
|
229
|
+
# padding
|
230
|
+
name = ''
|
231
|
+
type = 0x00 # empty
|
232
|
+
colour = 0x01 # black
|
233
|
+
did_left = -1
|
234
|
+
did_right = -1
|
235
|
+
did_root = -1
|
236
|
+
start_sid = -2
|
237
|
+
stream_sz = 0
|
238
|
+
@dir_stream += pack_directory(name, type, colour, did_left, did_right, did_root, start_sid, stream_sz) * 2
|
239
|
+
end
|
240
|
+
|
241
|
+
def pack_directory(name, type, colour, did_left, did_right, did_root, start_sid, stream_sz)
|
242
|
+
encoded_name = ''
|
243
|
+
0.upto(name.length) do |i|
|
244
|
+
encoded_name << name[i, 1] + "\000"
|
245
|
+
end
|
246
|
+
encoded_name << "\000"
|
247
|
+
name_sz = encoded_name.length
|
248
|
+
|
249
|
+
args = [encoded_name, name_sz, type, colour, did_left, did_right, did_root, 0, 0, 0, 0, 0, 0, 0, 0, 0, start_sid, stream_sz, 0]
|
250
|
+
args.pack('a64 v C2 V3 V9 V V2')
|
251
|
+
end
|
252
|
+
|
253
|
+
def build_sat
|
254
|
+
book_sect_count = @book_stream_len >> 9
|
255
|
+
dir_sect_count = @dir_stream.length >> 9
|
256
|
+
total_sect_count = book_sect_count + dir_sect_count
|
257
|
+
sat_sect_count = 0
|
258
|
+
msat_sect_count = 0
|
259
|
+
sat_sect_count_limit = 109
|
260
|
+
|
261
|
+
while (total_sect_count > 128*sat_sect_count) || (sat_sect_count > sat_sect_count_limit) do
|
262
|
+
sat_sect_count += 1
|
263
|
+
total_sect_count += 1
|
264
|
+
if sat_sect_count > sat_sect_count_limit
|
265
|
+
msat_sect_count += 1
|
266
|
+
total_sect_count += 1
|
267
|
+
sat_sect_count_limit += 127
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# initialize the sat array to be filled with the "empty" character specified by SID_FREE_SECTOR
|
272
|
+
sat = [SID_FREE_SECTOR]*128*sat_sect_count
|
273
|
+
|
274
|
+
sect = 0
|
275
|
+
while sect < book_sect_count - 1 do
|
276
|
+
@book_stream_sect << sect
|
277
|
+
sat[sect] = sect + 1
|
278
|
+
sect += 1
|
279
|
+
end
|
280
|
+
@book_stream_sect << sect
|
281
|
+
sat[sect] = SID_END_OF_CHAIN
|
282
|
+
sect += 1
|
283
|
+
|
284
|
+
while sect < book_sect_count + msat_sect_count do
|
285
|
+
@msat_sect_2nd << sect
|
286
|
+
sat[sect] = SID_USED_BY_MSAT
|
287
|
+
sect += 1
|
288
|
+
end
|
289
|
+
|
290
|
+
while sect < book_sect_count + msat_sect_count + sat_sect_count do
|
291
|
+
@sat_sect << sect
|
292
|
+
sat[sect] = SID_USED_BY_SAT
|
293
|
+
sect += 1
|
294
|
+
end
|
295
|
+
|
296
|
+
while sect < book_sect_count + msat_sect_count + sat_sect_count + dir_sect_count - 1 do
|
297
|
+
@dir_stream_sect << sect
|
298
|
+
sat[sect] = sect + 1
|
299
|
+
sect += 1
|
300
|
+
end
|
301
|
+
|
302
|
+
@dir_stream_sect << sect
|
303
|
+
sat[sect] = SID_END_OF_CHAIN
|
304
|
+
sect += 1
|
305
|
+
|
306
|
+
@packed_sat = sat.pack('V*')
|
307
|
+
|
308
|
+
msat_1st = []
|
309
|
+
109.times do |i|
|
310
|
+
msat_1st[i] = @sat_sect[i] || SID_FREE_SECTOR
|
311
|
+
end
|
312
|
+
@packed_msat_1st = msat_1st.pack('V*')
|
313
|
+
|
314
|
+
msat_2nd = [SID_FREE_SECTOR] * 128 * msat_sect_count
|
315
|
+
msat_2nd[-1] = SID_END_OF_CHAIN if msat_sect_count > 0
|
316
|
+
|
317
|
+
i = 109
|
318
|
+
msat_sect = 0
|
319
|
+
sid_num = 0
|
320
|
+
|
321
|
+
while i < sat_sect_count do
|
322
|
+
if (sid_num + 1) % 128 == 0
|
323
|
+
msat_sect += 1
|
324
|
+
msat_2nd[sid_num] = @msat_sect_2nd[msat_sect] if msat_sect < @msat_sect_2nd.length
|
325
|
+
else
|
326
|
+
msat_2nd[sid_num] = @sat_sect[i]
|
327
|
+
i += 1
|
328
|
+
end
|
329
|
+
sid_num += 1
|
330
|
+
end
|
331
|
+
|
332
|
+
@packed_msat_2nd = msat_2nd.pack('V*')
|
333
|
+
end
|
334
|
+
|
335
|
+
def build_header
|
336
|
+
doc_magic = "\320\317\021\340\241\261\032\341"
|
337
|
+
file_uid = "\000" * 16
|
338
|
+
rev_num = ">\000"
|
339
|
+
ver_num = "\003\000"
|
340
|
+
byte_order = "\376\377"
|
341
|
+
log_sect_size = [9].pack('v')
|
342
|
+
log_short_sect_size = [6].pack('v')
|
343
|
+
not_used0 = "\000"*10
|
344
|
+
total_sat_sectors = [@sat_sect.length].pack('V')
|
345
|
+
dir_start_sid = [@dir_stream_sect[0]].pack('V')
|
346
|
+
not_used1 = "\000"*4
|
347
|
+
min_stream_size = [0x1000].pack('V')
|
348
|
+
ssat_start_sid = [-2].pack('V')
|
349
|
+
total_ssat_sectors = [0].pack('V')
|
350
|
+
|
351
|
+
if @msat_sect_2nd.length == 0
|
352
|
+
msat_start_sid = [-2].pack('V')
|
353
|
+
else
|
354
|
+
msat_start_sid = [@msat_sect_2nd[0]].pack('V')
|
355
|
+
end
|
356
|
+
|
357
|
+
total_msat_sectors = [@msat_sect_2nd.length].pack('V')
|
358
|
+
|
359
|
+
@header = [
|
360
|
+
doc_magic,
|
361
|
+
file_uid,
|
362
|
+
rev_num,
|
363
|
+
ver_num,
|
364
|
+
byte_order,
|
365
|
+
log_sect_size,
|
366
|
+
log_short_sect_size,
|
367
|
+
not_used0,
|
368
|
+
total_sat_sectors,
|
369
|
+
dir_start_sid,
|
370
|
+
not_used1,
|
371
|
+
min_stream_size,
|
372
|
+
ssat_start_sid,
|
373
|
+
total_ssat_sectors,
|
374
|
+
msat_start_sid,
|
375
|
+
total_msat_sectors
|
376
|
+
].join
|
377
|
+
end
|
378
|
+
|
379
|
+
def data(stream)
|
380
|
+
distance_to_end_of_next_sector_boundary = 0x1000 - (stream.length % 0x1000)
|
381
|
+
@book_stream_len = stream.length + distance_to_end_of_next_sector_boundary
|
382
|
+
padding = "\000" * distance_to_end_of_next_sector_boundary
|
383
|
+
|
384
|
+
build_directory
|
385
|
+
build_sat
|
386
|
+
build_header
|
387
|
+
|
388
|
+
s = StringIO.new
|
389
|
+
s.write(@header)
|
390
|
+
s.write(@packed_msat_1st)
|
391
|
+
s.write(stream)
|
392
|
+
s.write(padding)
|
393
|
+
s.write(@packed_msat_2nd)
|
394
|
+
s.write(@packed_sat)
|
395
|
+
s.write(@dir_stream)
|
396
|
+
s.rewind
|
397
|
+
s
|
398
|
+
end
|
399
|
+
|
400
|
+
def save(file, stream)
|
401
|
+
we_own_it = !file.respond_to?(:write)
|
402
|
+
file = File.open(file, 'wb') if we_own_it
|
403
|
+
file.write data(stream).read
|
404
|
+
file.close if we_own_it
|
405
|
+
end
|
406
|
+
end
|