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
@@ -74,18 +74,7 @@ module Xlsxrb
74
74
 
75
75
  # Returns hyperlinks as { "A1" => "https://example.com" }.
76
76
  def hyperlinks(sheet: nil)
77
- sheets = discover_sheets
78
- raise ArgumentError, "workbook has no sheets" if sheets.empty?
79
-
80
- target = resolve_sheet_target(sheets, sheet)
81
- raise ArgumentError, "sheet not found: #{sheet.inspect}" if target.nil?
82
-
83
- entry_path = if target.start_with?("/")
84
- target.delete_prefix("/")
85
- else
86
- "xl/#{target}"
87
- end
88
-
77
+ entry_path = resolve_sheet_entry_path(sheet)
89
78
  worksheet_xml = extract_zip_entry(entry_path)
90
79
  return {} if worksheet_xml.nil? || worksheet_xml.empty?
91
80
 
@@ -129,14 +118,17 @@ module Xlsxrb
129
118
  parser.parse
130
119
 
131
120
  result = {}
121
+ cell_xfs = styles.respond_to?(:cell_xfs) ? styles.cell_xfs : styles[:cell_xfs]
122
+ cell_style_xfs = styles.respond_to?(:cell_style_xfs) ? styles.cell_style_xfs : styles[:cell_style_xfs]
123
+ n_fmts = styles.respond_to?(:num_fmts) ? styles.num_fmts : styles[:num_fmts]
132
124
  listener.cell_style_indices.each do |cell_ref, xf_index|
133
- xf = resolve_effective_xf(styles[:cell_xfs][xf_index], styles[:cell_style_xfs])
125
+ xf = resolve_effective_xf(cell_xfs ? cell_xfs[xf_index] : nil, cell_style_xfs)
134
126
  next unless xf
135
127
 
136
128
  fmt_id = xf[:num_fmt_id]
137
129
  next unless fmt_id && fmt_id != 0
138
130
 
139
- format_code = resolve_num_fmt_code(fmt_id, styles[:num_fmts])
131
+ format_code = resolve_num_fmt_code(fmt_id, n_fmts)
140
132
  result[cell_ref] = format_code if format_code
141
133
  end
142
134
  result
@@ -152,16 +144,23 @@ module Xlsxrb
152
144
 
153
145
  indices = parse_cell_style_indices(worksheet_xml)
154
146
  result = {}
147
+ cell_xfs = styles.respond_to?(:cell_xfs) ? styles.cell_xfs : styles[:cell_xfs]
148
+ cell_style_xfs = styles.respond_to?(:cell_style_xfs) ? styles.cell_style_xfs : styles[:cell_style_xfs]
149
+ fonts_arr = styles.respond_to?(:fonts) ? styles.fonts : styles[:fonts]
150
+ fills_arr = styles.respond_to?(:fills) ? styles.fills : styles[:fills]
151
+ borders_arr = styles.respond_to?(:borders) ? styles.borders : styles[:borders]
152
+ n_fmts = styles.respond_to?(:num_fmts) ? styles.num_fmts : styles[:num_fmts]
153
+
155
154
  indices.each do |cell_ref, xf_index|
156
- xf = resolve_effective_xf(styles[:cell_xfs][xf_index], styles[:cell_style_xfs])
155
+ xf = resolve_effective_xf(cell_xfs ? cell_xfs[xf_index] : nil, cell_style_xfs)
157
156
  next unless xf
158
157
 
159
158
  entry = {}
160
- entry[:font] = styles[:fonts][xf[:font_id]] if xf[:font_id]&.positive? && styles[:fonts][xf[:font_id]]
161
- entry[:fill] = styles[:fills][xf[:fill_id]] if xf[:fill_id]&.positive? && styles[:fills][xf[:fill_id]]
162
- entry[:border] = styles[:borders][xf[:border_id]] if xf[:border_id]&.positive? && styles[:borders][xf[:border_id]]
159
+ entry[:font] = fonts_arr[xf[:font_id]] if xf[:font_id]&.positive? && fonts_arr && fonts_arr[xf[:font_id]]
160
+ entry[:fill] = fills_arr[xf[:fill_id]] if xf[:fill_id]&.positive? && fills_arr && fills_arr[xf[:fill_id]]
161
+ entry[:border] = borders_arr[xf[:border_id]] if xf[:border_id]&.positive? && borders_arr && borders_arr[xf[:border_id]]
163
162
  if xf[:num_fmt_id]&.positive?
