write_xlsx 1.11.1 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -35,6 +35,9 @@ module Writexlsx
35
35
  attr_reader :max_url_length # :nodoc:
36
36
  attr_reader :strings_to_urls # :nodoc:
37
37
  attr_reader :read_only # :nodoc:
38
+ attr_reader :embedded_image_indexes # :nodec:
39
+ attr_reader :embedded_images # :nodoc:
40
+ attr_reader :embedded_descriptions # :nodoc:
38
41
 
39
42
  def initialize(file, *option_params)
40
43
  options, default_formats = process_workbook_options(*option_params)
@@ -80,6 +83,9 @@ module Writexlsx
80
83
  @has_comments = false
81
84
  @read_only = 0
82
85
  @has_metadata = false
86
+ @has_embedded_images = false
87
+ @has_embedded_descriptions = false
88
+
83
89
  if options[:max_url_length]
84
90
  @max_url_length = options[:max_url_length]
85
91
 
@@ -88,6 +94,10 @@ module Writexlsx
88
94
  # Structures for the shared strings data.
89
95
  @shared_strings = Package::SharedStrings.new
90
96
 
97
+ # Structures for embedded images.
98
+ @embedded_image_indexes = {}
99
+ @embedded_images = []
100
+
91
101
  # Formula calculation default settings.
92
102
  @calc_id = 124519
93
103
  @calc_mode = 'auto'
@@ -530,6 +540,10 @@ module Writexlsx
530
540
  !!@date_1904
531
541
  end
532
542
 
543
+ def has_dynamic_functions?
544
+ @has_dynamic_functions
545
+ end
546
+
533
547
  #
534
548
  # Add a string to the shared string table, if it isn't already there, and
535
549
  # return the string index.
@@ -597,6 +611,14 @@ module Writexlsx
597
611
  @has_metadata
598
612
  end
599
613
 
614
+ def has_embedded_images?
615
+ @has_embedded_images
616
+ end
617
+
618
+ def has_embedded_descriptions?
619
+ @has_embedded_descriptions
620
+ end
621
+
600
622
  private
601
623
 
602
624
  def filename
@@ -715,7 +737,7 @@ module Writexlsx
715
737
 
716
738
  # Split the cell range into 2 cells or else use single cell for both.
717
739
  if cells =~ /:/
718
- cell_1, cell_2 = cells.split(/:/)
740
+ cell_1, cell_2 = cells.split(":")
719
741
  else
720
742
  cell_1 = cells
721
743
  cell_2 = cells
@@ -724,7 +746,7 @@ module Writexlsx
724
746
  # Remove leading/trailing apostrophes and convert escaped quotes to single.
