xlsxrb 0.1.16 → 0.1.17

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/README.md +2 -2
  4. data/lib/xlsxrb/chart_builder.rb +41 -2
  5. data/lib/xlsxrb/dsl_helpers.rb +522 -6
  6. data/lib/xlsxrb/elements/cell.rb +34 -9
  7. data/lib/xlsxrb/elements/column.rb +2 -2
  8. data/lib/xlsxrb/elements/types.rb +1 -0
  9. data/lib/xlsxrb/elements/workbook.rb +48 -0
  10. data/lib/xlsxrb/elements/worksheet.rb +158 -11
  11. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +22 -1
  12. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +187 -24
  13. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +108 -1
  14. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +16 -11
  15. data/lib/xlsxrb/ooxml/reader.rb +88 -0
  16. data/lib/xlsxrb/ooxml/styles_parser.rb +19 -11
  17. data/lib/xlsxrb/ooxml/workbook_parser.rb +18 -3
  18. data/lib/xlsxrb/ooxml/workbook_writer.rb +266 -46
  19. data/lib/xlsxrb/ooxml/worksheet_parser.rb +18 -4
  20. data/lib/xlsxrb/ooxml/worksheet_writer.rb +289 -88
  21. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +579 -441
  22. data/lib/xlsxrb/ooxml/writer/features_xml.rb +26 -9
  23. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +23 -9
  24. data/lib/xlsxrb/ooxml/writer.rb +268 -31
  25. data/lib/xlsxrb/ooxml/zip_writer.rb +15 -5
  26. data/lib/xlsxrb/stream_sheet.rb +157 -3
  27. data/lib/xlsxrb/stream_writer.rb +946 -74
  28. data/lib/xlsxrb/style_builder.rb +59 -6
  29. data/lib/xlsxrb/utils.rb +65 -0
  30. data/lib/xlsxrb/version.rb +1 -1
  31. data/lib/xlsxrb/workbook_builder.rb +197 -14
  32. data/lib/xlsxrb/worksheet_builder.rb +1137 -44
  33. data/lib/xlsxrb.rb +302 -47
  34. data/sig/generated/xlsxrb/chart_builder.rbs +31 -3
  35. data/sig/generated/xlsxrb/dsl_helpers.rbs +119 -4
  36. data/sig/generated/xlsxrb/elements/cell.rbs +20 -3
  37. data/sig/generated/xlsxrb/elements/column.rbs +3 -3
  38. data/sig/generated/xlsxrb/elements/types.rbs +2 -0
  39. data/sig/generated/xlsxrb/elements/workbook.rbs +37 -0
  40. data/sig/generated/xlsxrb/elements/worksheet.rbs +82 -2
  41. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +2 -0
  42. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +74 -2
  43. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +36 -0
  44. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +3 -0
  45. data/sig/generated/xlsxrb/ooxml/reader.rbs +17 -0
  46. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +3 -0
  47. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +8 -0
  48. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +8 -4
  49. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +1 -1
  50. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +12 -1
  51. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +16 -0
  52. data/sig/generated/xlsxrb/ooxml/writer.rbs +42 -8
  53. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +8 -2
  54. data/sig/generated/xlsxrb/stream_sheet.rbs +101 -1
  55. data/sig/generated/xlsxrb/stream_writer.rbs +592 -23
  56. data/sig/generated/xlsxrb/style_builder.rbs +24 -6
  57. data/sig/generated/xlsxrb/utils.rbs +20 -0
  58. data/sig/generated/xlsxrb/workbook_builder.rbs +87 -5
  59. data/sig/generated/xlsxrb/worksheet_builder.rbs +554 -10
  60. data/sig/generated/xlsxrb.rbs +77 -16
  61. metadata +1 -1
@@ -32,19 +32,22 @@ module Xlsxrb
32
32
 
33
33
  def self.write(target, sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
34
34
  defined_names: nil, core_properties: nil, app_properties: nil,
35
- custom_properties: nil, workbook_protection: nil, workbook_properties: nil)
35
+ custom_properties: nil, workbook_protection: nil, workbook_properties: nil,
36
+ compression_level: Zlib::DEFAULT_COMPRESSION)
36
37
  writer = new(sheets: sheets, shared_strings: shared_strings, shared_strings_index: shared_strings_index, styles: styles,
37
38
  defined_names: defined_names, core_properties: core_properties,
38
39
  app_properties: app_properties, custom_properties: custom_properties,
39
- workbook_protection: workbook_protection, workbook_properties: workbook_properties)
40
+ workbook_protection: workbook_protection, workbook_properties: workbook_properties,
41
+ compression_level: compression_level)
40
42
  writer.write_to(target)
41
43
  end
42
44
 
43
45
  def initialize(sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
44
46
  defined_names: nil, core_properties: nil, app_properties: nil,
45
- custom_properties: nil, workbook_protection: nil, workbook_properties: nil)
47
+ custom_properties: nil, workbook_protection: nil, workbook_properties: nil,
48
+ compression_level: Zlib::DEFAULT_COMPRESSION)
46
49
  @sheets = sheets
