write_xlsx 1.10.2 → 1.11.1

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.
@@ -23,6 +23,7 @@ module Writexlsx
23
23
 
24
24
  MAX_DIGIT_WIDTH = 7 # For Calabri 11. # :nodoc:
25
25
  PADDING = 5 # :nodoc:
26
+ COLINFO = Struct.new('ColInfo', :width, :format, :hidden, :level, :collapsed, :autofit)
26
27
 
27
28
  attr_reader :index # :nodoc:
28
29
  attr_reader :charts, :images, :tables, :shapes, :drawings # :nodoc:
@@ -31,20 +32,25 @@ module Writexlsx
31
32
  attr_reader :vml_data_id # :nodoc:
32
33
  attr_reader :vml_header_id # :nodoc:
33
34
  attr_reader :autofilter_area # :nodoc:
34
- attr_reader :writer, :set_rows, :col_formats # :nodoc:
35
+ attr_reader :writer, :set_rows, :col_info # :nodoc:
35
36
  attr_reader :vml_shape_id # :nodoc:
36
37
  attr_reader :comments, :comments_author # :nodoc:
37
38
  attr_accessor :data_bars_2010, :dxf_priority # :nodoc:
38
39
  attr_reader :vba_codename # :nodoc:
39
- attr_writer :excel_version
40
+ attr_writer :excel_version # :nodoc:
41
+ attr_reader :filter_cells # :nodoc:
40
42
 
41
43
  def initialize(workbook, index, name) # :nodoc:
44
+ rowmax = 1_048_576
45
+ colmax = 16_384
46
+ strmax = 32_767
47
+
42
48
  @writer = Package::XMLWriterSimple.new
43
49
 
44
50
  @workbook = workbook
45
51
  @index = index
46
52
  @name = name
47
- @colinfo = {}
53
+ @col_info = {}
48
54
  @cell_data_table = []
49
55
  @excel_version = 2007
50
56
  @palette = workbook.palette
@@ -55,6 +61,10 @@ module Writexlsx
55
61
 
56
62
  @screen_gridlines = true
57
63
  @show_zeros = true
64
+
65
+ @xls_rowmax = rowmax
66
+ @xls_colmax = colmax
67
+ @xls_strmax = strmax
58
68
  @dim_rowmin = nil
59
69
  @dim_rowmax = nil
60
70
  @dim_colmin = nil
@@ -77,11 +87,10 @@ module Writexlsx
77
87
  @filter_on = false
78
88
  @filter_range = []
79
89
  @filter_cols = {}
90
+ @filter_cells = {}
80
91
  @filter_type = {}
81
92
 
82
- @col_sizes = {}
83
93
  @row_sizes = {}
84
- @col_formats = {}
85
94
 
86
95
  @last_shape_id = 1
87
96
  @rel_count = 0
@@ -90,8 +99,8 @@ module Writexlsx
90
99
  @external_drawing_links = []
91
100
  @external_comment_links = []
92
101
  @external_vml_links = []
93
- @external_table_links = []
94
102
  @external_background_links = []
103
+ @external_table_links = []
95
104
  @drawing_links = []
96
105
  @vml_drawing_links = []
97
106
  @charts = []
@@ -121,6 +130,7 @@ module Writexlsx
121
130
  @default_col_width = 8.43
122
131
  @default_col_pixels = 64
123
132
  @default_row_rezoed = 0
133
+ @default_date_pixels = 68
124
134
 
125
135
  @merge = []
126
136
 
@@ -305,7 +315,8 @@ module Writexlsx
305
315
  #
306
316
  def set_column(*args)
307
317
  # Check for a cell reference in A1 notation and substitute row and column
308
- if args[0].to_s =~ /^\D/
318
+ # ruby 3.2 no longer handles =~ for various types
319
+ if args[0].respond_to?(:=~) && args[0].to_s =~ /^\D/
309
320
  _row1, firstcol, _row2, lastcol, *data = substitute_cellref(*args)
310
321
  else
311
322
  firstcol, lastcol, *data = args
@@ -321,6 +332,7 @@ module Writexlsx
321
332
  firstcol, lastcol = lastcol, firstcol if firstcol > lastcol
