xlsxrb 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +22 -3
  4. data/benchmark/micro_ips.rb +70 -0
  5. data/lib/xlsxrb/elements/styles.rb +427 -15
  6. data/lib/xlsxrb/elements/workbook.rb +31 -4
  7. data/lib/xlsxrb/elements/worksheet.rb +22 -7
  8. data/lib/xlsxrb/ooxml/cfb.rb +10 -0
  9. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +56 -38
  10. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +7 -1
  11. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +12 -7
  12. data/lib/xlsxrb/ooxml/reader.rb +165 -136
  13. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +83 -28
  14. data/lib/xlsxrb/ooxml/styles_parser.rb +57 -14
  15. data/lib/xlsxrb/ooxml/utils.rb +59 -2
  16. data/lib/xlsxrb/ooxml/workbook_parser.rb +42 -3
  17. data/lib/xlsxrb/ooxml/workbook_writer.rb +215 -50
  18. data/lib/xlsxrb/ooxml/worksheet_parser.rb +192 -123
  19. data/lib/xlsxrb/ooxml/worksheet_writer.rb +238 -108
  20. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +16 -12
  21. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +64 -15
  22. data/lib/xlsxrb/ooxml/writer.rb +31 -28
  23. data/lib/xlsxrb/ooxml/xml_builder.rb +12 -4
  24. data/lib/xlsxrb/ooxml/xml_parser.rb +45 -0
  25. data/lib/xlsxrb/ooxml/zip_reader.rb +48 -29
  26. data/lib/xlsxrb/stream_row.rb +4 -3
  27. data/lib/xlsxrb/stream_sheet.rb +26 -6
  28. data/lib/xlsxrb/stream_writer.rb +35 -14
  29. data/lib/xlsxrb/utils.rb +15 -0
  30. data/lib/xlsxrb/version.rb +1 -1
  31. data/lib/xlsxrb/workbook_builder.rb +110 -10
  32. data/lib/xlsxrb/worksheet_builder.rb +15 -7
  33. data/lib/xlsxrb.rb +416 -108
  34. data/sig/generated/xlsxrb/elements/styles.rbs +271 -5
  35. data/sig/generated/xlsxrb/elements/workbook.rbs +26 -3
  36. data/sig/generated/xlsxrb/elements/worksheet.rbs +12 -2
  37. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +2 -0
  38. data/sig/generated/xlsxrb/ooxml/reader.rbs +12 -0
  39. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +3 -1
  40. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +2 -0
  41. data/sig/generated/xlsxrb/ooxml/utils.rbs +22 -0
  42. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +7 -0
  43. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +12 -0
  44. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +18 -2
  45. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
  46. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +5 -2
  47. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +4 -1
  48. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +6 -6
  49. data/sig/generated/xlsxrb/stream_row.rbs +3 -2
  50. data/sig/generated/xlsxrb/stream_sheet.rbs +21 -5
  51. data/sig/generated/xlsxrb/stream_writer.rbs +18 -11
  52. data/sig/generated/xlsxrb/utils.rbs +13 -0
  53. data/sig/generated/xlsxrb/worksheet_builder.rbs +3 -2
  54. data/sig/generated/xlsxrb.rbs +47 -10
  55. metadata +2 -1
@@ -7,19 +7,20 @@ require_relative "../ooxml/utils"
7
7
 
8
8
  module Xlsxrb
9
9
  module Elements
10
- # Represents the workbook stylesheet definitions (fonts, fills, borders, cellXfs, and numFmts).
11
- # Inherits from Hash for backward compatibility with existing styles hash structures,
12
- # while providing precomputed O(1) number format classifications.
10
+ # Represents the workbook stylesheet definitions (fonts, fills, borders, cellXfs, numFmts, etc.).
11
+ # Encapsulates stylesheet collections, item lookups, and precomputed O(1) number format classifications.
13
12
  #
14
13
  # @api public
15
- class Styles < Hash #[untyped, untyped]
16
- # Initializes the styles container, optionally wrapping an existing styles hash.
14
+ class Styles
15
+ EMPTY_ARRAY = [].freeze
16
+ EMPTY_HASH = {}.freeze
17
+
18
+ # Initializes the styles container, optionally wrapping an existing styles dictionary.
17
19
  #
18
20
  # @param initial_hash [Hash, nil] Initial styles dictionary.
19
21
  #: (?Hash[untyped, untyped]? initial_hash) -> void
20
22
  def initialize(initial_hash = nil)
21
- super()
22
- merge!(initial_hash) if initial_hash.is_a?(Hash)
23
+ @data = initial_hash.is_a?(Hash) ? initial_hash.dup : {}
23
24
  @format_codes = nil
24
25
  @format_types = nil
25
26
  @date_flags = nil