725
747
  sheetname.sub!(/^'/, '')
726
748
  sheetname.sub!(/'$/, '')
727
- sheetname.gsub!(/''/, "'")
749
+ sheetname.gsub!("''", "'")
728
750
 
729
751
  row_start, col_start = xl_cell_to_rowcol(cell_1)
730
752
  row_end, col_end = xl_cell_to_rowcol(cell_2)
@@ -1226,10 +1248,11 @@ module Writexlsx
1226
1248
  #
1227
1249
  def prepare_metadata
1228
1250
  @worksheets.each do |sheet|
1229
- if sheet.has_dynamic_arrays?
1230
- @has_metadata = true
1231
- break
1232
- end
1251
+ next unless sheet.has_dynamic_functions? || sheet.has_embedded_images?
1252
+
1253
+ @has_metadata = true
1254
+ @has_dynamic_functions ||= sheet.has_dynamic_functions?
1255
+ @has_embedded_images ||= sheet.has_embedded_images?
1233
1256
  end
1234
1257
  end
1235
1258
 
@@ -1380,12 +1403,22 @@ module Writexlsx
1380
1403
  #
1381
1404
  def prepare_drawings # :nodoc:
1382
1405
  chart_ref_id = 0
1383
- image_ref_id = 0
1384
1406
  drawing_id = 0
1385
1407
  ref_id = 0
1386
1408
  image_ids = {}
1387
1409
  header_image_ids = {}
1388
1410
  background_ids = {}
1411
+
1412
+ # Store the image types for any embedded images.
1413
+ @embedded_images.each do |image_data|
1414
+ store_image_types(image_data[1])
1415
+
1416
+ @has_embedded_descriptions = true if ptrue?(image_data[2])
1417
+ end
1418
+
1419
+ # The image IDs start from after the embedded images.
1420
+ image_ref_id = @embedded_images.size
1421
+
1389
1422
  @worksheets.each do |sheet|
1390
1423
  chart_count = sheet.charts.size
1391
1424
  image_count = sheet.images.size
@@ -1508,173 +1541,20 @@ module Writexlsx
1508
1541
  end
1509
1542
 
1510
1543
  #
1511
- # Extract information from the image file such as dimension, type, filename,
1512
- # and extension. Also keep track of previously seen images to optimise out
1513
- # any duplicates.
1544
+ # Store the image types (PNG/JPEG/etc) used in the workbook to use in these
1545
+ # Content_Types file.
1514
1546
  #
1515
- def get_image_properties(filename)
1516
- # Note the image_id, and previous_images mechanism isn't currently used.
1517
- x_dpi = 96
1518
- y_dpi = 96
1519
-
1520
- # Open the image file and import the data.
1521
- data = File.binread(filename)
1522
- md5 = Digest::MD5.hexdigest(data)
1523
- if data.unpack1('x A3') == 'PNG'
1524
- # Test for PNGs.
1525
- type, width, height, x_dpi, y_dpi = process_png(data)
1547
+ def store_image_types(type)
1548
+ case type
1549
+ when 'png'
1526
1550
  @image_types[:png] = 1
1527
- elsif data.unpack1('n') == 0xFFD8
1528
- # Test for JPEG files.
1529
- type, width, height, x_dpi, y_dpi = process_jpg(data, filename)
1551
+ when 'jpeg'
1530
1552
  @image_types[:jpeg] = 1
1531
- elsif data.unpack1('A4') == 'GIF8'
1532
- # Test for GIFs.
1533
- type, width, height, x_dpi, y_dpi = process_gif(data, filename)
1553
+ when 'gif'
1534
1554
  @image_types[:gif] = 1
1535
- elsif data.unpack1('A2') == 'BM'
1536
- # Test for BMPs.
1537
- type, width, height = process_bmp(data, filename)
1555
+ when 'bmp'
1538
1556
  @image_types[:bmp] = 1
1539
- else
1540
- # TODO. Add Image::Size to support other types.
1541
- raise "Unsupported image format for file: #{filename}\n"
1542
1557
  end
1543
-
1544
- # Set a default dpi for images with 0 dpi.
1545
- x_dpi = 96 if x_dpi == 0
1546
- y_dpi = 96 if y_dpi == 0
1547
-
1548
- [type, width, height, File.basename(filename), x_dpi, y_dpi, md5]
1549
- end
1550
-
1551
- #
1552
- # Extract width and height information from a PNG file.
1553
- #
1554
- def process_png(data)
1555
- type = 'png'
1556
- width = 0
1557
- height = 0
1558
- x_dpi = 96
1559
- y_dpi = 96
1560
-
1561
- offset = 8
1562
- data_length = data.size
1563
-
1564
- # Search through the image data to read the height and width in th the
1565
- # IHDR element. Also read the DPI in the pHYs element.
1566
- while offset < data_length
1567
-
1568
- length = data[offset + 0, 4].unpack1("N")
1569
- png_type = data[offset + 4, 4].unpack1("A4")
1570
-
1571
- case png_type
1572
- when "IHDR"
1573
- width = data[offset + 8, 4].unpack1("N")
1574
- height = data[offset + 12, 4].unpack1("N")
1575
- when "pHYs"
1576
- x_ppu = data[offset + 8, 4].unpack1("N")
1577
- y_ppu = data[offset + 12, 4].unpack1("N")
1578
- units = data[offset + 16, 1].unpack1("C")
1579
-
1580
- if units == 1
1581
- x_dpi = x_ppu * 0.0254
1582
- y_dpi = y_ppu * 0.0254
1583
- end
1584
- end
1585
-
1586
- offset = offset + length + 12
1587
-
1588
- break if png_type == "IEND"
1589
- end
1590
- raise "#{filename}: no size data found in png image.\n" unless height
1591
-
1592
- [type, width, height, x_dpi, y_dpi]
1593
- end
1594
-
1595
- def process_jpg(data, filename)
1596
- type = 'jpeg'
1597
- x_dpi = 96
1598
- y_dpi = 96
1599
-
1600
- offset = 2
1601
- data_length = data.bytesize
1602
-
1603
- # Search through the image data to read the JPEG markers.
1604
- while offset < data_length
1605
- marker = data[offset + 0, 2].unpack1("n")
1606
- length = data[offset + 2, 2].unpack1("n")
1607
-
1608
- # Read the height and width in the 0xFFCn elements
1609
- # (Except C4, C8 and CC which aren't SOF markers).
1610
- if (marker & 0xFFF0) == 0xFFC0 &&
1611
- marker != 0xFFC4 && marker != 0xFFCC
1612
- height = data[offset + 5, 2].unpack1("n")
1613
- width = data[offset + 7, 2].unpack1("n")
1614
- end
1615
-
1616
- # Read the DPI in the 0xFFE0 element.
1617
- if marker == 0xFFE0
1618
- units = data[offset + 11, 1].unpack1("C")
1619
- x_density = data[offset + 12, 2].unpack1("n")
1620
- y_density = data[offset + 14, 2].unpack1("n")
1621
-
1622
- if units == 1
1623
- x_dpi = x_density
1624
- y_dpi = y_density
1625
- elsif units == 2
1626
- x_dpi = x_density * 2.54
1627
- y_dpi = y_density * 2.54
1628
- end
1629
- end
1630
-
1631
- offset += length + 2
1632
- break if marker == 0xFFDA
1633
- end
1634
-
1635
- raise "#{filename}: no size data found in jpeg image.\n" unless height
1636
-
1637
- [type, width, height, x_dpi, y_dpi]
1638
- end
1639
-
1640
- #
1641
- # Extract width and height information from a GIF file.
1642
- #
1643
- def process_gif(data, filename)
1644
- type = 'gif'
1645
- x_dpi = 96
1646
- y_dpi = 96
1647
-
1648
- width = data[6, 2].unpack1("v")
1649
- height = data[8, 2].unpack1("v")
1650
-
1651
- raise "#{filename}: no size data found in gif image.\n" if height.nil?
1652
-
1653
- [type, width, height, x_dpi, y_dpi]
1654
- end
1655
-
1656
- # Extract width and height information from a BMP file.
1657
- def process_bmp(data, filename) # :nodoc:
1658
- type = 'bmp'
1659
-
1660
- # Check that the file is big enough to be a bitmap.
1661
- raise "#{filename} doesn't contain enough data." if data.bytesize <= 0x36
1662
-
1663
- # Read the bitmap width and height. Verify the sizes.
1664
- width, height = data.unpack("x18 V2")
1665
- raise "#{filename}: largest image width #{width} supported is 65k." if width > 0xFFFF
1666
- raise "#{filename}: largest image height supported is 65k." if height > 0xFFFF
1667
-
1668
- # Read the bitmap planes and bpp data. Verify them.
1669
- planes, bitcount = data.unpack("x26 v2")
1670
- raise "#{filename} isn't a 24bit true color bitmap." unless bitcount == 24
1671
- raise "#{filename}: only 1 plane supported in bitmap image." unless planes == 1
1672
-
1673
- # Read the bitmap compression. Verify compression.
1674
- compression = data.unpack1("x30 V")
1675
- raise "#{filename}: compression not supported in bitmap image." unless compression == 0
1676
-
1677
- [type, width, height]
1678
1558
  end
1679
1559
  end
1680
1560
  end
@@ -12,10 +12,10 @@ module Writexlsx
12
12
  # attributes for the <cell> element. This is the innermost loop so efficiency is
13
13
  # important where possible.
14
14
  #
15
- def cell_attributes(worksheet, row, col) # :nodoc:
15
+ def cell_attributes(worksheet, row, row_name, col) # :nodoc:
16
16
  xf_index = xf ? xf.get_xf_index : 0
17
17
  attributes = [
18
- ['r', xl_rowcol_to_cell(row, col)]
18
+ ['r', xl_rowcol_to_cell(row_name, col)]
19
19
  ]
20
20
 
21
21
  # Add the cell format index.
@@ -48,8 +48,8 @@ module Writexlsx
48
48
  @token
49
49
  end
50
50
 
51
- def write_cell(worksheet, row, col)
52
- worksheet.writer.tag_elements('c', cell_attributes(worksheet, row, col)) do
51
+ def write_cell(worksheet, row, row_name, col)
52
+ worksheet.writer.tag_elements('c', cell_attributes(worksheet, row, row_name, col)) do
53
53
  worksheet.write_cell_value(token)
54
54
  end
55
55
  end
@@ -69,8 +69,8 @@ module Writexlsx
69
69
  end
70
70
 
71
71
  TYPE_STR_ATTRS = %w[t s].freeze
72
- def write_cell(worksheet, row, col)
73
- attributes = cell_attributes(worksheet, row, col)
72
+ def write_cell(worksheet, row, row_name, col)
73
+ attributes = cell_attributes(worksheet, row, row_name, col)
74
74
  attributes << TYPE_STR_ATTRS
75
75
  worksheet.writer.tag_elements('c', attributes) do
76
76
  worksheet.write_cell_value(token)
@@ -101,11 +101,11 @@ module Writexlsx
101
101
  @result || 0
102
102
  end
103
103
 
104
- def write_cell(worksheet, row, col)
104
+ def write_cell(worksheet, row, row_name, col)
105
105
  truefalse = { 'TRUE' => 1, 'FALSE' => 0 }
106
106
  error_code = ['#DIV/0!', '#N/A', '#NAME?', '#NULL!', '#NUM!', '#REF!', '#VALUE!']
107
107
 
108
- attributes = cell_attributes(worksheet, row, col)
108
+ attributes = cell_attributes(worksheet, row, row_name, col)
109
109
  if @result && !(@result.to_s =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/)
110
110
  if truefalse[@result]
111
111
  attributes << %w[t b]
@@ -137,8 +137,8 @@ module Writexlsx
137
137
  @result || 0
138
138
  end
139
139
 
140
- def write_cell(worksheet, row, col)
141
- worksheet.writer.tag_elements('c', cell_attributes(worksheet, row, col)) do
140
+ def write_cell(worksheet, row, row_name, col)
141
+ worksheet.writer.tag_elements('c', cell_attributes(worksheet, row, row_name, col)) do
142
142
  worksheet.write_cell_array_formula(token, range)
143
143
  worksheet.write_cell_value(result)
144
144
  end
@@ -159,9 +159,9 @@ module Writexlsx
159
159
  @result || 0
160
160
  end
161
161
 
162
- def write_cell(worksheet, row, col)
162
+ def write_cell(worksheet, row, row_name, col)
163
163
  # Add metadata linkage for dynamic array formulas.
164
- attributes = cell_attributes(worksheet, row, col)
164
+ attributes = cell_attributes(worksheet, row, row_name, col)
165
165
  attributes << %w[cm 1]
166
166
 
167
167
  worksheet.writer.tag_elements('c', attributes) do
@@ -183,8 +183,8 @@ module Writexlsx
183
183
  @token
184
184
  end
185
185
 
186
- def write_cell(worksheet, row, col)
187
- attributes = cell_attributes(worksheet, row, col)
186
+ def write_cell(worksheet, row, row_name, col)
187
+ attributes = cell_attributes(worksheet, row, row_name, col)
188
188
 
189
189
  attributes << %w[t b]
190
190
  worksheet.writer.tag_elements('c', attributes) do
@@ -202,8 +202,27 @@ module Writexlsx
202
202
  ''
203
203
  end
204
204
 
205
- def write_cell(worksheet, row, col)
206
- worksheet.writer.empty_tag('c', cell_attributes(worksheet, row, col))
205
+ def write_cell(worksheet, row, row_name, col)
206
+ worksheet.writer.empty_tag('c', cell_attributes(worksheet, row, row_name, col))
207
+ end
208
+ end
209
+
210
+ class EmbedImageCellData < CellData # :nodoc:
211
+ def initialize(image_index, xf)
212
+ @image_index = image_index
213
+ @xf = xf
214
+ end
215
+
216
+ def write_cell(worksheet, row, row_name, col)
217
+ attributes = cell_attributes(worksheet, row, row_name, col)
218
+
219
+ # Write a error value (mainly for embedded images).
220
+ attributes << %w[t e]
221
+ attributes << ['vm', @image_index]
222
+
223
+ worksheet.writer.tag_elements('c', attributes) do
224
+ worksheet.write_cell_value('#VALUE!')
225
+ end
207
226
  end
208
227
  end
209
228
  end
@@ -28,7 +28,7 @@ module Writexlsx
28
28
  normalized_str = str.sub(/^mailto:/, '')
29
29
 
30
30
  # Split url into the link and optional anchor/location.
31
- url, @url_str = url.split(/#/, 2)
31
+ url, @url_str = url.split("#", 2)
32
32
 
33
33
  # Escape URL unless it looks already escaped.
34
34
  url = escape_url(url)
@@ -58,7 +58,8 @@ module Writexlsx
58
58
  end
59
59
 
60
60
  def display_on
61
- @display = @url_str
61
+ # @display = @url_str
62
+ @display = @str
62
63
  end
63
64
  end
64
65
 
@@ -106,7 +107,7 @@ module Writexlsx
106
107
  str = str.sub(/^mailto:/, '')
107
108
 
108
109
  # Split url into the link and optional anchor/location.
109
- url, url_str = url.split(/#/, 2)
110
+ url, url_str = url.split("#", 2)
110
111
 
111
112
  # Escape URL unless it looks already escaped.
112
113
  url = escape_url(url)