xlsxrb 0.1.14 → 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 +108 -0
- data/README.md +37 -17
- data/benchmark/micro_ips.rb +70 -0
- data/lib/xlsxrb/elements/styles.rb +427 -15
- data/lib/xlsxrb/elements/workbook.rb +67 -4
- data/lib/xlsxrb/elements/worksheet.rb +117 -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 +14 -2
- data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +12 -7
- data/lib/xlsxrb/ooxml/reader.rb +167 -139
- 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 +63 -4
- data/lib/xlsxrb/ooxml/workbook_writer.rb +232 -59
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +192 -123
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +307 -125
- data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +16 -12
- data/lib/xlsxrb/ooxml/writer/features_xml.rb +1 -1
- 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 +22 -2
- data/lib/xlsxrb/ooxml/xml_parser.rb +45 -0
- data/lib/xlsxrb/ooxml/zip_reader.rb +83 -32
- data/lib/xlsxrb/stream_row.rb +4 -3
- data/lib/xlsxrb/stream_sheet.rb +259 -7
- data/lib/xlsxrb/stream_writer.rb +197 -31
- data/lib/xlsxrb/utils.rb +68 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +110 -19
- data/lib/xlsxrb/worksheet_builder.rb +526 -15
- data/lib/xlsxrb.rb +889 -90
- data/sig/generated/xlsxrb/elements/styles.rbs +271 -5
- data/sig/generated/xlsxrb/elements/workbook.rbs +54 -3
- data/sig/generated/xlsxrb/elements/worksheet.rbs +63 -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 +13 -1
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +18 -2
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +13 -3
- data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +5 -2
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +6 -1
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +9 -6
- data/sig/generated/xlsxrb/stream_row.rbs +3 -2
- data/sig/generated/xlsxrb/stream_sheet.rbs +132 -6
- data/sig/generated/xlsxrb/stream_writer.rbs +118 -27
- data/sig/generated/xlsxrb/utils.rbs +45 -0
- data/sig/generated/xlsxrb/worksheet_builder.rbs +252 -9
- data/sig/generated/xlsxrb.rbs +84 -10
- metadata +2 -1
data/lib/xlsxrb/ooxml/reader.rb
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
require "zlib"
|
|
6
6
|
require "rexml/parsers/sax2parser"
|
|
7
7
|
require "rexml/sax2listener"
|
|
8
|
+
require_relative "../number_formatter"
|
|
8
9
|
|
|
9
10
|
module Xlsxrb
|
|
10
11
|
module Ooxml
|
|
@@ -73,18 +74,7 @@ module Xlsxrb
|
|
|
73
74
|
|
|
74
75
|
# Returns hyperlinks as { "A1" => "https://example.com" }.
|
|
75
76
|
def hyperlinks(sheet: nil)
|
|
76
|
-
|
|
77
|
-
raise ArgumentError, "workbook has no sheets" if sheets.empty?
|
|
78
|
-
|
|
79
|
-
target = resolve_sheet_target(sheets, sheet)
|
|
80
|
-
raise ArgumentError, "sheet not found: #{sheet.inspect}" if target.nil?
|
|
81
|
-
|
|
82
|
-
entry_path = if target.start_with?("/")
|
|
83
|
-
target.delete_prefix("/")
|
|
84
|
-
else
|
|
85
|
-
"xl/#{target}"
|
|
86
|
-
end
|
|
87
|
-
|
|
77
|
+
entry_path = resolve_sheet_entry_path(sheet)
|
|
88
78
|
worksheet_xml = extract_zip_entry(entry_path)
|
|
89
79
|
return {} if worksheet_xml.nil? || worksheet_xml.empty?
|
|
90
80
|
|
|
@@ -128,14 +118,17 @@ module Xlsxrb
|
|
|
128
118
|
parser.parse
|
|
129
119
|
|
|
130
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]
|
|
131
124
|
listener.cell_style_indices.each do |cell_ref, xf_index|
|
|
132
|
-
xf = resolve_effective_xf(
|
|
125
|
+
xf = resolve_effective_xf(cell_xfs ? cell_xfs[xf_index] : nil, cell_style_xfs)
|
|
133
126
|
next unless xf
|
|
134
127
|
|
|
135
128
|
fmt_id = xf[:num_fmt_id]
|
|
136
129
|
next unless fmt_id && fmt_id != 0
|
|
137
130
|
|
|
138
|
-
format_code = resolve_num_fmt_code(fmt_id,
|
|
131
|
+
format_code = resolve_num_fmt_code(fmt_id, n_fmts)
|
|
139
132
|
result[cell_ref] = format_code if format_code
|
|
140
133
|
end
|
|
141
134
|
result
|
|
@@ -151,16 +144,23 @@ module Xlsxrb
|
|
|
151
144
|
|
|
152
145
|
indices = parse_cell_style_indices(worksheet_xml)
|
|
153
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
|
+
|
|
154
154
|
indices.each do |cell_ref, xf_index|
|
|
155
|
-
xf = resolve_effective_xf(
|
|
155
|
+
xf = resolve_effective_xf(cell_xfs ? cell_xfs[xf_index] : nil, cell_style_xfs)
|
|
156
156
|
next unless xf
|
|
157
157
|
|
|
158
158
|
entry = {}
|
|
159
|
-
entry[:font] =
|
|
160
|
-
entry[:fill] =
|
|
161
|
-
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]]
|
|
162
162
|
if xf[:num_fmt_id]&.positive?
|
|
163
|
-
code = resolve_num_fmt_code(xf[:num_fmt_id],
|
|
163
|
+
code = resolve_num_fmt_code(xf[:num_fmt_id], n_fmts)
|
|
164
164
|
entry[:num_fmt] = code if code
|
|
165
165
|
end
|
|
166
166
|
entry[:alignment] = xf[:alignment] if xf[:alignment]
|
|
@@ -177,7 +177,7 @@ module Xlsxrb
|
|
|
177
177
|
styles = load_styles
|
|
178
178
|
return [] if styles.empty?
|
|
179
179
|
|
|
180
|
-
styles[:dxfs] || []
|
|
180
|
+
styles.respond_to?(:dxfs) ? styles.dxfs : (styles[:dxfs] || [])
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
# Returns array of font entries from the styles.
|
|
@@ -185,7 +185,7 @@ module Xlsxrb
|
|
|
185
185
|
styles = load_styles
|
|
186
186
|
return [] if styles.empty?
|
|
187
187
|
|
|
188
|
-
styles[:fonts] || []
|
|
188
|
+
styles.respond_to?(:fonts) ? styles.fonts : (styles[:fonts] || [])
|
|
189
189
|
end
|
|
190
190
|
|
|
191
191
|
# Returns array of fill entries from the styles.
|
|
@@ -193,7 +193,7 @@ module Xlsxrb
|
|
|
193
193
|
styles = load_styles
|
|
194
194
|
return [] if styles.empty?
|
|
195
195
|
|
|
196
|
-
styles[:fills] || []
|
|
196
|
+
styles.respond_to?(:fills) ? styles.fills : (styles[:fills] || [])
|
|
197
197
|
end
|
|
198
198
|
|
|
199
199
|
# Returns array of border entries from the styles.
|
|
@@ -201,7 +201,7 @@ module Xlsxrb
|
|
|
201
201
|
styles = load_styles
|
|
202
202
|
return [] if styles.empty?
|
|
203
203
|
|
|
204
|
-
styles[:borders] || []
|
|
204
|
+
styles.respond_to?(:borders) ? styles.borders : (styles[:borders] || [])
|
|
205
205
|
end
|
|
206
206
|
|
|
207
207
|
# Returns custom number formats as { numFmtId => formatCode }.
|
|
@@ -209,7 +209,7 @@ module Xlsxrb
|
|
|
209
209
|
styles = load_styles
|
|
210
210
|
return {} if styles.empty?
|
|
211
211
|
|
|
212
|
-
styles[:num_fmts] || {}
|
|
212
|
+
styles.respond_to?(:num_fmts) ? styles.num_fmts : (styles[:num_fmts] || {})
|
|
213
213
|
end
|
|
214
214
|
|
|
215
215
|
# Returns indexed colors palette (array of ARGB hex strings).
|
|
@@ -217,7 +217,7 @@ module Xlsxrb
|
|
|
217
217
|
styles = load_styles
|
|
218
218
|
return [] if styles.empty?
|
|
219
219
|
|
|
220
|
-
styles[:indexed_colors] || []
|
|
220
|
+
styles.respond_to?(:indexed_colors) ? styles.indexed_colors : (styles[:indexed_colors] || [])
|
|
221
221
|
end
|
|
222
222
|
|
|
223
223
|
# Returns MRU (most recently used) colors (array of color hashes).
|
|
@@ -225,7 +225,7 @@ module Xlsxrb
|
|
|
225
225
|
styles = load_styles
|
|
226
226
|
return [] if styles.empty?
|
|
227
227
|
|
|
228
|
-
styles[:mru_colors] || []
|
|
228
|
+
styles.respond_to?(:mru_colors) ? styles.mru_colors : (styles[:mru_colors] || [])
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
# Returns table styles configuration hash.
|
|
@@ -233,7 +233,7 @@ module Xlsxrb
|
|
|
233
233
|
styles = load_styles
|
|
234
234
|
return {} if styles.empty?
|
|
235
235
|
|
|
236
|
-
styles[:table_styles] || {}
|
|
236
|
+
styles.respond_to?(:table_styles) ? styles.table_styles : (styles[:table_styles] || {})
|
|
237
237
|
end
|
|
238
238
|
|
|
239
239
|
# Returns array of cellStyleXfs entries (base style format definitions).
|
|
@@ -241,7 +241,7 @@ module Xlsxrb
|
|
|
241
241
|
styles = load_styles
|
|
242
242
|
return [] if styles.empty?
|
|
243
243
|
|
|
244
|
-
styles[:cell_style_xfs] || []
|
|
244
|
+
styles.respond_to?(:cell_style_xfs) ? styles.cell_style_xfs : (styles[:cell_style_xfs] || [])
|
|
245
245
|
end
|
|
246
246
|
|
|
247
247
|
# Returns array of cellXfs entries (cell format definitions).
|
|
@@ -249,7 +249,7 @@ module Xlsxrb
|
|
|
249
249
|
styles = load_styles
|
|
250
250
|
return [] if styles.empty?
|
|
251
251
|
|
|
252
|
-
styles[:cell_xfs] || []
|
|
252
|
+
styles.respond_to?(:cell_xfs) ? styles.cell_xfs : (styles[:cell_xfs] || [])
|
|
253
253
|
end
|
|
254
254
|
|
|
255
255
|
# Returns array of named cell styles (cellStyle elements).
|
|
@@ -257,7 +257,7 @@ module Xlsxrb
|
|
|
257
257
|
styles = load_styles
|
|
258
258
|
return [] if styles.empty?
|
|
259
259
|
|
|
260
|
-
styles[:cell_styles] || []
|
|
260
|
+
styles.respond_to?(:cell_styles) ? styles.cell_styles : (styles[:cell_styles] || [])
|
|
261
261
|
end
|
|
262
262
|
|
|
263
263
|
# Returns the autoFilter range string (e.g. "A1:B10") or nil.
|
|
@@ -644,34 +644,15 @@ module Xlsxrb
|
|
|
644
644
|
|
|
645
645
|
# Returns all ZIP entry paths in the file.
|
|
646
646
|
def entry_names
|
|
647
|
-
|
|
648
|
-
File.open(@filepath, "rb") do |file|
|
|
649
|
-
loop do
|
|
650
|
-
sig = file.read(4)
|
|
651
|
-
break if sig.nil? || sig.bytesize < 4
|
|
652
|
-
|
|
653
|
-
sig_val = sig.unpack1("V")
|
|
654
|
-
break if [0x02014b50, 0x06054b50].include?(sig_val)
|
|
655
|
-
break unless sig_val == 0x04034b50
|
|
656
|
-
|
|
657
|
-
header = file.read(26)
|
|
658
|
-
break if header.nil? || header.bytesize < 26
|
|
659
|
-
|
|
660
|
-
_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")
|
|
661
|
-
break if flags.anybits?(0x0008)
|
|
662
|
-
|
|
663
|
-
fname = file.read(fname_len)
|
|
664
|
-
file.read(extra_len)
|
|
665
|
-
file.read(comp_size)
|
|
666
|
-
names << fname
|
|
667
|
-
end
|
|
668
|
-
end
|
|
669
|
-
names
|
|
647
|
+
ZipReader.open(@filepath, &:entry_names)
|
|
670
648
|
end
|
|
671
649
|
|
|
672
650
|
# Returns raw bytes for a ZIP entry by path.
|
|
673
651
|
def raw_entry(name)
|
|
674
|
-
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
|
|
675
656
|
end
|
|
676
657
|
|
|
677
658
|
# Returns true if the file contains VBA macros (vbaProject.bin).
|
|
@@ -971,10 +952,14 @@ module Xlsxrb
|
|
|
971
952
|
|
|
972
953
|
STRICT_SSML_NS = "http://purl.oclc.org/ooxml/spreadsheetml/main/2006/main"
|
|
973
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
|
|
974
959
|
|
|
975
960
|
# Returns :strict or :transitional based on the namespace of the workbook XML.
|
|
976
961
|
def format_variant
|
|
977
|
-
workbook_xml = extract_zip_entry(
|
|
962
|
+
workbook_xml = extract_zip_entry(resolve_workbook_entry)
|
|
978
963
|
return :transitional if workbook_xml.nil? || workbook_xml.empty?
|
|
979
964
|
|
|
980
965
|
if workbook_xml.include?(STRICT_SSML_NS)
|
|
@@ -986,6 +971,22 @@ module Xlsxrb
|
|
|
986
971
|
|
|
987
972
|
private
|
|
988
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
|
+
|
|
989
990
|
def resolve_sheet_for_defined_name(sheet)
|
|
990
991
|
sheets = discover_sheets
|
|
991
992
|
if sheet
|
|
@@ -1001,7 +1002,7 @@ module Xlsxrb
|
|
|
1001
1002
|
end
|
|
1002
1003
|
|
|
1003
1004
|
def parse_workbook_metadata
|
|
1004
|
-
workbook_xml = extract_zip_entry(
|
|
1005
|
+
workbook_xml = extract_zip_entry(resolve_workbook_entry)
|
|
1005
1006
|
if workbook_xml.nil? || workbook_xml.empty?
|
|
1006
1007
|
return { workbook_properties: {}, workbook_views: {}, calc_properties: {}, file_recovery_properties: {},
|
|
1007
1008
|
workbook_protection: nil }
|
|
@@ -1024,29 +1025,63 @@ module Xlsxrb
|
|
|
1024
1025
|
}
|
|
1025
1026
|
end
|
|
1026
1027
|
|
|
1027
|
-
def
|
|
1028
|
+
def resolve_sheet_entry_path(sheet)
|
|
1028
1029
|
sheets = discover_sheets
|
|
1029
1030
|
raise ArgumentError, "workbook has no sheets" if sheets.empty?
|
|
1030
1031
|
|
|
1031
1032
|
target = resolve_sheet_target(sheets, sheet)
|
|
1032
1033
|
raise ArgumentError, "sheet not found: #{sheet.inspect}" if target.nil?
|
|
1033
1034
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
target.delete_prefix("/")
|
|
1035
|
+
wb_dir = File.dirname(resolve_workbook_entry)
|
|
1036
|
+
candidates = if target.start_with?("/")
|
|
1037
|
+
[target.delete_prefix("/")]
|
|
1037
1038
|
else
|
|
1038
|
-
|
|
1039
|
+
[
|
|
1040
|
+
wb_dir == "." ? target : "#{wb_dir}/#{target}",
|
|
1041
|
+
"xl/#{target}",
|
|
1042
|
+
target
|
|
1043
|
+
]
|
|
1039
1044
|
end
|
|
1045
|
+
candidates.find { |c| extract_zip_entry(c) } || candidates.first
|
|
1046
|
+
end
|
|
1040
1047
|
|
|
1048
|
+
def load_worksheet_xml(sheet)
|
|
1049
|
+
entry_path = resolve_sheet_entry_path(sheet)
|
|
1041
1050
|
extract_zip_entry(entry_path)
|
|
1042
1051
|
end
|
|
1043
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
|
+
|
|
1044
1070
|
def discover_sheets
|
|
1045
|
-
|
|
1071
|
+
wb_path = resolve_workbook_entry
|
|
1072
|
+
workbook_xml = extract_zip_entry(wb_path)
|
|
1046
1073
|
return [{ name: "Sheet1", rid: "rId1", target: "worksheets/sheet1.xml" }] if workbook_xml.nil? || workbook_xml.empty?
|
|
1047
1074
|
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
|
1050
1085
|
|
|
1051
1086
|
sheets = []
|
|
1052
1087
|
parser = REXML::Parsers::SAX2Parser.new(workbook_xml)
|
|
@@ -1054,13 +1089,30 @@ module Xlsxrb
|
|
|
1054
1089
|
parser.listen(listener)
|
|
1055
1090
|
parser.parse
|
|
1056
1091
|
|
|
1092
|
+
ws_idx = 0
|
|
1057
1093
|
listener.sheets.each do |s|
|
|
1058
|
-
|
|
1059
|
-
|
|
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] }
|
|
1060
1101
|
end
|
|
1061
1102
|
sheets
|
|
1062
1103
|
end
|
|
1063
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
|
+
|
|
1064
1116
|
def parse_rels(rels_xml)
|
|
1065
1117
|
return {} if rels_xml.nil? || rels_xml.empty?
|
|
1066
1118
|
|
|
@@ -1097,14 +1149,23 @@ module Xlsxrb
|
|
|
1097
1149
|
end
|
|
1098
1150
|
|
|
1099
1151
|
def load_shared_strings
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
+
[]
|
|
1108
1169
|
end
|
|
1109
1170
|
|
|
1110
1171
|
def resolve_sheet_index(sheet)
|
|
@@ -1198,21 +1259,30 @@ module Xlsxrb
|
|
|
1198
1259
|
end
|
|
1199
1260
|
|
|
1200
1261
|
def load_styles
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
+
{}
|
|
1216
1286
|
end
|
|
1217
1287
|
|
|
1218
1288
|
def parse_cell_style_indices(xml)
|
|
@@ -1225,17 +1295,21 @@ module Xlsxrb
|
|
|
1225
1295
|
|
|
1226
1296
|
def resolve_date_cells(raw_cells, cell_style_map, styles)
|
|
1227
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
|
+
|
|
1228
1302
|
raw_cells.each do |cell_ref, value|
|
|
1229
1303
|
next unless value.is_a?(Numeric)
|
|
1230
1304
|
|
|
1231
1305
|
xf_index = cell_style_map[cell_ref]
|
|
1232
1306
|
next unless xf_index
|
|
1233
1307
|
|
|
1234
|
-
xf = resolve_effective_xf(
|
|
1308
|
+
xf = resolve_effective_xf(cell_xfs ? cell_xfs[xf_index] : nil, cell_style_xfs)
|
|
1235
1309
|
next unless xf
|
|
1236
1310
|
|
|
1237
1311
|
fmt_id = xf[:num_fmt_id]
|
|
1238
|
-
next unless date_format?(fmt_id,
|
|
1312
|
+
next unless date_format?(fmt_id, n_fmts)
|
|
1239
1313
|
|
|
1240
1314
|
raw_cells[cell_ref] = if value.is_a?(Float) && (value % 1).positive?
|
|
1241
1315
|
Xlsxrb::Ooxml::Utils.serial_to_datetime(value, date1904: is_date1904)
|
|
@@ -1280,57 +1354,11 @@ module Xlsxrb
|
|
|
1280
1354
|
end
|
|
1281
1355
|
|
|
1282
1356
|
def date_pattern?(code)
|
|
1283
|
-
|
|
1284
|
-
stripped = code.gsub(/"[^"]*"/, "").gsub(/\\[.]/, "")
|
|
1285
|
-
stripped.match?(/[ymdhsYMDHS]/)
|
|
1357
|
+
NumberFormatter.date_format?(code)
|
|
1286
1358
|
end
|
|
1287
1359
|
|
|
1288
1360
|
def extract_zip_entry(entry_name)
|
|
1289
|
-
|
|
1290
|
-
loop do
|
|
1291
|
-
signature = file.read(4)
|
|
1292
|
-
break if signature.nil? || signature.bytesize < 4
|
|
1293
|
-
|
|
1294
|
-
signature_value = signature.unpack1("V")
|
|
1295
|
-
break if [0x02014b50, 0x06054b50].include?(signature_value)
|
|
1296
|
-
|
|
1297
|
-
raise ZipError, "invalid ZIP local header signature" unless signature_value == 0x04034b50
|
|
1298
|
-
|
|
1299
|
-
header = file.read(26)
|
|
1300
|
-
raise ZipError, "truncated ZIP local header" if header.nil? || header.bytesize < 26
|
|
1301
|
-
|
|
1302
|
-
_version, flags, compression_method, _mtime, _mdate, _crc32, compressed_size,
|
|
1303
|
-
_uncompressed_size, file_name_length, extra_field_length = header.unpack("v v v v v V V V v v")
|
|
1304
|
-
|
|
1305
|
-
raise ZipError, "ZIP data descriptor is not supported" if flags.anybits?(0x0008)
|
|
1306
|
-
|
|
1307
|
-
file_name = file.read(file_name_length)
|
|
1308
|
-
raise ZipError, "truncated ZIP file name" if file_name.nil? || file_name.bytesize < file_name_length
|
|
1309
|
-
|
|
1310
|
-
file.read(extra_field_length)
|
|
1311
|
-
|
|
1312
|
-
compressed_data = file.read(compressed_size)
|
|
1313
|
-
raise ZipError, "truncated ZIP entry data" if compressed_data.nil? || compressed_data.bytesize < compressed_size
|
|
1314
|
-
|
|
1315
|
-
next unless file_name == entry_name
|
|
1316
|
-
|
|
1317
|
-
case compression_method
|
|
1318
|
-
when 0
|
|
1319
|
-
return compressed_data
|
|
1320
|
-
when 8
|
|
1321
|
-
inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
1322
|
-
begin
|
|
1323
|
-
return inflater.inflate(compressed_data)
|
|
1324
|
-
ensure
|
|
1325
|
-
inflater.close
|
|
1326
|
-
end
|
|
1327
|
-
else
|
|
1328
|
-
raise ZipError, "unsupported ZIP compression method: #{compression_method}"
|
|
1329
|
-
end
|
|
1330
|
-
end
|
|
1331
|
-
end
|
|
1332
|
-
|
|
1333
|
-
nil
|
|
1361
|
+
ZipReader.open(@filepath) { |zr| zr.read_entry(entry_name) }
|
|
1334
1362
|
end
|
|
1335
1363
|
|
|
1336
1364
|
def parse_worksheet_cells(xml, shared_strings)
|