@@ -27,6 +28,402 @@ module Xlsxrb
27
28
  @time_flags = nil
28
29
  end
29
30
 
31
+ # Returns array of font entries from the styles.
32
+ #
33
+ # @return [Array<Hash>]
34
+ # @api public
35
+ #: () -> Array[Hash[Symbol, untyped]]
36
+ def fonts
37
+ @data[:fonts] || EMPTY_ARRAY
38
+ end
39
+
40
+ # Returns array of fill entries from the styles.
41
+ #
42
+ # @return [Array<Hash>]
43
+ # @api public
44
+ #: () -> Array[Hash[Symbol, untyped]]
45
+ def fills
46
+ @data[:fills] || EMPTY_ARRAY
47
+ end
48
+
49
+ # Returns array of border entries from the styles.
50
+ #
51
+ # @return [Array<Hash>]
52
+ # @api public
53
+ #: () -> Array[Hash[Symbol, untyped]]
54
+ def borders
55
+ @data[:borders] || EMPTY_ARRAY
56
+ end
57
+
58
+ # Returns array of cell format (cellXfs) entries from the styles.
59
+ #
60
+ # @return [Array<Hash>]
61
+ # @api public
62
+ #: () -> Array[Hash[Symbol, untyped]]
63
+ def cell_xfs
64
+ @data[:cell_xfs] || EMPTY_ARRAY
65
+ end
66
+
67
+ # Returns custom number formats as { numFmtId => formatCode }.
68
+ #
69
+ # @return [Hash<Integer, String>]
70
+ # @api public
71
+ #: () -> Hash[Integer, String]
72
+ def num_fmts
73
+ @data[:num_fmts] || EMPTY_HASH
74
+ end
75
+
76
+ # Returns array of differential format (dxf) entries from the styles.
77
+ #
78
+ # @return [Array<Hash>]
79
+ # @api public
80
+ #: () -> Array[Hash[Symbol, untyped]]
81
+ def dxfs
82
+ @data[:dxfs] || EMPTY_ARRAY
83
+ end
84
+
85
+ # Returns array of cell style format (cellStyleXfs) entries from the styles.
86
+ #
87
+ # @return [Array<Hash>]
88
+ # @api public
89
+ #: () -> Array[Hash[Symbol, untyped]]
90
+ def cell_style_xfs
91
+ @data[:cell_style_xfs] || EMPTY_ARRAY
92
+ end
93
+
94
+ # Returns array of named cell styles from the styles.
95
+ #
96
+ # @return [Array<Hash>]
97
+ # @api public
98
+ #: () -> Array[Hash[Symbol, untyped]]
99
+ def cell_styles
100
+ @data[:cell_styles] || EMPTY_ARRAY
101
+ end
102
+
103
+ # Returns table styles configuration dictionary from the styles.
104
+ #
105
+ # @return [Hash, Array]
106
+ # @api public
107
+ #: () -> (Hash[Symbol, untyped] | Array[Hash[Symbol, untyped]])
108
+ def table_styles
109
+ @data[:table_styles] || EMPTY_HASH
110
+ end
111
+
112
+ # Returns array of indexed color strings (ARGB hex).
113
+ #
114
+ # @return [Array<String>]
115
+ # @api public
116
+ #: () -> Array[String]
117
+ def indexed_colors
118
+ @data[:indexed_colors] || EMPTY_ARRAY
119
+ end
120
+
121
+ # Returns array of MRU (most recently used) color dictionaries.
122
+ #
123
+ # @return [Array<Hash>]
124
+ # @api public
125
+ #: () -> Array[Hash[Symbol, untyped]]
126
+ def mru_colors
127
+ @data[:mru_colors] || EMPTY_ARRAY
128
+ end
129
+
130
+ # Returns colors dictionary or palette.
131
+ #
132
+ # @return [Hash, Array]
133
+ # @api public
134
+ #: () -> (Hash[Symbol, untyped] | Array[Hash[Symbol, untyped]])
135
+ def colors
136
+ @data[:colors] || EMPTY_HASH
137
+ end
138
+
139
+ # Looks up a font entry by 0-based index.
140
+ #
141
+ # @param id [Integer, nil]
142
+ # @return [Hash, nil]
143
+ # @api public
144
+ #: (Integer? id) -> Hash[Symbol, untyped]?
145
+ def font(id)
146
+ return nil unless id.is_a?(Integer) && id >= 0
147
+
148
+ @data[:fonts]&.at(id)
149
+ end
150
+
151
+ # Looks up a fill entry by 0-based index.
152
+ #
153
+ # @param id [Integer, nil]
154
+ # @return [Hash, nil]
155
+ # @api public
156
+ #: (Integer? id) -> Hash[Symbol, untyped]?
157
+ def fill(id)
158
+ return nil unless id.is_a?(Integer) && id >= 0
159
+
160
+ @data[:fills]&.at(id)
161
+ end
162
+
163
+ # Looks up a border entry by 0-based index.
164
+ #
165
+ # @param id [Integer, nil]
166
+ # @return [Hash, nil]
167
+ # @api public
168
+ #: (Integer? id) -> Hash[Symbol, untyped]?
169
+ def border(id)
170
+ return nil unless id.is_a?(Integer) && id >= 0
171
+
172
+ @data[:borders]&.at(id)
173
+ end
174
+
175
+ # Looks up a cell format (cellXf) entry by 0-based index.
176
+ #
177
+ # @param id [Integer, nil]
178
+ # @return [Hash, nil]
179
+ # @api public
180
+ #: (Integer? id) -> Hash[Symbol, untyped]?
181
+ def cell_xf(id)
182
+ return nil unless id.is_a?(Integer) && id >= 0
183
+
184
+ @data[:cell_xfs]&.at(id)
185
+ end
186
+ alias xf cell_xf
187
+
188
+ # Looks up a custom format code by numFmtId.
189
+ #
190
+ # @param id [Integer, nil]
191
+ # @return [String, nil]
192
+ # @api public
193
+ #: (Integer? id) -> String?
194
+ def num_fmt(id)
195
+ return nil unless id.is_a?(Integer)
196
+
197
+ @data[:num_fmts]&.[](id)
198
+ end
199
+
200
+ # Looks up a differential format (dxf) entry by 0-based index.
201
+ #
202
+ # @param id [Integer, nil]
203
+ # @return [Hash, nil]
204
+ # @api public
205
+ #: (Integer? id) -> Hash[Symbol, untyped]?
206
+ def dxf(id)
207
+ return nil unless id.is_a?(Integer) && id >= 0
208
+
209
+ @data[:dxfs]&.at(id)
210
+ end
211
+
212
+ # Looks up a cell style format (cellStyleXf) entry by 0-based index.
213
+ #
214
+ # @param id [Integer, nil]
215
+ # @return [Hash, nil]
216
+ # @api public
217
+ #: (Integer? id) -> Hash[Symbol, untyped]?
218
+ def cell_style_xf(id)
219
+ return nil unless id.is_a?(Integer) && id >= 0
220
+
221
+ @data[:cell_style_xfs]&.at(id)
222
+ end
223
+
224
+ # Looks up a named cell style entry by 0-based index.
225
+ #
226
+ # @param id [Integer, nil]
227
+ # @return [Hash, nil]
228
+ # @api public
229
+ #: (Integer? id) -> Hash[Symbol, untyped]?
230
+ def cell_style(id)
231
+ return nil unless id.is_a?(Integer) && id >= 0
232
+
233
+ @data[:cell_styles]&.at(id)
234
+ end
235
+
236
+ # Appends a font entry to the stylesheet and returns its 0-based index.
237
+ #
238
+ # @param spec [Hash, nil]
239
+ # @return [Integer]
240
+ # @api public
241
+ #: (?Hash[Symbol, untyped]? spec, **untyped kwargs) -> Integer
242
+ def add_font(spec = nil, **kwargs)
243
+ entry = (spec || kwargs).dup
244
+ fonts_arr = (@data[:fonts] ||= [])
245
+ fonts_arr << entry
246
+ fonts_arr.size - 1
247
+ end
248
+
249
+ # Appends a fill entry to the stylesheet and returns its 0-based index.
250
+ #
251
+ # @param spec [Hash, nil]
252
+ # @return [Integer]
253
+ # @api public
254
+ #: (?Hash[Symbol, untyped]? spec, **untyped kwargs) -> Integer
255
+ def add_fill(spec = nil, **kwargs)
256
+ entry = (spec || kwargs).dup
257
+ fills_arr = (@data[:fills] ||= [])
258
+ fills_arr << entry
259
+ fills_arr.size - 1
260
+ end
261
+
262
+ # Appends a border entry to the stylesheet and returns its 0-based index.
263
+ #
264
+ # @param spec [Hash, nil]
265
+ # @return [Integer]
266
+ # @api public
267
+ #: (?Hash[Symbol, untyped]? spec, **untyped kwargs) -> Integer
268
+ def add_border(spec = nil, **kwargs)
269
+ entry = (spec || kwargs).dup
270
+ borders_arr = (@data[:borders] ||= [])
271
+ borders_arr << entry
272
+ borders_arr.size - 1
273
+ end
274
+
275
+ # Sets a custom number format code and returns the num_fmt_id.
276
+ #
277
+ # @param id [Integer] Format ID (custom formats typically start at 164).
278
+ # @param code [String] Format code pattern.
279
+ # @return [Integer]
280
+ # @api public
281
+ #: (Integer id, String code) -> Integer
282
+ def add_num_fmt(id, code)
283
+ raise ArgumentError, "num_fmt_id must be an Integer" unless id.is_a?(Integer)
284
+ raise ArgumentError, "format_code must be a String" unless code.is_a?(String)
285
+
286
+ (@data[:num_fmts] ||= {})[id] = code
287
+ invalidate_cache!
288
+ id
289
+ end
290
+
291
+ # Appends a cell format (cellXf) entry and returns its 0-based index.
292
+ #
293
+ # @param spec [Hash, nil]
294
+ # @return [Integer]
295
+ # @api public
296
+ #: (?Hash[Symbol, untyped]? spec, **untyped kwargs) -> Integer
297
+ def add_xf(spec = nil, **kwargs)
298
+ entry = (spec || kwargs).dup
299
+ xfs_arr = (@data[:cell_xfs] ||= [])
300
+ xfs_arr << entry
301
+ invalidate_cache!
302
+ xfs_arr.size - 1
303
+ end
304
+
305
+ # Appends a differential format (dxf) entry and returns its 0-based index.
306
+ #
307
+ # @param spec [Hash, nil]
308
+ # @return [Integer]
309
+ # @api public
310
+ #: (?Hash[Symbol, untyped]? spec, **untyped kwargs) -> Integer
311
+ def add_dxf(spec = nil, **kwargs)
312
+ entry = (spec || kwargs).dup
313
+ dxfs_arr = (@data[:dxfs] ||= [])
314
+ dxfs_arr << entry
315
+ dxfs_arr.size - 1
316
+ end
317
+
318
+ # Appends a cell style format (cellStyleXf) entry and returns its 0-based index.
319
+ #
320
+ # @param spec [Hash, nil]
321
+ # @return [Integer]
322
+ # @api public
323
+ #: (?Hash[Symbol, untyped]? spec, **untyped kwargs) -> Integer
324
+ def add_cell_style_xf(spec = nil, **kwargs)
325
+ entry = (spec || kwargs).dup
326
+ xfs_arr = (@data[:cell_style_xfs] ||= [])
327
+ xfs_arr << entry
328
+ invalidate_cache!
329
+ xfs_arr.size - 1
330
+ end
331
+
332
+ # Appends a named cell style entry and returns its 0-based index.
333
+ #
334
+ # @param spec [Hash, nil]
335
+ # @return [Integer]
336
+ # @api public
337
+ #: (?Hash[Symbol, untyped]? spec, **untyped kwargs) -> Integer
338
+ def add_cell_style(spec = nil, **kwargs)
339
+ entry = (spec || kwargs).dup
340
+ cs_arr = (@data[:cell_styles] ||= [])
341
+ cs_arr << entry
342
+ cs_arr.size - 1
343
+ end
344
+
345
+ # High-level helper to compose and register a cell format (xf), adding subordinate
346
+ # font, fill, border, and number format definitions as needed. Returns the xf_id.
347
+ #
348
+ # @param spec [Hash, nil]
349
+ # @return [Integer]
350
+ # @api public
351
+ #: (?Hash[Symbol, untyped]? spec, **untyped kwargs) -> Integer
352
+ def add_style(spec = nil, **kwargs)
353
+ opts = (spec || kwargs).dup
354
+ font_id = opts[:font_id] || (opts[:font] ? add_font(opts[:font]) : 0)
355
+ fill_id = opts[:fill_id] || (opts[:fill] ? add_fill(opts[:fill]) : 0)
356
+ border_id = opts[:border_id] || (opts[:border] ? add_border(opts[:border]) : 0)
357
+ num_fmt_id = opts[:num_fmt_id] || 0
358
+ if opts[:num_fmt] || opts[:format_code]
359
+ code = (opts[:num_fmt] || opts[:format_code]).to_s
360
+ existing_id = num_fmts.key(code) || Ooxml::Utils::BUILTIN_NUM_FMT_CODES.key(code)
361
+ num_fmt_id = existing_id || add_num_fmt(next_num_fmt_id, code)
362
+ end
363
+
364
+ xf_spec = {
365
+ font_id: font_id,
366
+ fill_id: fill_id,
367
+ border_id: border_id,
368
+ num_fmt_id: num_fmt_id
369
+ }
370
+ xf_spec[:alignment] = opts[:alignment] if opts[:alignment]
371
+ xf_spec[:apply_alignment] = true if opts[:alignment] || opts[:apply_alignment]
372
+ xf_spec[:apply_fill] = true if opts[:fill] || opts[:apply_fill]
373
+ xf_spec[:apply_font] = true if opts[:font] || opts[:apply_font]
374
+ xf_spec[:apply_border] = true if opts[:border] || opts[:apply_border]
375
+ xf_spec[:apply_number_format] = true if opts[:num_fmt] || opts[:format_code] || opts[:apply_number_format]
376
+ xf_spec[:protection] = opts[:protection] if opts[:protection]
377
+ xf_spec[:apply_protection] = true if opts[:protection] || opts[:apply_protection]
378
+
379
+ add_xf(xf_spec)
380
+ end
381
+
382
+ # Returns true if the styles container has no registered definitions.
383
+ #
384
+ # @return [Boolean]
385
+ # @api public
386
+ #: () -> bool
387
+ def empty?
388
+ @data.empty?
389
+ end
390
+
391
+ # Converts the styles container to a standard Hash.
392
+ #
393
+ # @return [Hash]
394
+ # @api public
395
+ #: () -> Hash[Symbol, untyped]
396
+ def to_h
397
+ @data.dup
398
+ end
399
+ alias to_hash to_h
400
+
401
+ # Equality check against another Styles or Hash.
402
+ #
403
+ # @param other [Object]
404
+ # @return [Boolean]
405
+ # @api public
406
+ #: (untyped other) -> bool
407
+ def ==(other)
408
+ if other.is_a?(Styles)
409
+ @data == other.to_h
410
+ elsif other.is_a?(Hash)
411
+ @data == other
412
+ else
413
+ false
414
+ end
415
+ end
416
+ alias eql? ==
417
+
418
+ # Computes hash code based on underlying data.
419
+ #
420
+ # @return [Integer]
421
+ # @api public
422
+ #: () -> Integer
423
+ def hash
424
+ @data.hash
425
+ end
426
+
30
427
  # Returns the format code string for a given style index.