322
333
 
323
334
  width, format, hidden, level, collapsed = data
335
+ autofit = 0
324
336
 
325
337
  # Check that cols are valid and store max and min values with default row.
326
338
  # NOTE: The check shouldn't modify the row dimensions and should only modify
@@ -338,22 +350,19 @@ module Writexlsx
338
350
  level = 0 if level < 0
339
351
  level = 7 if level > 7
340
352
 
353
+ # Excel has a maximum column width of 255 characters.
354
+ width = 255.0 if width && width > 255.0
355
+
341
356
  @outline_col_level = level if level > @outline_col_level
342
357
 
343
358
  # Store the column data based on the first column. Padded for sorting.
344
- @colinfo[sprintf("%05d", firstcol)] = [firstcol, lastcol, width, format, hidden, level, collapsed]
359
+ (firstcol..lastcol).each do |col|
360
+ @col_info[col] =
361
+ COLINFO.new(width, format, hidden, level, collapsed, autofit)
362
+ end
345
363
 
346
364
  # Store the column change to allow optimisations.
347
365
  @col_size_changed = 1
348
-
349
- # Store the col sizes for use when calculating image vertices taking
350
- # hidden columns into account. Also store the column formats.
351
- width ||= @default_col_width
352
-
353
- (firstcol..lastcol).each do |col|
354
- @col_sizes[col] = [width, hidden]
355
- @col_formats[col] = format if format
356
- end
357
366
  end
358
367
 
359
368
  #
@@ -387,6 +396,112 @@ module Writexlsx
387
396
  set_column(first_col, last_col, width, format, hidden, level)
388
397
  end
389
398
 
399
+ #
400
+ # autofit()
401
+ #
402
+ # Simulate autofit based on the data, and datatypes in each column. We do this
403
+ # by estimating a pixel width for each cell data.
404
+ #
405
+ def autofit
406
+ col_width = {}
407
+
408
+ # Iterate through all the data in the worksheet.
409
+ (@dim_rowmin..@dim_rowmax).each do |row_num|
410
+ # Skip row if it doesn't contain cell data.
411
+ next unless @cell_data_table[row_num]
412
+
413
+ (@dim_colmin..@dim_colmax).each do |col_num|
414
+ length = 0
415
+ case (cell_data = @cell_data_table[row_num][col_num])
416
+ when StringCellData, RichStringCellData
417
+ # Handle strings and rich strings.
418
+ #
419
+ # For standard shared strings we do a reverse lookup
420
+ # from the shared string id to the actual string. For
421
+ # rich strings we use the unformatted string. We also
422
+ # split multiline strings and handle each part
423
+ # separately.
424
+ string = cell_data.raw_string
425
+
426
+ if string =~ /\n/
427
+ # Handle multiline strings.
428
+ length = max = string.split("\n").collect do |str|
429
+ xl_string_pixel_width(str)
430
+ end.max
431
+ else
432
+ length = xl_string_pixel_width(string)
433
+ end
434
+ when DateTimeCellData
435
+
436
+ # Handle dates.
437
+ #
438
+ # The following uses the default width for mm/dd/yyyy
439
+ # dates. It isn't feasible to parse the number format
440
+ # to get the actual string width for all format types.
441
+ length = @default_date_pixels
442
+ when NumberCellData
443
+
444
+ # Handle numbers.
445
+ #
446
+ # We use a workaround/optimization for numbers since
447
+ # digits all have a pixel width of 7. This gives a
448
+ # slightly greater width for the decimal place and
449
+ # minus sign but only by a few pixels and
450
+ # over-estimation is okay.
451
+ length = 7 * cell_data.token.to_s.length
452
+ when BooleanCellData
453
+
454
+ # Handle boolean values.
455
+ #
456
+ # Use the Excel standard widths for TRUE and FALSE.
457
+ if ptrue?(cell_data.token)
458
+ length = 31
459
+ else
460
+ length = 36
461
+ end
462
+ when FormulaCellData, FormulaArrayCellData, DynamicFormulaArrayCellData
463
+ # Handle formulas.
464
+ #
465
+ # We only try to autofit a formula if it has a
466
+ # non-zero value.
467
+ if ptrue?(cell_data.data)
468
+ length = xl_string_pixel_width(cell_data.data)
469
+ end
470
+ end
471
+
472
+ # If the cell is in an autofilter header we add an
473
+ # additional 16 pixels for the dropdown arrow.
474
+ if length > 0 &&
475
+ @filter_cells["#{row_num}:#{col_num}"]
476
+ length += 16
477
+ end
478
+
479
+ # Add the string lenght to the lookup hash.
480
+ max = col_width[col_num] || 0
481
+ col_width[col_num] = length if length > max
482
+ end
483
+ end
484
+
485
+ # Apply the width to the column.
486
+ col_width.each do |col_num, pixel_width|
487
+ # Convert the string pixel width to a character width using an
488
+ # additional padding of 7 pixels, like Excel.
489
+ width = pixels_to_width(pixel_width + 7)
490
+
491
+ # The max column character width in Excel is 255.
492
+ width = 255.0 if width > 255.0
493
+
494
+ # Add the width to an existing col info structure or add a new one.
495
+ if @col_info[col_num]
496
+ @col_info[col_num].width = width
497
+ @col_info[col_num].autofit = 1
498
+ else
499
+ @col_info[col_num] =
500
+ COLINFO.new(width, nil, 0, 0, 0, 1)
501
+ end
502
+ end
503
+ end
504
+
390
505
  #