164
- code = resolve_num_fmt_code(xf[:num_fmt_id], styles[:num_fmts])
163
+ code = resolve_num_fmt_code(xf[:num_fmt_id], n_fmts)
165
164
  entry[:num_fmt] = code if code
166
165
  end
167
166
  entry[:alignment] = xf[:alignment] if xf[:alignment]
@@ -178,7 +177,7 @@ module Xlsxrb
178
177
  styles = load_styles
179
178
  return [] if styles.empty?
180
179
 
181
- styles[:dxfs] || []
180
+ styles.respond_to?(:dxfs) ? styles.dxfs : (styles[:dxfs] || [])
182
181
  end
183
182
 
184
183
  # Returns array of font entries from the styles.
@@ -186,7 +185,7 @@ module Xlsxrb
186
185
  styles = load_styles
187
186
  return [] if styles.empty?
188
187
 
189
- styles[:fonts] || []
188
+ styles.respond_to?(:fonts) ? styles.fonts : (styles[:fonts] || [])
190
189
  end
191
190
 
192
191
  # Returns array of fill entries from the styles.
@@ -194,7 +193,7 @@ module Xlsxrb
194
193
  styles = load_styles
195
194
  return [] if styles.empty?
196
195
 
197
- styles[:fills] || []
196
+ styles.respond_to?(:fills) ? styles.fills : (styles[:fills] || [])
198
197
  end
199
198
 
200
199
  # Returns array of border entries from the styles.
@@ -202,7 +201,7 @@ module Xlsxrb
202
201
  styles = load_styles
203
202
  return [] if styles.empty?
204
203
 
205
- styles[:borders] || []
204
+ styles.respond_to?(:borders) ? styles.borders : (styles[:borders] || [])
206
205
  end
207
206
 
208
207
  # Returns custom number formats as { numFmtId => formatCode }.
@@ -210,7 +209,7 @@ module Xlsxrb
210
209
  styles = load_styles
211
210
  return {} if styles.empty?
212
211
 
213
- styles[:num_fmts] || {}
212
+ styles.respond_to?(:num_fmts) ? styles.num_fmts : (styles[:num_fmts] || {})
214
213
  end
215
214
 
216
215
  # Returns indexed colors palette (array of ARGB hex strings).
@@ -218,7 +217,7 @@ module Xlsxrb
218
217
  styles = load_styles
219
218
  return [] if styles.empty?
220
219
 
221
- styles[:indexed_colors] || []
220
+ styles.respond_to?(:indexed_colors) ? styles.indexed_colors : (styles[:indexed_colors] || [])
222
221
  end
223
222
 
224
223
  # Returns MRU (most recently used) colors (array of color hashes).
@@ -226,7 +225,7 @@ module Xlsxrb
226
225
  styles = load_styles
227
226
  return [] if styles.empty?
228
227
 
229
- styles[:mru_colors] || []
228
+ styles.respond_to?(:mru_colors) ? styles.mru_colors : (styles[:mru_colors] || [])
230
229
  end
231
230
 
232
231
  # Returns table styles configuration hash.
@@ -234,7 +233,7 @@ module Xlsxrb
234
233
  styles = load_styles
235
234
  return {} if styles.empty?
236
235
 
237
- styles[:table_styles] || {}
236
+ styles.respond_to?(:table_styles) ? styles.table_styles : (styles[:table_styles] || {})
238
237
  end
239
238
 
240
239
  # Returns array of cellStyleXfs entries (base style format definitions).
@@ -242,7 +241,7 @@ module Xlsxrb
242
241
  styles = load_styles
243
242
  return [] if styles.empty?
244
243
 
245
- styles[:cell_style_xfs] || []
244
+ styles.respond_to?(:cell_style_xfs) ? styles.cell_style_xfs : (styles[:cell_style_xfs] || [])
246
245
  end
247
246
 
248
247
  # Returns array of cellXfs entries (cell format definitions).
@@ -250,7 +249,7 @@ module Xlsxrb
250
249
  styles = load_styles
251
250
  return [] if styles.empty?
252
251
 
253
- styles[:cell_xfs] || []
252
+ styles.respond_to?(:cell_xfs) ? styles.cell_xfs : (styles[:cell_xfs] || [])
254
253
  end
255
254
 
