write_xlsx 1.11.1 → 1.12.0

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.
@@ -0,0 +1,103 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'write_xlsx/package/xml_writer_simple'
5
+ require 'write_xlsx/utility'
6
+
7
+ module Writexlsx
8
+ module Package
9
+ #
10
+ # RichValueTypes - A class for writing the Excel XLSX rdRichValueTypes.xml file.
11
+ #
12
+ # Used in conjunction with Excel::Writer::XLSX
13
+ #
14
+ # Copyright 2000-2024, John McNamara, jmcnamara@cpan.org
15
+ #
16
+ # Convert to Ruby by Hideo NAKAMURA, nakamura.hideo@gmail.com
17
+ #
18
+ class RichValueTypes
19
+ include Writexlsx::Utility
20
+
21
+ def initialize
22
+ @writer = Package::XMLWriterSimple.new
23
+ end
24
+
25
+ def set_xml_writer(filename)
26
+ @writer.set_xml_writer(filename)
27
+ end
28
+
29
+ def assemble_xml_file
30
+ write_xml_declaration do
31
+ write_rv_types_info
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ #
38
+ # Write the <rvTypesInfo> element.
39
+ #
40
+ def write_rv_types_info
41
+ xmlns = 'http://schemas.microsoft.com/office/spreadsheetml/2017/richdata2'
42
+ xmlns_mc = 'http://schemas.openxmlformats.org/markup-compatibility/2006'
43
+ xmlns_x = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
44
+
45
+ attributes = [
46
+ ['xmlns', xmlns],
47
+ ['xmlns:mc', xmlns_mc],
48
+ ['mc:Ignorable', 'x'],
49
+ ['xmlns:x', xmlns_x]
50
+ ]
51
+
52
+ key_flags = [
53
+ ['_Self', %w[ExcludeFromFile ExcludeFromCalcComparison]],
54
+ ['_DisplayString', ['ExcludeFromCalcComparison']],
55
+ ['_Flags', ['ExcludeFromCalcComparison']],
56
+ ['_Format', ['ExcludeFromCalcComparison']],
57
+ ['_SubLabel', ['ExcludeFromCalcComparison']],
58
+ ['_Attribution', ['ExcludeFromCalcComparison']],
59
+ ['_Icon', ['ExcludeFromCalcComparison']],
60
+ ['_Display', ['ExcludeFromCalcComparison']],
61
+ ['_CanonicalPropertyNames', ['ExcludeFromCalcComparison']],
62
+ ['_ClassificationId', ['ExcludeFromCalcComparison']]
63
+ ]
64
+
65
+ @writer.tag_elements('rvTypesInfo', attributes) do
66
+ @writer.tag_elements('global') do
67
+ @writer.tag_elements('keyFlags') do
68
+ # Write the keyFlags element.
69
+ key_flags.each do |key_flag|
70
+ write_key(key_flag[0], key_flag[1])
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ #
78
+ # Write the <key> element.
79
+ #
80
+ def write_key(name, flags = [])
81
+ attributes = [['name', name]]
82
+
83
+ @writer.tag_elements('key', attributes) do
84
+ flags.each do |flag|
85
+ write_flag(flag)
86
+ end
87
+ end
88
+ end
89
+
90
+ #
91
+ # Write the <flag> element.
92
+ #
93
+ def write_flag(name)
94
+ attributes = [
95
+ ['name', name],
96
+ ['value', 1]
97
+ ]
98
+
99
+ @writer.empty_tag('flag', attributes)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -12,13 +12,14 @@ module Writexlsx
12
12
  class ColumnData
13
13
  attr_reader :id
14
14
  attr_accessor :name, :format, :formula, :name_format
15
- attr_accessor :total_string, :total_function
15
+ attr_accessor :total_string, :total_function, :custom_total
16
16
 
17
17
  def initialize(id, param = {})
18
18
  @id = id
19
19
  @name = "Column#{id}"
20
20
  @total_string = ''
