write_xlsx 1.11.2 → 1.12.1

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
@@ -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
@@ -206,5 +206,24 @@ module Writexlsx
206
206
  worksheet.writer.empty_tag('c', cell_attributes(worksheet, row, row_name, col))
207
207
  end
208
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
226
+ end
227
+ end
209
228
  end
210
229
  end
@@ -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
 
@@ -113,7 +113,8 @@ module Writexlsx
113
113
  @drawing_rels_id = 0
114
114
  @vml_drawing_rels = {}
115
115
  @vml_drawing_rels_id = 0
116
- @has_dynamic_arrays = false
116
+ @has_dynamic_functions = false
117
+ @has_embedded_images = false
117
118
 
118
119
  @use_future_functions = false
119
120
 
@@ -160,6 +161,8 @@ module Writexlsx
160
161
  @page_setup.margin_footer = 0.5
161
162
  @page_setup.header_footer_aligns = false
162
163
  end
164
+
165
+ @embedded_image_indexes = @workbook.embedded_image_indexes
163
166
  end
164
167
 
165
168
  def set_xml_writer(filename) # :nodoc:
@@ -1446,6 +1449,7 @@ module Writexlsx
1446
1449
  formula = expand_formula(formula, 'HYPGEOM.DIST\(')
1447
1450
  formula = expand_formula(formula, 'IFNA\(')
1448
1451
  formula = expand_formula(formula, 'IFS\(')
1452
+ formula = expand_formula(formula, 'IMAGE\(')
1449
1453
  formula = expand_formula(formula, 'IMCOSH\(')
1450
1454
  formula = expand_formula(formula, 'IMCOT\(')
1451
1455
  formula = expand_formula(formula, 'IMCSCH\(')
@@ -1536,7 +1540,7 @@ module Writexlsx
1536
1540
  raise WriteXLSXInsufficientArgumentError if [_row, _col, _formula].include?(nil)
1537
1541
 
1538
1542
  # Check for dynamic array functions.