256
255
  # Returns array of named cell styles (cellStyle elements).
@@ -258,7 +257,7 @@ module Xlsxrb
258
257
  styles = load_styles
259
258
  return [] if styles.empty?
260
259
 
261
- styles[:cell_styles] || []
260
+ styles.respond_to?(:cell_styles) ? styles.cell_styles : (styles[:cell_styles] || [])
262
261
  end
263
262
 
264
263
  # Returns the autoFilter range string (e.g. "A1:B10") or nil.
@@ -645,34 +644,15 @@ module Xlsxrb
645
644
 
646
645
  # Returns all ZIP entry paths in the file.
647
646
  def entry_names
648
- names = []
649
- File.open(@filepath, "rb") do |file|
650
- loop do
651
- sig = file.read(4)
652
- break if sig.nil? || sig.bytesize < 4
653
-
654
- sig_val = sig.unpack1("V")
655
- break if [0x02014b50, 0x06054b50].include?(sig_val)
656
- break unless sig_val == 0x04034b50
657
-
658
- header = file.read(26)
659
- break if header.nil? || header.bytesize < 26
660
-
661
- _ver, flags, _cm, _mt, _md, _crc, comp_size, _unc, fname_len, extra_len = header.unpack("v v v v v V V V v v")
662
- break if flags.anybits?(0x0008)
663
-
664
- fname = file.read(fname_len)
665
- file.read(extra_len)
666
- file.read(comp_size)
667
- names << fname
668
- end
669
- end
670
- names
647
+ ZipReader.open(@filepath, &:entry_names)
671
648
  end
672
649
 
673
650
  # Returns raw bytes for a ZIP entry by path.
674
651
  def raw_entry(name)
675
- extract_zip_entry(name)
652
+ data = extract_zip_entry(name)
653
+ return nil unless data
654
+
655
+ name.end_with?(".xml", ".rels", ".txt") && data.valid_encoding? ? data : data.b
676
656
  end
677
657
 
678
658
  # Returns true if the file contains VBA macros (vbaProject.bin).
@@ -972,10 +952,14 @@ module Xlsxrb
972
952
 
973
953
  STRICT_SSML_NS = "http://purl.oclc.org/ooxml/spreadsheetml/main/2006/main"
974
954
  TRANSITIONAL_SSML_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
955
+ OFFICE_DOC_REL_TYPES = [
956
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
957
+ "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"
958
+ ].freeze
975
959
 
976
960
  # Returns :strict or :transitional based on the namespace of the workbook XML.
977
961
  def format_variant
978
- workbook_xml = extract_zip_entry("xl/workbook.xml")
962
+ workbook_xml = extract_zip_entry(resolve_workbook_entry)
979
963
  return :transitional if workbook_xml.nil? || workbook_xml.empty?
980
964
 
981
965
  if workbook_xml.include?(STRICT_SSML_NS)
@@ -987,6 +971,22 @@ module Xlsxrb
987
971
 
988
972
  private
989
973
 
974
+ def resolve_workbook_entry
975
+ @resolve_workbook_entry ||= begin
976
+ rels_xml = extract_zip_entry("_rels/.rels")
977
+ target_path = nil
978
+ if rels_xml && !rels_xml.empty?
979
+ entries = parse_rels_with_types(rels_xml)
980
+ doc_rel = entries.find { |e| OFFICE_DOC_REL_TYPES.include?(e[:type]) }
981
+ if doc_rel
982
+ clean = doc_rel[:target].delete_prefix("/")
983
+ target_path = clean if extract_zip_entry(clean)
984
+ end
985
+ end
986
+ target_path || (extract_zip_entry("xl/workbook.xml") ? "xl/workbook.xml" : "workbook.xml")
987
+ end
988
+ end
989
+
990
990
  def resolve_sheet_for_defined_name(sheet)
991
991
  sheets = discover_sheets
992
992
  if sheet
@@ -1002,7 +1002,7 @@ module Xlsxrb
1002
1002
  end
1003
1003
 
1004
1004
  def parse_workbook_metadata
1005
- workbook_xml = extract_zip_entry("xl/workbook.xml")
1005
+ workbook_xml = extract_zip_entry(resolve_workbook_entry)
1006
1006
  if workbook_xml.nil? || workbook_xml.empty?
1007
1007
  return { workbook_properties: {}, workbook_views: {}, calc_properties: {}, file_recovery_properties: {},
1008
1008
  workbook_protection: nil }