31
428
  #
32
429
  # @param style_index [Integer, nil]
@@ -111,8 +508,8 @@ module Xlsxrb
111
508
  # @api public
112
509
  #: () -> self
113
510
  def precompute!
114
- cell_xfs = self[:cell_xfs]
115
- size = cell_xfs.is_a?(Array) ? cell_xfs.size : 0
511
+ xfs = cell_xfs
512
+ size = xfs.is_a?(Array) ? xfs.size : 0
116
513
 
117
514
  codes = Array.new(size)
118
515
  types = Array.new(size)
@@ -121,20 +518,20 @@ module Xlsxrb
121
518
  t_flags = Array.new(size, false)
122
519
 
123
520
  if size.positive?
124
- cell_style_xfs = self[:cell_style_xfs]
125
- num_fmts = self[:num_fmts] || {}
521
+ cs_xfs = cell_style_xfs
522
+ n_fmts = num_fmts
126
523
 
127
524
  size.times do |i|
128
- xf = cell_xfs[i]
525
+ xf = xfs[i]
129
526
  next unless xf.is_a?(Hash)
130
527
 
131
528
  num_fmt_id = xf[:num_fmt_id]
132
- if (num_fmt_id.nil? || num_fmt_id.zero?) && xf[:xf_id] && cell_style_xfs.is_a?(Array)
133
- parent_xf = cell_style_xfs[xf[:xf_id]]
529
+ if (num_fmt_id.nil? || num_fmt_id.zero?) && xf[:xf_id] && cs_xfs.is_a?(Array)
530
+ parent_xf = cs_xfs[xf[:xf_id]]
134
531
  num_fmt_id = parent_xf[:num_fmt_id] if parent_xf.is_a?(Hash)