21
21
  @total_function = ''
22
+ @custom_total = ''
22
23
  @formula = ''
23
24
  @format = nil
24
25
  @name_format = nil
@@ -51,6 +52,7 @@ module Writexlsx
51
52
 
52
53
  add_the_table_columns
53
54
  write_the_cell_data_if_supplied
55
+ write_any_columns_formulas_after_the_user_supplied_table_data
54
56
  store_filter_cell_positions
55
57
  end
56
58
 
@@ -78,7 +80,7 @@ module Writexlsx
78
80
  # Set up the default column data.
79
81
  col_data = Package::Table::ColumnData.new(col_id + 1, @param[:columns])
80
82
 
81
- overrite_the_defaults_with_any_use_defined_values(col_id, col_data, col_num)
83
+ overwrite_the_defaults_with_any_use_defined_values(col_id, col_data, col_num)
82
84
 
83
85
  # Store the column data.
84
86
  @columns << col_data
@@ -89,7 +91,7 @@ module Writexlsx
89
91
  end # Table columns.
90
92
  end
91
93
 
92
- def overrite_the_defaults_with_any_use_defined_values(col_id, col_data, col_num)
94
+ def overwrite_the_defaults_with_any_use_defined_values(col_id, col_data, col_num)
93
95
  # Check if there are user defined values for this column.
94
96
  if @param[:columns] && (user_data = @param[:columns][col_id])
95
97
  # Map user defined values to internal values.
@@ -156,6 +158,23 @@ module Writexlsx
156
158
  end
157
159
  end
158
160
 
161
+ def write_any_columns_formulas_after_the_user_supplied_table_data
162
+ col_id = 0
163
+
164
+ (@col1..@col2).each do |col|
165
+ column_data = @columns[col_id]
166
+ if ptrue?(column_data) && ptrue?(column_data.formula)
167
+ formula_format = @col_formats[col_id]
168
+ formula = column_data.formula
169
+
170
+ (@first_data_row..@last_data_row).each do |row|
171
+ @worksheet.write_formula(row, col, formula, formula_format)
172
+ end
173
+ end
174
+ col_id += 1
175
+ end
176
+ end
177
+
159
178
  def store_filter_cell_positions
160
179
  if ptrue?(@param[:autofilter])
161
180
  (@col1..@col2).each do |col|
@@ -234,25 +253,45 @@ module Writexlsx
234
253
  ]
235
254
  end
236
255
 
237
- def handle_the_column_formula(col_data, col_num, formula, format)
256
+ def handle_the_column_formula(col_data, _col_num, formula, _format)
238
257
  return unless formula
239
258
 
240
- col_data.formula = formula.sub(/^=/, '').gsub(/@/, '[#This Row],')
259
+ formula = formula.sub(/^=/, '').gsub(/@/, '[#This Row],')
241
260
 
242
- (@first_data_row..@last_data_row).each do |row|
243
- @worksheet.write_formula(row, col_num, col_data.formula, format)
244
- end
261
+ # Escape any future functions.
262
+ col_data.formula = @worksheet.prepare_formula(formula, 1)
263
+
264
+ # (@first_data_row..@last_data_row).each do |row|
265
+ # @worksheet.write_formula(row, col_num, col_data.formula, format)
266
+ # end
245
267
  end
246
268
 
247
269
  def handle_the_function_for_the_table_row(row2, col_data, col_num, user_data)
248
- function = user_data[:total_function].downcase.gsub(/[_\s]/, '')
270
+ formula = ''
271
+ function = user_data[:total_function]
272
+ function = 'countNums' if function == 'count_nums'
273
+ function = 'stdDev' if function == 'std_dev'
249
274
 
250
- function = 'countNums' if function == 'countnums'
251
- function = 'stdDev' if function == 'stddev'
275
+ subtotals = {
276
+ average: 101,
277
+ countNums: 102,
278
+ count: 103,
279
+ max: 104,
280
+ min: 105,
281
+ stdDev: 106,
282
+ sum: 109,
283
+ var: 110
284
+ }
252
285
 