47
- @shared_strings = shared_strings
50
+ @shared_strings = shared_strings || []
48
51
  @shared_strings_index = shared_strings_index
49
52
  @styles = styles || {}
50
53
  @defined_names = defined_names || []
@@ -53,27 +56,42 @@ module Xlsxrb
53
56
  @custom_properties = custom_properties || []
54
57
  @workbook_protection = workbook_protection
55
58
  @workbook_properties = workbook_properties || { update_links: "never" }
59
+ @compression_level = compression_level || Zlib::DEFAULT_COMPRESSION
56
60
  @drawing_count = 0
57
61
  @chart_count = 0
58
62
  @comment_count = 0
59
63
  @table_count = 0
60
64
  @pivot_cache_count = 0
61
65
  @pivot_table_count = 0
66
+ @threaded_comment_count = 0
67
+ @persons = []
62
68
  end
63
69
 
64
70
  def write_to(target)
65
71
  preprocess_conditional_formats!
66
72
 
67
- ZipWriter.open(target) do |zip|
73
+ active_tab_idx = resolve_active_tab_index
74
+ effective_active_idx = active_tab_idx || 0
75
+
76
+ ZipWriter.open(target, compression_level: @compression_level) do |zip|
68
77
  @sheets.each_with_index do |sheet, idx|
69
78
  info = prepare_sheet_auxiliary(sheet, idx)
70
79
  sheet_tables = sheet[:tables] || []
71
80
 
81
+ sv = sheet[:sheet_view]
82
+ if idx == effective_active_idx
83
+ sv = (sv || {}).merge(tab_selected: true)
84
+ elsif sv&.key?(:tab_selected)
85
+ sv = sv.dup
86
+ sv.delete(:tab_selected)
87
+ end
88
+
72
89
  zip.start_entry("xl/worksheets/sheet#{idx + 1}.xml")
73
90
  write_worksheet_xml(
74
91
  ZipEntryIO.new(zip),
75
92
  sheet,
76
93
  drawing_rid: info[:drawing_rid],
94
+ picture_rid: info[:picture_rid],
77
95
  sheet_protection: sheet[:sheet_protection],
78
96
  auto_filter: sheet[:auto_filter],
79
97
  filter_columns: sheet[:filter_columns],
@@ -91,11 +109,14 @@ module Xlsxrb
91
109
  freeze_pane: sheet[:freeze_pane],
92
110
  split_pane: sheet[:split_pane],
93
111
  selection: sheet[:selection],
94
- sheet_view: sheet[:sheet_view],
112
+ sheet_view: sv,
95
113
  sheet_properties: sheet[:sheet_properties],
96
114
  sheet_format: sheet[:sheet_format],
97
115
  protected_ranges: sheet[:protected_ranges],
98
116
  cell_watches: sheet[:cell_watches],
117
+ ignored_errors: sheet[:ignored_errors],
118
+ phonetic_pr: sheet[:phonetic_properties] || sheet[:phonetic_pr],
119
+ sheet_calc_pr: sheet[:sheet_calc_pr],
99
120
  tables: sheet_tables,
100
121
  table_start_rid: info[:table_start_rid],
101
122
  legacy_drawing_rid: info[:vml_rid],
@@ -108,20 +129,23 @@ module Xlsxrb
108
129
  end
109
130
  end
110
131
 
111
- def prepare_sheet_auxiliary(sheet, _idx)
132
+ def prepare_sheet_auxiliary(sheet, idx)
112
133
  sheet_images = sheet[:images] || []
113
134
  sheet_charts = sheet[:charts] || []
114
135
  sheet_shapes = sheet[:shapes] || []
115
136
  sheet_comments = sheet[:comments] || []
137
+ sheet_threaded_comments = sheet[:threaded_comments] || []
116
138
  sheet_tables = sheet[:tables] || []
117
139
  sheet_hyperlinks = sheet[:hyperlinks] || []
118
140
  has_drawing = sheet_images.any? || sheet_charts.any? || sheet_shapes.any?
119
141
  has_comments = sheet_comments.any?
142
+ has_threaded_comments = sheet_threaded_comments.any?
120
143
 
121
144
  sheet_rels = []
122
145
  drawing_rid = nil
123
146
  vml_rid = nil
124
147
  table_start_rid = nil
148
+ picture_rid = nil
125
149
  hyperlink_rels = []
126
150
 
127
151
  if has_drawing
@@ -140,6 +164,12 @@ module Xlsxrb
140
164
  vml_rid = "rId#{vml_rid_num}"
141
165
  end
142
166
 
167
+ if has_threaded_comments
168
+ @threaded_comment_count += 1
169
+ tc_rid_num = sheet_rels.size + 1
170
+ sheet_rels << { id: "rId#{tc_rid_num}", type: "http://schemas.microsoft.com/office/2017/10/relationships/threadedComment", target: "../threadedComments/threadedComment#{@threaded_comment_count}.xml" }
171
+ end
172
+
143
173
  sheet_hyperlinks.each do |link|
144
174
  next unless link[:url]
145
175
 
@@ -178,6 +208,33 @@ module Xlsxrb
178
208
  end
179
209
  end
180
210
 
211
+ bg_img = sheet[:background_image] || sheet[:background_picture] || sheet[:picture]
212
+ if bg_img
213
+ bg_data = if bg_img.is_a?(Hash)
214
+ bg_img[:file_data] || bg_img[:data]
215
+ else
216
+ (bg_img.respond_to?(:data) ? bg_img.data : bg_img)
217
+ end
218
+ if bg_data.is_a?(String) && !bg_data.include?("\x00") && File.file?(bg_data)
219
+ bg_ext = File.extname(bg_data).delete_prefix(".").downcase
220
+ bg_data = File.binread(bg_data)
221
+ else
222
+ bg_ext = if bg_img.is_a?(Hash)
223
+ (bg_img[:ext] || (bg_img[:filename] && File.extname(bg_img[:filename]).delete_prefix(".")) || "png").to_s.downcase
224
+ elsif bg_img.respond_to?(:filename)
225
+ File.extname(bg_img.filename).delete_prefix(".").downcase
226
+ else
227
+ "png"
228
+ end
229
+ end
230
+ if bg_data && !bg_data.empty?
231
+ p_rid_num = sheet_rels.size + 1
232
+ picture_rid = "rId#{p_rid_num}"
233
+ sheet_rels << { id: picture_rid, type: "#{DOC_REL}/image", target: "../media/sheet#{idx + 1}_bg.#{bg_ext}" }
234
+ sheet[:_bg_media] = { path: "xl/media/sheet#{idx + 1}_bg.#{bg_ext}", data: bg_data.b, ext: bg_ext }
235
+ end
236
+ end
237
+
181
238
  sheet[:_prepared_rels] = sheet_rels
182
239
  sheet[:_enriched_hyperlinks] = enriched_hyperlinks
183
240
 
@@ -185,6 +242,7 @@ module Xlsxrb
185
242
  drawing_rid: drawing_rid,
186
243
  vml_rid: vml_rid,
187
244
  table_start_rid: table_start_rid,
245
+ picture_rid: picture_rid,
188
246
  enriched_hyperlinks: enriched_hyperlinks,
189
247
  sheet_rels: sheet_rels
190
248
  }