@@ -1025,29 +1025,63 @@ module Xlsxrb
1025
1025
  }
1026
1026
  end
1027
1027
 
1028
- def load_worksheet_xml(sheet)
1028
+ def resolve_sheet_entry_path(sheet)
1029
1029
  sheets = discover_sheets
1030
1030
  raise ArgumentError, "workbook has no sheets" if sheets.empty?
1031
1031
 
1032
1032
  target = resolve_sheet_target(sheets, sheet)
1033
1033
  raise ArgumentError, "sheet not found: #{sheet.inspect}" if target.nil?
1034
1034
 
1035
- # Target may be absolute (/xl/worksheets/sheet1.xml) or relative (worksheets/sheet1.xml).
1036
- entry_path = if target.start_with?("/")
1037
- target.delete_prefix("/")
1035
+ wb_dir = File.dirname(resolve_workbook_entry)
1036
+ candidates = if target.start_with?("/")
1037
+ [target.delete_prefix("/")]
1038
1038
  else
1039
- "xl/#{target}"
1039
+ [
1040
+ wb_dir == "." ? target : "#{wb_dir}/#{target}",
1041
+ "xl/#{target}",
1042
+ target
1043
+ ]
1040
1044
  end
1045
+ candidates.find { |c| extract_zip_entry(c) } || candidates.first
1046
+ end
1041
1047
 
1048
+ def load_worksheet_xml(sheet)
1049
+ entry_path = resolve_sheet_entry_path(sheet)
1042
1050
  extract_zip_entry(entry_path)
1043
1051
  end
1044
1052
 
1053
+ NON_WORKSHEET_REL_SUFFIXES = %w[
1054
+ /chartsheet
1055
+ /dialogsheet
1056
+ /xlMacrosheet
1057
+ /xlIntlMacrosheet
1058
+ /macrosheet
1059
+ ].freeze
1060
+ NON_WORKSHEET_TARGET_PREFIXES = %w[
1061
+ chartsheets/
1062
+ dialogsheets/
1063
+ macrosheets/
1064
+ xl/chartsheets/
1065
+ xl/dialogsheets/
1066
+ xl/macrosheets/
1067
+ ].freeze
1068
+ private_constant :NON_WORKSHEET_REL_SUFFIXES, :NON_WORKSHEET_TARGET_PREFIXES
1069
+
1045
1070
  def discover_sheets
1046
- workbook_xml = extract_zip_entry("xl/workbook.xml")
1071
+ wb_path = resolve_workbook_entry
1072
+ workbook_xml = extract_zip_entry(wb_path)
1047
1073
  return [{ name: "Sheet1", rid: "rId1", target: "worksheets/sheet1.xml" }] if workbook_xml.nil? || workbook_xml.empty?
1048
1074
 
1049
- rels_xml = extract_zip_entry("xl/_rels/workbook.xml.rels")
1050
- rid_to_target = parse_rels(rels_xml)
1075
+ wb_dir = File.dirname(wb_path)
1076
+ rels_path = wb_dir == "." ? "_rels/#{File.basename(wb_path)}.rels" : "#{wb_dir}/_rels/#{File.basename(wb_path)}.rels"
1077
+ rels_xml = extract_zip_entry(rels_path)
1078
+ rel_entries = parse_rels_with_types(rels_xml)
1079
+ rid_to_target = {}
1080
+ rid_to_type = {}
1081
+ rel_entries.each do |r|
1082
+ rid_to_target[r[:id]] = r[:target]
1083
+ rid_to_type[r[:id]] = r[:type]
1084
+ end
1051
1085
 
1052
1086
  sheets = []
1053
1087
  parser = REXML::Parsers::SAX2Parser.new(workbook_xml)
@@ -1055,13 +1089,30 @@ module Xlsxrb
1055
1089
  parser.listen(listener)
1056
1090
  parser.parse
1057
1091
 
1092
+ ws_idx = 0
1058
1093
  listener.sheets.each do |s|