135
532
  end
136
533
 
137
- code = num_fmt_id ? (num_fmts[num_fmt_id] || Ooxml::Utils::BUILTIN_NUM_FMT_CODES[num_fmt_id]) : nil
534
+ code = num_fmt_id ? (n_fmts[num_fmt_id] || Ooxml::Utils::BUILTIN_NUM_FMT_CODES[num_fmt_id]) : nil
138
535
 
139
536
  codes[i] = code
140
537
  type = NumberFormatter.format_type(code)
@@ -152,6 +549,21 @@ module Xlsxrb
152
549
  @time_flags = t_flags
153
550
  self
154
551
  end
552
+
553
+ private
554
+
555
+ def next_num_fmt_id
556
+ max_id = [163, *num_fmts.keys].max
557
+ max_id + 1
558
+ end
559
+
560
+ def invalidate_cache!
561
+ @format_codes = nil
562
+ @format_types = nil
563
+ @date_flags = nil
564
+ @datetime_flags = nil
565
+ @time_flags = nil
566
+ end
155
567
  end
156
568
  end
157
569
  end
@@ -19,12 +19,12 @@ module Xlsxrb
19
19
 
20
20
  # @param sheets [Array<Elements::Worksheet, StreamSheet>] Worksheets in the workbook.
21
21
  # @param shared_strings [Array<String>] Shared strings table.
