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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +22 -3
- data/benchmark/micro_ips.rb +70 -0
- data/lib/xlsxrb/elements/styles.rb +427 -15
- data/lib/xlsxrb/elements/workbook.rb +31 -4
- data/lib/xlsxrb/elements/worksheet.rb +22 -7
- data/lib/xlsxrb/ooxml/cfb.rb +10 -0
- data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +56 -38
- data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +7 -1
- data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +12 -7
- data/lib/xlsxrb/ooxml/reader.rb +165 -136
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +83 -28
- data/lib/xlsxrb/ooxml/styles_parser.rb +57 -14
- data/lib/xlsxrb/ooxml/utils.rb +59 -2
- data/lib/xlsxrb/ooxml/workbook_parser.rb +42 -3
- data/lib/xlsxrb/ooxml/workbook_writer.rb +215 -50
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +192 -123
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +238 -108
- data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +16 -12
- data/lib/xlsxrb/ooxml/writer/styles_xml.rb +64 -15
- data/lib/xlsxrb/ooxml/writer.rb +31 -28
- data/lib/xlsxrb/ooxml/xml_builder.rb +12 -4
- data/lib/xlsxrb/ooxml/xml_parser.rb +45 -0
- data/lib/xlsxrb/ooxml/zip_reader.rb +48 -29
- data/lib/xlsxrb/stream_row.rb +4 -3
- data/lib/xlsxrb/stream_sheet.rb +26 -6
- data/lib/xlsxrb/stream_writer.rb +35 -14
- data/lib/xlsxrb/utils.rb +15 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +110 -10
- data/lib/xlsxrb/worksheet_builder.rb +15 -7
- data/lib/xlsxrb.rb +416 -108
- data/sig/generated/xlsxrb/elements/styles.rbs +271 -5
- data/sig/generated/xlsxrb/elements/workbook.rbs +26 -3
- data/sig/generated/xlsxrb/elements/worksheet.rbs +12 -2
- data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +12 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +3 -1
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/utils.rbs +22 -0
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +7 -0
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +12 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +18 -2
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
- data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +5 -2
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +4 -1
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +6 -6
- data/sig/generated/xlsxrb/stream_row.rbs +3 -2
- data/sig/generated/xlsxrb/stream_sheet.rbs +21 -5
- data/sig/generated/xlsxrb/stream_writer.rbs +18 -11
- data/sig/generated/xlsxrb/utils.rbs +13 -0
- data/sig/generated/xlsxrb/worksheet_builder.rbs +3 -2
- data/sig/generated/xlsxrb.rbs +47 -10
- metadata +2 -1
data/lib/xlsxrb/ooxml/reader.rb
CHANGED
|
@@ -74,18 +74,7 @@ module Xlsxrb
|
|
|
74
74
|
|
|
75
75
|
# Returns hyperlinks as { "A1" => "https://example.com" }.
|
|
76
76
|
def hyperlinks(sheet: nil)
|
|
77
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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] =
|
|
161
|
-
entry[:fill] =
|
|
162
|
-
entry[:border] =
|
|
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],
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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)
|