253
- col_data.total_function = function
286
+ if subtotals[function.to_sym]
287
+ formula = table_function_to_formula(function, col_data.name)
288
+ else
289
+ formula = @worksheet.prepare_formula(function, 1)
290
+ col_data.custom_total = formula
291
+ function = 'custom'
292
+ end
254
293
 
255
- formula = table_function_to_formula(function, col_data.name)
294
+ col_data.total_function = function
256
295
  @worksheet.write_formula(row2, col_num, formula, user_data[:format], user_data[:total_value])
257
296
  end
258
297
 
@@ -260,10 +299,10 @@ module Writexlsx
260
299
  # Convert a table total function to a worksheet formula.
261
300
  #
262
301
  def table_function_to_formula(function, col_name)
263
- col_name = col_name.gsub(/'/, "''")
264
- .gsub(/#/, "'#")
265
- .gsub(/\[/, "'[")
266
- .gsub(/\]/, "']")
302
+ col_name = col_name.gsub("'", "''")
303
+ .gsub("#", "'#")
304
+ .gsub("[", "'[")
305
+ .gsub("]", "']")
267
306
 
268
307
  subtotals = {
269
308
  average: 101,
@@ -395,10 +434,16 @@ module Writexlsx
395
434
 
396
435
  attributes << [:dataDxfId, col_data.format] if col_data.format
397
436
 
398
- if ptrue?(col_data.formula)
437
+ if ptrue?(col_data.formula) || ptrue?(col_data.custom_total)
399
438
  @writer.tag_elements('tableColumn', attributes) do
400
- # Write the calculatedColumnFormula element.
401
- write_calculated_column_formula(col_data.formula)
439
+ if ptrue?(col_data.formula)
440
+ # Write the calculatedColumnFormula element.
441
+ write_calculated_column_formula(col_data.formula)
442
+ end
443
+ if ptrue?(col_data.custom_total)
444
+ # Write the totalsRowFormula element.
445
+ write_totals_row_formula(col_data.custom_total)
446
+ end
402
447
  end
403
448
  else
404
449
  @writer.empty_tag('tableColumn', attributes)
@@ -425,6 +470,15 @@ module Writexlsx
425
470
  def write_calculated_column_formula(formula)
426
471
  @writer.data_element('calculatedColumnFormula', formula)
427
472
  end
473
+
474
+ #
475
+ # _write_totals_row_formula()
476
+ #
477
+ # Write the <totalsRowFormula> element.
478
+ #
479
+ def write_totals_row_formula(formula)
480
+ @writer.data_element('totalsRowFormula', formula)
481
+ end
428
482
  end
429
483
  end
430
484
  end
@@ -29,33 +29,28 @@ module Writexlsx
29
29
  io_write(str)
30
30
  end
31
31
 
32
- def tag_elements(tag, attributes = [])
32
+ def tag_elements(tag, attributes = nil)
33
33
  start_tag(tag, attributes)
34
34
  yield
35
35
  end_tag(tag)
36
36
  end
37
37
 
38
- def tag_elements_str(tag, attributes = [])
38
+ def tag_elements_str(tag, attributes = nil)
39
39
  start_tag_str(tag, attributes) +
40
40
  yield +
41
41
  end_tag_str(tag)
42
42
  end
43
43
 
44
- def start_tag(tag, attr = [])
44
+ def start_tag(tag, attr = nil)
45
45
  io_write(start_tag_str(tag, attr))
46
46
  end
47
47
 
48
- def start_tag_str(tag, attr = [])
49
- if attr.empty?
50
- result = @tag_start_cache[tag]
51
- unless result
52
- result = "<#{tag}>"
53
- @tag_start_cache[tag] = result
54
- end
48
+ def start_tag_str(tag, attr = nil)
49
+ if attr.nil? || attr.empty?
50
+ @tag_start_cache[tag] ||= "<#{tag}>"
55
51
  else
56
- result = "<#{tag}#{key_vals(attr)}>"
52
+ "<#{tag}#{key_vals(attr)}>"
57
53
  end
58
- result
59
54
  end
60
55
 
61
56
  def end_tag(tag)
@@ -63,28 +58,15 @@ module Writexlsx
63
58
  end
64
59
 
65
60
  def end_tag_str(tag)
66
- result = @tag_end_cache[tag]
67
- unless result
68
- result = "</#{tag}>"
69
- @tag_end_cache[tag] = result
70
- end
71
- result
61
+ @tag_end_cache[tag] ||= "</#{tag}>"
72
62
  end
73
63
 
74
- def empty_tag(tag, attr = [])
64
+ def empty_tag(tag, attr = nil)
75
65
  str = "<#{tag}#{key_vals(attr)}/>"
76
66
  io_write(str)
77
67
  end
78
68
 
79
- def empty_tag_encoded(tag, attr = [])
80
- io_write(empty_tag_encoded_str(tag, attr))
81
- end
82
-
83
- def empty_tag_encoded_str(tag, attr = [])
84
- "<#{tag}#{key_vals(attr)}/>"
85
- end
86
-
87
- def data_element(tag, data, attr = [])
69
+ def data_element(tag, data, attr = nil)
88
70
  tag_elements(tag, attr) { io_write(escape_data(data)) }
89
71
  end
90
72
 
@@ -126,31 +108,37 @@ module Writexlsx
126
108
 
127
109
  private
128
110
 
129
- def key_val(key, val)
130
- %( #{key}="#{val}")
131
- end
132
-
133
111
  def key_vals(attribute)
134
- attribute
135
- .inject('') { |str, attr| str + key_val(attr.first, escape_attributes(attr.last)) }
112
+ if attribute
113
+ result = "".dup
114
+ attribute.each do |attr|
115
+ # Generate and concat %( #{key}="#{val}") values for attribute pair
116
+ result << " "
117
+ result << attr.first.to_s
118
+ result << '="'
119
+ result << escape_attributes(attr.last).to_s
120
+ result << '"'
121
+ end
122
+ result
123
+ end
136
124
  end
137
125
 
138
126
  def escape_attributes(str = '')
139
- return str unless str.to_s =~ /["&<>\n]/
127
+ return str unless str.respond_to?(:match) && str =~ /["&<>\n]/
140
128
 
141
129
  str
142
- .gsub(/&/, "&amp;")
143
- .gsub(/"/, "&quot;")
144
- .gsub(/</, "&lt;")
145
- .gsub(/>/, "&gt;")
146
- .gsub(/\n/, "&#xA;")
130
+ .gsub("&", "&amp;")
131
+ .gsub('"', "&quot;")
132
+ .gsub("<", "&lt;")
133
+ .gsub(">", "&gt;")
134
+ .gsub("\n", "&#xA;")
147
135
  end
148
136
 
149
137
  def escape_data(str = '')
150
- if str.to_s =~ /[&<>]/
151
- str.gsub(/&/, '&amp;')
152
- .gsub(/</, '&lt;')
153
- .gsub(/>/, '&gt;')
138
+ if str.respond_to?(:match) && str =~ /[&<>]/
139
+ str.gsub("&", '&amp;')
140
+ .gsub("<", '&lt;')
141
+ .gsub(">", '&gt;')
154
142
  else
155
143
  str
156
144
  end
@@ -252,9 +252,13 @@ module Writexlsx
252
252
  ['sheetId', sheet_id]
253
253
  ]
254
254
 
255
- attributes << %w[state hidden] if sheet.hidden?
255
+ if sheet.hidden?
256
+ attributes << %w[state hidden]
257
+ elsif sheet.very_hidden?
258
+ attributes << %w[state veryHidden]
259
+ end
256
260
  attributes << r_id_attributes(sheet_id)
257
- writer.empty_tag_encoded('sheet', attributes)
261
+ writer.empty_tag('sheet', attributes)
258
262
  end
259
263
  end
260
264
  end
@@ -43,13 +43,13 @@ module Writexlsx
43
43
  # Cleanup the input ranges.
44
44
  @ranges.collect! do |range|
45
45
  # Remove the absolute reference $ symbols.
46
- range = range.gsub(/\$/, '')
46
+ range = range.gsub("$", '')
47
47
  # Convert a simple range into a full Sheet1!A1:D1 range.
48
48
  range = "#{sheetname}!#{range}" unless range =~ /!/
49
49
  range
50
50
  end
51
51
  # Cleanup the input locations.
52
- @locations.collect! { |location| location.gsub(/\$/, '') }
52
+ @locations.collect! { |location| location.gsub("$", '') }
53
53
 
54
54
  # Map options.
55
55
  @high = param[:high_point]
@@ -31,10 +31,12 @@ module Writexlsx
31
31
  #
32
32
  # xl_rowcol_to_cell($row, col, row_absolute, col_absolute)
33
33
  #
34
- def xl_rowcol_to_cell(row, col, row_absolute = false, col_absolute = false)
35
- row += 1 # Change from 0-indexed to 1 indexed.
34
+ def xl_rowcol_to_cell(row_or_name, col, row_absolute = false, col_absolute = false)
35
+ if row_or_name.is_a?(Integer)
36
+ row_or_name += 1 # Change from 0-indexed to 1 indexed.
37
+ end
36
38
  col_str = xl_col_to_name(col, col_absolute)
37
- "#{col_str}#{absolute_char(row_absolute)}#{row}"
39
+ "#{col_str}#{absolute_char(row_absolute)}#{row_or_name}"
38
40
  end
39
41
 
40
42
  #
@@ -53,7 +55,7 @@ module Writexlsx
53
55
 
54
56
  # Convert base26 column string to number
55
57
  # All your Base are belong to us.
56
- chars = col.split(//)
58
+ chars = col.split("")
57
59
  expn = 0
58
60
  col = 0
59
61
 
@@ -112,7 +114,7 @@ module Writexlsx
112
114
  #
113
115
  def xl_string_pixel_width(string)
114
116
  length = 0
115
- string.to_s.split(//).each { |char| length += CHAR_WIDTHS[char] || 8 }
117
+ string.to_s.split("").each { |char| length += CHAR_WIDTHS[char] || 8 }
116
118
 
117
119
  length
118
120
  end
@@ -128,7 +130,7 @@ module Writexlsx
128
130
  name = sheetname.dup
129
131
  if name =~ /\W/ && !(name =~ /^'/)
130
132
  # Double quote and single quoted strings.
131
- name = name.gsub(/'/, "''")
133
+ name = name.gsub("'", "''")
132
134
  name = "'#{name}'"
133
135
  end
134
136
  name
@@ -159,7 +161,7 @@ module Writexlsx
159
161
  seconds = 0 # Time expressed as fraction of 24h hours in seconds
160
162
 
161
163
  # Split into date and time.
162
- date, time = date_time.split(/T/)
164
+ date, time = date_time.split("T")
163
165
 
164
166
  # We allow the time portion of the input DateTime to be optional.
165
167
  if time
@@ -207,7 +209,7 @@ module Writexlsx
207
209
  # becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
208
210
  #
209
211
  epoch = date_1904? ? 1904 : 1900
210
- offset = date_1904? ? 4 : 0
212
+ offset = date_1904? ? 4 : 0
211
213
  norm = 300
212
214
  range = year - epoch
213
215
 
@@ -248,7 +250,7 @@ module Writexlsx
248
250
  def escape_url(url)
249
251
  unless url =~ /%[0-9a-fA-F]{2}/
250
252
  # Escape the URL escape symbol.
251
- url = url.gsub(/%/, "%25")
253
+ url = url.gsub("%", "%25")
252
254
 
253
255
  # Escape whitespae in URL.
254
256
  url = url.gsub(/[\s\x00]/, '%20')
@@ -986,6 +988,178 @@ module Writexlsx
986
988
  def write_a_end_para_rpr # :nodoc:
987
989
  @writer.empty_tag('a:endParaRPr', [%w[lang en-US]])
988
990
  end
991
+
992
+ #
993
+ # Extract information from the image file such as dimension, type, filename,
994
+ # and extension. Also keep track of previously seen images to optimise out
995
+ # any duplicates.
996
+ #
997
+ def get_image_properties(filename)
998
+ # Note the image_id, and previous_images mechanism isn't currently used.
999
+ x_dpi = 96
1000
+ y_dpi = 96
1001
+
1002
+ workbook = @workbook || self
1003
+
1004
+ # Open the image file and import the data.
1005
+ data = File.binread(filename)
1006
+ md5 = Digest::MD5.hexdigest(data)
1007
+ if data.unpack1('x A3') == 'PNG'
1008
+ # Test for PNGs.
1009
+ type, width, height, x_dpi, y_dpi = process_png(data)
1010
+ workbook.image_types[:png] = 1
1011
+ elsif data.unpack1('n') == 0xFFD8
1012
+ # Test for JPEG files.
1013
+ type, width, height, x_dpi, y_dpi = process_jpg(data, filename)
1014
+ workbook.image_types[:jpeg] = 1
1015
+ elsif data.unpack1('A4') == 'GIF8'
1016
+ # Test for GIFs.
1017
+ type, width, height, x_dpi, y_dpi = process_gif(data, filename)
1018
+ workbook.image_types[:gif] = 1
1019
+ elsif data.unpack1('A2') == 'BM'
1020
+ # Test for BMPs.
1021
+ type, width, height = process_bmp(data, filename)
1022
+ workbook.image_types[:bmp] = 1
1023
+ else
1024
+ # TODO. Add Image::Size to support other types.
1025
+ raise "Unsupported image format for file: #{filename}\n"
1026
+ end
1027
+
1028
+ # Set a default dpi for images with 0 dpi.
1029
+ x_dpi = 96 if x_dpi == 0
1030
+ y_dpi = 96 if y_dpi == 0
1031
+
1032
+ [type, width, height, File.basename(filename), x_dpi, y_dpi, md5]
1033
+ end
1034
+
1035
+ #
1036
+ # Extract width and height information from a PNG file.
1037
+ #
1038
+ def process_png(data)
1039
+ type = 'png'
1040
+ width = 0
1041
+ height = 0
1042
+ x_dpi = 96
1043
+ y_dpi = 96
1044
+
1045
+ offset = 8
1046
+ data_length = data.size
1047
+
1048
+ # Search through the image data to read the height and width in th the
1049
+ # IHDR element. Also read the DPI in the pHYs element.
1050
+ while offset < data_length
1051
+
1052
+ length = data[offset + 0, 4].unpack1("N")
1053
+ png_type = data[offset + 4, 4].unpack1("A4")
1054
+
1055
+ case png_type
1056
+ when "IHDR"
1057
+ width = data[offset + 8, 4].unpack1("N")
1058
+ height = data[offset + 12, 4].unpack1("N")
1059
+ when "pHYs"
1060
+ x_ppu = data[offset + 8, 4].unpack1("N")
1061
+ y_ppu = data[offset + 12, 4].unpack1("N")
1062
+ units = data[offset + 16, 1].unpack1("C")
1063
+
1064
+ if units == 1
1065
+ x_dpi = x_ppu * 0.0254
1066
+ y_dpi = y_ppu * 0.0254
1067
+ end
1068
+ end
1069
+
1070
+ offset = offset + length + 12
1071
+
1072
+ break if png_type == "IEND"
1073
+ end
1074
+ raise "#{filename}: no size data found in png image.\n" unless height
1075
+
1076
+ [type, width, height, x_dpi, y_dpi]
1077
+ end
1078
+
1079
+ def process_jpg(data, filename)
1080
+ type = 'jpeg'
1081
+ x_dpi = 96
1082
+ y_dpi = 96
1083
+
1084
+ offset = 2
1085
+ data_length = data.bytesize
1086
+
1087
+ # Search through the image data to read the JPEG markers.
1088
+ while offset < data_length
1089
+ marker = data[offset + 0, 2].unpack1("n")
1090
+ length = data[offset + 2, 2].unpack1("n")
1091
+
1092
+ # Read the height and width in the 0xFFCn elements
1093
+ # (Except C4, C8 and CC which aren't SOF markers).
1094
+ if (marker & 0xFFF0) == 0xFFC0 &&
1095
+ marker != 0xFFC4 && marker != 0xFFCC
1096
+ height = data[offset + 5, 2].unpack1("n")
1097
+ width = data[offset + 7, 2].unpack1("n")
1098
+ end
1099
+
1100
+ # Read the DPI in the 0xFFE0 element.
1101
+ if marker == 0xFFE0
1102
+ units = data[offset + 11, 1].unpack1("C")
1103
+ x_density = data[offset + 12, 2].unpack1("n")
1104
+ y_density = data[offset + 14, 2].unpack1("n")
1105
+
1106
+ if units == 1
1107
+ x_dpi = x_density
1108
+ y_dpi = y_density
1109
+ elsif units == 2
1110
+ x_dpi = x_density * 2.54
1111
+ y_dpi = y_density * 2.54
1112
+ end
1113
+ end
1114
+
1115
+ offset += length + 2
1116
+ break if marker == 0xFFDA
1117
+ end
1118
+
1119
+ raise "#{filename}: no size data found in jpeg image.\n" unless height
1120
+
1121
+ [type, width, height, x_dpi, y_dpi]
1122
+ end
1123
+
1124
+ #
1125
+ # Extract width and height information from a GIF file.
1126
+ #
1127
+ def process_gif(data, filename)
1128
+ type = 'gif'
1129
+ x_dpi = 96
1130
+ y_dpi = 96
1131
+
1132
+ width = data[6, 2].unpack1("v")
1133
+ height = data[8, 2].unpack1("v")
1134
+
1135
+ raise "#{filename}: no size data found in gif image.\n" if height.nil?
1136
+
1137
+ [type, width, height, x_dpi, y_dpi]
1138
+ end
1139
+
1140
+ # Extract width and height information from a BMP file.
1141
+ def process_bmp(data, filename) # :nodoc:
1142
+ type = 'bmp'
1143
+
1144
+ # Check that the file is big enough to be a bitmap.
1145
+ raise "#{filename} doesn't contain enough data." if data.bytesize <= 0x36
1146
+
1147
+ # Read the bitmap width and height. Verify the sizes.
1148
+ width, height = data.unpack("x18 V2")
1149
+ raise "#{filename}: largest image width #{width} supported is 65k." if width > 0xFFFF
1150
+ raise "#{filename}: largest image height supported is 65k." if height > 0xFFFF
1151
+
1152
+ # Read the bitmap planes and bpp data. Verify them.
1153
+ planes, bitcount = data.unpack("x26 v2")
1154
+ raise "#{filename} isn't a 24bit true color bitmap." unless bitcount == 24
1155
+ raise "#{filename}: only 1 plane supported in bitmap image." unless planes == 1
1156
+
1157
+ # Read the bitmap compression. Verify compression.
1158
+ compression = data.unpack1("x30 V")
1159
+ raise "#{filename}: compression not supported in bitmap image." unless compression == 0
1160
+
1161
+ [type, width, height]
1162
+ end
989
1163
  end
990
1164
 
991
1165
  module WriteDPtPoint
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- WriteXLSX_VERSION = "1.11.1"
3
+ WriteXLSX_VERSION = "1.12.0"