22
- # @param styles [Hash] Styles definition.
22
+ # @param styles [Elements::Styles, Hash] Styles definition.
23
23
  # @param unmapped_data [Hash] Additional metadata for round-tripping.
24
24
  # @param errors [Array<String>, nil] Validation errors.
25
25
  # @param defined_names [Array<Hash>, nil] Defined names list.
26
26
  # @param date1904 [Boolean] Whether the workbook uses the 1904 date system.
27
- #: (?sheets: Array[Elements::Worksheet | StreamSheet], ?shared_strings: Array[String], ?styles: Hash[untyped, untyped], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?defined_names: Array[Hash[Symbol, untyped]]?, ?date1904: bool) -> void
27
+ #: (?sheets: Array[Elements::Worksheet | StreamSheet], ?shared_strings: Array[String], ?styles: Elements::Styles | Hash[untyped, untyped], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?defined_names: Array[Hash[Symbol, untyped]]?, ?date1904: bool) -> void
28
28
  def initialize(sheets: [], shared_strings: [], styles: {}, unmapped_data: {}, errors: nil, defined_names: nil, date1904: false)
29
29
  dns = defined_names || unmapped_data[:defined_names] || unmapped_data.dig(:facade, :defined_names) || []
30
30
  computed_errors = errors || self.class.validate(sheets)