@@ -193,6 +251,7 @@ module Xlsxrb
193
251
  def write_auxiliary_parts(zip)
194
252
  drawing_idx = 0
195
253
  comment_idx = 0
254
+ tc_idx = 0
196
255
  table_idx = 0
197
256
  pivot_cache_idx = 0
198
257
  pivot_table_idx = 0
@@ -268,6 +327,29 @@ module Xlsxrb
268
327
  zip.add_entry("xl/drawings/vmlDrawing#{comment_idx}.vml", comment_writer.send(:generate_vml_drawing_xml, comment_writer.comments))
269
328
  end
270
329
 
330
+ sheet_threaded_comments = sheet[:threaded_comments] || []
331
+ if sheet_threaded_comments.any?
332
+ tc_idx += 1
333
+ tc_writer = Xlsxrb::Ooxml::Writer.new
334
+ sheet_threaded_comments.each do |c|
335
+ tc_writer.add_threaded_comment(
336
+ c[:cell] || c[:ref],
337
+ c[:text],
338
+ author: c[:author] || "Author",
339
+ timestamp: c[:timestamp] || c[:dt],
340
+ id: c[:id],
341
+ parent_id: c[:parent_id],
342
+ done: c[:done],
343
+ user_id: c[:user_id],
344
+ provider_id: c[:provider_id]
345
+ )
346
+ end
347
+ tc_writer.persons.each do |p|
348
+ @persons << p unless @persons.any? { |ep| ep[:id] == p[:id] || ep[:display_name] == p[:display_name] }
349
+ end
350
+ zip.add_entry("xl/threadedComments/threadedComment#{tc_idx}.xml", tc_writer.send(:generate_threaded_comments_xml, tc_writer.threaded_comments))
351
+ end
352
+
271
353
  sheet_tables.each do |tbl|
272
354
  table_idx += 1
273
355
  zip.add_entry("xl/tables/table#{table_idx}.xml", build_table_xml(tbl, table_idx))
@@ -308,8 +390,15 @@ module Xlsxrb
308
390
  end
309
391
  end
310
392
 
393
+ zip.add_binary_entry(sheet[:_bg_media][:path], sheet[:_bg_media][:data]) if sheet[:_bg_media]
394
+
311
395
  zip.add_entry("xl/worksheets/_rels/sheet#{idx + 1}.xml.rels", build_sheet_rels_from_list(sheet_rels)) unless sheet_rels.empty?