1059
- target = rid_to_target[s[:rid]]
1060
- sheets << { name: s[:name], rid: s[:rid], target: target, state: s[:state] } if target
1094
+ raw_target = rid_to_target[s[:rid]]
1095
+ rel_type = rid_to_type[s[:rid]]
1096
+ next if non_worksheet_relationship?(rel_type, raw_target)
1097
+
1098
+ target = raw_target || (wb_dir == "." ? "worksheets/sheet#{ws_idx + 1}.xml" : "#{wb_dir}/worksheets/sheet#{ws_idx + 1}.xml")
1099
+ ws_idx += 1
1100
+ sheets << { name: s[:name], rid: s[:rid], target: target, state: s[:state] }
1061
1101
  end
1062
1102
  sheets
1063
1103
  end
1064
1104
 
1105
+ def non_worksheet_relationship?(rel_type, target_path)
1106
+ return true if rel_type && NON_WORKSHEET_REL_SUFFIXES.any? { |suffix| rel_type.end_with?(suffix) }
1107
+
1108
+ if target_path
1109
+ clean_target = target_path.delete_prefix("/")
1110
+ return true if NON_WORKSHEET_TARGET_PREFIXES.any? { |prefix| clean_target.start_with?(prefix) }
1111
+ end
1112
+
1113
+ false
1114
+ end
1115
+
1065
1116
  def parse_rels(rels_xml)
1066
1117
  return {} if rels_xml.nil? || rels_xml.empty?
1067
1118
 
@@ -1098,14 +1149,23 @@ module Xlsxrb
1098
1149
  end
1099
1150
 
1100
1151
  def load_shared_strings
1101
- sst_xml = extract_zip_entry("xl/sharedStrings.xml")
1102
- return [] if sst_xml.nil? || sst_xml.empty?
1103
-
1104
- parser = REXML::Parsers::SAX2Parser.new(sst_xml)
1105
- listener = SharedStringsListener.new
1106
- parser.listen(listener)
1107
- parser.parse
1108
- listener.strings
1152
+ wb_dir = File.dirname(resolve_workbook_entry)
1153
+ candidates = [
1154
+ wb_dir == "." ? "sharedStrings.xml" : "#{wb_dir}/sharedStrings.xml",
1155
+ "xl/sharedStrings.xml",
1156
+ "sharedStrings.xml"
1157
+ ]
1158
+ candidates.each do |c|
1159
+ sst_xml = extract_zip_entry(c)
1160
+ next if sst_xml.nil? || sst_xml.empty?
1161
+
1162
+ parser = REXML::Parsers::SAX2Parser.new(sst_xml)
1163
+ listener = SharedStringsListener.new
1164
+ parser.listen(listener)
1165
+ parser.parse
1166
+ return listener.strings
1167
+ end
1168
+ []
1109
1169
  end
1110
1170
 
1111
1171
  def resolve_sheet_index(sheet)
@@ -1199,21 +1259,30 @@ module Xlsxrb
1199
1259
  end
1200
1260
 
1201
1261
  def load_styles
1202
- styles_xml = extract_zip_entry("xl/styles.xml")
1203
- return {} if styles_xml.nil? || styles_xml.empty?
1204
-
1205
- parser = REXML::Parsers::SAX2Parser.new(styles_xml)
1206
- listener = StylesListener.new
1207
- parser.listen(listener)
1208
- parser.parse
1209
- {
1210
- num_fmts: listener.num_fmts, cell_xfs: listener.cell_xfs,
1211
- cell_style_xfs: listener.cell_style_xfs, cell_styles: listener.cell_styles,
1212
- fonts: listener.fonts, fills: listener.fills,
1213
- borders: listener.borders, dxfs: listener.dxfs,
1214
- indexed_colors: listener.indexed_colors, mru_colors: listener.mru_colors,
1215
- table_styles: listener.table_styles
1216
- }
1262
+ wb_dir = File.dirname(resolve_workbook_entry)
1263
+ candidates = [
1264
+ wb_dir == "." ? "styles.xml" : "#{wb_dir}/styles.xml",
1265
+ "xl/styles.xml",
1266
+ "styles.xml"
1267
+ ]
1268
+ candidates.each do |c|
1269
+ styles_xml = extract_zip_entry(c)
1270
+ next if styles_xml.nil? || styles_xml.empty?
1271
+
1272
+ parser = REXML::Parsers::SAX2Parser.new(styles_xml)
1273
+ listener = StylesListener.new
1274
+ parser.listen(listener)
1275
+ parser.parse
1276
+ return {
1277
+ num_fmts: listener.num_fmts, cell_xfs: listener.cell_xfs,
1278
+ cell_style_xfs: listener.cell_style_xfs, cell_styles: listener.cell_styles,
1279
+ fonts: listener.fonts, fills: listener.fills,
1280
+ borders: listener.borders, dxfs: listener.dxfs,
1281
+ indexed_colors: listener.indexed_colors, mru_colors: listener.mru_colors,
1282
+ table_styles: listener.table_styles
1283
+ }
1284
+ end
1285
+ {}
1217
1286
  end