@@ -62,15 +62,42 @@ module Xlsxrb
62
62
  errors.empty?
63
63
  end
64
64
 
65
+ # Returns workbook-level properties (e.g. date1904, active_tab, full_calc_on_load), or an empty hash.
66
+ #
67
+ # @return [Hash<Symbol, untyped>]
68
+ # @api public
69
+ #: () -> Hash[Symbol, untyped]
70
+ def workbook_properties
71
+ unmapped_data[:workbook_properties] || unmapped_data.dig(:facade, :workbook_properties) || {}
72
+ end
73
+
65
74
  # Returns whether the workbook uses the 1904 date system.
66
75
  #
67
76
  # @return [Boolean]
68
77
  # @api public
69
78
  #: () -> bool
70
79
  def date1904?
71
- props = unmapped_data[:workbook_properties] || unmapped_data.dig(:facade, :workbook_properties)
72
- props&.[](:date1904) ? true : false
80
+ workbook_properties[:date1904] ? true : false
81
+ end
82
+
83
+ # Returns the 0-based active worksheet tab index from workbookView, or nil if not set.
84
+ #
85
+ # @return [Integer, nil]
86
+ # @api public
87
+ #: () -> Integer?
88
+ def active_tab
89
+ workbook_properties[:active_tab]
90
+ end
91
+
92
+ # Returns whether Excel is instructed to perform a full recalculation when loading the workbook.
93
+ #
94
+ # @return [Boolean]
95
+ # @api public
96
+ #: () -> bool
97
+ def full_calc_on_load?
98
+ workbook_properties[:full_calc_on_load] ? true : false
73
99
  end
100
+ alias full_calc_on_load full_calc_on_load?
74
101
 
75
102
  # Returns core document properties (e.g. title, creator, created), or an empty hash.
76
103
  #
@@ -17,7 +17,7 @@ module Xlsxrb
17
17
  class Worksheet
18
18
  [Enumerable, CoordinateAccess].each { |m| include m }
19
19
 
20
- attr_reader :name, :rows, :columns, :charts, :conditional_formatting, :data_validations, :unmapped_data, :errors, :state, :hyperlinks, :comments, :images, :styles, :date1904, :print_area, :print_titles, :sparkline_groups, :merged_cells, :auto_filter, :tables, :shapes, :freeze_pane, :page_setup, :page_margins, :header_footer, :print_options, :sheet_protection, :sheet_view, :sheet_properties, :sheet_format, :row_breaks, :col_breaks, :protected_ranges, :cell_watches
20
+ attr_reader :name, :rows, :columns, :charts, :conditional_formatting, :data_validations, :unmapped_data, :errors, :state, :hyperlinks, :comments, :images, :styles, :date1904, :print_area, :print_titles, :sparkline_groups, :merged_cells, :auto_filter, :tables, :shapes, :freeze_pane, :page_setup, :page_margins, :header_footer, :print_options, :sheet_protection, :sheet_view, :sheet_properties, :sheet_format, :row_breaks, :col_breaks, :protected_ranges, :cell_watches, :selection
21
21
  attr_writer :dimension
22
22
  alias conditional_formats conditional_formatting
23
23
 
@@ -59,8 +59,9 @@ module Xlsxrb
59
59
  # @param col_breaks [Array<Integer | Hash>] Explicit page column breaks.
60
60
  # @param protected_ranges [Array<Hash>] Protected range definitions.
61
61
  # @param cell_watches [Array<String>] Watched cell references.
