write_xlsx 1.12.2 → 1.13.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 +7 -0
- data/Changes +6 -0
- data/README.md +1 -1
- data/lib/write_xlsx/chartsheet.rb +4 -1
- data/lib/write_xlsx/object_positioning.rb +189 -0
- data/lib/write_xlsx/package/app.rb +2 -1
- data/lib/write_xlsx/page_setup.rb +190 -0
- data/lib/write_xlsx/sheets.rb +3 -3
- data/lib/write_xlsx/utility.rb +57 -9
- data/lib/write_xlsx/version.rb +1 -1
- data/lib/write_xlsx/workbook.rb +34 -34
- data/lib/write_xlsx/worksheet/asset_manager.rb +60 -0
- data/lib/write_xlsx/worksheet/autofilter.rb +388 -0
- data/lib/write_xlsx/worksheet/cell_data.rb +8 -5
- data/lib/write_xlsx/worksheet/cell_data_manager.rb +47 -0
- data/lib/write_xlsx/worksheet/cell_data_store.rb +61 -0
- data/lib/write_xlsx/worksheet/columns.rb +199 -0
- data/lib/write_xlsx/worksheet/comments_support.rb +61 -0
- data/lib/write_xlsx/worksheet/conditional_formats.rb +30 -0
- data/lib/write_xlsx/worksheet/data_writing.rb +990 -0
- data/lib/write_xlsx/worksheet/drawing_methods.rb +308 -0
- data/lib/write_xlsx/worksheet/drawing_preparation.rb +290 -0
- data/lib/write_xlsx/worksheet/drawing_relations.rb +76 -0
- data/lib/write_xlsx/worksheet/drawing_xml_writer.rb +50 -0
- data/lib/write_xlsx/worksheet/formatting.rb +416 -0
- data/lib/write_xlsx/worksheet/initialization.rb +146 -0
- data/lib/write_xlsx/worksheet/panes.rb +64 -0
- data/lib/write_xlsx/worksheet/print_options.rb +72 -0
- data/lib/write_xlsx/worksheet/protection.rb +65 -0
- data/lib/write_xlsx/worksheet/rich_text_helpers.rb +78 -0
- data/lib/write_xlsx/worksheet/row_col_sizing.rb +67 -0
- data/lib/write_xlsx/worksheet/rows.rb +84 -0
- data/lib/write_xlsx/worksheet/selection.rb +41 -0
- data/lib/write_xlsx/worksheet/xml_writer.rb +1241 -0
- data/lib/write_xlsx/worksheet.rb +359 -4530
- data/lib/write_xlsx/zip_file_utils.rb +1 -1
- data/write_xlsx.gemspec +1 -1
- metadata +34 -5
- data/lib/write_xlsx/worksheet/page_setup.rb +0 -192
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Writexlsx
|
|
5
|
+
class Worksheet
|
|
6
|
+
module DrawingMethods
|
|
7
|
+
###############################################################################
|
|
8
|
+
#
|
|
9
|
+
# DrawingMethods
|
|
10
|
+
#
|
|
11
|
+
# Provides high-level Worksheet APIs for inserting drawing-related objects
|
|
12
|
+
# such as charts, images, shapes, tables, and sparklines.
|
|
13
|
+
#
|
|
14
|
+
# Responsibilities:
|
|
15
|
+
# - Public insertion entry points used by Worksheet users
|
|
16
|
+
# - Argument normalization and option extraction
|
|
17
|
+
# - Shape insertion workflow and helper utilities
|
|
18
|
+
#
|
|
19
|
+
# This module handles *what gets added to the sheet*.
|
|
20
|
+
# It does not manage relationships or XML output.
|
|
21
|
+
#
|
|
22
|
+
###############################################################################
|
|
23
|
+
#
|
|
24
|
+
# This method can be used to insert a Chart object into a worksheet.
|
|
25
|
+
# The Chart must be created by the add_chart() Workbook method and
|
|
26
|
+
# it must have the embedded option set.
|
|
27
|
+
#
|
|
28
|
+
def insert_chart(row, col, chart = nil, *options)
|
|
29
|
+
normalized_row, normalized_col, normalized_chart, normalized_options =
|
|
30
|
+
normalize_row_col_args(row, col, chart, options)
|
|
31
|
+
raise WriteXLSXInsufficientArgumentError if [normalized_row, normalized_col, normalized_chart].include?(nil)
|
|
32
|
+
|
|
33
|
+
x_offset, y_offset, x_scale, y_scale, anchor, description, decorative =
|
|
34
|
+
extract_chart_options(normalized_options)
|
|
35
|
+
|
|
36
|
+
raise "Not a Chart object in insert_chart()" unless normalized_chart.is_a?(Chart) || normalized_chart.is_a?(Chartsheet)
|
|
37
|
+
raise "Not a embedded style Chart object in insert_chart()" if normalized_chart.respond_to?(:embedded) && normalized_chart.embedded == 0
|
|
38
|
+
|
|
39
|
+
if normalized_chart.already_inserted? || (normalized_chart.combined && normalized_chart.combined.already_inserted?)
|
|
40
|
+
raise "Chart cannot be inserted in a worksheet more than once"
|
|
41
|
+
else
|
|
42
|
+
normalized_chart.already_inserted = true
|
|
43
|
+
normalized_chart.combined.already_inserted = true if normalized_chart.combined
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Use the values set with chart.set_size, if any.
|
|
47
|
+
x_scale = normalized_chart.x_scale if normalized_chart.x_scale != 1
|
|
48
|
+
y_scale = normalized_chart.y_scale if normalized_chart.y_scale != 1
|
|
49
|
+
x_offset = normalized_chart.x_offset if ptrue?(normalized_chart.x_offset)
|
|
50
|
+
y_offset = normalized_chart.y_offset if ptrue?(normalized_chart.y_offset)
|
|
51
|
+
|
|
52
|
+
@assets.add_chart(
|
|
53
|
+
InsertedChart.new(
|
|
54
|
+
normalized_row, normalized_col, normalized_chart,
|
|
55
|
+
x_offset, y_offset, x_scale, y_scale, anchor, description, decorative
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def insert_image(row, col, image = nil, *options)
|
|
61
|
+
normalized_row, normalized_col, normalized_image, normalized_options =
|
|
62
|
+
normalize_row_col_args(row, col, image, options)
|
|
63
|
+
raise WriteXLSXInsufficientArgumentError if [normalized_row, normalized_col, normalized_image].include?(nil)
|
|
64
|
+
|
|
65
|
+
x_offset, y_offset, x_scale, y_scale,
|
|
66
|
+
anchor, url, tip, description, decorative = extract_image_options(normalized_options)
|
|
67
|
+
|
|
68
|
+
@assets.add_image(
|
|
69
|
+
Image.new(
|
|
70
|
+
normalized_row, normalized_col, normalized_image, x_offset, y_offset,
|
|
71
|
+
x_scale, y_scale, url, tip, anchor, description, decorative
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def embed_image(row, col, filename, options = nil)
|
|
77
|
+
normalize_row, normalize_col, image, normalize_options = normalize_row_col_args(row, col, filename, options)
|
|
78
|
+
|
|
79
|
+
raise WriteXLSXInsufficientArgumentError if [normalize_row, normalize_col, image].include?(nil)
|
|
80
|
+
raise "Couldn't locate #{image}" unless File.exist?(image)
|
|
81
|
+
|
|
82
|
+
# Check that row and col are valid and store max and min values
|
|
83
|
+
check_dimensions(normalize_row, normalize_col)
|
|
84
|
+
store_row_col_max_min_values(normalize_row, normalize_col)
|
|
85
|
+
|
|
86
|
+
if options
|
|
87
|
+
xf = options[:cell_format]
|
|
88
|
+
url = options[:url]
|
|
89
|
+
tip = options[:tip]
|
|
90
|
+
description = options[:description]
|
|
91
|
+
decorative = options[:decorative]
|
|
92
|
+
else
|
|
93
|
+
xf, url, tip, description, decorative = []
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Write the url without writing a string.
|
|
97
|
+
if url
|
|
98
|
+
xf ||= @default_url_format
|
|
99
|
+
|
|
100
|
+
write_url(row, col, url, xf, nil, tip, true)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get the image properties, mainly for the type and checksum.
|
|
104
|
+
image_property = ImageProperty.new(
|
|
105
|
+
image, description: description, decorative: decorative
|
|
106
|
+
)
|
|
107
|
+
@workbook.store_image_types(image_property.type)
|
|
108
|
+
|
|
109
|
+
# Check for duplicate images.
|
|
110
|
+
image_index = @embedded_image_indexes[image_property.md5]
|
|
111
|
+
|
|
112
|
+
unless ptrue?(image_index)
|
|
113
|
+
@workbook.embedded_images << image_property
|
|
114
|
+
|
|
115
|
+
image_index = @workbook.embedded_images.size
|
|
116
|
+
@embedded_image_indexes[image_property.md5] = image_index
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Write the cell placeholder.
|
|
120
|
+
store_data_to_table(EmbedImageCellData.new(image_index, xf), normalize_row, normalize_col)
|
|
121
|
+
@has_embedded_images = true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#
|
|
125
|
+
# :call-seq:
|
|
126
|
+
# insert_shape(row, col, shape [ , x, y, x_scale, y_scale ])
|
|
127
|
+
#
|
|
128
|
+
# Insert a shape into the worksheet.
|
|
129
|
+
#
|
|
130
|
+
def insert_shape(
|
|
131
|
+
row_start, column_start, shape = nil, x_offset = nil, y_offset = nil,
|
|
132
|
+
x_scale = nil, y_scale = nil, anchor = nil
|
|
133
|
+
)
|
|
134
|
+
row, col, normalized_shape, normalized_options =
|
|
135
|
+
normalize_shape_args(row_start, column_start, shape, x_offset, y_offset, x_scale, y_scale, anchor)
|
|
136
|
+
raise "Insufficient arguments in insert_shape()" if [row, col, normalized_shape].include?(nil)
|
|
137
|
+
|
|
138
|
+
set_shape_position(normalized_shape, row, col, normalized_options)
|
|
139
|
+
assign_shape_id(normalized_shape)
|
|
140
|
+
|
|
141
|
+
# Allow lookup of entry into shape array by shape ID.
|
|
142
|
+
@shape_hash[normalized_shape.id] = normalized_shape.element = shapes.size
|
|
143
|
+
|
|
144
|
+
inserted_shape = build_inserted_shape(normalized_shape)
|
|
145
|
+
|
|
146
|
+
# For connectors change x/y coords based on location of connected shapes.
|
|
147
|
+
auto_locate_shape_connectors(inserted_shape)
|
|
148
|
+
|
|
149
|
+
# Insert a link to the shape on the list of shapes. Connection to
|
|
150
|
+
# the parent shape is maintained.
|
|
151
|
+
@assets.add_shape(inserted_shape)
|
|
152
|
+
inserted_shape
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
#
|
|
156
|
+
# :call-seq:
|
|
157
|
+
# add_table(row1, col1, row2, col2, properties)
|
|
158
|
+
#
|
|
159
|
+
# Add an Excel table to a worksheet.
|
|
160
|
+
#
|
|
161
|
+
def add_table(*args)
|
|
162
|
+
# Table count is a member of Workbook, global to all Worksheet.
|
|
163
|
+
table = Package::Table.new(self, *args)
|
|
164
|
+
@assets.add_table(table)
|
|
165
|
+
table
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
#
|
|
169
|
+
# :call-seq:
|
|
170
|
+
# add_sparkline(properties)
|
|
171
|
+
#
|
|
172
|
+
# Add sparklines to the worksheet.
|
|
173
|
+
#
|
|
174
|
+
def add_sparkline(param)
|
|
175
|
+
@assets.add_sparkline(Sparkline.new(self, param, quote_sheetname(@name)))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def normalize_row_col_args(row, col, object, options)
|
|
181
|
+
if (row_col_array = row_col_notation(row))
|
|
182
|
+
normalized_row, normalized_col = row_col_array
|
|
183
|
+
normalized_object = col
|
|
184
|
+
normalized_options = [object] + options
|
|
185
|
+
else
|
|
186
|
+
normalized_row = row
|
|
187
|
+
normalized_col = col
|
|
188
|
+
normalized_object = object
|
|
189
|
+
normalized_options = options
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
[normalized_row, normalized_col, normalized_object, normalized_options]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def extract_chart_options(options)
|
|
196
|
+
if options.first.instance_of?(Hash)
|
|
197
|
+
params = options.first
|
|
198
|
+
[
|
|
199
|
+
params[:x_offset] || 0,
|
|
200
|
+
params[:y_offset] || 0,
|
|
201
|
+
params[:x_scale] || 1,
|
|
202
|
+
params[:y_scale] || 1,
|
|
203
|
+
params[:object_position] || 1,
|
|
204
|
+
params[:description],
|
|
205
|
+
params[:decorative]
|
|
206
|
+
]
|
|
207
|
+
else
|
|
208
|
+
x_offset, y_offset, x_scale, y_scale, anchor = options
|
|
209
|
+
[x_offset || 0, y_offset || 0, x_scale || 1, y_scale || 1, anchor || 1, nil, nil]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def extract_image_options(options)
|
|
214
|
+
if options.first.instance_of?(Hash)
|
|
215
|
+
params = options.first
|
|
216
|
+
[
|
|
217
|
+
params[:x_offset] || 0,
|
|
218
|
+
params[:y_offset] || 0,
|
|
219
|
+
params[:x_scale] || 1,
|
|
220
|
+
params[:y_scale] || 1,
|
|
221
|
+
params[:object_position] || 2,
|
|
222
|
+
params[:url],
|
|
223
|
+
params[:tip],
|
|
224
|
+
params[:description],
|
|
225
|
+
params[:decorative]
|
|
226
|
+
]
|
|
227
|
+
else
|
|
228
|
+
x_offset, y_offset, x_scale, y_scale, anchor = options
|
|
229
|
+
[x_offset || 0, y_offset || 0, x_scale || 1, y_scale || 1, anchor || 2, nil, nil, nil, nil]
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def normalize_shape_args(row_start, column_start, shape,
|
|
234
|
+
x_offset, y_offset, x_scale, y_scale, anchor)
|
|
235
|
+
if (row_col_array = row_col_notation(row_start))
|
|
236
|
+
normalized_row, normalized_col = row_col_array
|
|
237
|
+
normalized_shape = column_start
|
|
238
|
+
normalized_x_offset = shape
|
|
239
|
+
normalized_y_offset = x_offset
|
|
240
|
+
normalized_x_scale = y_offset
|
|
241
|
+
normalized_y_scale = x_scale
|
|
242
|
+
normalized_anchor = y_scale
|
|
243
|
+
else
|
|
244
|
+
normalized_row = row_start
|
|
245
|
+
normalized_col = column_start
|
|
246
|
+
normalized_shape = shape
|
|
247
|
+
normalized_x_offset = x_offset
|
|
248
|
+
normalized_y_offset = y_offset
|
|
249
|
+
normalized_x_scale = x_scale
|
|
250
|
+
normalized_y_scale = y_scale
|
|
251
|
+
normalized_anchor = anchor
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
[
|
|
255
|
+
normalized_row,
|
|
256
|
+
normalized_col,
|
|
257
|
+
normalized_shape,
|
|
258
|
+
{
|
|
259
|
+
x_offset: normalized_x_offset,
|
|
260
|
+
y_offset: normalized_y_offset,
|
|
261
|
+
x_scale: normalized_x_scale,
|
|
262
|
+
y_scale: normalized_y_scale,
|
|
263
|
+
anchor: normalized_anchor
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def set_shape_position(shape, row, col, options)
|
|
269
|
+
shape.set_position(
|
|
270
|
+
row, col,
|
|
271
|
+
options[:x_offset],
|
|
272
|
+
options[:y_offset],
|
|
273
|
+
options[:x_scale],
|
|
274
|
+
options[:y_scale],
|
|
275
|
+
options[:anchor]
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def assign_shape_id(shape)
|
|
280
|
+
loop do
|
|
281
|
+
id = shape.id || 0
|
|
282
|
+
used = @shape_hash[id]
|
|
283
|
+
|
|
284
|
+
if !used && id != 0
|
|
285
|
+
break
|
|
286
|
+
else
|
|
287
|
+
@last_shape_id += 1
|
|
288
|
+
shape.id = @last_shape_id
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
@shape_hash[shape.id] = shape.element = shapes.size
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def build_inserted_shape(shape)
|
|
296
|
+
if ptrue?(shape.stencil)
|
|
297
|
+
shape.dup
|
|
298
|
+
else
|
|
299
|
+
shape
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def auto_locate_shape_connectors(shape)
|
|
304
|
+
shape.auto_locate_connectors(shapes, @shape_hash)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Writexlsx
|
|
5
|
+
class Worksheet
|
|
6
|
+
module DrawingPreparation
|
|
7
|
+
###############################################################################
|
|
8
|
+
#
|
|
9
|
+
# DrawingPreparation
|
|
10
|
+
#
|
|
11
|
+
# Prepares drawing-related assets for XLSX output.
|
|
12
|
+
#
|
|
13
|
+
# Responsibilities:
|
|
14
|
+
# - Convert inserted charts, images, shapes, and media into Drawing objects
|
|
15
|
+
# - Register drawing assets with the Workbook and Drawings containers
|
|
16
|
+
# - Prepare background, header/footer images, and VML objects
|
|
17
|
+
# - Coordinate drawing setup before XML serialization
|
|
18
|
+
#
|
|
19
|
+
# This module handles *how drawing assets are assembled for output*.
|
|
20
|
+
# Relationship management and XML writing are handled elsewhere.
|
|
21
|
+
#
|
|
22
|
+
###############################################################################
|
|
23
|
+
|
|
24
|
+
# Drawing preparation
|
|
25
|
+
|
|
26
|
+
def prepare_drawings(drawing_id, chart_ref_id, image_ref_id, image_ids, header_image_ids, background_ids)
|
|
27
|
+
has_drawings = false
|
|
28
|
+
|
|
29
|
+
# Check that some image or drawing needs to be processed.
|
|
30
|
+
unless some_image_or_drawing_to_be_processed?
|
|
31
|
+
|
|
32
|
+
# Don't increase the drawing_id header/footer images.
|
|
33
|
+
unless charts.empty? && images.empty? && shapes.empty?
|
|
34
|
+
drawing_id += 1
|
|
35
|
+
has_drawings = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Prepare the background images.
|
|
39
|
+
image_ref_id = prepare_background_image(background_ids, image_ref_id)
|
|
40
|
+
|
|
41
|
+
# Prepare the worksheet images.
|
|
42
|
+
images.each do |image|
|
|
43
|
+
image_ref_id = prepare_image(image, drawing_id, image_ids, image_ref_id)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Prepare the worksheet charts.
|
|
47
|
+
charts.each_with_index do |_chart, index|
|
|
48
|
+
chart_ref_id += 1
|
|
49
|
+
prepare_chart(index, chart_ref_id, drawing_id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Prepare the worksheet shapes.
|
|
53
|
+
shapes.each_with_index do |_shape, index|
|
|
54
|
+
prepare_shape(index, drawing_id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Prepare the header and footer images.
|
|
58
|
+
[header_images, footer_images].each do |images|
|
|
59
|
+
images.each do |image|
|
|
60
|
+
image_ref_id = prepare_header_footer_image(
|
|
61
|
+
image, header_image_ids, image_ref_id
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if has_drawings
|
|
67
|
+
@workbook.drawings << drawings
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
[drawing_id, chart_ref_id, image_ref_id]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
#
|
|
75
|
+
# Set up chart/drawings.
|
|
76
|
+
#
|
|
77
|
+
def prepare_chart(index, chart_id, drawing_id) # :nodoc:
|
|
78
|
+
drawing_type = 1
|
|
79
|
+
|
|
80
|
+
inserted_chart = charts[index]
|
|
81
|
+
inserted_chart.chart.id = chart_id - 1
|
|
82
|
+
|
|
83
|
+
dimensions = position_object_emus(inserted_chart)
|
|
84
|
+
|
|
85
|
+
# Create a Drawing object to use with worksheet unless one already exists.
|
|
86
|
+
drawing = Drawing.new(
|
|
87
|
+
drawing_type, dimensions, 0, 0, nil, inserted_chart.anchor,
|
|
88
|
+
drawing_rel_index, 0, nil, inserted_chart.name,
|
|
89
|
+
inserted_chart.description, inserted_chart.decorative
|
|
90
|
+
)
|
|
91
|
+
if drawings?
|
|
92
|
+
@drawings.add_drawing_object(drawing)
|
|
93
|
+
else
|
|
94
|
+
@drawings = Drawings.new
|
|
95
|
+
@drawings.add_drawing_object(drawing)
|
|
96
|
+
@drawings.embedded = true
|
|
97
|
+
|
|
98
|
+
@external_drawing_links << ['/drawing', "../drawings/drawing#{drawing_id}.xml"]
|
|
99
|
+
end
|
|
100
|
+
@drawing_links << ['/chart', "../charts/chart#{chart_id}.xml"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
#
|
|
104
|
+
# Set up image/drawings.
|
|
105
|
+
#
|
|
106
|
+
def prepare_image(image, drawing_id, image_ids, image_ref_id) # :nodoc:
|
|
107
|
+
image_type = image.type
|
|
108
|
+
x_dpi = image.x_dpi || 96
|
|
109
|
+
y_dpi = image.y_dpi || 96
|
|
110
|
+
md5 = image.md5
|
|
111
|
+
drawing_type = 2
|
|
112
|
+
|
|
113
|
+
@workbook.store_image_types(image_type)
|
|
114
|
+
|
|
115
|
+
if image_ids[md5]
|
|
116
|
+
image_id = image_ids[md5]
|
|
117
|
+
else
|
|
118
|
+
image_ref_id += 1
|
|
119
|
+
image_ids[md5] = image_id = image_ref_id
|
|
120
|
+
@workbook.images << image
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
dimensions = position_object_emus(image)
|
|
124
|
+
|
|
125
|
+
# Create a Drawing object to use with worksheet unless one already exists.
|
|
126
|
+
drawing = Drawing.new(
|
|
127
|
+
drawing_type, dimensions, image.width_emus, image.height_emus,
|
|
128
|
+
nil, image.anchor, 0, 0, image.tip, image.name,
|
|
129
|
+
image.description || image.name, image.decorative
|
|
130
|
+
)
|
|
131
|
+
unless drawings?
|
|
132
|
+
@drawings = Drawings.new
|
|
133
|
+
@drawings.embedded = true
|
|
134
|
+
|
|
135
|
+
@external_drawing_links << ['/drawing', "../drawings/drawing#{drawing_id}.xml"]
|
|
136
|
+
end
|
|
137
|
+
@drawings.add_drawing_object(drawing)
|
|
138
|
+
|
|
139
|
+
if image.url
|
|
140
|
+
target_mode = 'External'
|
|
141
|
+
target = escape_url(image.url) if image.url =~ %r{^[fh]tt?ps?://} || image.url =~ /^mailto:/
|
|
142
|
+
if image.url =~ /^external:/
|
|
143
|
+
target = escape_url(image.url.sub(/^external:/, ''))
|
|
144
|
+
|
|
145
|
+
# Additional escape not required in worksheet hyperlinks
|
|
146
|
+
target = target.gsub("#", '%23')
|
|
147
|
+
|
|
148
|
+
# Prefix absolute paths (not relative) with file:///
|
|
149
|
+
target = if target =~ /^\w:/ || target =~ /^\\\\/
|
|
150
|
+
"file:///#{target}"
|
|
151
|
+
else
|
|
152
|
+
target.gsub("\\", '/')
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if image.url =~ /^internal:/
|
|
157
|
+
target = image.url.sub(/^internal:/, '#')
|
|
158
|
+
target_mode = nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if target.length > 255
|
|
162
|
+
raise <<"EOS"
|
|
163
|
+
Ignoring URL #{target} where link or anchor > 255 characters since it exceeds Excel's limit for URLS. See LIMITATIONS section of the WriteXLSX documentation.
|
|
164
|
+
EOS
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
@drawing_links << ['/hyperlink', target, target_mode] if target && !@drawing_rels[image.url]
|
|
168
|
+
drawing.url_rel_index = drawing_rel_index(image.url)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
@drawing_links << ['/image', "../media/image#{image_id}.#{image_type}"] unless @drawing_rels[md5]
|
|
172
|
+
|
|
173
|
+
drawing.rel_index = drawing_rel_index(md5)
|
|
174
|
+
|
|
175
|
+
image_ref_id
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
#
|
|
179
|
+
# Set up drawing shapes
|
|
180
|
+
#
|
|
181
|
+
def prepare_shape(index, drawing_id)
|
|
182
|
+
shape = shapes[index]
|
|
183
|
+
|
|
184
|
+
# Create a Drawing object to use with worksheet unless one already exists.
|
|
185
|
+
unless drawings?
|
|
186
|
+
@drawings = Drawings.new
|
|
187
|
+
@drawings.embedded = true
|
|
188
|
+
@external_drawing_links << ['/drawing', "../drawings/drawing#{drawing_id}.xml"]
|
|
189
|
+
@has_shapes = true
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Validate the he shape against various rules.
|
|
193
|
+
shape.validate(index)
|
|
194
|
+
shape.calc_position_emus(self)
|
|
195
|
+
|
|
196
|
+
drawing_type = 3
|
|
197
|
+
drawing = Drawing.new(
|
|
198
|
+
drawing_type, shape.dimensions, shape.width_emu, shape.height_emu,
|
|
199
|
+
shape, shape.anchor, drawing_rel_index, 0, shape.name, nil, 0
|
|
200
|
+
)
|
|
201
|
+
drawings.add_drawing_object(drawing)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Image preparation
|
|
205
|
+
|
|
206
|
+
#
|
|
207
|
+
# Set up an image without a drawing object for the background image.
|
|
208
|
+
#
|
|
209
|
+
def prepare_background(image_id, image_type)
|
|
210
|
+
@external_background_links <<
|
|
211
|
+
['/image', "../media/image#{image_id}.#{image_type}"]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def prepare_background_image(background_ids, image_ref_id)
|
|
215
|
+
unless background_image.nil?
|
|
216
|
+
@workbook.store_image_types(background_image.type)
|
|
217
|
+
|
|
218
|
+
if background_ids[background_image.md5]
|
|
219
|
+
ref_id = background_ids[background_image.md5]
|
|
220
|
+
else
|
|
221
|
+
image_ref_id += 1
|
|
222
|
+
ref_id = image_ref_id
|
|
223
|
+
background_ids[background_image.md5] = ref_id
|
|
224
|
+
@workbook.images << background_image
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
prepare_background(ref_id, background_image.type)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
image_ref_id
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def prepare_header_image(image_id, image_property)
|
|
234
|
+
# Strip the extension from the filename.
|
|
235
|
+
body = image_property.name.dup
|
|
236
|
+
body[/\.[^.]+$/, 0] = ''
|
|
237
|
+
image_property.body = body
|
|
238
|
+
|
|
239
|
+
@vml_drawing_links << ['/image', "../media/image#{image_id}.#{image_property.type}"] unless @vml_drawing_rels[image_property.md5]
|
|
240
|
+
|
|
241
|
+
image_property.ref_id = get_vml_drawing_rel_index(image_property.md5)
|
|
242
|
+
@header_images_array << image_property
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def prepare_header_footer_image(image, header_image_ids, image_ref_id)
|
|
246
|
+
@workbook.store_image_types(image.type)
|
|
247
|
+
|
|
248
|
+
if header_image_ids[image.md5]
|
|
249
|
+
ref_id = header_image_ids[image.md5]
|
|
250
|
+
else
|
|
251
|
+
image_ref_id += 1
|
|
252
|
+
header_image_ids[image.md5] = ref_id = image_ref_id
|
|
253
|
+
@workbook.images << image
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
prepare_header_image(ref_id, image)
|
|
257
|
+
|
|
258
|
+
image_ref_id
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# VML preparation
|
|
262
|
+
|
|
263
|
+
#
|
|
264
|
+
# Turn the HoH that stores the comments into an array for easier handling
|
|
265
|
+
# and set the external links for comments and buttons.
|
|
266
|
+
#
|
|
267
|
+
def prepare_vml_objects(vml_data_id, vml_shape_id, vml_drawing_id, comment_id)
|
|
268
|
+
set_external_vml_links(vml_drawing_id)
|
|
269
|
+
set_external_comment_links(comment_id) if has_comments?
|
|
270
|
+
|
|
271
|
+
# The VML o:idmap data id contains a comma separated range when there is
|
|
272
|
+
# more than one 1024 block of comments, like this: data="1,2".
|
|
273
|
+
data = vml_data_id.to_s
|
|
274
|
+
(1..num_comments_block).each do |i|
|
|
275
|
+
data += ",#{vml_data_id + i}"
|
|
276
|
+
end
|
|
277
|
+
@vml_data_id = data
|
|
278
|
+
@vml_shape_id = vml_shape_id
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
#
|
|
282
|
+
# Setup external linkage for VML header/footer images.
|
|
283
|
+
#
|
|
284
|
+
def prepare_header_vml_objects(vml_header_id, vml_drawing_id)
|
|
285
|
+
@vml_header_id = vml_header_id
|
|
286
|
+
@external_vml_links << ['/vmlDrawing', "../drawings/vmlDrawing#{vml_drawing_id}.vml"]
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Writexlsx
|
|
5
|
+
class Worksheet
|
|
6
|
+
module DrawingRelations
|
|
7
|
+
###############################################################################
|
|
8
|
+
#
|
|
9
|
+
# DrawingRelations
|
|
10
|
+
#
|
|
11
|
+
# Manages drawing relationships and external linkage information.
|
|
12
|
+
#
|
|
13
|
+
# Responsibilities:
|
|
14
|
+
# - Relationship ID allocation for drawings and VML objects
|
|
15
|
+
# - Tracking drawing and VML relationship mappings
|
|
16
|
+
# - Managing external links for drawings, comments, tables, and media
|
|
17
|
+
# - Providing relationship data for rels file generation
|
|
18
|
+
#
|
|
19
|
+
# This module handles *how drawing resources are linked together*.
|
|
20
|
+
# It does not prepare drawing objects or write XML output.
|
|
21
|
+
#
|
|
22
|
+
###############################################################################
|
|
23
|
+
#
|
|
24
|
+
# Get the index used to address a drawing rel link.
|
|
25
|
+
#
|
|
26
|
+
def drawing_rel_index(target = nil)
|
|
27
|
+
if !target
|
|
28
|
+
# Undefined values for drawings like charts will always be unique.
|
|
29
|
+
@drawing_rels_id += 1
|
|
30
|
+
elsif ptrue?(@drawing_rels[target])
|
|
31
|
+
@drawing_rels[target]
|
|
32
|
+
else
|
|
33
|
+
@drawing_rels_id += 1
|
|
34
|
+
@drawing_rels[target] = @drawing_rels_id
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Get the index used to address a vml_drawing rel link.
|
|
40
|
+
#
|
|
41
|
+
def get_vml_drawing_rel_index(target)
|
|
42
|
+
if @vml_drawing_rels[target]
|
|
43
|
+
@vml_drawing_rels[target]
|
|
44
|
+
else
|
|
45
|
+
@vml_drawing_rels_id += 1
|
|
46
|
+
@vml_drawing_rels[target] = @vml_drawing_rels_id
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def set_external_vml_links(vml_drawing_id) # :nodoc:
|
|
51
|
+
@external_vml_links <<
|
|
52
|
+
['/vmlDrawing', "../drawings/vmlDrawing#{vml_drawing_id}.vml"]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def set_external_comment_links(comment_id) # :nodoc:
|
|
56
|
+
@external_comment_links <<
|
|
57
|
+
['/comments', "../comments#{comment_id}.xml"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def external_links
|
|
61
|
+
[
|
|
62
|
+
@external_hyper_links,
|
|
63
|
+
@external_drawing_links,
|
|
64
|
+
@external_vml_links,
|
|
65
|
+
@external_background_links,
|
|
66
|
+
@external_table_links,
|
|
67
|
+
@external_comment_links
|
|
68
|
+
].reject(&:empty?)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def drawing_links
|
|
72
|
+
[@drawing_links]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|