1218
1287
 
1219
1288
  def parse_cell_style_indices(xml)
@@ -1226,17 +1295,21 @@ module Xlsxrb
1226
1295
 
1227
1296
  def resolve_date_cells(raw_cells, cell_style_map, styles)
1228
1297
  is_date1904 = workbook_properties&.[](:date1904) || false
1298
+ cell_xfs = styles.respond_to?(:cell_xfs) ? styles.cell_xfs : styles[:cell_xfs]
1299
+ cell_style_xfs = styles.respond_to?(:cell_style_xfs) ? styles.cell_style_xfs : styles[:cell_style_xfs]
1300
+ n_fmts = styles.respond_to?(:num_fmts) ? styles.num_fmts : styles[:num_fmts]
1301
+
1229
1302
  raw_cells.each do |cell_ref, value|
1230
1303
  next unless value.is_a?(Numeric)
1231
1304
 
1232
1305
  xf_index = cell_style_map[cell_ref]
1233
1306
  next unless xf_index
1234
1307
 
1235
- xf = resolve_effective_xf(styles[:cell_xfs][xf_index], styles[:cell_style_xfs])
1308
+ xf = resolve_effective_xf(cell_xfs ? cell_xfs[xf_index] : nil, cell_style_xfs)
1236
1309
  next unless xf
1237
1310
 
1238
1311
  fmt_id = xf[:num_fmt_id]
1239
- next unless date_format?(fmt_id, styles[:num_fmts])
1312
+ next unless date_format?(fmt_id, n_fmts)
1240
1313
 
1241
1314
  raw_cells[cell_ref] = if value.is_a?(Float) && (value % 1).positive?
1242
1315
  Xlsxrb::Ooxml::Utils.serial_to_datetime(value, date1904: is_date1904)
@@ -1285,51 +1358,7 @@ module Xlsxrb
1285
1358
  end
1286
1359
 
1287
1360
  def extract_zip_entry(entry_name)
1288
- File.open(@filepath, "rb") do |file|
1289
- loop do
1290
- signature = file.read(4)
1291
- break if signature.nil? || signature.bytesize < 4
1292
-
1293
- signature_value = signature.unpack1("V")
1294
- break if [0x02014b50, 0x06054b50].include?(signature_value)
1295
-
1296
- raise ZipError, "invalid ZIP local header signature" unless signature_value == 0x04034b50
1297
-
1298
- header = file.read(26)
1299
- raise ZipError, "truncated ZIP local header" if header.nil? || header.bytesize < 26
1300
-
1301
- _version, flags, compression_method, _mtime, _mdate, _crc32, compressed_size,
1302
- _uncompressed_size, file_name_length, extra_field_length = header.unpack("v v v v v V V V v v")
1303
-
1304
- raise ZipError, "ZIP data descriptor is not supported" if flags.anybits?(0x0008)
1305
-
1306
- file_name = file.read(file_name_length)
1307
- raise ZipError, "truncated ZIP file name" if file_name.nil? || file_name.bytesize < file_name_length
1308
-
1309
- file.read(extra_field_length)
1310
-
1311
- compressed_data = file.read(compressed_size)
1312
- raise ZipError, "truncated ZIP entry data" if compressed_data.nil? || compressed_data.bytesize < compressed_size
1313
-
1314
- next unless file_name == entry_name
1315
-
1316
- case compression_method
1317
- when 0
1318
- return compressed_data
1319
- when 8
1320
- inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
1321
- begin
1322
- return inflater.inflate(compressed_data)
1323
- ensure
1324
- inflater.close
1325
- end
1326
- else
1327
- raise ZipError, "unsupported ZIP compression method: #{compression_method}"
1328
- end
1329
- end
1330
- end
1331
-
1332
- nil
1361
+ ZipReader.open(@filepath) { |zr| zr.read_entry(entry_name) }
1333
1362
  end
1334
1363
 
1335
1364
  def parse_worksheet_cells(xml, shared_strings)