62
- #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?conditional_formatting: Array[Hash[Symbol, untyped]]?, ?data_validations: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?conditional_formats: Array[Hash[Symbol, untyped]]?, ?state: Symbol, ?hyperlinks: Hash[String, untyped], ?comments: Array[Hash[Symbol, untyped]], ?images: Array[Elements::Image], ?styles: Hash[untyped, untyped]?, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?print_area: String?, ?print_titles: Hash[Symbol, untyped]?, ?sparkline_groups: Array[Hash[Symbol, untyped]], ?merged_cells: Array[String], ?auto_filter: String?, ?tables: Array[Hash[Symbol, untyped]], ?shapes: Array[Hash[Symbol, untyped]], ?freeze_pane: Hash[Symbol, untyped]?, ?page_setup: Hash[Symbol, untyped], ?page_margins: Hash[Symbol, untyped]?, ?header_footer: Hash[Symbol, untyped], ?print_options: Hash[Symbol, untyped], ?sheet_protection: Hash[Symbol, untyped]?, ?sheet_view: Hash[Symbol, untyped], ?sheet_properties: Hash[Symbol, untyped], ?sheet_format: Hash[Symbol, untyped], ?row_breaks: Array[untyped], ?col_breaks: Array[untyped], ?protected_ranges: Array[Hash[Symbol, untyped]], ?cell_watches: Array[String]) -> void
63
- def initialize(name:, rows: [], columns: [], charts: [], conditional_formatting: nil, data_validations: [], unmapped_data: {}, errors: nil, conditional_formats: nil, state: :visible, hyperlinks: {}, comments: [], images: [], styles: nil, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, print_area: nil, print_titles: nil, sparkline_groups: [], merged_cells: [], auto_filter: nil, tables: [], shapes: [], freeze_pane: nil, page_setup: {}, page_margins: nil, header_footer: {}, print_options: {}, sheet_protection: nil, sheet_view: {}, sheet_properties: {}, sheet_format: {}, row_breaks: [], col_breaks: [], protected_ranges: [], cell_watches: [])
62
+ # @param selection [Hash, Array<Hash>, nil] Active cell and selection settings.
63
+ #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?conditional_formatting: Array[Hash[Symbol, untyped]]?, ?data_validations: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?conditional_formats: Array[Hash[Symbol, untyped]]?, ?state: Symbol, ?hyperlinks: Hash[String, untyped], ?comments: Array[Hash[Symbol, untyped]], ?images: Array[Elements::Image], ?styles: (Elements::Styles | Hash[untyped, untyped])?, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?print_area: String?, ?print_titles: Hash[Symbol, untyped]?, ?sparkline_groups: Array[Hash[Symbol, untyped]], ?merged_cells: Array[String], ?auto_filter: String?, ?tables: Array[Hash[Symbol, untyped]], ?shapes: Array[Hash[Symbol, untyped]], ?freeze_pane: Hash[Symbol, untyped]?, ?page_setup: Hash[Symbol, untyped], ?page_margins: Hash[Symbol, untyped]?, ?header_footer: Hash[Symbol, untyped], ?print_options: Hash[Symbol, untyped], ?sheet_protection: Hash[Symbol, untyped]?, ?sheet_view: Hash[Symbol, untyped], ?sheet_properties: Hash[Symbol, untyped], ?sheet_format: Hash[Symbol, untyped], ?row_breaks: Array[untyped], ?col_breaks: Array[untyped], ?protected_ranges: Array[Hash[Symbol, untyped]], ?cell_watches: Array[String], ?selection: (Hash[Symbol, untyped] | Array[Hash[Symbol, untyped]])?) -> void
64
+ def initialize(name:, rows: [], columns: [], charts: [], conditional_formatting: nil, data_validations: [], unmapped_data: {}, errors: nil, conditional_formats: nil, state: :visible, hyperlinks: {}, comments: [], images: [], styles: nil, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, print_area: nil, print_titles: nil, sparkline_groups: [], merged_cells: [], auto_filter: nil, tables: [], shapes: [], freeze_pane: nil, page_setup: {}, page_margins: nil, header_footer: {}, print_options: {}, sheet_protection: nil, sheet_view: {}, sheet_properties: {}, sheet_format: {}, row_breaks: [], col_breaks: [], protected_ranges: [], cell_watches: [], selection: nil)
64
65
  @name = name
65
66
  @rows = (rows || []).freeze
66
67
  @columns = (columns || []).freeze
@@ -101,6 +102,7 @@ module Xlsxrb
101
102
  @col_breaks = (col_breaks || []).freeze
102
103
  @protected_ranges = (protected_ranges || unmapped_data[:protected_ranges] || unmapped_data.dig(:facade, :protected_ranges) || []).freeze