312
396
  end
397
+
398
+ return unless @persons.any?
399
+
400
+ p_writer = Xlsxrb::Ooxml::Writer.new
401
+ zip.add_entry("xl/persons/person.xml", p_writer.send(:generate_persons_xml, @persons))
313
402
  end
314
403
 
315
404
  def write_package_metadata(zip)
@@ -407,6 +496,23 @@ module Xlsxrb
407
496
  end
408
497
  end
409
498
 
499
+ if sheet[:_bg_media]
500
+ bg_ext = sheet[:_bg_media][:ext]
501
+ unless image_exts[bg_ext]
502
+ mime = case bg_ext
503
+ when "png" then "image/png"
504
+ when "jpg", "jpeg" then "image/jpeg"
505
+ when "gif" then "image/gif"
506
+ when "bmp" then "image/bmp"
507
+ when "emf" then "image/x-emf"
508
+ when "wmf" then "image/x-wmf"
509
+ else "application/octet-stream"
510
+ end
511
+ b.empty_tag("Default", { Extension: bg_ext, ContentType: mime })
512
+ image_exts[bg_ext] = true
513
+ end
514
+ end
515
+
410
516
  if sheet_comments.any?
411
517
  comment_count += 1
412
518
  b.empty_tag("Override", { PartName: "/xl/comments#{comment_count}.xml", ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" })
@@ -427,6 +533,11 @@ module Xlsxrb
427
533
  end
428
534
  end
429
535
 
536
+ b.empty_tag("Override", { PartName: "/xl/persons/person.xml", ContentType: "application/vnd.ms-excel.person+xml" }) if @persons.any?
537
+ (1..@threaded_comment_count).each do |tc|
538
+ b.empty_tag("Override", { PartName: "/xl/threadedComments/threadedComment#{tc}.xml", ContentType: "application/vnd.ms-excel.threadedcomments+xml" })
539
+ end
540
+
430
541
  b.close_tag("Types")
431
542
  io.string
432
543
  end
@@ -484,6 +595,7 @@ module Xlsxrb
484
595
  attrs = {}
485
596
  attrs[:date1904] = "1" if @workbook_properties[:date1904]
486
597
  attrs[:updateLinks] = @workbook_properties[:update_links] if @workbook_properties[:update_links]
598
+ attrs[:codeName] = @workbook_properties[:code_name] if @workbook_properties[:code_name]
487
599
  b.empty_tag("workbookPr", attrs) unless attrs.empty?
488
600
  end
489
601
 
@@ -496,11 +608,23 @@ module Xlsxrb
496
608
  b.empty_tag("workbookProtection", wp_attrs) unless wp_attrs.empty?
497
609
  end
498
610
 
499
- # Workbook view: which sheet tab is selected when the file is opened
500
- active_tab = @workbook_properties&.[](:active_tab)
501
- if active_tab
611
+ # Workbook view: activeTab, firstSheet, showSheetTabs, tabRatio
612
+ active_tab_idx = resolve_active_tab_index
613
+ first_sheet_idx = resolve_first_sheet_index
614
+ show_sheet_tabs = @workbook_properties&.[](:show_sheet_tabs)
615
+ tab_ratio = @workbook_properties&.[](:tab_ratio)
616
+
617
+ emit_active_tab = !@workbook_properties&.[](:active_tab).nil? || active_tab_idx&.positive?
618
+
619
+ has_book_views = emit_active_tab || first_sheet_idx || !show_sheet_tabs.nil? || tab_ratio
620
+ if has_book_views
621
+ bv_attrs = {}
622
+ bv_attrs[:activeTab] = active_tab_idx.to_s if emit_active_tab
623
+ bv_attrs[:firstSheet] = first_sheet_idx.to_s if first_sheet_idx
624
+ bv_attrs[:showSheetTabs] = show_sheet_tabs ? "1" : "0" unless show_sheet_tabs.nil?
625
+ bv_attrs[:tabRatio] = tab_ratio.to_s if tab_ratio
502
626
  b.open_tag("bookViews")
503
- b.empty_tag("workbookView", { activeTab: active_tab.to_s })
627
+ b.empty_tag("workbookView", bv_attrs)
504
628
  b.close_tag("bookViews")
505
629
  end
506
630
 
@@ -597,6 +721,15 @@ module Xlsxrb
597
721
  })
598
722
  end
599
723
 
724
+ if @persons.any?
725
+ rid += 1
726
+ b.empty_tag("Relationship", {
727
+ Id: "rId#{rid}",
728
+ Type: "http://schemas.microsoft.com/office/2017/10/relationships/person",
729
+ Target: "persons/person.xml"
730
+ })
731
+ end
732
+
600
733
  b.close_tag("Relationships")
601
734
  io.string
602
735
  end
@@ -650,12 +783,28 @@ module Xlsxrb
650
783
  b.empty_tag("i") if font_props[:italic]