391
506
  # :call-seq:
392
507
  # set_selection(cell_or_cell_range)
@@ -936,31 +1051,35 @@ module Writexlsx
936
1051
  write_row(_row, _col, _token, _format, _value1, _value2)
937
1052
  elsif _token.respond_to?(:coerce) # Numeric
938
1053
  write_number(_row, _col, _token, _format)
939
- # Match integer with leading zero(s)
940
- elsif @leading_zeros && _token =~ /^0\d*$/
941
- write_string(_row, _col, _token, _format)
942
- elsif _token =~ /\A([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\Z/
943
- write_number(_row, _col, _token, _format)
944
- # Match formula
945
- elsif _token =~ /^=/
946
- write_formula(_row, _col, _token, _format, _value1)
947
- # Match array formula
948
- elsif _token =~ /^\{=.*\}$/
949
- write_formula(_row, _col, _token, _format, _value1)
950
- # Match blank
951
- elsif _token == ''
952
- # row_col_args.delete_at(2) # remove the empty string from the parameter list
953
- write_blank(_row, _col, _format)
954
- elsif @workbook.strings_to_urls
955
- # Match http, https or ftp URL
956
- if _token =~ %r{\A[fh]tt?ps?://}
957
- write_url(_row, _col, _token, _format, _value1, _value2)
958
- # Match mailto:
959
- elsif _token =~ /\Amailto:/
960
- write_url(_row, _col, _token, _format, _value1, _value2)
961
- # Match internal or external sheet link
962
- elsif _token =~ /\A(?:in|ex)ternal:/
963
- write_url(_row, _col, _token, _format, _value1, _value2)
1054
+ elsif _token.respond_to?(:=~) # String
1055
+ # Match integer with leading zero(s)
1056
+ if @leading_zeros && _token =~ /^0\d*$/
1057
+ write_string(_row, _col, _token, _format)
1058
+ elsif _token =~ /\A([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\Z/
1059
+ write_number(_row, _col, _token, _format)
1060
+ # Match formula
1061
+ elsif _token =~ /^=/
1062
+ write_formula(_row, _col, _token, _format, _value1)
1063
+ # Match array formula
1064
+ elsif _token =~ /^\{=.*\}$/
1065
+ write_formula(_row, _col, _token, _format, _value1)
1066
+ # Match blank
1067
+ elsif _token == ''
1068
+ # row_col_args.delete_at(2) # remove the empty string from the parameter list
1069
+ write_blank(_row, _col, _format)
1070
+ elsif @workbook.strings_to_urls
1071
+ # Match http, https or ftp URL
1072
+ if _token =~ %r{\A[fh]tt?ps?://}
1073
+ write_url(_row, _col, _token, _format, _value1, _value2)
1074
+ # Match mailto:
1075
+ elsif _token =~ /\Amailto:/
1076
+ write_url(_row, _col, _token, _format, _value1, _value2)
1077
+ # Match internal or external sheet link
1078
+ elsif _token =~ /\A(?:in|ex)ternal:/
1079
+ write_url(_row, _col, _token, _format, _value1, _value2)
1080
+ else
1081
+ write_string(_row, _col, _token, _format)
1082
+ end
964
1083
  else
965
1084
  write_string(_row, _col, _token, _format)
966
1085
  end
@@ -1114,7 +1233,7 @@ module Writexlsx
1114
1233
 
1115
1234
  index = shared_string_index(_string.length > STR_MAX ? _string[0, STR_MAX] : _string)
1116
1235
 
1117
- store_data_to_table(StringCellData.new(index, _format), _row, _col)
1236
+ store_data_to_table(StringCellData.new(index, _format, _string), _row, _col)
1118
1237
  end
1119
1238
 
1120
1239
  #
@@ -1143,13 +1262,16 @@ module Writexlsx
1143
1262
  check_dimensions(_row, _col)
1144
1263
  store_row_col_max_min_values(_row, _col)
1145
1264
 
1146
- _fragments, _length = rich_strings_fragments(_rich_strings)
1265
+ _fragments, _raw_string = rich_strings_fragments(_rich_strings)
1147
1266
  # can't allow 2 formats in a row
1148
1267
  return -4 unless _fragments
1149
1268
 
1269
+ # Check that the string si < 32767 chars.
1270
+ return 3 if _raw_string.size > @xls_strmax
1271
+
1150
1272
  index = shared_string_index(xml_str_of_rich_string(_fragments))
1151
1273
 
1152
- store_data_to_table(StringCellData.new(index, _xf), _row, _col)
1274
+ store_data_to_table(RichStringCellData.new(index, _xf, _raw_string), _row, _col)
1153
1275
  end
1154
1276
 
1155
1277
  #
@@ -1679,7 +1801,7 @@ module Writexlsx
1679
1801
  date_time = convert_date_time(_str)
1680
1802
 
1681
1803
  if date_time
1682
- store_data_to_table(NumberCellData.new(date_time, _format), _row, _col)
1804
+ store_data_to_table(DateTimeCellData.new(date_time, _format), _row, _col)
1683
1805
  else
1684
1806
  # If the date isn't valid then write it as a string.
1685
1807
  write_string(_row, _col, _str, _format)
@@ -2164,6 +2286,11 @@ module Writexlsx
2164
2286
  @autofilter_area = convert_name_area(_row1, _col1, _row2, _col2)
2165
2287
  @autofilter_ref = xl_range(_row1, _row2, _col1, _col2)
2166
2288
  @filter_range = [_col1, _col2]
2289
+
2290
+ # Store the filter cell positions for use in the autofit calculation.
2291
+ (_col1.._col2).each do |col|
2292
+ @filter_cells["#{_row1}:#{col}"] = 1
2293
+ end
2167
2294
  end
2168
2295
 
2169
2296
  #
@@ -2506,8 +2633,8 @@ module Writexlsx
2506
2633
  @external_hyper_links,
2507
2634
  @external_drawing_links,
2508
2635
  @external_vml_links,
2509
- @external_table_links,
2510
2636
  @external_background_links,
2637
+ @external_table_links,
2511
2638
  @external_comment_links
2512
2639
  ].reject { |a| a.empty? }
2513
2640
  end
@@ -2634,6 +2761,33 @@ module Writexlsx
2634
2761
 
2635
2762
  private
2636
2763
 
2764
+ #
2765
+ # Compare adjacent column information structures.
2766
+ #
2767
+ def compare_col_info(col_options, previous_options)
2768
+ if !col_options.width.nil? != !previous_options.width.nil?
2769
+ return nil
2770
+ end
2771
+ if col_options.width && previous_options.width &&
2772
+ col_options.width != previous_options.width
2773
+ return nil
2774
+ end
2775
+
2776
+ if !col_options.format.nil? != !previous_options.format.nil?
2777
+ return nil
2778
+ end
2779
+ if col_options.format && previous_options.format &&
2780
+ col_options.format != previous_options.format
2781
+ return nil
2782
+ end
2783
+
2784
+ return nil if col_options.hidden != previous_options.hidden
2785
+ return nil if col_options.level != previous_options.level
2786
+ return nil if col_options.collapsed != previous_options.collapsed
2787
+
2788
+ true
2789
+ end
2790
+
2637
2791
  #
2638
2792
  # Get the index used to address a drawing rel link.
2639
2793
  #
@@ -2685,9 +2839,9 @@ module Writexlsx
2685
2839
  # Create a temp format with the default font for unformatted fragments.
2686
2840
  default = Format.new(0)
2687
2841
 
2688
- length = 0 # String length.
2689
2842
  last = 'format'
2690
2843
  pos = 0
2844
+ raw_string = ''
2691
2845
 
2692
2846
  fragments = []
2693
2847
  rich_strings.each do |token|
@@ -2708,12 +2862,12 @@ module Writexlsx
2708
2862
  fragments << default << token
2709
2863
  end
2710
2864
 
2711
- length += token.size # Keep track of actual string length.
2865
+ raw_string += token # Keep track of actual string length.
2712
2866
  last = 'string'
2713
2867
  end
2714
2868
  pos += 1
2715
2869
  end
2716
- [fragments, length]
2870
+ [fragments, raw_string]
2717
2871
  end
2718
2872
 
2719
2873
  def xml_str_of_rich_string(fragments)
@@ -2944,8 +3098,9 @@ module Writexlsx
2944
3098
  #
2945
3099
  def size_col(col, anchor = 0) # :nodoc:
2946
3100
  # Look up the cell value to see if it has been changed.
2947
- if @col_sizes[col]
2948
- width, hidden = @col_sizes[col]
3101
+ if @col_info[col]
3102
+ width = @col_info[col].width || @default_col_width
3103
+ hidden = @col_info[col].hidden
2949
3104
 
2950
3105
  # Convert to pixels.
2951
3106
  pixels = if hidden == 1 && anchor != 4
@@ -3276,28 +3431,22 @@ EOS
3276
3431
  end
3277
3432
 
3278
3433
  #
3279
- # Based on the algorithm provided by Daniel Rentz of OpenOffice.
3280
- #
3434
+ # Hash a worksheet password. Based on the algorithm in ECMA-376-4:2016,
3435
+ # Office Open XML File Foemats -- Transitional Migration Features,
3436
+ # Additional attributes for workbookProtection element (Part 1, §18.2.29). #
3281
3437
  def encode_password(password) # :nodoc:
3282
- i = 0
3283
- chars = password.split(//)
3284
- count = chars.size
3438
+ hash = 0
3285
3439
 
3286
- chars.collect! do |char|
3287
- i += 1
3288
- char = char.ord << i
3289
- low_15 = char & 0x7fff
3290
- high_15 = char & (0x7fff << 15)
3291
- high_15 = high_15 >> 15
3292
- char = low_15 | high_15
3440
+ password.reverse.split(//).each do |char|
3441
+ hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff)
3442
+ hash ^= char.ord
3293
3443
  end
3294
3444
 
3295
- encoded_password = 0x0000
3296
- chars.each { |c| encoded_password ^= c }
3297
- encoded_password ^= count
3298
- encoded_password ^= 0xCE4B
3445
+ hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff)
3446
+ hash ^= password.length
3447
+ hash ^= 0xCE4B
3299
3448
 
3300
- sprintf("%X", encoded_password)
3449
+ sprintf("%X", hash)
3301
3450
  end
3302
3451
 
3303
3452
  #
@@ -3475,10 +3624,42 @@ EOS
3475
3624
  #
3476
3625
  def write_cols # :nodoc:
3477
3626
  # Exit unless some column have been formatted.
3478
- return if @colinfo.empty?
3627
+ return if @col_info.empty?
3479
3628
 
3480
3629
  @writer.tag_elements('cols') do
3481
- @colinfo.keys.sort.each { |col| write_col_info(@colinfo[col]) }
3630
+ # Use the first element of the column informatin structure to set
3631
+ # the initial/previous properties.
3632
+ first_col = @col_info.keys.min
3633
+ last_col = first_col
3634
+ previous_options = @col_info[first_col]
3635
+ deleted_col = first_col
3636
+ deleted_col_options = previous_options
3637
+
3638
+ @col_info.delete(first_col)
3639
+
3640
+ @col_info.keys.sort.each do |col|
3641
+ col_options = @col_info[col]
3642
+
3643
+ # Check if the column number is contiguous with the previous
3644
+ # column and if the properties are the same.
3645
+ if (col == last_col + 1) &&
3646
+ compare_col_info(col_options, previous_options)
3647
+ last_col = col
3648
+ else
3649
+ # If not contiguous/equal then we write out the current range
3650
+ # of columns and start again.
3651
+ write_col_info([first_col, last_col, previous_options])
3652
+ first_col = col
3653
+ last_col = first_col
3654
+ previous_options = col_options
3655
+ end
3656
+ end
3657
+
3658
+ # We will exit the previous loop with one unhandled column range.
3659
+ write_col_info([first_col, last_col, previous_options])
3660
+
3661
+ # Put back the deleted first column information structure:
3662
+ @col_info[deleted_col] = deleted_col_options
3482
3663
  end
3483
3664
  end
3484
3665
 
@@ -3490,13 +3671,14 @@ EOS
3490
3671
  end
3491
3672
 
3492
3673
  def col_info_attributes(args)
3493
- min = args[0] || 0 # First formatted column.
3494
- max = args[1] || 0 # Last formatted column.
3495
- width = args[2] # Col width in user units.
3496
- format = args[3] # Format index.
3497
- hidden = args[4] || 0 # Hidden flag.
3498
- level = args[5] || 0 # Outline level.
3499
- collapsed = args[6] || 0 # Outline level.
3674
+ min = args[0] || 0 # First formatted column.
3675
+ max = args[1] || 0 # Last formatted column.
3676
+ width = args[2].width # Col width in user units.
3677
+ format = args[2].format # Format index.
3678
+ hidden = args[2].hidden || 0 # Hidden flag.
3679
+ level = args[2].level || 0 # Outline level.
3680
+ collapsed = args[2].collapsed || 0 # Outline Collapsed
3681
+ autofit = args[2].autofit || 0 # Best fit for autofit numbers.
3500
3682
  xf_index = format ? format.get_xf_index : 0
3501
3683
 
3502
3684
  custom_width = true
@@ -3519,10 +3701,11 @@ EOS
3519
3701
  ['width', width]
3520
3702
  ]
3521
3703
 
3522
- attributes << ['style', xf_index] if xf_index != 0
3523
- attributes << ['hidden', 1] if hidden != 0
3704
+ attributes << ['style', xf_index] if xf_index != 0
3705
+ attributes << ['hidden', 1] if hidden != 0
3706
+ attributes << ['bestFit', 1] if autofit != 0
3524
3707
  attributes << ['customWidth', 1] if custom_width
3525
- attributes << ['outlineLevel', level] if level != 0
3708
+ attributes << ['outlineLevel', level] if level != 0
3526
3709
  attributes << ['collapsed', 1] if collapsed != 0
3527
3710
  attributes
3528
3711
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: write_xlsx
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.2
4
+ version: 1.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hideo NAKAMURA
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-16 00:00:00.000000000 Z
11
+ date: 2023-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubyzip
@@ -130,6 +130,7 @@ files:
130
130
  - examples/add_vba_project.rb
131
131
  - examples/array_formula.rb
132
132
  - examples/autofilter.rb
133
+ - examples/autofit.rb
133
134
  - examples/background.rb
134
135
  - examples/chart_area.rb
135
136
  - examples/chart_bar.rb
@@ -275,7 +276,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
275
276
  - !ruby/object:Gem::Version
276
277
  version: '0'
277
278
  requirements: []
278
- rubygems_version: 3.4.5
279
+ rubygems_version: 3.4.10
279
280
  signing_key:
280
281
  specification_version: 4
281
282
  summary: write_xlsx is a gem to create a new file in the Excel 2007+ XLSX format.