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
@@ -0,0 +1,103 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'write_xlsx/package/xml_writer_simple'
|
5
|
+
require 'write_xlsx/utility'
|
6
|
+
|
7
|
+
module Writexlsx
|
8
|
+
module Package
|
9
|
+
#
|
10
|
+
# RichValueTypes - A class for writing the Excel XLSX rdRichValueTypes.xml file.
|
11
|
+
#
|
12
|
+
# Used in conjunction with Excel::Writer::XLSX
|
13
|
+
#
|
14
|
+
# Copyright 2000-2024, John McNamara, jmcnamara@cpan.org
|
15
|
+
#
|
16
|
+
# Convert to Ruby by Hideo NAKAMURA, nakamura.hideo@gmail.com
|
17
|
+
#
|
18
|
+
class RichValueTypes
|
19
|
+
include Writexlsx::Utility
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@writer = Package::XMLWriterSimple.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_xml_writer(filename)
|
26
|
+
@writer.set_xml_writer(filename)
|
27
|
+
end
|
28
|
+
|
29
|
+
def assemble_xml_file
|
30
|
+
write_xml_declaration do
|
31
|
+
write_rv_types_info
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
#
|
38
|
+
# Write the <rvTypesInfo> element.
|
39
|
+
#
|
40
|
+
def write_rv_types_info
|
41
|
+
xmlns = 'http://schemas.microsoft.com/office/spreadsheetml/2017/richdata2'
|
42
|
+
xmlns_mc = 'http://schemas.openxmlformats.org/markup-compatibility/2006'
|
43
|
+
xmlns_x = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
|
44
|
+
|
45
|
+
attributes = [
|
46
|
+
['xmlns', xmlns],
|
47
|
+
['xmlns:mc', xmlns_mc],
|
48
|
+
['mc:Ignorable', 'x'],
|
49
|
+
['xmlns:x', xmlns_x]
|
50
|
+
]
|
51
|
+
|
52
|
+
key_flags = [
|
53
|
+
['_Self', %w[ExcludeFromFile ExcludeFromCalcComparison]],
|
54
|
+
['_DisplayString', ['ExcludeFromCalcComparison']],
|
55
|
+
['_Flags', ['ExcludeFromCalcComparison']],
|
56
|
+
['_Format', ['ExcludeFromCalcComparison']],
|
57
|
+
['_SubLabel', ['ExcludeFromCalcComparison']],
|
58
|
+
['_Attribution', ['ExcludeFromCalcComparison']],
|
59
|
+
['_Icon', ['ExcludeFromCalcComparison']],
|
60
|
+
['_Display', ['ExcludeFromCalcComparison']],
|
61
|
+
['_CanonicalPropertyNames', ['ExcludeFromCalcComparison']],
|
62
|
+
['_ClassificationId', ['ExcludeFromCalcComparison']]
|
63
|
+
]
|
64
|
+
|
65
|
+
@writer.tag_elements('rvTypesInfo', attributes) do
|
66
|
+
@writer.tag_elements('global') do
|
67
|
+
@writer.tag_elements('keyFlags') do
|
68
|
+
# Write the keyFlags element.
|
69
|
+
key_flags.each do |key_flag|
|
70
|
+
write_key(key_flag[0], key_flag[1])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Write the <key> element.
|
79
|
+
#
|
80
|
+
def write_key(name, flags = [])
|
81
|
+
attributes = [['name', name]]
|
82
|
+
|
83
|
+
@writer.tag_elements('key', attributes) do
|
84
|
+
flags.each do |flag|
|
85
|
+
write_flag(flag)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Write the <flag> element.
|
92
|
+
#
|
93
|
+
def write_flag(name)
|
94
|
+
attributes = [
|
95
|
+
['name', name],
|
96
|
+
['value', 1]
|
97
|
+
]
|
98
|
+
|
99
|
+
@writer.empty_tag('flag', attributes)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -12,13 +12,14 @@ module Writexlsx
|
|
12
12
|
class ColumnData
|
13
13
|
attr_reader :id
|
14
14
|
attr_accessor :name, :format, :formula, :name_format
|
15
|
-
attr_accessor :total_string, :total_function
|
15
|
+
attr_accessor :total_string, :total_function, :custom_total
|
16
16
|
|
17
17
|
def initialize(id, param = {})
|
18
18
|
@id = id
|
19
19
|
@name = "Column#{id}"
|
20
20
|
@total_string = ''
|
21
21
|
@total_function = ''
|
22
|
+
@custom_total = ''
|
22
23
|
@formula = ''
|
23
24
|
@format = nil
|
24
25
|
@name_format = nil
|
@@ -51,6 +52,7 @@ module Writexlsx
|
|
51
52
|
|
52
53
|
add_the_table_columns
|
53
54
|
write_the_cell_data_if_supplied
|
55
|
+
write_any_columns_formulas_after_the_user_supplied_table_data
|
54
56
|
store_filter_cell_positions
|
55
57
|
end
|
56
58
|
|
@@ -78,7 +80,7 @@ module Writexlsx
|
|
78
80
|
# Set up the default column data.
|
79
81
|
col_data = Package::Table::ColumnData.new(col_id + 1, @param[:columns])
|
80
82
|
|
81
|
-
|
83
|
+
overwrite_the_defaults_with_any_use_defined_values(col_id, col_data, col_num)
|
82
84
|
|
83
85
|
# Store the column data.
|
84
86
|
@columns << col_data
|
@@ -89,7 +91,7 @@ module Writexlsx
|
|
89
91
|
end # Table columns.
|
90
92
|
end
|
91
93
|
|
92
|
-
def
|
94
|
+
def overwrite_the_defaults_with_any_use_defined_values(col_id, col_data, col_num)
|
93
95
|
# Check if there are user defined values for this column.
|
94
96
|
if @param[:columns] && (user_data = @param[:columns][col_id])
|
95
97
|
# Map user defined values to internal values.
|
@@ -156,6 +158,23 @@ module Writexlsx
|
|
156
158
|
end
|
157
159
|
end
|
158
160
|
|
161
|
+
def write_any_columns_formulas_after_the_user_supplied_table_data
|
162
|
+
col_id = 0
|
163
|
+
|
164
|
+
(@col1..@col2).each do |col|
|
165
|
+
column_data = @columns[col_id]
|
166
|
+
if ptrue?(column_data) && ptrue?(column_data.formula)
|
167
|
+
formula_format = @col_formats[col_id]
|
168
|
+
formula = column_data.formula
|
169
|
+
|
170
|
+
(@first_data_row..@last_data_row).each do |row|
|
171
|
+
@worksheet.write_formula(row, col, formula, formula_format)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
col_id += 1
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
159
178
|
def store_filter_cell_positions
|
160
179
|
if ptrue?(@param[:autofilter])
|
161
180
|
(@col1..@col2).each do |col|
|
@@ -234,25 +253,45 @@ module Writexlsx
|
|
234
253
|
]
|
235
254
|
end
|
236
255
|
|
237
|
-
def handle_the_column_formula(col_data,
|
256
|
+
def handle_the_column_formula(col_data, _col_num, formula, _format)
|
238
257
|
return unless formula
|
239
258
|
|
240
|
-
|
259
|
+
formula = formula.sub(/^=/, '').gsub(/@/, '[#This Row],')
|
241
260
|
|
242
|
-
|
243
|
-
|
244
|
-
|
261
|
+
# Escape any future functions.
|
262
|
+
col_data.formula = @worksheet.prepare_formula(formula, 1)
|
263
|
+
|
264
|
+
# (@first_data_row..@last_data_row).each do |row|
|
265
|
+
# @worksheet.write_formula(row, col_num, col_data.formula, format)
|
266
|
+
# end
|
245
267
|
end
|
246
268
|
|
247
269
|
def handle_the_function_for_the_table_row(row2, col_data, col_num, user_data)
|
248
|
-
|
270
|
+
formula = ''
|
271
|
+
function = user_data[:total_function]
|
272
|
+
function = 'countNums' if function == 'count_nums'
|
273
|
+
function = 'stdDev' if function == 'std_dev'
|
249
274
|
|
250
|
-
|
251
|
-
|
275
|
+
subtotals = {
|
276
|
+
average: 101,
|
277
|
+
countNums: 102,
|
278
|
+
count: 103,
|
279
|
+
max: 104,
|
280
|
+
min: 105,
|
281
|
+
stdDev: 106,
|
282
|
+
sum: 109,
|
283
|
+
var: 110
|
284
|
+
}
|
252
285
|
|
253
|
-
|
286
|
+
if subtotals[function.to_sym]
|
287
|
+
formula = table_function_to_formula(function, col_data.name)
|
288
|
+
else
|
289
|
+
formula = @worksheet.prepare_formula(function, 1)
|
290
|
+
col_data.custom_total = formula
|
291
|
+
function = 'custom'
|
292
|
+
end
|
254
293
|
|
255
|
-
|
294
|
+
col_data.total_function = function
|
256
295
|
@worksheet.write_formula(row2, col_num, formula, user_data[:format], user_data[:total_value])
|
257
296
|
end
|
258
297
|
|
@@ -260,10 +299,10 @@ module Writexlsx
|
|
260
299
|
# Convert a table total function to a worksheet formula.
|
261
300
|
#
|
262
301
|
def table_function_to_formula(function, col_name)
|
263
|
-
col_name = col_name.gsub(
|
264
|
-
.gsub(
|
265
|
-
.gsub(
|
266
|
-
.gsub(
|
302
|
+
col_name = col_name.gsub("'", "''")
|
303
|
+
.gsub("#", "'#")
|
304
|
+
.gsub("[", "'[")
|
305
|
+
.gsub("]", "']")
|
267
306
|
|
268
307
|
subtotals = {
|
269
308
|
average: 101,
|
@@ -395,10 +434,16 @@ module Writexlsx
|
|
395
434
|
|
396
435
|
attributes << [:dataDxfId, col_data.format] if col_data.format
|
397
436
|
|
398
|
-
if ptrue?(col_data.formula)
|
437
|
+
if ptrue?(col_data.formula) || ptrue?(col_data.custom_total)
|
399
438
|
@writer.tag_elements('tableColumn', attributes) do
|
400
|
-
|
401
|
-
|
439
|
+
if ptrue?(col_data.formula)
|
440
|
+
# Write the calculatedColumnFormula element.
|
441
|
+
write_calculated_column_formula(col_data.formula)
|
442
|
+
end
|
443
|
+
if ptrue?(col_data.custom_total)
|
444
|
+
# Write the totalsRowFormula element.
|
445
|
+
write_totals_row_formula(col_data.custom_total)
|
446
|
+
end
|
402
447
|
end
|
403
448
|
else
|
404
449
|
@writer.empty_tag('tableColumn', attributes)
|
@@ -425,6 +470,15 @@ module Writexlsx
|
|
425
470
|
def write_calculated_column_formula(formula)
|
426
471
|
@writer.data_element('calculatedColumnFormula', formula)
|
427
472
|
end
|
473
|
+
|
474
|
+
#
|
475
|
+
# _write_totals_row_formula()
|
476
|
+
#
|
477
|
+
# Write the <totalsRowFormula> element.
|
478
|
+
#
|
479
|
+
def write_totals_row_formula(formula)
|
480
|
+
@writer.data_element('totalsRowFormula', formula)
|
481
|
+
end
|
428
482
|
end
|
429
483
|
end
|
430
484
|
end
|
@@ -29,33 +29,28 @@ module Writexlsx
|
|
29
29
|
io_write(str)
|
30
30
|
end
|
31
31
|
|
32
|
-
def tag_elements(tag, attributes =
|
32
|
+
def tag_elements(tag, attributes = nil)
|
33
33
|
start_tag(tag, attributes)
|
34
34
|
yield
|
35
35
|
end_tag(tag)
|
36
36
|
end
|
37
37
|
|
38
|
-
def tag_elements_str(tag, attributes =
|
38
|
+
def tag_elements_str(tag, attributes = nil)
|
39
39
|
start_tag_str(tag, attributes) +
|
40
40
|
yield +
|
41
41
|
end_tag_str(tag)
|
42
42
|
end
|
43
43
|
|
44
|
-
def start_tag(tag, attr =
|
44
|
+
def start_tag(tag, attr = nil)
|
45
45
|
io_write(start_tag_str(tag, attr))
|
46
46
|
end
|
47
47
|
|
48
|
-
def start_tag_str(tag, attr =
|
49
|
-
if attr.empty?
|
50
|
-
|
51
|
-
unless result
|
52
|
-
result = "<#{tag}>"
|
53
|
-
@tag_start_cache[tag] = result
|
54
|
-
end
|
48
|
+
def start_tag_str(tag, attr = nil)
|
49
|
+
if attr.nil? || attr.empty?
|
50
|
+
@tag_start_cache[tag] ||= "<#{tag}>"
|
55
51
|
else
|
56
|
-
|
52
|
+
"<#{tag}#{key_vals(attr)}>"
|
57
53
|
end
|
58
|
-
result
|
59
54
|
end
|
60
55
|
|
61
56
|
def end_tag(tag)
|
@@ -63,28 +58,15 @@ module Writexlsx
|
|
63
58
|
end
|
64
59
|
|
65
60
|
def end_tag_str(tag)
|
66
|
-
|
67
|
-
unless result
|
68
|
-
result = "</#{tag}>"
|
69
|
-
@tag_end_cache[tag] = result
|
70
|
-
end
|
71
|
-
result
|
61
|
+
@tag_end_cache[tag] ||= "</#{tag}>"
|
72
62
|
end
|
73
63
|
|
74
|
-
def empty_tag(tag, attr =
|
64
|
+
def empty_tag(tag, attr = nil)
|
75
65
|
str = "<#{tag}#{key_vals(attr)}/>"
|
76
66
|
io_write(str)
|
77
67
|
end
|
78
68
|
|
79
|
-
def
|
80
|
-
io_write(empty_tag_encoded_str(tag, attr))
|
81
|
-
end
|
82
|
-
|
83
|
-
def empty_tag_encoded_str(tag, attr = [])
|
84
|
-
"<#{tag}#{key_vals(attr)}/>"
|
85
|
-
end
|
86
|
-
|
87
|
-
def data_element(tag, data, attr = [])
|
69
|
+
def data_element(tag, data, attr = nil)
|
88
70
|
tag_elements(tag, attr) { io_write(escape_data(data)) }
|
89
71
|
end
|
90
72
|
|
@@ -126,31 +108,37 @@ module Writexlsx
|
|
126
108
|
|
127
109
|
private
|
128
110
|
|
129
|
-
def key_val(key, val)
|
130
|
-
%( #{key}="#{val}")
|
131
|
-
end
|
132
|
-
|
133
111
|
def key_vals(attribute)
|
134
|
-
attribute
|
135
|
-
|
112
|
+
if attribute
|
113
|
+
result = "".dup
|
114
|
+
attribute.each do |attr|
|
115
|
+
# Generate and concat %( #{key}="#{val}") values for attribute pair
|
116
|
+
result << " "
|
117
|
+
result << attr.first.to_s
|
118
|
+
result << '="'
|
119
|
+
result << escape_attributes(attr.last).to_s
|
120
|
+
result << '"'
|
121
|
+
end
|
122
|
+
result
|
123
|
+
end
|
136
124
|
end
|
137
125
|
|
138
126
|
def escape_attributes(str = '')
|
139
|
-
return str unless str.
|
127
|
+
return str unless str.respond_to?(:match) && str =~ /["&<>\n]/
|
140
128
|
|
141
129
|
str
|
142
|
-
.gsub(
|
143
|
-
.gsub(
|
144
|
-
.gsub(
|
145
|
-
.gsub(
|
146
|
-
.gsub(
|
130
|
+
.gsub("&", "&")
|
131
|
+
.gsub('"', """)
|
132
|
+
.gsub("<", "<")
|
133
|
+
.gsub(">", ">")
|
134
|
+
.gsub("\n", "
")
|
147
135
|
end
|
148
136
|
|
149
137
|
def escape_data(str = '')
|
150
|
-
if str.
|
151
|
-
str.gsub(
|
152
|
-
.gsub(
|
153
|
-
.gsub(
|
138
|
+
if str.respond_to?(:match) && str =~ /[&<>]/
|
139
|
+
str.gsub("&", '&')
|
140
|
+
.gsub("<", '<')
|
141
|
+
.gsub(">", '>')
|
154
142
|
else
|
155
143
|
str
|
156
144
|
end
|
data/lib/write_xlsx/sheets.rb
CHANGED
@@ -252,9 +252,13 @@ module Writexlsx
|
|
252
252
|
['sheetId', sheet_id]
|
253
253
|
]
|
254
254
|
|
255
|
-
|
255
|
+
if sheet.hidden?
|
256
|
+
attributes << %w[state hidden]
|
257
|
+
elsif sheet.very_hidden?
|
258
|
+
attributes << %w[state veryHidden]
|
259
|
+
end
|
256
260
|
attributes << r_id_attributes(sheet_id)
|
257
|
-
writer.
|
261
|
+
writer.empty_tag('sheet', attributes)
|
258
262
|
end
|
259
263
|
end
|
260
264
|
end
|
data/lib/write_xlsx/sparkline.rb
CHANGED
@@ -43,13 +43,13 @@ module Writexlsx
|
|
43
43
|
# Cleanup the input ranges.
|
44
44
|
@ranges.collect! do |range|
|
45
45
|
# Remove the absolute reference $ symbols.
|
46
|
-
range = range.gsub(
|
46
|
+
range = range.gsub("$", '')
|
47
47
|
# Convert a simple range into a full Sheet1!A1:D1 range.
|
48
48
|
range = "#{sheetname}!#{range}" unless range =~ /!/
|
49
49
|
range
|
50
50
|
end
|
51
51
|
# Cleanup the input locations.
|
52
|
-
@locations.collect! { |location| location.gsub(
|
52
|
+
@locations.collect! { |location| location.gsub("$", '') }
|
53
53
|
|
54
54
|
# Map options.
|
55
55
|
@high = param[:high_point]
|
data/lib/write_xlsx/utility.rb
CHANGED
@@ -31,10 +31,12 @@ module Writexlsx
|
|
31
31
|
#
|
32
32
|
# xl_rowcol_to_cell($row, col, row_absolute, col_absolute)
|
33
33
|
#
|
34
|
-
def xl_rowcol_to_cell(
|
35
|
-
|
34
|
+
def xl_rowcol_to_cell(row_or_name, col, row_absolute = false, col_absolute = false)
|
35
|
+
if row_or_name.is_a?(Integer)
|
36
|
+
row_or_name += 1 # Change from 0-indexed to 1 indexed.
|
37
|
+
end
|
36
38
|
col_str = xl_col_to_name(col, col_absolute)
|
37
|
-
"#{col_str}#{absolute_char(row_absolute)}#{
|
39
|
+
"#{col_str}#{absolute_char(row_absolute)}#{row_or_name}"
|
38
40
|
end
|
39
41
|
|
40
42
|
#
|
@@ -53,7 +55,7 @@ module Writexlsx
|
|
53
55
|
|
54
56
|
# Convert base26 column string to number
|
55
57
|
# All your Base are belong to us.
|
56
|
-
chars = col.split(
|
58
|
+
chars = col.split("")
|
57
59
|
expn = 0
|
58
60
|
col = 0
|
59
61
|
|
@@ -112,7 +114,7 @@ module Writexlsx
|
|
112
114
|
#
|
113
115
|
def xl_string_pixel_width(string)
|
114
116
|
length = 0
|
115
|
-
string.to_s.split(
|
117
|
+
string.to_s.split("").each { |char| length += CHAR_WIDTHS[char] || 8 }
|
116
118
|
|
117
119
|
length
|
118
120
|
end
|
@@ -128,7 +130,7 @@ module Writexlsx
|
|
128
130
|
name = sheetname.dup
|
129
131
|
if name =~ /\W/ && !(name =~ /^'/)
|
130
132
|
# Double quote and single quoted strings.
|
131
|
-
name = name.gsub(
|
133
|
+
name = name.gsub("'", "''")
|
132
134
|
name = "'#{name}'"
|
133
135
|
end
|
134
136
|
name
|
@@ -159,7 +161,7 @@ module Writexlsx
|
|
159
161
|
seconds = 0 # Time expressed as fraction of 24h hours in seconds
|
160
162
|
|
161
163
|
# Split into date and time.
|
162
|
-
date, time = date_time.split(
|
164
|
+
date, time = date_time.split("T")
|
163
165
|
|
164
166
|
# We allow the time portion of the input DateTime to be optional.
|
165
167
|
if time
|
@@ -207,7 +209,7 @@ module Writexlsx
|
|
207
209
|
# becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
|
208
210
|
#
|
209
211
|
epoch = date_1904? ? 1904 : 1900
|
210
|
-
offset = date_1904? ?
|
212
|
+
offset = date_1904? ? 4 : 0
|
211
213
|
norm = 300
|
212
214
|
range = year - epoch
|
213
215
|
|
@@ -248,7 +250,7 @@ module Writexlsx
|
|
248
250
|
def escape_url(url)
|
249
251
|
unless url =~ /%[0-9a-fA-F]{2}/
|
250
252
|
# Escape the URL escape symbol.
|
251
|
-
url = url.gsub(
|
253
|
+
url = url.gsub("%", "%25")
|
252
254
|
|
253
255
|
# Escape whitespae in URL.
|
254
256
|
url = url.gsub(/[\s\x00]/, '%20')
|
@@ -986,6 +988,178 @@ module Writexlsx
|
|
986
988
|
def write_a_end_para_rpr # :nodoc:
|
987
989
|
@writer.empty_tag('a:endParaRPr', [%w[lang en-US]])
|
988
990
|
end
|
991
|
+
|
992
|
+
#
|
993
|
+
# Extract information from the image file such as dimension, type, filename,
|
994
|
+
# and extension. Also keep track of previously seen images to optimise out
|
995
|
+
# any duplicates.
|
996
|
+
#
|
997
|
+
def get_image_properties(filename)
|
998
|
+
# Note the image_id, and previous_images mechanism isn't currently used.
|
999
|
+
x_dpi = 96
|
1000
|
+
y_dpi = 96
|
1001
|
+
|
1002
|
+
workbook = @workbook || self
|
1003
|
+
|
1004
|
+
# Open the image file and import the data.
|
1005
|
+
data = File.binread(filename)
|
1006
|
+
md5 = Digest::MD5.hexdigest(data)
|
1007
|
+
if data.unpack1('x A3') == 'PNG'
|
1008
|
+
# Test for PNGs.
|
1009
|
+
type, width, height, x_dpi, y_dpi = process_png(data)
|
1010
|
+
workbook.image_types[:png] = 1
|
1011
|
+
elsif data.unpack1('n') == 0xFFD8
|
1012
|
+
# Test for JPEG files.
|
1013
|
+
type, width, height, x_dpi, y_dpi = process_jpg(data, filename)
|
1014
|
+
workbook.image_types[:jpeg] = 1
|
1015
|
+
elsif data.unpack1('A4') == 'GIF8'
|
1016
|
+
# Test for GIFs.
|
1017
|
+
type, width, height, x_dpi, y_dpi = process_gif(data, filename)
|
1018
|
+
workbook.image_types[:gif] = 1
|
1019
|
+
elsif data.unpack1('A2') == 'BM'
|
1020
|
+
# Test for BMPs.
|
1021
|
+
type, width, height = process_bmp(data, filename)
|
1022
|
+
workbook.image_types[:bmp] = 1
|
1023
|
+
else
|
1024
|
+
# TODO. Add Image::Size to support other types.
|
1025
|
+
raise "Unsupported image format for file: #{filename}\n"
|
1026
|
+
end
|
1027
|
+
|
1028
|
+
# Set a default dpi for images with 0 dpi.
|
1029
|
+
x_dpi = 96 if x_dpi == 0
|
1030
|
+
y_dpi = 96 if y_dpi == 0
|
1031
|
+
|
1032
|
+
[type, width, height, File.basename(filename), x_dpi, y_dpi, md5]
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
#
|
1036
|
+
# Extract width and height information from a PNG file.
|
1037
|
+
#
|
1038
|
+
def process_png(data)
|
1039
|
+
type = 'png'
|
1040
|
+
width = 0
|
1041
|
+
height = 0
|
1042
|
+
x_dpi = 96
|
1043
|
+
y_dpi = 96
|
1044
|
+
|
1045
|
+
offset = 8
|
1046
|
+
data_length = data.size
|
1047
|
+
|
1048
|
+
# Search through the image data to read the height and width in th the
|
1049
|
+
# IHDR element. Also read the DPI in the pHYs element.
|
1050
|
+
while offset < data_length
|
1051
|
+
|
1052
|
+
length = data[offset + 0, 4].unpack1("N")
|
1053
|
+
png_type = data[offset + 4, 4].unpack1("A4")
|
1054
|
+
|
1055
|
+
case png_type
|
1056
|
+
when "IHDR"
|
1057
|
+
width = data[offset + 8, 4].unpack1("N")
|
1058
|
+
height = data[offset + 12, 4].unpack1("N")
|
1059
|
+
when "pHYs"
|
1060
|
+
x_ppu = data[offset + 8, 4].unpack1("N")
|
1061
|
+
y_ppu = data[offset + 12, 4].unpack1("N")
|
1062
|
+
units = data[offset + 16, 1].unpack1("C")
|
1063
|
+
|
1064
|
+
if units == 1
|
1065
|
+
x_dpi = x_ppu * 0.0254
|
1066
|
+
y_dpi = y_ppu * 0.0254
|
1067
|
+
end
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
offset = offset + length + 12
|
1071
|
+
|
1072
|
+
break if png_type == "IEND"
|
1073
|
+
end
|
1074
|
+
raise "#{filename}: no size data found in png image.\n" unless height
|
1075
|
+
|
1076
|
+
[type, width, height, x_dpi, y_dpi]
|
1077
|
+
end
|
1078
|
+
|
1079
|
+
def process_jpg(data, filename)
|
1080
|
+
type = 'jpeg'
|
1081
|
+
x_dpi = 96
|
1082
|
+
y_dpi = 96
|
1083
|
+
|
1084
|
+
offset = 2
|
1085
|
+
data_length = data.bytesize
|
1086
|
+
|
1087
|
+
# Search through the image data to read the JPEG markers.
|
1088
|
+
while offset < data_length
|
1089
|
+
marker = data[offset + 0, 2].unpack1("n")
|
1090
|
+
length = data[offset + 2, 2].unpack1("n")
|
1091
|
+
|
1092
|
+
# Read the height and width in the 0xFFCn elements
|
1093
|
+
# (Except C4, C8 and CC which aren't SOF markers).
|
1094
|
+
if (marker & 0xFFF0) == 0xFFC0 &&
|
1095
|
+
marker != 0xFFC4 && marker != 0xFFCC
|
1096
|
+
height = data[offset + 5, 2].unpack1("n")
|
1097
|
+
width = data[offset + 7, 2].unpack1("n")
|
1098
|
+
end
|
1099
|
+
|
1100
|
+
# Read the DPI in the 0xFFE0 element.
|
1101
|
+
if marker == 0xFFE0
|
1102
|
+
units = data[offset + 11, 1].unpack1("C")
|
1103
|
+
x_density = data[offset + 12, 2].unpack1("n")
|
1104
|
+
y_density = data[offset + 14, 2].unpack1("n")
|
1105
|
+
|
1106
|
+
if units == 1
|
1107
|
+
x_dpi = x_density
|
1108
|
+
y_dpi = y_density
|
1109
|
+
elsif units == 2
|
1110
|
+
x_dpi = x_density * 2.54
|
1111
|
+
y_dpi = y_density * 2.54
|
1112
|
+
end
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
offset += length + 2
|
1116
|
+
break if marker == 0xFFDA
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
raise "#{filename}: no size data found in jpeg image.\n" unless height
|
1120
|
+
|
1121
|
+
[type, width, height, x_dpi, y_dpi]
|
1122
|
+
end
|
1123
|
+
|
1124
|
+
#
|
1125
|
+
# Extract width and height information from a GIF file.
|
1126
|
+
#
|
1127
|
+
def process_gif(data, filename)
|
1128
|
+
type = 'gif'
|
1129
|
+
x_dpi = 96
|
1130
|
+
y_dpi = 96
|
1131
|
+
|
1132
|
+
width = data[6, 2].unpack1("v")
|
1133
|
+
height = data[8, 2].unpack1("v")
|
1134
|
+
|
1135
|
+
raise "#{filename}: no size data found in gif image.\n" if height.nil?
|
1136
|
+
|
1137
|
+
[type, width, height, x_dpi, y_dpi]
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
# Extract width and height information from a BMP file.
|
1141
|
+
def process_bmp(data, filename) # :nodoc:
|
1142
|
+
type = 'bmp'
|
1143
|
+
|
1144
|
+
# Check that the file is big enough to be a bitmap.
|
1145
|
+
raise "#{filename} doesn't contain enough data." if data.bytesize <= 0x36
|
1146
|
+
|
1147
|
+
# Read the bitmap width and height. Verify the sizes.
|
1148
|
+
width, height = data.unpack("x18 V2")
|
1149
|
+
raise "#{filename}: largest image width #{width} supported is 65k." if width > 0xFFFF
|
1150
|
+
raise "#{filename}: largest image height supported is 65k." if height > 0xFFFF
|
1151
|
+
|
1152
|
+
# Read the bitmap planes and bpp data. Verify them.
|
1153
|
+
planes, bitcount = data.unpack("x26 v2")
|
1154
|
+
raise "#{filename} isn't a 24bit true color bitmap." unless bitcount == 24
|
1155
|
+
raise "#{filename}: only 1 plane supported in bitmap image." unless planes == 1
|
1156
|
+
|
1157
|
+
# Read the bitmap compression. Verify compression.
|
1158
|
+
compression = data.unpack1("x30 V")
|
1159
|
+
raise "#{filename}: compression not supported in bitmap image." unless compression == 0
|
1160
|
+
|
1161
|
+
[type, width, height]
|
1162
|
+
end
|
989
1163
|
end
|
990
1164
|
|
991
1165
|
module WriteDPtPoint
|
data/lib/write_xlsx/version.rb
CHANGED