1539
- regex = /\bLET\(|\bSORT\(|\bLAMBDA\(|\bSINGLE\(|\bSORTBY\(|\bUNIQUE\(|\bXMATCH\(|\bFILTER\(|\bXLOOKUP\(|\bSEQUENCE\(|\bRANDARRAY\(|\bANCHORARRAY\(/
1543
+ regex = /\bANCHORARRAY\(|\bBYCOL\(|\bBYROW\(|\bCHOOSECOLS\(|\bCHOOSEROWS\(|\bDROP\(|\bEXPAND\(|\bFILTER\(|\bHSTACK\(|\bLAMBDA\(|\bMAKEARRAY\(|\bMAP\(|\bRANDARRAY\(|\bREDUCE\(|\bSCAN\(|\bSEQUENCE\(|\bSINGLE\(|\bSORT\(|\bSORTBY\(|\bSWITCH\(|\bTAKE\(|\bTEXTSPLIT\(|\bTOCOL\(|\bTOROW\(|\bUNIQUE\(|\bVSTACK\(|\bWRAPCOLS\(|\bWRAPROWS\(|\bXLOOKUP\(/
1540
1544
  if _formula =~ regex
1541
1545
  return write_dynamic_array_formula(
1542
1546
  _row, _col, _row, _col, _formula, _format, _value
@@ -1549,7 +1553,7 @@ module Writexlsx
1549
1553
  else
1550
1554
  check_dimensions(_row, _col)
1551
1555
  store_row_col_max_min_values(_row, _col)
1552
- _formula = _formula.sub(/^=/, '')
1556
+ _formula = prepare_formula(_formula)
1553
1557
 
1554
1558
  store_data_to_table(FormulaCellData.new(_formula, _format, _value), _row, _col)
1555
1559
  end
@@ -1634,7 +1638,7 @@ module Writexlsx
1634
1638
  #
1635
1639
  def write_dynamic_array_formula(row1, col1, row2 = nil, col2 = nil, formula = nil, format = nil, value = nil)
1636
1640
  write_array_formula_base('d', row1, col1, row2, col2, formula, format, value)
1637
- @has_dynamic_arrays = true
1641
+ @has_dynamic_functions = true
1638
1642
  end
1639
1643
 
1640
1644
  #
@@ -1782,21 +1786,23 @@ module Writexlsx
1782
1786
  # The label is written using the {#write()}[#method-i-write] method. Therefore it is
1783
1787
  # possible to write strings, numbers or formulas as labels.
1784
1788
  #
1785
- def write_url(row, col, url = nil, format = nil, str = nil, tip = nil)
1789
+ def write_url(row, col, url = nil, format = nil, str = nil, tip = nil, ignore_write_string = false)
1786
1790
  # Check for a cell reference in A1 notation and substitute row and column
1787
1791
  if (row_col_array = row_col_notation(row))
1788
- _row, _col = row_col_array
1789
- _url = col
1790
- _format = url
1791
- _str = format
1792
- _tip = str
1792
+ _row, _col = row_col_array
1793
+ _url = col
1794
+ _format = url
1795
+ _str = format
1796
+ _tip = str
1797
+ _ignore_write_string = tip
1793
1798
  else
1794
- _row = row
1795
- _col = col
1796
- _url = url
1797
- _format = format
1798
- _str = str
1799
- _tip = tip
1799
+ _row = row
1800
+ _col = col
1801
+ _url = url
1802
+ _format = format
1803
+ _str = str
1804
+ _tip = tip
1805
+ _ignore_write_string = ignore_write_string
1800
1806
  end
1801
1807
  _format, _str = _str, _format if _str.respond_to?(:xf_index) || !_format.respond_to?(:xf_index)
1802
1808
  raise WriteXLSXInsufficientArgumentError if [_row, _col, _url].include?(nil)
@@ -1814,7 +1820,7 @@ module Writexlsx
1814
1820
  _format ||= @default_url_format
1815
1821
 
1816
1822
  # Write the hyperlink string.
1817
- write_string(_row, _col, hyperlink.str, _format)
1823
+ write_string(_row, _col, hyperlink.str, _format) unless _ignore_write_string
1818
1824
  end
1819
1825
 
1820
1826
  #
@@ -1959,6 +1965,65 @@ module Writexlsx
1959
1965
  ]
1960
1966
  end
1961
1967
 
1968
+ #
1969
+ # Embed an image into the worksheet.
1970
+ #
1971
+ def embed_image(row, col, filename, options = nil)
1972
+ # Check for a cell reference in A1 notation and substitute row and column.
1973
+ if (row_col_array = row_col_notation(row))
1974
+ _row, _col = row_col_array
1975
+ image = col
1976
+ _options = filename
1977
+ else
1978
+ _row = row
1979
+ _col = col
1980
+ image = filename
1981
+ _options = options
1982
+ end
1983
+ xf, url, tip, description, decorative = []
1984
+
1985
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, image].include?(nil)
1986
+ raise "Couldn't locate #{image}" unless File.exist?(image)
1987
+
1988
+ # Check that row and col are valid and store max and min values
1989
+ check_dimensions(_row, _col)
1990
+ store_row_col_max_min_values(_row, _col)
1991
+
1992
+ if options
1993
+ xf = options[:cell_format]
1994
+ url = options[:url]
1995
+ tip = options[:tip]
1996
+ description = options[:description]
1997
+ decorative = options[:decorative]
1998
+ end
1999
+
2000
+ # Write the url without writing a string.
2001
+ if url
2002
+ xf ||= @default_url_format
2003
+
2004
+ write_url(row, col, url, xf, nil, tip, true)
2005
+ end
2006
+
2007
+ # Get the image properties, mainly for the type and checksum.
2008
+ image_properties = get_image_properties(image)
2009
+ type = image_properties[0]
2010
+ md5 = image_properties[6]
2011
+
2012
+ # Check for duplicate images.
2013
+ image_index = @embedded_image_indexes[md5]
2014
+
2015
+ unless ptrue?(image_index)
2016
+ @workbook.embedded_images << [image, type, description, decorative]
2017
+
2018
+ image_index = @workbook.embedded_images.size
2019
+ @embedded_image_indexes[md5] = image_index
2020
+ end
2021
+
2022
+ # Write the cell placeholder.
2023
+ store_data_to_table(EmbedImageCellData.new(image_index, xf), _row, _col)
2024
+ @has_embedded_images = true
2025
+ end
2026
+
1962
2027
  #
1963
2028
  # :call-seq:
1964
2029
  # repeat_formula(row, column, formula [ , format ])
@@ -2799,8 +2864,12 @@ module Writexlsx
2799
2864
  end
2800
2865
  end
2801
2866
 
2802
- def has_dynamic_arrays?
2803
- @has_dynamic_arrays
2867
+ def has_dynamic_functions?
2868
+ @has_dynamic_functions
2869
+ end
2870
+
2871
+ def has_embedded_images?
2872
+ @has_embedded_images
2804
2873
  end
2805
2874
 
2806
2875
  private
@@ -3278,6 +3347,7 @@ EOS
3278
3347
  end
3279
3348
 
3280
3349
  @drawing_links << ['/image', "../media/image#{image_id}.#{image_type}"] unless @drawing_rels[md5]
3350
+
3281
3351
  drawing.rel_index = drawing_rel_index(md5)
3282
3352
  end
3283
3353
  public :prepare_image
@@ -4256,9 +4326,10 @@ EOS
4256
4326
 
4257
4327
  # If the cell isn't a string then we have to add the url as
4258
4328
  # the string to display
4259
- if ptrue?(@cell_data_table) &&
4260
- ptrue?(@cell_data_table[row_num]) &&
4261
- ptrue?(@cell_data_table[row_num][col_num]) && @cell_data_table[row_num][col_num].display_url_string?
4329
+ if ptrue?(@cell_data_table) &&
4330
+ ptrue?(@cell_data_table[row_num]) &&
4331
+ ptrue?(@cell_data_table[row_num][col_num]) &&
4332
+ @cell_data_table[row_num][col_num].display_url_string?
4262
4333
  link.display_on
4263
4334
  end
4264
4335
 
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.11.2
4
+ version: 1.12.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-12-26 00:00:00.000000000 Z
11
+ date: 2024-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nkf
@@ -266,6 +266,10 @@ files:
266
266
  - lib/write_xlsx/package/metadata.rb
267
267
  - lib/write_xlsx/package/packager.rb
268
268
  - lib/write_xlsx/package/relationships.rb
269
+ - lib/write_xlsx/package/rich_value.rb
270
+ - lib/write_xlsx/package/rich_value_rel.rb
271
+ - lib/write_xlsx/package/rich_value_structure.rb
272
+ - lib/write_xlsx/package/rich_value_types.rb
269
273
  - lib/write_xlsx/package/shared_strings.rb
270
274
  - lib/write_xlsx/package/styles.rb
271
275
  - lib/write_xlsx/package/table.rb
@@ -304,7 +308,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
304
308
  - !ruby/object:Gem::Version
305
309
  version: '0'
306
310
  requirements: []
307
- rubygems_version: 3.5.3
311
+ rubygems_version: 3.4.19
308
312
  signing_key:
309
313
  specification_version: 4
310
314
  summary: write_xlsx is a gem to create a new file in the Excel 2007+ XLSX format.