651
784
  b.empty_tag("strike") if font_props[:strike]
652
785
  if font_props[:underline]
653
- b.empty_tag("u", font_props[:underline] == "single" ? {} : { val: font_props[:underline] })
786
+ u_val = font_props[:underline]
787
+ if u_val == true || u_val.to_s == "single"
788
+ b.empty_tag("u")
789
+ else
790
+ mapped_u = StyleBuilder::UNDERLINE_MAP[u_val.to_sym] || u_val.to_s
791
+ b.empty_tag("u", { val: mapped_u })
792
+ end
793
+ end
794
+ va_val = font_props[:vert_align] || font_props[:vertical_align]
795
+ va_val ||= "superscript" if font_props[:superscript]
796
+ va_val ||= "subscript" if font_props[:subscript]
797
+ if va_val
798
+ mapped_va = StyleBuilder::VERT_ALIGN_MAP[va_val.to_sym] || va_val.to_s
799
+ b.empty_tag("vertAlign", { val: mapped_va })
654
800
  end
655
801
  b.empty_tag("sz", { val: (font_props[:sz] || 11).to_s }) if font_props[:sz] || fonts == [{}]
656
802
  color_attrs = color_tag_attrs(font_props[:color] || (font_props.key?(:rgb) || font_props.key?(:theme) || font_props.key?(:indexed) ? font_props : nil))
657
803
  b.empty_tag("color", color_attrs) if color_attrs
658
804
  b.empty_tag("name", { val: font_props[:name] || "Calibri" })
805
+ b.empty_tag("family", { val: font_props[:family].to_s }) if font_props[:family]
806
+ b.empty_tag("charset", { val: font_props[:charset].to_s }) if font_props[:charset]
807
+ b.empty_tag("scheme", { val: font_props[:scheme].to_s }) if font_props[:scheme]
659
808
  end
660
809
  end
661
810
  end
@@ -834,22 +983,7 @@ module Xlsxrb
834
983
  end
835
984
 
836
985
  def extract_dxf_from_rule!(rule)
837
- font = {}
838
- font[:color] = rule.delete(:font_color) if rule.key?(:font_color)
839
- font[:bold] = rule.delete(:bold) if rule.key?(:bold)
840
- font[:italic] = rule.delete(:italic) if rule.key?(:italic)
841
- font[:underline] = rule.delete(:underline) if rule.key?(:underline)
842
-
843
- fill = {}
844
- if rule.key?(:fill_color)
845
- fill[:pattern] = "solid"
846
- fill[:fg_color] = rule.delete(:fill_color)
847
- end
848
-
849
- dxf = {}
850
- dxf[:font] = font unless font.empty?
851
- dxf[:fill] = fill unless fill.empty?
852
- dxf.empty? ? nil : dxf
986
+ DslHelpers.extract_dxf_from_rule!(rule)
853
987
  end
854
988
 
855
989
  def write_dxf_font(builder, font)
@@ -867,22 +1001,34 @@ module Xlsxrb
867
1001
  if [true, "single"].include?(font[:underline])
868
1002
  builder.empty_tag("u")
869
1003
  else
870
- builder.empty_tag("u", { val: font[:underline] })
1004
+ mapped_u = StyleBuilder::UNDERLINE_MAP[font[:underline].to_sym] || font[:underline]
1005
+ builder.empty_tag("u", { val: mapped_u })
871
1006
  end
872
1007
  end
1008
+ va_val = font[:vert_align] || font[:vertical_align]
1009
+ va_val ||= "superscript" if font[:superscript]
1010
+ va_val ||= "subscript" if font[:subscript]
1011
+ if va_val
1012
+ mapped_va = StyleBuilder::VERT_ALIGN_MAP[va_val.to_sym] || va_val.to_s
1013
+ builder.empty_tag("vertAlign", { val: mapped_va })
1014
+ end
873
1015
  color_attrs = color_tag_attrs(font[:color] || font)
874
1016
  builder.empty_tag("color", color_attrs) if color_attrs
875
1017
  builder.empty_tag("name", { val: font[:name] }) if font[:name]
876
- builder.empty_tag("sz", { val: font[:sz].to_s }) if font[:sz]
1018
+ sz_val = font[:sz] || font[:size]
1019
+ builder.empty_tag("sz", { val: sz_val.to_s }) if sz_val
877
1020
  end
878
1021
  end
879
1022
 
880
1023
  def write_dxf_fill(builder, fill)
881
1024
  builder.tag("fill") do |_|
882
1025
  pattern_attrs = {}
883
- pattern_attrs[:patternType] = fill[:pattern] if fill[:pattern]
1026
+ pattern = fill[:pattern] || fill[:pattern_type]
1027
+ pattern ||= "solid" if (fill[:color] || fill[:fill_color] || fill[:fg_color]) && !fill[:bg_color]
1028
+ pattern_attrs[:patternType] = pattern if pattern
884
1029
  builder.tag("patternFill", pattern_attrs) do |_|
