write_xlsx 0.59.0 → 0.60.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.
Files changed (41) hide show
  1. data/README.rdoc +11 -2
  2. data/bin/extract_vba.rb +5 -0
  3. data/lib/write_xlsx/package/comments.rb +0 -2
  4. data/lib/write_xlsx/package/packager.rb +3 -2
  5. data/lib/write_xlsx/package/vml.rb +250 -31
  6. data/lib/write_xlsx/package/xml_writer_simple.rb +1 -1
  7. data/lib/write_xlsx/version.rb +1 -1
  8. data/lib/write_xlsx/workbook.rb +28 -18
  9. data/lib/write_xlsx/worksheet.rb +139 -4
  10. data/test/package/vml/test_write_div.rb +1 -1
  11. data/test/package/vml/test_write_fill.rb +10 -2
  12. data/test/package/vml/test_write_path.rb +10 -2
  13. data/test/package/vml/test_write_shapetype.rb +9 -1
  14. data/test/package/vml/test_write_textbox.rb +1 -1
  15. data/test/regression/test_button01.rb +23 -0
  16. data/test/regression/test_button02.rb +29 -0
  17. data/test/regression/test_button03.rb +24 -0
  18. data/test/regression/test_button04.rb +25 -0
  19. data/test/regression/test_button05.rb +28 -0
  20. data/test/regression/test_button06.rb +28 -0
  21. data/test/regression/test_button07.rb +32 -0
  22. data/test/regression/test_escapes07.rb +29 -0
  23. data/test/regression/test_escapes08.rb +30 -0
  24. data/test/regression/test_vml01.rb +29 -0
  25. data/test/regression/test_vml02.rb +31 -0
  26. data/test/regression/test_vml03.rb +40 -0
  27. data/test/regression/test_vml04.rb +41 -0
  28. data/test/regression/xlsx_files/button01.xlsx +0 -0
  29. data/test/regression/xlsx_files/button02.xlsx +0 -0
  30. data/test/regression/xlsx_files/button03.xlsx +0 -0
  31. data/test/regression/xlsx_files/button04.xlsx +0 -0
  32. data/test/regression/xlsx_files/button05.xlsx +0 -0
  33. data/test/regression/xlsx_files/button07.xlsm +0 -0
  34. data/test/regression/xlsx_files/escapes07.xlsx +0 -0
  35. data/test/regression/xlsx_files/escapes08.xlsx +0 -0
  36. data/test/regression/xlsx_files/vbaProject02.bin +0 -0
  37. data/test/regression/xlsx_files/vml01.xlsx +0 -0
  38. data/test/regression/xlsx_files/vml02.xlsx +0 -0
  39. data/test/regression/xlsx_files/vml03.xlsx +0 -0
  40. data/test/regression/xlsx_files/vml04.xlsx +0 -0
  41. metadata +54 -2
@@ -112,7 +112,7 @@ def escape_attributes(str = '')
112
112
  end
113
113
 
114
114
  def escape_data(str = '')