103
104
  @cell_watches = (cell_watches || unmapped_data[:cell_watches] || unmapped_data.dig(:facade, :cell_watches) || []).freeze
105
+ @selection = selection&.freeze
104
106
  end
105
107
 
106
108
  # Returns whether trailing empty rows are omitted during iteration.
@@ -362,6 +364,15 @@ module Xlsxrb
362
364
  @state == :visible
363
365
  end
364
366
 
367
+ # Returns whether the worksheet is very hidden (:very_hidden).
368
+ #
369
+ # @return [Boolean]
370
+ # @api public
371
+ #: () -> bool
372
+ def very_hidden?
373
+ @state == :very_hidden
374
+ end
375
+
365
376
  # Returns hyperlink metadata for the given cell reference, or nil.
366
377
  #
367
378
  # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
@@ -539,6 +550,7 @@ module Xlsxrb
539
550
  new_col_breaks = changes.key?(:col_breaks) ? changes[:col_breaks] : col_breaks
540
551
  new_protected_ranges = changes.key?(:protected_ranges) ? changes[:protected_ranges] : protected_ranges
541
552
  new_cell_watches = changes.key?(:cell_watches) ? changes[:cell_watches] : cell_watches
553
+ new_selection = changes.key?(:selection) ? changes[:selection] : selection
542
554
 
543
555
  self.class.new(
544
556
  name: new_name,
@@ -575,7 +587,8 @@ module Xlsxrb
575
587
  row_breaks: new_row_breaks,
576
588
  col_breaks: new_col_breaks,
577
589
  protected_ranges: new_protected_ranges,
578
- cell_watches: new_cell_watches
590
+ cell_watches: new_cell_watches,
591
+ selection: new_selection
579
592
  )
580
593
  end
581
594
 
@@ -616,7 +629,8 @@ module Xlsxrb
616
629
  sheet_properties: sheet_properties,
617
630
  sheet_format: sheet_format,
618
631
  row_breaks: row_breaks,
619
- col_breaks: col_breaks
632
+ col_breaks: col_breaks,
633
+ selection: selection
620
634
  }
621
635
  end
622
636
 
@@ -634,7 +648,8 @@ module Xlsxrb
634
648
  header_footer == other.header_footer && print_options == other.print_options &&
635
649
  sheet_protection == other.sheet_protection && sheet_view == other.sheet_view &&
636
650
  sheet_properties == other.sheet_properties && sheet_format == other.sheet_format &&
637
- row_breaks == other.row_breaks && col_breaks == other.col_breaks
651
+ row_breaks == other.row_breaks && col_breaks == other.col_breaks &&
652
+ selection == other.selection
638
653
  end
639
654
  alias eql? ==
640
655
 
@@ -644,7 +659,7 @@ module Xlsxrb
644
659
  self.class, name, rows, columns, charts, conditional_formatting, data_validations,
645
660
  state, hyperlinks, comments, merged_cells, auto_filter, tables, shapes, freeze_pane,
646
661
  page_setup, page_margins, header_footer, print_options, sheet_protection, sheet_view,
647
- sheet_properties, sheet_format, row_breaks, col_breaks
662
+ sheet_properties, sheet_format, row_breaks, col_breaks, selection
648
663
  ].hash
649
664
  end
650
665
 
@@ -100,9 +100,16 @@ module Xlsxrb
100
100
  raise Xlsxrb::Error, "CFB file is too small" if @data.bytesize < 512
101
101
 
102
102
  _, major_ver = @data[0x18, 4].unpack("v2")
103
+ raise Xlsxrb::Error, "Unsupported CFB major version: #{major_ver}" unless [3, 4].include?(major_ver)
104
+
103
105
  @major_version = major_ver
104
106
  @sector_shift = @data[0x1E, 2].unpack1("v")
105
107
  @mini_sector_shift = @data[0x20, 2].unpack1("v")
108
+
109
+ expected_shift = @major_version == 4 ? 12 : 9
110
+ raise Xlsxrb::Error, "Invalid CFB sector shift in header: #{@sector_shift}" unless @sector_shift == expected_shift
111
+ raise Xlsxrb::Error, "Invalid CFB mini sector shift in header: #{@mini_sector_shift}" unless @mini_sector_shift == 6
112
+
106
113
  @sector_size = 1 << @sector_shift
107
114
  @mini_sector_size = 1 << @mini_sector_shift
108
115
 
@@ -123,6 +130,9 @@ module Xlsxrb
123
130
  end
124
131
 
125
132
  def read_sector(sector_id)
133
+ max_sectors = (@data.bytesize - 512) / @sector_size
134
+ return "".b if sector_id.negative? || sector_id >= max_sectors
135
+
126
136
  offset = sector_offset(sector_id)
127
137
  @data[offset, @sector_size] || "".b
128
138
  end