885
- fg_attrs = color_tag_attrs(fill[:fg_color])
1030
+ fg = fill[:fg_color] || fill[:color] || fill[:fill_color]
1031
+ fg_attrs = color_tag_attrs(fg)
886
1032
  builder.empty_tag("fgColor", fg_attrs) if fg_attrs
887
1033
  bg_attrs = color_tag_attrs(fill[:bg_color])
888
1034
  builder.empty_tag("bgColor", bg_attrs) if bg_attrs
@@ -1028,14 +1174,22 @@ module Xlsxrb
1028
1174
  buf << "<i/>" if font[:italic]
1029
1175
  buf << "<strike/>" if font[:strike]
1030
1176
  if font[:underline]
1031
- if font[:underline] == true
1177
+ if font[:underline] == true || font[:underline].to_s == "single"
1032
1178
  buf << "<u/>"
1033
1179
  else
1034
- buf << '<u val="' << font[:underline].to_s << '"/>'
1180
+ mapped_u = StyleBuilder::UNDERLINE_MAP[font[:underline].to_sym] || font[:underline]
1181
+ buf << '<u val="' << mapped_u.to_s << '"/>'
1035
1182
  end
1036
1183
  end
1037
- buf << '<vertAlign val="' << font[:vert_align].to_s << '"/>' if font[:vert_align]
1038
- buf << '<sz val="' << font[:sz].to_s << '"/>' if font[:sz]
1184
+ va_val = font[:vert_align] || font[:vertical_align]
1185
+ va_val ||= "superscript" if font[:superscript]
1186
+ va_val ||= "subscript" if font[:subscript]
1187
+ if va_val
1188
+ mapped_va = StyleBuilder::VERT_ALIGN_MAP[va_val.to_sym] || va_val.to_s
1189
+ buf << '<vertAlign val="' << mapped_va << '"/>'
1190
+ end
1191
+ font_sz = font[:sz] || font[:size]
1192
+ buf << '<sz val="' << font_sz.to_s << '"/>' if font_sz
1039
1193
  color_attrs = color_tag_attrs(font[:color] || font)
1040
1194
  if color_attrs
