rmasalov-surpass 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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