write_xlsx 1.11.2 → 1.12.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.
@@ -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.