115
- if str =~ /[&<>"]/
115
+ if str =~ /[&<>]/
116
116
  str.gsub(/&/, '&amp;').
117
117
  gsub(/</, '&lt;').
118
118
  gsub(/>/, '&gt;')
@@ -1,5 +1,5 @@
1
1
  require 'write_xlsx/workbook'
2
2
 
3
3
  class WriteXLSX < Writexlsx::Workbook
4
- VERSION = "0.59.0"
4
+ VERSION = "0.60.0"
5
5
  end
@@ -20,7 +20,8 @@ class Workbook
20
20
  attr_writer :firstsheet
21
21
  attr_reader :palette
22
22
  attr_reader :font_count, :num_format_count, :border_count, :fill_count, :custom_colors
23
- attr_reader :worksheets, :sheetnames, :charts, :drawings, :num_comment_files, :named_ranges
23
+ attr_reader :worksheets, :sheetnames, :charts, :drawings
24
+ attr_reader :num_comment_files, :num_vml_files, :named_ranges
24
25
  attr_reader :doc_properties
25
26
  attr_reader :image_types, :images
26
27
  attr_reader :shared_strings
@@ -100,6 +101,7 @@ def initialize(file, default_formats = {})
100
101
  @custom_colors = []
101
102
  @doc_properties = {}
102
103
  @local_time = Time.now
104
+ @num_vml_files = 0
103
105
  @num_comment_files = 0
104
106
  @optimization = 0
105
107
  @x_window = 240
@@ -1084,7 +1086,9 @@ def write_file_version #:nodoc:
1084
1086
  end
1085
1087
 
1086
1088
  def write_workbook_pr #:nodoc:
1087
- attributes = date_1904? ? ['date1904', 1] : []
1089
+ attributes = []
1090
+ attributes << 'codeName' << @vba_codename if ptrue?(@vba_codename)
1091
+ attributes << 'date1904' << 1 if date_1904?
1088
1092
  attributes << 'defaultThemeVersion' << 124226
1089
1093
  @writer.empty_tag('workbookPr', attributes)
1090
1094
  end
@@ -1207,10 +1211,14 @@ def store_workbook #:nodoc:
1207
1211
  # Set the active sheet.
1208
1212
  @worksheets[@activesheet].activate
1209
1213
 
1210
- prepare_comments # Prepare the worksheet cell comments.
1211
- prepare_defined_names # Set the defined names for the worksheets such as Print Titles.
1212
- prepare_drawings # Prepare the drawings, charts and images.
1213
- add_chart_data # Add cached data to charts.
1214
+ # Prepare the worksheet VML elements such as comments and buttons.
1215
+ prepare_vml_objects
1216
+ # Set the defined names for the worksheets such as Print Titles.
1217
+ prepare_defined_names
1218
+ # Prepare the drawings, charts and images.
1219
+ prepare_drawings
1220
+ # Add cached data to charts.
1221
+ add_chart_data
1214
1222
 
1215
1223
  # Package the workbook.
1216
1224
  packager = Package::Packager.new
@@ -1458,31 +1466,33 @@ def prepare_defined_names #:nodoc:
1458
1466
  end
1459
1467
 
1460
1468
  #
1461
- # Iterate through the worksheets and set up the comment data.
1469
+ # Iterate through the worksheets and set up the VML objects.
1462
1470
  #
1463
- def prepare_comments #:nodoc:
1464
- comment_id = 0
1465
- vml_data_id = 1
1466
- vml_shape_id = 1024
1471
+ def prepare_vml_objects #:nodoc:
1472
+ comment_id = 0
1473
+ vml_data_id = 1
1474
+ vml_shape_id = 1024
1475
+ vml_files = 0
1476
+ comment_files = 0
1467
1477
 
1468
1478
  @worksheets.each do |sheet|
1469
- next unless sheet.has_comments?
1479
+ next unless sheet.has_vml?
1480
+ vml_files += 1
1481
+ comment_files += 1 if sheet.has_comments?
1470
1482
 
1471
1483
  comment_id += 1
1472
- sheet.set_external_vml_links(comment_id)
1473
- sheet.set_external_comment_links(comment_id)
1474
- sheet.set_vml_data_id(vml_data_id)
1475
- sheet.vml_shape_id = vml_shape_id
1484
+ count = sheet.prepare_vml_objects(vml_data_id, vml_shape_id, comment_id)
1476
1485
 
1477
1486
  # Each VML file should start with a shape id incremented by 1024.
1478
1487
  vml_data_id += 1 * ( ( 1024 + sheet.comments_count ) / 1024.0 ).to_i
1479
1488
  vml_shape_id += 1024 * ( ( 1024 + sheet.comments_count ) / 1024.0 ).to_i
1480
1489
  end
1481
1490
 
1482
- @num_comment_files = comment_id
1491
+ @num_vml_files = vml_files
1492
+ @num_comment_files = comment_files
1483
1493
 
1484
1494
  # Add a font format for cell comments.
1485
- if comment_id > 0
1495
+ if comment_files > 0
1486
1496
  format = Format.new(
1487
1497
  @xf_format_indices,
1488
1498
  @dxf_format_indices,
@@ -356,6 +356,7 @@ def orientation?
356
356
  attr_accessor :vml_shape_id, :rel_count, :hlink_refs # :nodoc:
357
357
  attr_reader :comments_author # :nodoc:
358
358
  attr_accessor :dxf_priority # :nodoc:
359
+ attr_reader :vba_codename # :nodoc:
359
360
 
360
361
  def initialize(workbook, index, name) #:nodoc:
361
362
  @writer = Package::XMLWriterSimple.new
@@ -421,7 +422,9 @@ def initialize(workbook, index, name) #:nodoc:
421
422
 
422
423
  @merge = []
423
424
 
425
+ @has_vml = false
424
426
  @comments = Package::Comments.new(self)
427
+ @buttons_array = []
425
428
 
426
429
  @validations = []
427
430
 
@@ -2102,6 +2105,8 @@ def write_comment(*args)
2102
2105
  check_dimensions(row, col)
2103
2106
  store_row_col_max_min_values(row, col)
2104
2107
 
2108
+ @has_vml = true
2109
+
2105
2110
  # Process the properties of the cell comment.
2106
2111
  @comments.add(Package::Comment.new(@workbook, self, row, col, string, options))
2107
2112
  end
@@ -2643,8 +2648,21 @@ def write_url(*args)
2643
2648
  # External links to URLs and to other Excel workbooks have slightly
2644
2649
  # different characteristics that we have to account for.
2645
2650
  if link_type == 1
2646
- # Substiture white space in url.
2647
- url = url.sub(/[\s\x00]/, '%20')
2651
+ # Escape URL unless it looks already escaped.
2652
+ unless url =~ /%[0-9a-fA-F]{2}/
2653
+ # Escape the URL escape symbol.
2654
+ url = url.gsub(/%/, "%25")
2655
+
2656
+ # Escape whitespae in URL.
2657
+ url = url.gsub(/[\s\x00]/, '%20')
2658
+
2659
+ # Escape other special characters in URL.
2660
+ re = /(["<>\[\]`^{}])/
2661
+ while re =~ url
2662
+ match = $~[1]
2663
+ url = url.sub(re, sprintf("%%%x", match.ord))
2664
+ end
2665
+ end
2648
2666
 
2649
2667
  # Ordinary URL style external links don't have a "location" string.
2650
2668
  str = nil
@@ -3868,6 +3886,14 @@ def add_sparkline(param)
3868
3886
  @sparklines << sparkline
3869
3887
  end
3870
3888
 
3889
+ #
3890
+ # Insert a button form object into the worksheet.
3891
+ #
3892
+ def insert_button(*args)
3893
+ @buttons_array << button_params(*(row_col_notation(args)))
3894
+ @has_vml = 1
3895
+ end
3896
+
3871
3897
  #
3872
3898
  # :call-seq:
3873
3899
  # data_validation(cell_or_cell_range, options)
@@ -4729,6 +4755,10 @@ def comments_count # :nodoc:
4729
4755
  @comments.size
4730
4756
  end
4731
4757
 
4758
+ def has_vml? # :nodoc:
4759
+ @has_vml
4760
+ end
4761
+
4732
4762
  def has_comments? # :nodoc:
4733
4763
  !@comments.empty?
4734
4764
  end
@@ -4994,6 +5024,38 @@ def get_palette_color(index) #:nodoc:
4994
5024
  sprintf("FF%02X%02X%02X", *rgb[0, 3])
4995
5025
  end
4996
5026
 
5027
+ def buttons_data # :nodoc:
5028
+ @buttons_array
5029
+ end
5030
+
5031
+ #
5032
+ # Turn the HoH that stores the comments into an array for easier handling
5033
+ # and set the external links for comments and buttons.
5034
+ #
5035
+ def prepare_vml_objects(vml_data_id, vml_shape_id, comment_id)
5036
+ @external_vml_links <<
5037
+ [ '/vmlDrawing', "../drawings/vmlDrawing#{comment_id}.vml"]
5038
+
5039
+ if has_comments?
5040
+ @comments_array = @comments.sorted_comments
5041
+ @external_comment_links <<
5042
+ [ '/comments', "../comments#{comment_id}.xml" ]
5043
+ end
5044
+
5045
+ count = @comments.size
5046
+ start_data_id = vml_data_id
5047
+
5048
+ # The VML o:idmap data id contains a comma separated range when there is
5049
+ # more than one 1024 block of comments, like this: data="1,2".
5050
+ (1 .. (count / 1024)).each do |i|
5051
+ vml_data_id = "#{vml_data_id},#{start_data_id + i}"
5052
+ end
5053
+ @vml_data_id = vml_data_id
5054
+ @vml_shape_id = vml_shape_id
5055
+
5056
+ count
5057
+ end
5058
+
4997
5059
  private
4998
5060
 
4999
5061
  def check_for_valid_input_params(param)
@@ -5712,6 +5774,70 @@ def validate_shape(shape, index)
5712
5774
  end
5713
5775
  end
5714
5776
 
5777
+ #
5778
+ # This method handles the parameters passed to insert_button as well as
5779
+ # calculating the comment object position and vertices.
5780
+ #
5781
+ def button_params(row, col, params)
5782
+ button = { :_row => row, :_col => col }
5783
+
5784
+ button_number = 1 + @buttons_array.size
5785
+
5786
+ # Set the button caption.
5787
+ caption = params[:caption] || "Button #{button_number}"
5788
+
5789
+ button[:_font] = { :_caption => caption }
5790
+
5791
+ # Set the macro name.
5792
+ if params[:macro]
5793
+ button[:_macro] = "[0]!#{params[:macro]}"
5794
+ else
5795
+ button[:_macro] = "[0]!Button#{button_number}_Click"
5796
+ end
5797
+
5798
+ # Ensure that a width and height have been set.
5799
+ default_width = 64
5800
+ default_height = 20
5801
+ params[:width] = default_width if !params[:width]
5802
+ params[:height] = default_height if !params[:height]
5803
+
5804
+ # Set the x/y offsets.
5805
+ params[:x_offset] = 0 if !params[:x_offset]
5806
+ params[:y_offset] = 0 if !params[:y_offset]
5807
+
5808
+ # Scale the size of the comment box if required.
5809
+ if params[:x_scale]
5810
+ params[:width] = params[:width] * params[:x_scale]
5811
+ end
5812
+ if params[:y_scale]
5813
+ params[:height] = params[:height] * params[:y_scale]
5814
+ end
5815
+
5816
+ # Round the dimensions to the nearest pixel.
5817
+ params[:width] = (0.5 + params[:width]).to_i
5818
+ params[:height] = (0.5 + params[:height]).to_i
5819
+
5820
+ params[:start_row] = row
5821
+ params[:start_col] = col
5822
+
5823
+ # Calculate the positions of comment object.
5824
+ vertices = position_object_pixels(
5825
+ params[:start_col],
5826
+ params[:start_row],
5827
+ params[:x_offset],
5828
+ params[:y_offset],
5829
+ params[:width],
5830
+ params[:height]
5831
+ )
5832
+
5833
+ # Add the width and height for VML.
5834
+ vertices << [params[:width], params[:height]]
5835
+
5836
+ button[:_vertices] = vertices
5837
+
5838
+ button
5839
+ end
5840
+
5715
5841
  #
5716
5842
  # Based on the algorithm provided by Daniel Rentz of OpenOffice.
5717
5843
  #
@@ -5757,8 +5883,13 @@ def write_worksheet #:nodoc:
5757
5883
  # Write the <sheetPr> element for Sheet level properties.
5758
5884
  #
5759
5885
  def write_sheet_pr #:nodoc:
5760
- return if !fit_page? && !filter_on? && !tab_color? && !outline_changed?
5886
+ if !fit_page? && !filter_on? && !tab_color? &&
5887
+ !outline_changed? && !vba_codename?
5888
+ return
5889
+ end
5890
+ codename = @vba_codename
5761
5891
  attributes = []
5892
+ (attributes << 'codeName' << codename) if codename
5762
5893
  (attributes << 'filterMode' << 1) if filter_on?
5763
5894
 
5764
5895
  if fit_page? || tab_color? || outline_changed?
@@ -6678,7 +6809,7 @@ def write_drawing(id) #:nodoc:
6678
6809
  # Write the <legacyDrawing> element.
6679
6810
  #
6680
6811
  def write_legacy_drawing #:nodoc:
6681
- return unless has_comments?
6812
+ return unless @has_vml
6682
6813
 
6683
6814
  # Increment the relationship id for any drawings or comments.
6684
6815
  @rel_count += 1
@@ -7653,6 +7784,10 @@ def outline_changed?
7653
7784
  ptrue?(@outline_changed)
7654
7785
  end
7655
7786
 
7787
+ def vba_codename?
7788
+ ptrue?(@vba_codename)
7789
+ end
7790
+
7656
7791
  def zoom_scale_normal? #:nodoc:
7657
7792
  ptrue?(@zoom_scale_normal)
7658
7793
  end
@@ -6,7 +6,7 @@
6
6
  class TestWriteDiv < Test::Unit::TestCase
7
7
  def test_write_div
8
8
  vml = Writexlsx::Package::Vml.new
9
- vml.__send__('write_div')
9
+ vml.__send__('write_div', 'left')
10
10
  result = vml.instance_variable_get(:@writer).string
11
11
  expected = '<div style="text-align:left"></div>'
12
12
  assert_equal(expected, result)
@@ -4,11 +4,19 @@
4
4
  require 'write_xlsx/package/vml'
5
5
 
6
6
  class TestWriteFill < Test::Unit::TestCase
7
- def test_write_fill
7
+ def test_write_comment_fill
8
8
  vml = Writexlsx::Package::Vml.new
9
- vml.__send__('write_fill')
9
+ vml.__send__('write_comment_fill')
10
10
  result = vml.instance_variable_get(:@writer).string
11
11
  expected = '<v:fill color2="#ffffe1"/>'
12
12
  assert_equal(expected, result)
13
13
  end
14
+
15
+ def test_write_button_fill
16
+ vml = Writexlsx::Package::Vml.new
17
+ vml.__send__('write_button_fill')
18
+ result = vml.instance_variable_get(:@writer).string
19
+ expected = '<v:fill color2="buttonFace [67]" o:detectmouseclick="t"/>'
20
+ assert_equal(expected, result)
21
+ end
14
22
  end
@@ -6,7 +6,7 @@
6
6
  class TestWritePath < Test::Unit::TestCase
7
7
  def test_write_path
8
8
  vml = Writexlsx::Package::Vml.new
9
- vml.__send__('write_path', 't', 'rect')
9
+ vml.__send__('write_comment_path', 't', 'rect')
10
10
  result = vml.instance_variable_get(:@writer).string
11
11
  expected = '<v:path gradientshapeok="t" o:connecttype="rect"/>'
12
12
  assert_equal(expected, result)
@@ -14,9 +14,17 @@ def test_write_path
14
14
 
15
15
  def test_write_path_without_gradientshapeok
16
16
  vml = Writexlsx::Package::Vml.new
17
- vml.__send__('write_path', nil, 'none')
17
+ vml.__send__('write_comment_path', nil, 'none')
18
18
  result = vml.instance_variable_get(:@writer).string
19
19
  expected = '<v:path o:connecttype="none"/>'
20
20
  assert_equal(expected, result)
21
21
  end
22
+
23
+ def test_write_button_path
24
+ vml = Writexlsx::Package::Vml.new
25
+ vml.__send__('write_button_path')
26
+ result = vml.instance_variable_get(:@writer).string
27
+ expected = '<v:path shadowok="f" o:extrusionok="f" strokeok="f" fillok="f" o:connecttype="rect"/>'
28
+ assert_equal(expected, result)
29
+ end
22
30
  end
@@ -6,9 +6,17 @@
6
6
  class TestWriteShapetype < Test::Unit::TestCase
7
7
  def test_write_shapetype
8
8
  vml = Writexlsx::Package::Vml.new
9
- vml.__send__('write_shapetype')
9
+ vml.__send__('write_comment_shapetype')
10
10
  result = vml.instance_variable_get(:@writer).string
11
11
  expected = '<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>'
12
12
  assert_equal(expected, result)
13
13
  end
14
+
15
+ def test_write_shapetype2
16
+ vml = Writexlsx::Package::Vml.new
17
+ vml.__send__('write_button_shapetype')
18
+ result = vml.instance_variable_get(:@writer).string
19
+ expected = '<v:shapetype id="_x0000_t201" coordsize="21600,21600" o:spt="201" path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/><v:path shadowok="f" o:extrusionok="f" strokeok="f" fillok="f" o:connecttype="rect"/><o:lock v:ext="edit" shapetype="t"/></v:shapetype>'
20
+ assert_equal(expected, result)
21
+ end
14
22
  end
@@ -6,7 +6,7 @@
6
6
  class TestWriteTextbox < Test::Unit::TestCase
7
7
  def test_write_textbox
8
8
  vml = Writexlsx::Package::Vml.new
9
- vml.__send__('write_textbox')
9
+ vml.__send__('write_comment_textbox')
10
10
  result = vml.instance_variable_get(:@writer).string
11
11
  expected = '<v:textbox style="mso-direction-alt:auto"><div style="text-align:left"></div></v:textbox>'
12
12
  assert_equal(expected, result)
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'helper'
3
+
4
+ class TestRegressionButton01 < Test::Unit::TestCase
5
+ def setup
6
+ setup_dir_var
7
+ end
8
+
9
+ def teardown
10
+ File.delete(@xlsx) if File.exist?(@xlsx)
11
+ end
12
+
13
+ def test_button01
14
+ @xlsx = 'button01.xlsx'
15
+ workbook = WriteXLSX.new(@xlsx)
16
+ worksheet = workbook.add_worksheet
17
+
18
+ worksheet.insert_button('C2', {})
19
+
20
+ workbook.close
21
+ compare_xlsx_for_regression(File.join(@regression_output, @xlsx), @xlsx)
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'helper'
3
+
4
+ class TestRegressionButton02 < Test::Unit::TestCase
5
+ def setup
6
+ setup_dir_var
7
+ end
8
+
9
+ def teardown
10
+ File.delete(@xlsx) if File.exist?(@xlsx)
11
+ end
12
+
13
+ def test_button02
14
+ @xlsx = 'button02.xlsx'
15
+ workbook = WriteXLSX.new(@xlsx)
16
+ worksheet = workbook.add_worksheet
17
+
18
+ worksheet.insert_button('B4',
19
+ {
20
+ :x_offset => 4,
21
+ :y_offset => 3,
22
+ :caption => 'my text'
23
+ }
24
+ )
25
+
26
+ workbook.close
27
+ compare_xlsx_for_regression(File.join(@regression_output, @xlsx), @xlsx)
28
+ end
29
+ end