1041
1195
  attrs_str = color_attrs.map { |k, v| %(#{k}="#{v}") }.join(" ")
@@ -1067,16 +1221,17 @@ module Xlsxrb
1067
1221
  zip.finish_entry
1068
1222
  end
1069
1223
 
1070
- def write_worksheet_xml(io, sheet, drawing_rid: nil, sheet_protection: nil,
1224
+ def write_worksheet_xml(io, sheet, drawing_rid: nil, picture_rid: nil, sheet_protection: nil,
1071
1225
  protected_ranges: nil, auto_filter: nil, filter_columns: nil,
1072
1226
  sort_state: nil, merge_cells: nil, conditional_formats: nil,
1073
1227
  data_validations: nil, hyperlinks: nil,
1074
1228
  print_options: nil, page_margins: nil, page_setup: nil,
1075
1229
  header_footer: nil, row_breaks: nil, col_breaks: nil,
1076
- cell_watches: nil, freeze_pane: nil, split_pane: nil,
1230
+ cell_watches: nil, ignored_errors: nil, freeze_pane: nil, split_pane: nil,
1077
1231
  selection: nil, sheet_view: nil, sheet_properties: nil,
1078
1232
  sheet_format: nil, tables: nil, table_start_rid: nil,
1079
- legacy_drawing_rid: nil, sparkline_groups: nil)
1233
+ legacy_drawing_rid: nil, sparkline_groups: nil, phonetic_pr: nil,
1234
+ sheet_calc_pr: nil)
1080
1235
  date1904 = @workbook_properties&.[](:date1904) ? true : false
1081
1236
  ws = WorksheetWriter.new(io, date1904: date1904)
1082
1237
  ws.start(
@@ -1112,6 +1267,7 @@ module Xlsxrb
1112
1267
  end
1113
1268
  ws.finish(
1114
1269
  drawing_rid: drawing_rid,
1270
+ picture_rid: picture_rid,
1115
1271
  sheet_protection: sheet_protection,
1116
1272
  protected_ranges: protected_ranges,
1117
1273
  auto_filter: auto_filter,
@@ -1128,10 +1284,13 @@ module Xlsxrb
1128
1284
  row_breaks: row_breaks,
1129
1285
  col_breaks: col_breaks,
1130
1286
  cell_watches: cell_watches,
1287
+ ignored_errors: ignored_errors,
1131
1288
  tables: tables,
1132
1289
  table_start_rid: table_start_rid,
1133
1290
  legacy_drawing_rid: legacy_drawing_rid,
1134
- sparkline_groups: sparkline_groups
1291
+ sparkline_groups: sparkline_groups,
1292
+ phonetic_pr: phonetic_pr,
1293
+ sheet_calc_pr: sheet_calc_pr
1135
1294
  )
1136
1295
  end
1137
1296
 
@@ -1150,8 +1309,8 @@ module Xlsxrb
1150
1309
  t_attrs[:totalsRowCount] = tbl[:totals_row_count].to_s if tbl[:totals_row_count]&.positive?
1151
1310
  b.open_tag("table", t_attrs)
1152
1311
 
1153
- # Auto filter for the table range
1154
- b.empty_tag("autoFilter", { ref: tbl[:ref] })
1312
+ # Auto filter for the table range unless auto_filter: false
1313
+ b.empty_tag("autoFilter", { ref: tbl[:ref] }) unless tbl[:auto_filter] == false
1155
1314
 
1156
1315
  # Table columns
1157
1316
  columns = tbl[:columns] || []
@@ -1159,18 +1318,23 @@ module Xlsxrb
1159
1318
  columns.each_with_index do |col, ci|
1160
1319
  col_name = col.is_a?(Hash) ? col[:name] : col
1161
1320
  col_attrs = { id: (ci + 1).to_s, name: col_name.to_s }
1321
+ calc_formula = nil
1322
+ tot_formula = nil
1162
1323
  if col.is_a?(Hash)
1163
- col_attrs[:totalsRowFunction] = col[:totals_row_function].to_s if col[:totals_row_function]
1324
+ calc_formula = col[:calculated_column_formula] || col[:data_formula] || col[:formula]
1325
+ tot_formula = col[:totals_row_formula] || col[:total_row_formula] || col[:total_formula]
1326
+ trf = col[:totals_row_function] || ("custom" if tot_formula)
1327
+ col_attrs[:totalsRowFunction] = trf.to_s if trf
1164
1328
  col_attrs[:totalsRowLabel] = col[:totals_row_label].to_s if col[:totals_row_label]
1165
1329
  col_attrs[:dataDxfId] = col[:data_dxf_id].to_s if col[:data_dxf_id]
1166
1330
  col_attrs[:totalsRowDxfId] = col[:totals_row_dxf_id].to_s if col[:totals_row_dxf_id]
1167
1331
  col_attrs[:headerRowDxfId] = col[:header_row_dxf_id].to_s if col[:header_row_dxf_id]
1168
1332
  col_attrs[:dataCellStyle] = col[:data_cell_style].to_s if col[:data_cell_style]
1169
1333
  end
1170
- if col.is_a?(Hash) && (col[:calculated_column_formula] || col[:totals_row_formula])
1334
+ if calc_formula || tot_formula
1171
1335
  b.open_tag("tableColumn", col_attrs)
1172
- b.tag("calculatedColumnFormula") { |cb| cb.text(col[:calculated_column_formula].to_s) } if col[:calculated_column_formula]
1173
- b.tag("totalsRowFormula") { |tb| tb.text(col[:totals_row_formula].to_s) } if col[:totals_row_formula]
1336
+ b.tag("calculatedColumnFormula") { |cb| cb.text(calc_formula.to_s) } if calc_formula
1337
+ b.tag("totalsRowFormula") { |tb| tb.text(tot_formula.to_s) } if tot_formula
1174
1338
  b.close_tag("tableColumn")
1175
1339
  else
1176
1340
  b.empty_tag("tableColumn", col_attrs)
@@ -1289,6 +1453,62 @@ module Xlsxrb
1289
1453
  b.close_tag("Properties")
1290
1454
  io.string
1291
1455
  end
1456
+
1457
+ def resolve_active_tab_index
1458
+ active_tab = @workbook_properties&.[](:active_tab)
1459
+ if active_tab.nil?
1460
+ first_vis_idx = @sheets.find_index do |s|
1461
+ state = s.respond_to?(:state) ? s.state : s[:state]
1462
+ state.nil? || state.to_sym == :visible
1463
+ end
1464
+ active_tab = first_vis_idx if first_vis_idx&.positive?
1465
+ end
1466
+
1467
+ if active_tab.is_a?(String) || active_tab.is_a?(Symbol)
1468
+ target_name = active_tab.to_s
1469
+ idx = @sheets.find_index do |s|
1470
+ (s.respond_to?(:name) ? s.name : s[:name]) == target_name
1471
+ end
1472
+ raise ArgumentError, "Active sheet '#{target_name}' not found" if idx.nil?
1473
+
1474
+ active_tab = idx
1475
+ end
1476
+
1477
+ if active_tab
1478
+ target_sheet = @sheets[active_tab]
1479
+ raise ArgumentError, "Active sheet index #{active_tab} out of bounds" if target_sheet.nil?
1480
+
1481
+ state = target_sheet.respond_to?(:state) ? target_sheet.state : target_sheet[:state]
1482
+ if state && state.to_sym != :visible
1483
+ name = target_sheet.respond_to?(:name) ? target_sheet.name : target_sheet[:name]
1484
+ raise ArgumentError, "Cannot set hidden sheet '#{name}' as active sheet"
1485
+ end
1486
+ end
1487
+
1488
+ active_tab
1489
+ end
1490
+
1491
+ def resolve_first_sheet_index
1492
+ first_sheet = @workbook_properties&.[](:first_sheet)
1493
+ return nil if first_sheet.nil?
1494
+
1495
+ if first_sheet.is_a?(String) || first_sheet.is_a?(Symbol)
1496
+ target_name = first_sheet.to_s
1497
+ idx = @sheets.find_index do |s|
1498
+ (s.respond_to?(:name) ? s.name : s[:name]) == target_name
1499
+ end
1500
+ raise ArgumentError, "First sheet '#{target_name}' not found" if idx.nil?
1501
+
1502
+ first_sheet = idx
1503
+ end
1504
+
1505
+ if first_sheet
1506
+ target_sheet = @sheets[first_sheet]
1507
+ raise ArgumentError, "First sheet index #{first_sheet} out of bounds" if target_sheet.nil?
1508
+ end
1509
+
1510
+ first_sheet
1511
+ end
1292
1512
  end
1293
1513
  end
1294
1514
  end
@@ -201,6 +201,7 @@ module Xlsxrb
201
201
 
202
202
  pos = sd_open_end + 1
203
203
  last_row_index = -1
204
+ shared_masters = {}
204
205
 
205
206
  while pos < sd_end
206
207
  row_start = xml.index(row_start_pattern, pos)
@@ -245,7 +246,7 @@ module Xlsxrb
245
246
  row_end = xml.index(row_end_tag, tag_end + 1)
246
247
  break unless row_end
247
248
 
248
- fast_scan_cells_events(xml, tag_end + 1, row_end, shared_strings, row_source, prefix, &block)
249
+ fast_scan_cells_events(xml, tag_end + 1, row_end, shared_strings, row_source, prefix, shared_masters, &block)
249
250
 
250
251
  block.call(Event.new(
251
252
  type: :row_end,
@@ -314,7 +315,7 @@ module Xlsxrb
314
315
 
315
316
  private_class_method :tag_attr
316
317
 
317
- def self.fast_scan_cells_events(xml, from, to, shared_strings, row_source, prefix = "", &block)
318
+ def self.fast_scan_cells_events(xml, from, to, shared_strings, row_source, prefix = "", shared_masters = {}, &block)
318
319
  pos = from
319
320
  c_start_pattern = "<#{prefix}c"
320
321
  c_start_len = c_start_pattern.bytesize
@@ -393,13 +394,26 @@ module Xlsxrb
393
394
  elsif xml.byteslice(tag_pos, f_tag_start_str.bytesize) == f_tag_start_str
394
395
  f_tag_end = xml.index(">", tag_pos + f_tag_start_str.bytesize)
395
396
  if f_tag_end && f_tag_end < c_end
397
+ f_tag_slice = xml.byteslice(tag_pos, f_tag_end - tag_pos)
398
+ is_shared = f_tag_slice.include?('t="shared"') || f_tag_slice.include?("t='shared'")
399
+ si = is_shared ? tag_attr(f_tag_slice, ' si="') : nil
396
400
  if xml.getbyte(f_tag_end - 1) == 47 # self-closing <f ... />
401
+ if si && (master = shared_masters[si])
402
+ formula = Xlsxrb::Utils.expand_formula(master[:formula], from_ref: master[:ref], to_ref: ref)
403
+ end
397
404
  cpos = f_tag_end + 1
398
405
  else
399
406
  f_end = xml.index(f_tag_end_str, f_tag_end + 1)
400
407
  if f_end && f_end <= c_end
401
408
  formula = xml.byteslice(f_tag_end + 1, f_end - f_tag_end - 1).force_encoding("UTF-8")
402
409
  formula = decode_xml_entities(formula) if formula.include?("&")
410
+ if is_shared && si
411
+ if formula && !formula.empty?
412
+ shared_masters[si] = { ref: ref, formula: formula }
413
+ elsif (master = shared_masters[si])
414
+ formula = Xlsxrb::Utils.expand_formula(master[:formula], from_ref: master[:ref], to_ref: ref)
415
+ end
416
+ end
403
417
  cpos = f_end + f_tag_end_len
404
418
  else
405
419
  cpos = f_tag_end + 1
@@ -598,8 +612,8 @@ module Xlsxrb
598
612
  min: attrs["min"]&.to_i,
599
613
  max: attrs["max"]&.to_i,
600
614
  width: attrs["width"]&.to_f,
601
- hidden: attrs["hidden"] == "1",
602
- custom_width: attrs["customWidth"] == "1",
615
+ hidden: attrs["hidden"] == "1" || (attrs["hidden"]&.casecmp?("true") || false),
616
+ custom_width: attrs["customWidth"] == "1" || (attrs["customWidth"]&.casecmp?("true") || false),
603
617
  outline_level: attrs["outlineLevel"]&.to_i,
604
618
  style_index: attrs["style"]&.to_i
605
619
  }