write_xlsx 1.11.1 → 1.12.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/Changes +9 -0
- data/README.md +1 -1
- data/examples/shape_all.rb +1 -1
- data/lib/write_xlsx/chart/pie.rb +14 -2
- data/lib/write_xlsx/chart/series.rb +48 -0
- data/lib/write_xlsx/chart.rb +65 -12
- data/lib/write_xlsx/chartsheet.rb +10 -1
- data/lib/write_xlsx/col_name.rb +7 -3
- data/lib/write_xlsx/format.rb +6 -6
- data/lib/write_xlsx/package/app.rb +9 -5
- data/lib/write_xlsx/package/conditional_format.rb +2 -2
- data/lib/write_xlsx/package/content_types.rb +22 -0
- data/lib/write_xlsx/package/metadata.rb +139 -22
- data/lib/write_xlsx/package/packager.rb +122 -6
- data/lib/write_xlsx/package/relationships.rb +25 -0
- data/lib/write_xlsx/package/rich_value.rb +70 -0
- data/lib/write_xlsx/package/rich_value_rel.rb +70 -0
- data/lib/write_xlsx/package/rich_value_structure.rb +83 -0
- data/lib/write_xlsx/package/rich_value_types.rb +103 -0
- data/lib/write_xlsx/package/table.rb +74 -20
- data/lib/write_xlsx/package/xml_writer_simple.rb +32 -44
- data/lib/write_xlsx/sheets.rb +6 -2
- data/lib/write_xlsx/sparkline.rb +2 -2
- data/lib/write_xlsx/utility.rb +183 -9
- data/lib/write_xlsx/version.rb +1 -1
- data/lib/write_xlsx/workbook.rb +48 -168
- data/lib/write_xlsx/worksheet/cell_data.rb +35 -16
- data/lib/write_xlsx/worksheet/hyperlink.rb +4 -3
- data/lib/write_xlsx/worksheet.rb +180 -57
- data/write_xlsx.gemspec +2 -0
- metadata +35 -3
data/lib/write_xlsx/workbook.rb
CHANGED
@@ -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
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
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
|
-
#
|
1512
|
-
#
|
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
|
1516
|
-
|
1517
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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(
|
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(
|
110
|
+
url, url_str = url.split("#", 2)
|
110
111
|
|
111
112
|
# Escape URL unless it looks already escaped.
|
112
113
|
url = escape_url(url)
|