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.
- 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)
|