write_xlsx 0.0.2
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.
- data/.document +5 -0
- data/.gitattributes +1 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +82 -0
- data/Rakefile +78 -0
- data/VERSION +1 -0
- data/examples/a_simple.rb +45 -0
- data/examples/array_formula.rb +33 -0
- data/examples/autofilter.rb +235 -0
- data/examples/chart_area.rb +59 -0
- data/examples/chart_bar.rb +59 -0
- data/examples/chart_column.rb +58 -0
- data/examples/chart_line.rb +59 -0
- data/examples/chart_pie.rb +49 -0
- data/examples/chart_scatter.rb +59 -0
- data/examples/chart_stock.rb +65 -0
- data/examples/colors.rb +130 -0
- data/examples/comments1.rb +12 -0
- data/examples/comments2.rb +335 -0
- data/examples/conditional_format.rb +67 -0
- data/examples/data_validate.rb +279 -0
- data/examples/defined_name.rb +28 -0
- data/examples/demo.rb +104 -0
- data/examples/diag_border.rb +26 -0
- data/examples/headers.rb +119 -0
- data/examples/hide_sheet.rb +30 -0
- data/examples/hyperlink1.rb +58 -0
- data/examples/indent.rb +28 -0
- data/examples/merge1.rb +38 -0
- data/examples/merge2.rb +48 -0
- data/examples/merge3.rb +43 -0
- data/examples/merge4.rb +82 -0
- data/examples/merge5.rb +70 -0
- data/examples/merge6.rb +48 -0
- data/examples/outline.rb +252 -0
- data/examples/properties.rb +33 -0
- data/examples/protection.rb +34 -0
- data/examples/rich_strings.rb +42 -0
- data/examples/right_to_left.rb +24 -0
- data/examples/tab_colors.rb +26 -0
- data/lib/write_xlsx.rb +77 -0
- data/lib/write_xlsx/chart.rb +3027 -0
- data/lib/write_xlsx/chart/area.rb +52 -0
- data/lib/write_xlsx/chart/bar.rb +126 -0
- data/lib/write_xlsx/chart/column.rb +132 -0
- data/lib/write_xlsx/chart/line.rb +51 -0
- data/lib/write_xlsx/chart/pie.rb +210 -0
- data/lib/write_xlsx/chart/scatter.rb +252 -0
- data/lib/write_xlsx/chart/stock.rb +134 -0
- data/lib/write_xlsx/chartsheet.rb +173 -0
- data/lib/write_xlsx/colors.rb +65 -0
- data/lib/write_xlsx/compatibility.rb +71 -0
- data/lib/write_xlsx/drawing.rb +547 -0
- data/lib/write_xlsx/format.rb +683 -0
- data/lib/write_xlsx/package/app.rb +218 -0
- data/lib/write_xlsx/package/comments.rb +221 -0
- data/lib/write_xlsx/package/content_types.rb +189 -0
- data/lib/write_xlsx/package/core.rb +196 -0
- data/lib/write_xlsx/package/packager.rb +510 -0
- data/lib/write_xlsx/package/relationships.rb +98 -0
- data/lib/write_xlsx/package/shared_strings.rb +96 -0
- data/lib/write_xlsx/package/styles.rb +705 -0
- data/lib/write_xlsx/package/theme.rb +45 -0
- data/lib/write_xlsx/package/vml.rb +386 -0
- data/lib/write_xlsx/package/xml_writer_simple.rb +90 -0
- data/lib/write_xlsx/utility.rb +113 -0
- data/lib/write_xlsx/workbook.rb +1488 -0
- data/lib/write_xlsx/worksheet.rb +6578 -0
- data/lib/write_xlsx/zip_file_utils.rb +98 -0
- data/test/chart/test_add_series.rb +113 -0
- data/test/chart/test_process_names.rb +27 -0
- data/test/chart/test_write_auto.rb +15 -0
- data/test/chart/test_write_ax_id.rb +15 -0
- data/test/chart/test_write_ax_pos.rb +15 -0
- data/test/chart/test_write_chart_space.rb +15 -0
- data/test/chart/test_write_cross_ax.rb +15 -0
- data/test/chart/test_write_crosses.rb +15 -0
- data/test/chart/test_write_format_code.rb +15 -0
- data/test/chart/test_write_idx.rb +15 -0
- data/test/chart/test_write_label_align.rb +15 -0
- data/test/chart/test_write_label_offset.rb +15 -0
- data/test/chart/test_write_lang.rb +15 -0
- data/test/chart/test_write_layout.rb +15 -0
- data/test/chart/test_write_legend.rb +16 -0
- data/test/chart/test_write_legend_pos.rb +15 -0
- data/test/chart/test_write_major_gridlines.rb +15 -0
- data/test/chart/test_write_marker.rb +17 -0
- data/test/chart/test_write_marker_size.rb +15 -0
- data/test/chart/test_write_marker_value.rb +16 -0
- data/test/chart/test_write_num_cache.rb +16 -0
- data/test/chart/test_write_num_fmt.rb +16 -0
- data/test/chart/test_write_number_format.rb +15 -0
- data/test/chart/test_write_order.rb +15 -0
- data/test/chart/test_write_orientation.rb +15 -0
- data/test/chart/test_write_page_margins.rb +15 -0
- data/test/chart/test_write_page_setup.rb +15 -0
- data/test/chart/test_write_plot_vis_only.rb +15 -0
- data/test/chart/test_write_pt.rb +16 -0
- data/test/chart/test_write_pt_count.rb +16 -0
- data/test/chart/test_write_series_formula.rb +16 -0
- data/test/chart/test_write_style.rb +41 -0
- data/test/chart/test_write_symbol.rb +16 -0
- data/test/chart/test_write_tick_lbl_pos.rb +16 -0
- data/test/chart/test_write_v.rb +16 -0
- data/test/drawing/test_drawing_chart_01.rb +50 -0
- data/test/drawing/test_drawing_image_01.rb +59 -0
- data/test/helper.rb +90 -0
- data/test/package/app/test_app01.rb +44 -0
- data/test/package/app/test_app02.rb +46 -0
- data/test/package/app/test_app03.rb +53 -0
- data/test/package/comments/test_comments01.rb +36 -0
- data/test/package/comments/test_write_text_t.rb +44 -0
- data/test/package/content_types/test_content_types.rb +35 -0
- data/test/package/content_types/test_write_default.rb +13 -0
- data/test/package/content_types/test_write_override.rb +13 -0
- data/test/package/core/test_core01.rb +28 -0
- data/test/package/core/test_core02.rb +42 -0
- data/test/package/relationships/test_relationships.rb +28 -0
- data/test/package/relationships/test_sheet_rels.rb +22 -0
- data/test/package/shared_strings/test_shared_strings01.rb +30 -0
- data/test/package/shared_strings/test_shared_strings02.rb +30 -0
- data/test/package/shared_strings/test_write_si.rb +13 -0
- data/test/package/shared_strings/test_write_sst.rb +15 -0
- data/test/package/styles/test_styles_01.rb +69 -0
- data/test/package/styles/test_styles_02.rb +104 -0
- data/test/package/styles/test_styles_03.rb +90 -0
- data/test/package/styles/test_styles_04.rb +216 -0
- data/test/package/styles/test_styles_05.rb +150 -0
- data/test/package/styles/test_styles_06.rb +104 -0
- data/test/package/styles/test_styles_07.rb +104 -0
- data/test/package/styles/test_styles_08.rb +109 -0
- data/test/package/styles/test_styles_09.rb +95 -0
- data/test/package/vml/test_vml_01.rb +42 -0
- data/test/package/vml/test_write_anchor.rb +14 -0
- data/test/package/vml/test_write_auto_fill.rb +14 -0
- data/test/package/vml/test_write_column.rb +14 -0
- data/test/package/vml/test_write_div.rb +14 -0
- data/test/package/vml/test_write_fill.rb +14 -0
- data/test/package/vml/test_write_idmap.rb +14 -0
- data/test/package/vml/test_write_move_with_cells.rb +14 -0
- data/test/package/vml/test_write_path.rb +22 -0
- data/test/package/vml/test_write_row.rb +14 -0
- data/test/package/vml/test_write_shadow.rb +14 -0
- data/test/package/vml/test_write_shapelayout.rb +14 -0
- data/test/package/vml/test_write_shapetype.rb +14 -0
- data/test/package/vml/test_write_size_with_cells.rb +14 -0
- data/test/package/vml/test_write_stroke.rb +14 -0
- data/test/package/vml/test_write_textbox.rb +14 -0
- data/test/perl_output/a_simple.xlsx +0 -0
- data/test/perl_output/array_formula.xlsx +0 -0
- data/test/perl_output/autofilter.xlsx +0 -0
- data/test/perl_output/chart_area.xlsx +0 -0
- data/test/perl_output/chart_bar.xlsx +0 -0
- data/test/perl_output/chart_column.xlsx +0 -0
- data/test/perl_output/chart_line.xlsx +0 -0
- data/test/perl_output/chart_pie.xlsx +0 -0
- data/test/perl_output/chart_scatter.xlsx +0 -0
- data/test/perl_output/chart_stock.xlsx +0 -0
- data/test/perl_output/comments1.xlsx +0 -0
- data/test/perl_output/comments2.xlsx +0 -0
- data/test/perl_output/conditional_format.xlsx +0 -0
- data/test/perl_output/data_validate.xlsx +0 -0
- data/test/perl_output/defined_name.xlsx +0 -0
- data/test/perl_output/demo.xlsx +0 -0
- data/test/perl_output/diag_border.xlsx +0 -0
- data/test/perl_output/fit_to_pages.xlsx +0 -0
- data/test/perl_output/headers.xlsx +0 -0
- data/test/perl_output/hide_sheet.xlsx +0 -0
- data/test/perl_output/hyperlink.xlsx +0 -0
- data/test/perl_output/indent.xlsx +0 -0
- data/test/perl_output/merge1.xlsx +0 -0
- data/test/perl_output/merge2.xlsx +0 -0
- data/test/perl_output/merge3.xlsx +0 -0
- data/test/perl_output/merge4.xlsx +0 -0
- data/test/perl_output/merge5.xlsx +0 -0
- data/test/perl_output/merge6.xlsx +0 -0
- data/test/perl_output/outline.xlsx +0 -0
- data/test/perl_output/print_scale.xlsx +0 -0
- data/test/perl_output/properties.xlsx +0 -0
- data/test/perl_output/protection.xlsx +0 -0
- data/test/perl_output/rich_strings.xlsx +0 -0
- data/test/perl_output/right_to_left.xlsx +0 -0
- data/test/perl_output/tab_colors.xlsx +0 -0
- data/test/test_delete_files.rb +37 -0
- data/test/test_example_match.rb +2281 -0
- data/test/test_xml_writer_simple.rb +63 -0
- data/test/workbook/test_get_chart_range.rb +59 -0
- data/test/workbook/test_sort_defined_names.rb +77 -0
- data/test/workbook/test_workbook_01.rb +29 -0
- data/test/workbook/test_workbook_02.rb +31 -0
- data/test/workbook/test_workbook_03.rb +31 -0
- data/test/workbook/test_workbook_new.rb +18 -0
- data/test/workbook/test_write_defined_name.rb +17 -0
- data/test/workbook/test_write_defined_names.rb +41 -0
- data/test/worksheet/test_calculate_spans.rb +58 -0
- data/test/worksheet/test_convert_date_time_01.rb +439 -0
- data/test/worksheet/test_convert_date_time_02.rb +478 -0
- data/test/worksheet/test_convert_date_time_03.rb +435 -0
- data/test/worksheet/test_extract_filter_tokens.rb +109 -0
- data/test/worksheet/test_parse_filter_expression.rb +143 -0
- data/test/worksheet/test_position_object.rb +50 -0
- data/test/worksheet/test_repeat_formula.rb +55 -0
- data/test/worksheet/test_worksheet_01.rb +32 -0
- data/test/worksheet/test_worksheet_02.rb +38 -0
- data/test/worksheet/test_worksheet_03.rb +44 -0
- data/test/worksheet/test_worksheet_04.rb +45 -0
- data/test/worksheet/test_write_array_formula_01.rb +99 -0
- data/test/worksheet/test_write_autofilter.rb +260 -0
- data/test/worksheet/test_write_brk.rb +18 -0
- data/test/worksheet/test_write_cell.rb +49 -0
- data/test/worksheet/test_write_cell_value.rb +33 -0
- data/test/worksheet/test_write_col_breaks.rb +27 -0
- data/test/worksheet/test_write_col_info.rb +95 -0
- data/test/worksheet/test_write_conditional_formatting.rb +72 -0
- data/test/worksheet/test_write_custom_filter.rb +18 -0
- data/test/worksheet/test_write_custom_filters.rb +25 -0
- data/test/worksheet/test_write_data_validation_01.rb +113 -0
- data/test/worksheet/test_write_data_validation_02.rb +528 -0
- data/test/worksheet/test_write_dimension.rb +94 -0
- data/test/worksheet/test_write_ext.rb +18 -0
- data/test/worksheet/test_write_ext_lst.rb +18 -0
- data/test/worksheet/test_write_filter.rb +18 -0
- data/test/worksheet/test_write_filter_column.rb +18 -0
- data/test/worksheet/test_write_filters.rb +32 -0
- data/test/worksheet/test_write_header_footer.rb +53 -0
- data/test/worksheet/test_write_hyperlink.rb +39 -0
- data/test/worksheet/test_write_hyperlinks.rb +27 -0
- data/test/worksheet/test_write_legacy_drawing.rb +19 -0
- data/test/worksheet/test_write_merge_cell.rb +18 -0
- data/test/worksheet/test_write_merge_cells.rb +192 -0
- data/test/worksheet/test_write_methods.rb +353 -0
- data/test/worksheet/test_write_mx_plv.rb +19 -0
- data/test/worksheet/test_write_page_margins.rb +98 -0
- data/test/worksheet/test_write_page_set_up_pr.rb +19 -0
- data/test/worksheet/test_write_page_setup.rb +54 -0
- data/test/worksheet/test_write_pane.rb +123 -0
- data/test/worksheet/test_write_phonetic_pr.rb +19 -0
- data/test/worksheet/test_write_print_options.rb +77 -0
- data/test/worksheet/test_write_row_breaks.rb +27 -0
- data/test/worksheet/test_write_row_element.rb +69 -0
- data/test/worksheet/test_write_selection.rb +18 -0
- data/test/worksheet/test_write_sheet_calc_pr.rb +18 -0
- data/test/worksheet/test_write_sheet_data.rb +18 -0
- data/test/worksheet/test_write_sheet_format_pr.rb +18 -0
- data/test/worksheet/test_write_sheet_pr.rb +36 -0
- data/test/worksheet/test_write_sheet_protection.rb +174 -0
- data/test/worksheet/test_write_sheet_view.rb +62 -0
- data/test/worksheet/test_write_sheet_view1.rb +64 -0
- data/test/worksheet/test_write_sheet_view2.rb +56 -0
- data/test/worksheet/test_write_sheet_view3.rb +83 -0
- data/test/worksheet/test_write_sheet_view4.rb +83 -0
- data/test/worksheet/test_write_sheet_view5.rb +74 -0
- data/test/worksheet/test_write_sheet_view6.rb +51 -0
- data/test/worksheet/test_write_sheet_view7.rb +71 -0
- data/test/worksheet/test_write_sheet_view8.rb +51 -0
- data/test/worksheet/test_write_sheet_view9.rb +51 -0
- data/test/worksheet/test_write_tab_color.rb +23 -0
- data/test/worksheet/test_write_worksheet.rb +19 -0
- data/write_xlsx.gemspec +308 -0
- metadata +363 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
module Writexlsx
|
|
3
|
+
module Utility
|
|
4
|
+
#
|
|
5
|
+
# xl_rowcol_to_cell($row, $col, $row_absolute, $col_absolute)
|
|
6
|
+
#
|
|
7
|
+
def xl_rowcol_to_cell(row, col, row_absolute = false, col_absolute = false)
|
|
8
|
+
row += 1 # Change from 0-indexed to 1 indexed.
|
|
9
|
+
row_abs = row_absolute ? '$' : ''
|
|
10
|
+
col_abs = col_absolute ? '$' : ''
|
|
11
|
+
col_str = xl_col_to_name(col, col_absolute)
|
|
12
|
+
"#{col_str}#{absolute_char(row_absolute)}#{row}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Returns: ($row, $col, $row_absolute, $col_absolute)
|
|
17
|
+
#
|
|
18
|
+
# The $row_absolute and $col_absolute parameters aren't documented because they
|
|
19
|
+
# mainly used internally and aren't very useful to the user.
|
|
20
|
+
#
|
|
21
|
+
def xl_cell_to_rowcol(cell)
|
|
22
|
+
cell =~ /(\$?)([A-Z]{1,3})(\$?)(\d+)/
|
|
23
|
+
|
|
24
|
+
col_abs = $1 != ""
|
|
25
|
+
col = $2
|
|
26
|
+
row_abs = $3 != ""
|
|
27
|
+
row = $4.to_i
|
|
28
|
+
|
|
29
|
+
# Convert base26 column string to number
|
|
30
|
+
# All your Base are belong to us.
|
|
31
|
+
chars = col.split(//)
|
|
32
|
+
expn = 0
|
|
33
|
+
col = 0
|
|
34
|
+
|
|
35
|
+
chars.reverse.each do |char|
|
|
36
|
+
col += (char.ord - 'A'.ord + 1) * (26 ** expn)
|
|
37
|
+
expn += 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Convert 1-index to zero-index
|
|
41
|
+
row -= 1
|
|
42
|
+
col -= 1
|
|
43
|
+
|
|
44
|
+
return [row, col, row_abs, col_abs]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def xl_col_to_name(col, col_absolute)
|
|
48
|
+
# Change from 0-indexed to 1 indexed.
|
|
49
|
+
col += 1
|
|
50
|
+
col_str = ''
|
|
51
|
+
|
|
52
|
+
while col > 0
|
|
53
|
+
# Set remainder from 1 .. 26
|
|
54
|
+
remainder = col % 26
|
|
55
|
+
remainder = 26 if remainder == 0
|
|
56
|
+
|
|
57
|
+
# Convert the remainder to a character. C-ishly.
|
|
58
|
+
col_letter = ("A".ord + remainder - 1).chr
|
|
59
|
+
|
|
60
|
+
# Accumulate the column letters, right to left.
|
|
61
|
+
col_str = col_letter + col_str
|
|
62
|
+
|
|
63
|
+
# Get the next order of magnitude.
|
|
64
|
+
col = (col - 1) / 26
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
"#{absolute_char(col_absolute)}#{col_str}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def xl_range(row_1, row_2, col_1, col_2,
|
|
71
|
+
row_abs_1 = false, row_abs_2 = false, col_abs_1 = false, col_abs_2 = false)
|
|
72
|
+
range1 = xl_rowcol_to_cell(row_1, col_1, row_abs_1, col_abs_1)
|
|
73
|
+
range2 = xl_rowcol_to_cell(row_2, col_2, row_abs_2, col_abs_2)
|
|
74
|
+
|
|
75
|
+
"#{range1}:#{range2}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def xl_range_formula(sheetname, row_1, row_2, col_1, col_2)
|
|
79
|
+
# Use Excel's conventions and quote the sheet name if it contains any
|
|
80
|
+
# non-word character or if it isn't already quoted.
|
|
81
|
+
sheetname = "'#{sheetname}'" if sheetname =~ /\W/ && !(sheetname =~ /^'/)
|
|
82
|
+
|
|
83
|
+
range1 = xl_rowcol_to_cell( row_1, col_1, 1, 1 )
|
|
84
|
+
range2 = xl_rowcol_to_cell( row_2, col_2, 1, 1 )
|
|
85
|
+
|
|
86
|
+
"=#{sheetname}!#{range1}:#{range2}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def absolute_char(absolute)
|
|
90
|
+
absolute ? '$' : ''
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def xml_str
|
|
94
|
+
@writer.string
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.delete_files(path)
|
|
98
|
+
if FileTest.file?(path)
|
|
99
|
+
File.delete(path)
|
|
100
|
+
elsif FileTest.directory?(path)
|
|
101
|
+
Dir.foreach(path) do |file|
|
|
102
|
+
next if file =~ /^\.\.?$/ # '.' or '..'
|
|
103
|
+
delete_files(path.sub(/\/+$/,"") + '/' + file)
|
|
104
|
+
end
|
|
105
|
+
Dir.rmdir(path)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def put_deprecate_message(method)
|
|
110
|
+
$stderr.puts("Warning: calling deprecated method #{method}. This method will be removed in a future release.")
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,1488 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
require 'write_xlsx/package/xml_writer_simple'
|
|
3
|
+
require 'write_xlsx/package/packager'
|
|
4
|
+
require 'write_xlsx/worksheet'
|
|
5
|
+
require 'write_xlsx/format'
|
|
6
|
+
require 'write_xlsx/utility'
|
|
7
|
+
require 'write_xlsx/chart'
|
|
8
|
+
require 'write_xlsx/zip_file_utils'
|
|
9
|
+
require 'tmpdir'
|
|
10
|
+
require 'tempfile'
|
|
11
|
+
require 'digest/md5'
|
|
12
|
+
|
|
13
|
+
module Writexlsx
|
|
14
|
+
class Workbook
|
|
15
|
+
|
|
16
|
+
include Writexlsx::Utility
|
|
17
|
+
|
|
18
|
+
attr_accessor :str_total, :str_unique
|
|
19
|
+
attr_writer :firstsheet
|
|
20
|
+
attr_reader :palette
|
|
21
|
+
attr_reader :font_count, :num_format_count, :border_count, :fill_count, :custom_colors
|
|
22
|
+
attr_reader :worksheets, :sheetnames, :charts, :drawings, :num_comment_files, :named_ranges
|
|
23
|
+
attr_reader :str_array, :doc_properties
|
|
24
|
+
attr_reader :image_types, :images
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# A new Excel workbook is created using the new() constructor which accepts either a filename
|
|
28
|
+
# or a IO object as a parameter.
|
|
29
|
+
# The following example creates a new Excel file based on a filename:
|
|
30
|
+
#
|
|
31
|
+
# workbook = WriteXLSX.new('filename.xlsx')
|
|
32
|
+
# worksheet = workbook.add_worksheet
|
|
33
|
+
# worksheet.write(0, 0, 'Hi Excel!')
|
|
34
|
+
# workbook.close
|
|
35
|
+
#
|
|
36
|
+
# Here are some other examples of using new() with filenames:
|
|
37
|
+
#
|
|
38
|
+
# workbook1 = WriteXLSX.new(filename)
|
|
39
|
+
# workbook2 = WriteXLSX.new('/tmp/filename.xlsx')
|
|
40
|
+
# workbook3 = WriteXLSX.new("c:\\tmp\\filename.xlsx")
|
|
41
|
+
# workbook4 = WriteXLSX.new('c:\tmp\filename.xlsx')
|
|
42
|
+
#
|
|
43
|
+
# The last two examples demonstrates how to create a file on DOS or Windows where it is
|
|
44
|
+
# necessary to either escape the directory separator \ or to use single quotes to ensure
|
|
45
|
+
# that it isn't interpolated.
|
|
46
|
+
#
|
|
47
|
+
# It is recommended that the filename uses the extension .xlsx rather than .xls since
|
|
48
|
+
# the latter causes an Excel warning when used with the XLSX format.
|
|
49
|
+
#
|
|
50
|
+
# The new() constructor returns a WriteXLSX object that you can use to add worksheets and
|
|
51
|
+
# store data.
|
|
52
|
+
#
|
|
53
|
+
# You can also pass a valid IO object to the new() constructor.
|
|
54
|
+
#
|
|
55
|
+
# xlsx = StringIO.new
|
|
56
|
+
# workbook = WriteXLSX.new(xlsx)
|
|
57
|
+
# ....
|
|
58
|
+
# workbook.close
|
|
59
|
+
# # you can get XLSX binary data as xlsx.string
|
|
60
|
+
#
|
|
61
|
+
# And you can pass default_formats parameter like this:
|
|
62
|
+
#
|
|
63
|
+
# formats = {
|
|
64
|
+
# :font => 'Arial',
|
|
65
|
+
# :size => 10.5
|
|
66
|
+
# }
|
|
67
|
+
# workbook = WriteXLSX.new('file.xlsx', formats)
|
|
68
|
+
#
|
|
69
|
+
def initialize(file, default_formats = {})
|
|
70
|
+
@writer = Package::XMLWriterSimple.new
|
|
71
|
+
|
|
72
|
+
@tempdir = File.join(Dir.tmpdir, Digest::MD5.hexdigest(Time.now.to_s))
|
|
73
|
+
setup_filename(file)
|
|
74
|
+
@date_1904 = false
|
|
75
|
+
@activesheet = 0
|
|
76
|
+
@firstsheet = 0
|
|
77
|
+
@selected = 0
|
|
78
|
+
@fileclosed = false
|
|
79
|
+
@sheet_name = 'Sheet'
|
|
80
|
+
@chart_name = 'Chart'
|
|
81
|
+
@sheetname_count = 0
|
|
82
|
+
@chartname_count = 0
|
|
83
|
+
@worksheets = []
|
|
84
|
+
@charts = []
|
|
85
|
+
@drawings = []
|
|
86
|
+
@sheetnames = []
|
|
87
|
+
@formats = []
|
|
88
|
+
@xf_formats = []
|
|
89
|
+
@xf_format_indices = {}
|
|
90
|
+
@dxf_formats = []
|
|
91
|
+
@dxf_format_indices = {}
|
|
92
|
+
@font_count = 0
|
|
93
|
+
@num_format_count = 0
|
|
94
|
+
@defined_names = []
|
|
95
|
+
@named_ranges = []
|
|
96
|
+
@custom_colors = []
|
|
97
|
+
@doc_properties = {}
|
|
98
|
+
@local_time = Time.now
|
|
99
|
+
@num_comment_files = 0
|
|
100
|
+
@image_types = {}
|
|
101
|
+
@images = []
|
|
102
|
+
|
|
103
|
+
# Structures for the shared strings data.
|
|
104
|
+
@str_total = 0
|
|
105
|
+
@str_unique = 0
|
|
106
|
+
@str_table = {}
|
|
107
|
+
@str_array = []
|
|
108
|
+
|
|
109
|
+
add_format(default_formats.merge(:xf_index => 0))
|
|
110
|
+
set_color_palette
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#
|
|
114
|
+
# write XLSX data to file or IO object.
|
|
115
|
+
#
|
|
116
|
+
def close
|
|
117
|
+
# In case close() is called twice, by user and by DESTROY.
|
|
118
|
+
return if @fileclosed
|
|
119
|
+
|
|
120
|
+
@fileclosed = 1
|
|
121
|
+
store_workbook
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# get array of Worksheet objects
|
|
125
|
+
#
|
|
126
|
+
# :call-seq:
|
|
127
|
+
# sheets -> array of all Wordsheet object
|
|
128
|
+
# sheets(1, 3, 4) -> array of spcified Worksheet object.
|
|
129
|
+
#
|
|
130
|
+
# The sheets() method returns a array, or a sliced array, of the worksheets
|
|
131
|
+
# in a workbook.
|
|
132
|
+
#
|
|
133
|
+
# If no arguments are passed the method returns a list of all the worksheets
|
|
134
|
+
# in the workbook. This is useful if you want to repeat an operation on each
|
|
135
|
+
# worksheet:
|
|
136
|
+
#
|
|
137
|
+
# workbook.sheets.each do |worksheet|
|
|
138
|
+
# print worksheet.get_name
|
|
139
|
+
# end
|
|
140
|
+
#
|
|
141
|
+
# You can also specify a slice list to return one or more worksheet objects:
|
|
142
|
+
#
|
|
143
|
+
# worksheet = workbook.sheets(0)
|
|
144
|
+
# worksheet.write('A1', 'Hello')
|
|
145
|
+
#
|
|
146
|
+
# you can write the above example as:
|
|
147
|
+
#
|
|
148
|
+
# workbook.sheets(0).write('A1', 'Hello')
|
|
149
|
+
#
|
|
150
|
+
# The following example returns the first and last worksheet in a workbook:
|
|
151
|
+
#
|
|
152
|
+
# workbook.sheets(0, -1).each do |sheet|
|
|
153
|
+
# # Do something
|
|
154
|
+
# end
|
|
155
|
+
#
|
|
156
|
+
def sheets(*args)
|
|
157
|
+
if args.empty?
|
|
158
|
+
@worksheets
|
|
159
|
+
else
|
|
160
|
+
args.collect{|i| @worksheets[i] }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
#
|
|
165
|
+
# Set the date system: false = 1900 (the default), true = 1904
|
|
166
|
+
#
|
|
167
|
+
# Excel stores dates as real numbers where the integer part stores
|
|
168
|
+
# the number of days since the epoch and the fractional part stores
|
|
169
|
+
# the percentage of the day. The epoch can be either 1900 or 1904.
|
|
170
|
+
# Excel for Windows uses 1900 and Excel for Macintosh uses 1904.
|
|
171
|
+
# However, Excel on either platform will convert automatically between
|
|
172
|
+
# one system and the other.
|
|
173
|
+
#
|
|
174
|
+
# WriteXLSX stores dates in the 1900 format by default. If you wish to
|
|
175
|
+
# change this you can call the set_1904() workbook method.
|
|
176
|
+
# You can query the current value by calling the get_1904() workbook method.
|
|
177
|
+
# This returns 0 for 1900 and 1 for 1904.
|
|
178
|
+
#
|
|
179
|
+
# In general you probably won't need to use set_1904().
|
|
180
|
+
#
|
|
181
|
+
def set_1904(mode = true)
|
|
182
|
+
unless sheets.empty?
|
|
183
|
+
raise "set_1904() must be called before add_worksheet()"
|
|
184
|
+
end
|
|
185
|
+
@date_1904 = (!mode || mode == 0) ? false : true
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def get_1904
|
|
189
|
+
@date_1904
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
#
|
|
193
|
+
# user must not use. it is internal method.
|
|
194
|
+
#
|
|
195
|
+
def set_xml_writer(filename) #:nodoc:
|
|
196
|
+
@writer.set_xml_writer(filename)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
#
|
|
200
|
+
# user must not use. it is internal method.
|
|
201
|
+
#
|
|
202
|
+
def xml_str #:nodoc:
|
|
203
|
+
@writer.string
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
#
|
|
207
|
+
# user must not use. it is internal method.
|
|
208
|
+
#
|
|
209
|
+
def assemble_xml_file #:nodoc:
|
|
210
|
+
return unless @writer
|
|
211
|
+
|
|
212
|
+
# Prepare format object for passing to Style.pm.
|
|
213
|
+
prepare_format_properties
|
|
214
|
+
|
|
215
|
+
write_xml_declaration
|
|
216
|
+
|
|
217
|
+
# Write the root workbook element.
|
|
218
|
+
write_workbook
|
|
219
|
+
|
|
220
|
+
# Write the XLSX file version.
|
|
221
|
+
write_file_version
|
|
222
|
+
|
|
223
|
+
# Write the workbook properties.
|
|
224
|
+
write_workbook_pr
|
|
225
|
+
|
|
226
|
+
# Write the workbook view properties.
|
|
227
|
+
write_book_views
|
|
228
|
+
|
|
229
|
+
# Write the worksheet names and ids.
|
|
230
|
+
write_sheets
|
|
231
|
+
|
|
232
|
+
# Write the workbook defined names.
|
|
233
|
+
write_defined_names
|
|
234
|
+
|
|
235
|
+
# Write the workbook calculation properties.
|
|
236
|
+
write_calc_pr
|
|
237
|
+
|
|
238
|
+
# Write the workbook extension storage.
|
|
239
|
+
#write_ext_lst
|
|
240
|
+
|
|
241
|
+
# Close the workbook tag.
|
|
242
|
+
write_workbook_end
|
|
243
|
+
|
|
244
|
+
# Close the XML writer object and filehandle.
|
|
245
|
+
@writer.crlf
|
|
246
|
+
@writer.close
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
#
|
|
250
|
+
# At least one worksheet should be added to a new workbook. A worksheet is used to write data into cells:
|
|
251
|
+
#
|
|
252
|
+
# worksheet1 = workbook.add_worksheet # Sheet1
|
|
253
|
+
# worksheet2 = workbook.add_worksheet('Foglio2') # Foglio2
|
|
254
|
+
# worksheet3 = workbook.add_worksheet('Data') # Data
|
|
255
|
+
# worksheet4 = workbook.add_worksheet # Sheet4
|
|
256
|
+
# If name is not specified the default Excel convention will be followed, i.e. Sheet1, Sheet2, etc.
|
|
257
|
+
#
|
|
258
|
+
# The worksheet name must be a valid Excel worksheet name,
|
|
259
|
+
# i.e. it cannot contain any of the following characters,
|
|
260
|
+
# [ ] : * ? / \
|
|
261
|
+
#
|
|
262
|
+
# and it must be less than 32 characters.
|
|
263
|
+
# In addition, you cannot use the same, case insensitive,
|
|
264
|
+
# sheetname for more than one worksheet.
|
|
265
|
+
#
|
|
266
|
+
def add_worksheet(name = '')
|
|
267
|
+
index = @worksheets.size
|
|
268
|
+
name = check_sheetname(name)
|
|
269
|
+
|
|
270
|
+
worksheet = Worksheet.new(self, index, name)
|
|
271
|
+
@worksheets[index] = worksheet
|
|
272
|
+
@sheetnames[index] = name
|
|
273
|
+
|
|
274
|
+
worksheet
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
#
|
|
278
|
+
# This method is use to create a new chart either as a standalone worksheet
|
|
279
|
+
# (the default) or as an embeddable object that can be inserted into
|
|
280
|
+
# a worksheet via the insert_chart() Worksheet method.
|
|
281
|
+
#
|
|
282
|
+
# chart = workbook.add_chart(:type => 'column')
|
|
283
|
+
#
|
|
284
|
+
# The properties that can be set are:
|
|
285
|
+
#
|
|
286
|
+
# :type (required)
|
|
287
|
+
# :subtype (optional)
|
|
288
|
+
# :name (optional)
|
|
289
|
+
# :embedded (optional)
|
|
290
|
+
#
|
|
291
|
+
# :type
|
|
292
|
+
#
|
|
293
|
+
# This is a required parameter. It defines the type of chart that will be created.
|
|
294
|
+
#
|
|
295
|
+
# chart = workbook.add_chart(:type => 'line')
|
|
296
|
+
#
|
|
297
|
+
# The available types are:
|
|
298
|
+
#
|
|
299
|
+
# area
|
|
300
|
+
# bar
|
|
301
|
+
# column
|
|
302
|
+
# line
|
|
303
|
+
# pie
|
|
304
|
+
# scatter
|
|
305
|
+
# stock
|
|
306
|
+
#
|
|
307
|
+
# :subtype
|
|
308
|
+
#
|
|
309
|
+
# Used to define a chart subtype where available.
|
|
310
|
+
#
|
|
311
|
+
# chart = workbook.add_chart(:type => 'bar', :subtype => 'stacked')
|
|
312
|
+
#
|
|
313
|
+
# Currently only Bar and Column charts support subtypes
|
|
314
|
+
# (stacked and percent_stacked). See the documentation for those chart
|
|
315
|
+
# types.
|
|
316
|
+
#
|
|
317
|
+
# :name
|
|
318
|
+
#
|
|
319
|
+
# Set the name for the chart sheet. The name property is optional and
|
|
320
|
+
# if it isn't supplied will default to Chart1 .. n. The name must be
|
|
321
|
+
# a valid Excel worksheet name. See add_worksheet for more details on
|
|
322
|
+
# valid sheet names. The name property can be omitted for embedded charts.
|
|
323
|
+
#
|
|
324
|
+
# chart = workbook.add_chart(:type => 'line', :name => 'Results Chart')
|
|
325
|
+
#
|
|
326
|
+
# :embedded
|
|
327
|
+
#
|
|
328
|
+
# Specifies that the Chart object will be inserted in a worksheet
|
|
329
|
+
# via the insert_chart Worksheet method. It is an error to try insert
|
|
330
|
+
# a Chart that doesn't have this flag set.
|
|
331
|
+
#
|
|
332
|
+
# chart = workbook.add_chart(:type => 'line', :embedded => 1)
|
|
333
|
+
#
|
|
334
|
+
# # Configure the chart.
|
|
335
|
+
# ...
|
|
336
|
+
#
|
|
337
|
+
# # Insert the chart into the a worksheet.
|
|
338
|
+
# worksheet.insert_chart('E2', chart)
|
|
339
|
+
#
|
|
340
|
+
# See Chart for details on how to configure the chart object
|
|
341
|
+
# once it is created. See also the chart_*.pl programs in the examples
|
|
342
|
+
# directory of the distro.
|
|
343
|
+
#
|
|
344
|
+
def add_chart(params = {})
|
|
345
|
+
name = ''
|
|
346
|
+
index = @worksheets.size
|
|
347
|
+
|
|
348
|
+
# Type must be specified so we can create the required chart instance.
|
|
349
|
+
type = params[:type]
|
|
350
|
+
raise "Must define chart type in add_chart()" unless type
|
|
351
|
+
|
|
352
|
+
# Ensure that the chart defaults to non embedded.
|
|
353
|
+
embedded = params[:embedded] || 0
|
|
354
|
+
|
|
355
|
+
# Check the worksheet name for non-embedded charts.
|
|
356
|
+
name = check_sheetname(params[:name], 1) unless embedded
|
|
357
|
+
|
|
358
|
+
chart = Chart.factory(type)
|
|
359
|
+
|
|
360
|
+
# Get an incremental id to use for axes ids.
|
|
361
|
+
chart.id = @charts.size
|
|
362
|
+
|
|
363
|
+
# If the chart isn't embedded let the workbook control it.
|
|
364
|
+
if embedded
|
|
365
|
+
# Set index to 0 so that the activate() and set_first_sheet() methods
|
|
366
|
+
# point back to the first worksheet if used for embedded charts.
|
|
367
|
+
chart.index = 0
|
|
368
|
+
chart.palette = @palette
|
|
369
|
+
chart.set_embedded_config_data
|
|
370
|
+
@charts << chart
|
|
371
|
+
|
|
372
|
+
return chart
|
|
373
|
+
else
|
|
374
|
+
chartsheet = Chartsheet.new(self, name, index)
|
|
375
|
+
chart.palette = @palette
|
|
376
|
+
chartsheet.chart = chart
|
|
377
|
+
chartsheet.drawing = Drawing.new
|
|
378
|
+
@worksheets.index = chartsheet
|
|
379
|
+
@sheetnames.index = name
|
|
380
|
+
|
|
381
|
+
@charts << chart
|
|
382
|
+
|
|
383
|
+
return chartsheet
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
#
|
|
388
|
+
# The add_format method can be used to create new Format objects
|
|
389
|
+
# which are used to apply formatting to a cell. You can either define
|
|
390
|
+
# the properties at creation time via a hash of property values
|
|
391
|
+
# or later via method calls.
|
|
392
|
+
#
|
|
393
|
+
# format1 = workbook.add_format(property_hash) # Set properties at creation
|
|
394
|
+
# format2 = workbook.add_format # Set properties later
|
|
395
|
+
#
|
|
396
|
+
# See the Format Class's rdoc for more details about Format properties and how to set them.
|
|
397
|
+
#
|
|
398
|
+
def add_format(properties = {})
|
|
399
|
+
init_data = [
|
|
400
|
+
@xf_format_indices,
|
|
401
|
+
@dxf_format_indices,
|
|
402
|
+
properties
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
format = Format.new(*init_data)
|
|
406
|
+
|
|
407
|
+
@formats.push(format) # Store format reference
|
|
408
|
+
|
|
409
|
+
format
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
#
|
|
413
|
+
# Create a defined name in Excel. We handle global/workbook level names and
|
|
414
|
+
# local/worksheet names.
|
|
415
|
+
#
|
|
416
|
+
# This method is used to defined a name that can be used to represent a value,
|
|
417
|
+
# a single cell or a range of cells in a workbook.
|
|
418
|
+
#
|
|
419
|
+
# For example to set a global/workbook name:
|
|
420
|
+
#
|
|
421
|
+
# # Global/workbook names.
|
|
422
|
+
# workbook.define_name('Exchange_rate', '=0.96')
|
|
423
|
+
# workbook.define_name('Sales', '=Sheet1!$G$1:$H$10')
|
|
424
|
+
#
|
|
425
|
+
# It is also possible to define a local/worksheet name by prefixing the name
|
|
426
|
+
# with the sheet name using the syntax sheetname!definedname:
|
|
427
|
+
#
|
|
428
|
+
# # Local/worksheet name.
|
|
429
|
+
# workbook.define_name('Sheet2!Sales', '=Sheet2!$G$1:$G$10')
|
|
430
|
+
# If the sheet name contains spaces or special characters
|
|
431
|
+
# you must enclose it in single quotes like in Excel:
|
|
432
|
+
#
|
|
433
|
+
# workbook.define_name("'New Data'!Sales", '=Sheet2!$G$1:$G$10')
|
|
434
|
+
#
|
|
435
|
+
# See the defined_name.rb program in the examples dir of the distro.
|
|
436
|
+
#
|
|
437
|
+
def define_name(name, formula)
|
|
438
|
+
sheet_index = nil
|
|
439
|
+
sheetname = ''
|
|
440
|
+
full_name = name
|
|
441
|
+
|
|
442
|
+
# Remove the = sign from the formula if it exists.
|
|
443
|
+
formula.sub!(/^=/, '')
|
|
444
|
+
|
|
445
|
+
# Local defined names are formatted like "Sheet1!name".
|
|
446
|
+
if name =~ /^(.*)!(.*)$/
|
|
447
|
+
sheetname = $1
|
|
448
|
+
name = $2
|
|
449
|
+
sheet_index = get_sheet_index(sheetname)
|
|
450
|
+
else
|
|
451
|
+
sheet_index =-1 # Use -1 to indicate global names.
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Warn if the sheet index wasn't found.
|
|
455
|
+
if !sheet_index
|
|
456
|
+
raise "Unknown sheet name #{sheetname} in defined_name()\n"
|
|
457
|
+
return -1
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Warn if the sheet name contains invalid chars as defined by Excel help.
|
|
461
|
+
if name !~ %r!^[a-zA-Z_\\][a-zA-Z_.]+!
|
|
462
|
+
raise "Invalid characters in name '#{name}' used in defined_name()\n"
|
|
463
|
+
return -1
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Warn if the sheet name looks like a cell name.
|
|
467
|
+
if name =~ %r(^[a-zA-Z][a-zA-Z]?[a-dA-D]?[0-9]+$)
|
|
468
|
+
raise "Invalid name '#{name}' looks like a cell name in defined_name()\n"
|
|
469
|
+
return -1
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
@defined_names.push([ name, sheet_index, formula])
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
#
|
|
476
|
+
# The set_properties method can be used to set the document properties
|
|
477
|
+
# of the Excel file created by WriteXLSX. These properties are visible
|
|
478
|
+
# when you use the Office Button -> Prepare -> Properties option in Excel
|
|
479
|
+
# and are also available to external applications that read or index windows files.
|
|
480
|
+
#
|
|
481
|
+
# The properties should be passed in hash format as follows:
|
|
482
|
+
#
|
|
483
|
+
# workbook.set_properties(
|
|
484
|
+
# :title => 'This is an example spreadsheet',
|
|
485
|
+
# :author => 'Hideo NAKAMURA',
|
|
486
|
+
# :comments => 'Created with Ruby and WriteXLSX'
|
|
487
|
+
# )
|
|
488
|
+
#
|
|
489
|
+
# The properties that can be set are:
|
|
490
|
+
#
|
|
491
|
+
# :title
|
|
492
|
+
# :subject
|
|
493
|
+
# :author
|
|
494
|
+
# :manager
|
|
495
|
+
# :company
|
|
496
|
+
# :category
|
|
497
|
+
# :keywords
|
|
498
|
+
# :comments
|
|
499
|
+
# :status
|
|
500
|
+
#
|
|
501
|
+
# See also the properties.rb program in the examples directory of the distro.
|
|
502
|
+
#
|
|
503
|
+
def set_properties(params)
|
|
504
|
+
# Ignore if no args were passed.
|
|
505
|
+
return -1 if params.empty?
|
|
506
|
+
|
|
507
|
+
# List of valid input parameters.
|
|
508
|
+
valid = {
|
|
509
|
+
:title => 1,
|
|
510
|
+
:subject => 1,
|
|
511
|
+
:author => 1,
|
|
512
|
+
:keywords => 1,
|
|
513
|
+
:comments => 1,
|
|
514
|
+
:last_author => 1,
|
|
515
|
+
:created => 1,
|
|
516
|
+
:category => 1,
|
|
517
|
+
:manager => 1,
|
|
518
|
+
:company => 1,
|
|
519
|
+
:status => 1
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
# Check for valid input parameters.
|
|
523
|
+
params.each_key do |key|
|
|
524
|
+
return -1 unless valid.has_key?(key)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Set the creation time unless specified by the user.
|
|
528
|
+
params[:created] = @local_time unless params.has_key?(:created)
|
|
529
|
+
|
|
530
|
+
@doc_properties = params.dup
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
#
|
|
534
|
+
# Change the RGB components of the elements in the colour palette.
|
|
535
|
+
#
|
|
536
|
+
# The set_custom_color() method can be used to override one of the built-in
|
|
537
|
+
# palette values with a more suitable colour.
|
|
538
|
+
#
|
|
539
|
+
# The value for _index_ should be in the range 8..63, see "COLOURS IN EXCEL".
|
|
540
|
+
#
|
|
541
|
+
# The default named colours use the following indices:
|
|
542
|
+
#
|
|
543
|
+
# 8 => black
|
|
544
|
+
# 9 => white
|
|
545
|
+
# 10 => red
|
|
546
|
+
# 11 => lime
|
|
547
|
+
# 12 => blue
|
|
548
|
+
# 13 => yellow
|
|
549
|
+
# 14 => magenta
|
|
550
|
+
# 15 => cyan
|
|
551
|
+
# 16 => brown
|
|
552
|
+
# 17 => green
|
|
553
|
+
# 18 => navy
|
|
554
|
+
# 20 => purple
|
|
555
|
+
# 22 => silver
|
|
556
|
+
# 23 => gray
|
|
557
|
+
# 33 => pink
|
|
558
|
+
# 53 => orange
|
|
559
|
+
#
|
|
560
|
+
# A new colour is set using its RGB (red green blue) components. The red,
|
|
561
|
+
# green and blue values must be in the range 0..255. You can determine the
|
|
562
|
+
# required values in Excel using the Tools->Options->Colors->Modify dialog.
|
|
563
|
+
#
|
|
564
|
+
# The set_custom_color() workbook method can also be used with a HTML style
|
|
565
|
+
# #rrggbb hex value:
|
|
566
|
+
#
|
|
567
|
+
# workbook.set_custom_color(40, 255, 102, 0 ) # Orange
|
|
568
|
+
# workbook.set_custom_color(40, 0xFF, 0x66, 0x00) # Same thing
|
|
569
|
+
# workbook.set_custom_color(40, '#FF6600' ) # Same thing
|
|
570
|
+
#
|
|
571
|
+
# font = workbook.add_format(:color => 40) # Use the modified colour
|
|
572
|
+
#
|
|
573
|
+
# The return value from set_custom_color() is the index of the colour that
|
|
574
|
+
# was changed:
|
|
575
|
+
#
|
|
576
|
+
# ferrari = workbook.set_custom_color(40, 216, 12, 12)
|
|
577
|
+
#
|
|
578
|
+
# format = workbook.add_format(
|
|
579
|
+
# :bg_color => ferrari,
|
|
580
|
+
# :pattern => 1,
|
|
581
|
+
# :border => 1
|
|
582
|
+
# )
|
|
583
|
+
#
|
|
584
|
+
# Note, In the XLSX format the color palette isn't actually confined to 53
|
|
585
|
+
# unique colors. The WriteXLSX gem will be extended at a later stage to
|
|
586
|
+
# support the newer, semi-infinite, palette.
|
|
587
|
+
#
|
|
588
|
+
def set_custom_color(index, red = 0, green = 0, blue = 0)
|
|
589
|
+
# Match a HTML #xxyyzz style parameter
|
|
590
|
+
if !red.nil? && red =~ /^#(\w\w)(\w\w)(\w\w)/
|
|
591
|
+
red = $1.hex
|
|
592
|
+
green = $2.hex
|
|
593
|
+
blue = $3.hex
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
# Check that the colour index is the right range
|
|
597
|
+
if index < 8 || index > 64
|
|
598
|
+
raise "Color index #{index} outside range: 8 <= index <= 64"
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Check that the colour components are in the right range
|
|
602
|
+
if (red < 0 || red > 255) ||
|
|
603
|
+
(green < 0 || green > 255) ||
|
|
604
|
+
(blue < 0 || blue > 255)
|
|
605
|
+
raise "Color component outside range: 0 <= color <= 255"
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
index -=8 # Adjust colour index (wingless dragonfly)
|
|
609
|
+
|
|
610
|
+
# Set the RGB value
|
|
611
|
+
@palette[index] = [red, green, blue]
|
|
612
|
+
|
|
613
|
+
# Store the custome colors for the style.xml file.
|
|
614
|
+
@custom_colors << sprintf("FF%02X%02X%02X", red, green, blue)
|
|
615
|
+
|
|
616
|
+
index + 8
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def activesheet=(worksheet) #:nodoc:
|
|
620
|
+
@activesheet = worksheet
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def writer #:nodoc:
|
|
624
|
+
@writer
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def date_1904? #:nodoc:
|
|
628
|
+
@date_1904 ||= false
|
|
629
|
+
!!@date_1904
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
#
|
|
633
|
+
# Add a string to the shared string table, if it isn't already there, and
|
|
634
|
+
# return the string index.
|
|
635
|
+
#
|
|
636
|
+
def shared_string_index(str) #:nodoc:
|
|
637
|
+
# Add the string to the shared string table.
|
|
638
|
+
unless @str_table[str]
|
|
639
|
+
@str_table[str] = @str_unique
|
|
640
|
+
@str_unique += 1
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
@str_total += 1
|
|
644
|
+
@str_table[str]
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def xf_formats # :nodoc:
|
|
648
|
+
@xf_formats.dup
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def dxf_formats # :nodoc:
|
|
652
|
+
@dxf_formats.dup
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
private
|
|
656
|
+
|
|
657
|
+
def setup_filename(file) #:nodoc:
|
|
658
|
+
if file.respond_to?(:to_str) && file != ''
|
|
659
|
+
@filename = file
|
|
660
|
+
@fileobj = nil
|
|
661
|
+
elsif file.respond_to?(:write)
|
|
662
|
+
@filename = File.join(@tempdir, Digest::MD5.hexdigest(Time.now.to_s) + '.xlsx.tmp')
|
|
663
|
+
@fileobj = file
|
|
664
|
+
else
|
|
665
|
+
raise "'file' must be valid filename String of IO object."
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
#
|
|
670
|
+
# Sets the colour palette to the Excel defaults.
|
|
671
|
+
#
|
|
672
|
+
def set_color_palette #:nodoc:
|
|
673
|
+
@palette = [
|
|
674
|
+
[ 0x00, 0x00, 0x00, 0x00 ], # 8
|
|
675
|
+
[ 0xff, 0xff, 0xff, 0x00 ], # 9
|
|
676
|
+
[ 0xff, 0x00, 0x00, 0x00 ], # 10
|
|
677
|
+
[ 0x00, 0xff, 0x00, 0x00 ], # 11
|
|
678
|
+
[ 0x00, 0x00, 0xff, 0x00 ], # 12
|
|
679
|
+
[ 0xff, 0xff, 0x00, 0x00 ], # 13
|
|
680
|
+
[ 0xff, 0x00, 0xff, 0x00 ], # 14
|
|
681
|
+
[ 0x00, 0xff, 0xff, 0x00 ], # 15
|
|
682
|
+
[ 0x80, 0x00, 0x00, 0x00 ], # 16
|
|
683
|
+
[ 0x00, 0x80, 0x00, 0x00 ], # 17
|
|
684
|
+
[ 0x00, 0x00, 0x80, 0x00 ], # 18
|
|
685
|
+
[ 0x80, 0x80, 0x00, 0x00 ], # 19
|
|
686
|
+
[ 0x80, 0x00, 0x80, 0x00 ], # 20
|
|
687
|
+
[ 0x00, 0x80, 0x80, 0x00 ], # 21
|
|
688
|
+
[ 0xc0, 0xc0, 0xc0, 0x00 ], # 22
|
|
689
|
+
[ 0x80, 0x80, 0x80, 0x00 ], # 23
|
|
690
|
+
[ 0x99, 0x99, 0xff, 0x00 ], # 24
|
|
691
|
+
[ 0x99, 0x33, 0x66, 0x00 ], # 25
|
|
692
|
+
[ 0xff, 0xff, 0xcc, 0x00 ], # 26
|
|
693
|
+
[ 0xcc, 0xff, 0xff, 0x00 ], # 27
|
|
694
|
+
[ 0x66, 0x00, 0x66, 0x00 ], # 28
|
|
695
|
+
[ 0xff, 0x80, 0x80, 0x00 ], # 29
|
|
696
|
+
[ 0x00, 0x66, 0xcc, 0x00 ], # 30
|
|
697
|
+
[ 0xcc, 0xcc, 0xff, 0x00 ], # 31
|
|
698
|
+
[ 0x00, 0x00, 0x80, 0x00 ], # 32
|
|
699
|
+
[ 0xff, 0x00, 0xff, 0x00 ], # 33
|
|
700
|
+
[ 0xff, 0xff, 0x00, 0x00 ], # 34
|
|
701
|
+
[ 0x00, 0xff, 0xff, 0x00 ], # 35
|
|
702
|
+
[ 0x80, 0x00, 0x80, 0x00 ], # 36
|
|
703
|
+
[ 0x80, 0x00, 0x00, 0x00 ], # 37
|
|
704
|
+
[ 0x00, 0x80, 0x80, 0x00 ], # 38
|
|
705
|
+
[ 0x00, 0x00, 0xff, 0x00 ], # 39
|
|
706
|
+
[ 0x00, 0xcc, 0xff, 0x00 ], # 40
|
|
707
|
+
[ 0xcc, 0xff, 0xff, 0x00 ], # 41
|
|
708
|
+
[ 0xcc, 0xff, 0xcc, 0x00 ], # 42
|
|
709
|
+
[ 0xff, 0xff, 0x99, 0x00 ], # 43
|
|
710
|
+
[ 0x99, 0xcc, 0xff, 0x00 ], # 44
|
|
711
|
+
[ 0xff, 0x99, 0xcc, 0x00 ], # 45
|
|
712
|
+
[ 0xcc, 0x99, 0xff, 0x00 ], # 46
|
|
713
|
+
[ 0xff, 0xcc, 0x99, 0x00 ], # 47
|
|
714
|
+
[ 0x33, 0x66, 0xff, 0x00 ], # 48
|
|
715
|
+
[ 0x33, 0xcc, 0xcc, 0x00 ], # 49
|
|
716
|
+
[ 0x99, 0xcc, 0x00, 0x00 ], # 50
|
|
717
|
+
[ 0xff, 0xcc, 0x00, 0x00 ], # 51
|
|
718
|
+
[ 0xff, 0x99, 0x00, 0x00 ], # 52
|
|
719
|
+
[ 0xff, 0x66, 0x00, 0x00 ], # 53
|
|
720
|
+
[ 0x66, 0x66, 0x99, 0x00 ], # 54
|
|
721
|
+
[ 0x96, 0x96, 0x96, 0x00 ], # 55
|
|
722
|
+
[ 0x00, 0x33, 0x66, 0x00 ], # 56
|
|
723
|
+
[ 0x33, 0x99, 0x66, 0x00 ], # 57
|
|
724
|
+
[ 0x00, 0x33, 0x00, 0x00 ], # 58
|
|
725
|
+
[ 0x33, 0x33, 0x00, 0x00 ], # 59
|
|
726
|
+
[ 0x99, 0x33, 0x00, 0x00 ], # 60
|
|
727
|
+
[ 0x99, 0x33, 0x66, 0x00 ], # 61
|
|
728
|
+
[ 0x33, 0x33, 0x99, 0x00 ], # 62
|
|
729
|
+
[ 0x33, 0x33, 0x33, 0x00 ], # 63
|
|
730
|
+
]
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
#
|
|
734
|
+
# Check for valid worksheet names. We check the length, if it contains any
|
|
735
|
+
# invalid characters and if the name is unique in the workbook.
|
|
736
|
+
#
|
|
737
|
+
def check_sheetname(name, chart = nil) #:nodoc:
|
|
738
|
+
name ||= ''
|
|
739
|
+
invalid_char = /[\[\]:*?\/\\]/
|
|
740
|
+
|
|
741
|
+
# Increment the Sheet/Chart number used for default sheet names below.
|
|
742
|
+
if chart
|
|
743
|
+
@chartname_count += 1
|
|
744
|
+
else
|
|
745
|
+
@sheetname_count += 1
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# Supply default Sheet/Chart name if none has been defined.
|
|
749
|
+
if name == ''
|
|
750
|
+
if chart
|
|
751
|
+
name = "#{@chart_name}#{@chartname_count}"
|
|
752
|
+
else
|
|
753
|
+
name = "#{@sheet_name}#{@sheetname_count}"
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
# Check that sheet name is <= 31. Excel limit.
|
|
758
|
+
raise "Sheetname #{name} must be <= 31 chars" if name.bytesize > 31
|
|
759
|
+
|
|
760
|
+
# Check that sheetname doesn't contain any invalid characters
|
|
761
|
+
if name =~ invalid_char
|
|
762
|
+
raise 'Invalid character []:*?/\\ in worksheet name: ' + name
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
# Check that the worksheet name doesn't already exist since this is a fatal
|
|
766
|
+
# error in Excel 97. The check must also exclude case insensitive matches.
|
|
767
|
+
@worksheets.each do |worksheet|
|
|
768
|
+
name_a = name
|
|
769
|
+
name_b = worksheet.name
|
|
770
|
+
|
|
771
|
+
if name_a.downcase == name_b.downcase
|
|
772
|
+
raise "Worksheet name '#{name}', with case ignored, is already used."
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
name
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
#
|
|
780
|
+
# Convert a range formula such as Sheet1!$B$1:$B$5 into a sheet name and cell
|
|
781
|
+
# range such as ( 'Sheet1', 0, 1, 4, 1 ).
|
|
782
|
+
#
|
|
783
|
+
def get_chart_range(range) #:nodoc:
|
|
784
|
+
# Split the range formula into sheetname and cells at the last '!'.
|
|
785
|
+
pos = range.rindex('!')
|
|
786
|
+
return nil unless pos
|
|
787
|
+
|
|
788
|
+
if pos > 0
|
|
789
|
+
sheetname = range[0, pos]
|
|
790
|
+
cells = range[pos + 1 .. -1]
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# Split the cell range into 2 cells or else use single cell for both.
|
|
794
|
+
if cells =~ /:/
|
|
795
|
+
cell_1, cell_2 = cells.split(/:/)
|
|
796
|
+
else
|
|
797
|
+
cell_1, cell_2 = cells, cells
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# Remove leading/trailing apostrophes and convert escaped quotes to single.
|
|
801
|
+
sheetname.sub!(/^'/, '')
|
|
802
|
+
sheetname.sub!(/'$/, '')
|
|
803
|
+
sheetname.gsub!(/''/, "'")
|
|
804
|
+
|
|
805
|
+
row_start, col_start = xl_cell_to_rowcol(cell_1)
|
|
806
|
+
row_end, col_end = xl_cell_to_rowcol(cell_2)
|
|
807
|
+
|
|
808
|
+
# Check that we have a 1D range only.
|
|
809
|
+
return nil if row_start != row_end && col_start != col_end
|
|
810
|
+
return [sheetname, row_start, col_start, row_end, col_end]
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def write_xml_declaration #:nodoc:
|
|
814
|
+
@writer.xml_decl
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
def write_workbook #:nodoc:
|
|
818
|
+
schema = 'http://schemas.openxmlformats.org'
|
|
819
|
+
attributes = [
|
|
820
|
+
'xmlns',
|
|
821
|
+
schema + '/spreadsheetml/2006/main',
|
|
822
|
+
'xmlns:r',
|
|
823
|
+
schema + '/officeDocument/2006/relationships'
|
|
824
|
+
]
|
|
825
|
+
@writer.start_tag('workbook', attributes)
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
def write_workbook_end #:nodoc:
|
|
829
|
+
@writer.end_tag('workbook')
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def write_file_version #:nodoc:
|
|
833
|
+
attributes = [
|
|
834
|
+
'appName', 'xl',
|
|
835
|
+
'lastEdited', 4,
|
|
836
|
+
'lowestEdited', 4,
|
|
837
|
+
'rupBuild', 4505
|
|
838
|
+
]
|
|
839
|
+
@writer.empty_tag('fileVersion', attributes)
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
def write_workbook_pr #:nodoc:
|
|
843
|
+
attributes = date_1904? ? ['date1904', 1] : []
|
|
844
|
+
attributes << 'defaultThemeVersion' << 124226
|
|
845
|
+
@writer.empty_tag('workbookPr', attributes)
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def write_book_views #:nodoc:
|
|
849
|
+
@writer.start_tag('bookViews') << write_workbook_view << @writer.end_tag('bookViews')
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
def write_workbook_view #:nodoc:
|
|
853
|
+
attributes = [
|
|
854
|
+
'xWindow', 240,
|
|
855
|
+
'yWindow', 15,
|
|
856
|
+
'windowWidth', 16095,
|
|
857
|
+
'windowHeight', 9660
|
|
858
|
+
]
|
|
859
|
+
if @firstsheet > 0
|
|
860
|
+
attributes << 'firstSheet' << @firstsheet
|
|
861
|
+
end
|
|
862
|
+
if @activesheet > 0
|
|
863
|
+
attributes << 'activeTab' << @activesheet
|
|
864
|
+
end
|
|
865
|
+
@writer.empty_tag('workbookView', attributes)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def write_sheets #:nodoc:
|
|
869
|
+
str = @writer.start_tag('sheets')
|
|
870
|
+
id_num = 1
|
|
871
|
+
@worksheets.each do |sheet|
|
|
872
|
+
str << write_sheet(sheet.name, id_num, sheet.hidden)
|
|
873
|
+
id_num += 1
|
|
874
|
+
end
|
|
875
|
+
str << @writer.end_tag('sheets')
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def write_sheet(name, sheet_id, hidden = false) #:nodoc:
|
|
879
|
+
attributes = [
|
|
880
|
+
'name', name,
|
|
881
|
+
'sheetId', sheet_id
|
|
882
|
+
]
|
|
883
|
+
|
|
884
|
+
if hidden
|
|
885
|
+
attributes << 'state' << 'hidden'
|
|
886
|
+
end
|
|
887
|
+
attributes << 'r:id' << "rId#{sheet_id}"
|
|
888
|
+
@writer.empty_tag('sheet', attributes)
|
|
889
|
+
end
|
|
890
|
+
|
|
891
|
+
def write_calc_pr #:nodoc:
|
|
892
|
+
attributes = ['calcId', 124519]
|
|
893
|
+
@writer.empty_tag('calcPr', attributes)
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def write_ext_lst #:nodoc:
|
|
897
|
+
tag = 'extLst'
|
|
898
|
+
@writer.start_tag(tag) << write_ext << @writer.end_tag(tag)
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def write_ext #:nodoc:
|
|
902
|
+
tag = 'ext'
|
|
903
|
+
attributes = [
|
|
904
|
+
'xmlns:mx', 'http://schemas.microsoft.com/office/mac/excel/2008/main',
|
|
905
|
+
'uri', 'http://schemas.microsoft.com/office/mac/excel/2008/main'
|
|
906
|
+
]
|
|
907
|
+
@writer.start_tag(tag, attributes) << write_mx_arch_id << @writer.end_tag(tag)
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
def write_mx_arch_id #:nodoc:
|
|
911
|
+
@writer.empty_tag('mx:ArchID', ['Flags', 2])
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def write_defined_names #:nodoc:
|
|
915
|
+
return if @defined_names.nil? || @defined_names.empty?
|
|
916
|
+
tag = 'definedNames'
|
|
917
|
+
str = @writer.start_tag(tag)
|
|
918
|
+
@defined_names.each { |defined_name| str << write_defined_name(defined_name) }
|
|
919
|
+
str << @writer.end_tag(tag)
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def write_defined_name(data) #:nodoc:
|
|
923
|
+
name, id, range, hidden = data
|
|
924
|
+
|
|
925
|
+
attributes = ['name', name]
|
|
926
|
+
attributes << 'localSheetId' << "#{id}" unless id == -1
|
|
927
|
+
attributes << 'hidden' << '1' if hidden
|
|
928
|
+
|
|
929
|
+
@writer.data_element('definedName', range, attributes)
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
def write_io(str) #:nodoc:
|
|
933
|
+
@writer << str
|
|
934
|
+
str
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def firstsheet #:nodoc:
|
|
938
|
+
@firstsheet ||= 0
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
def activesheet #:nodoc:
|
|
942
|
+
@activesheet ||= 0
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
# for test
|
|
946
|
+
def defined_names #:nodoc:
|
|
947
|
+
@defined_names ||= []
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
#
|
|
951
|
+
# Assemble worksheets into a workbook.
|
|
952
|
+
#
|
|
953
|
+
def store_workbook #:nodoc:
|
|
954
|
+
packager = Package::Packager.new
|
|
955
|
+
|
|
956
|
+
# Add a default worksheet if non have been added.
|
|
957
|
+
add_worksheet if @worksheets.empty?
|
|
958
|
+
|
|
959
|
+
# Ensure that at least one worksheet has been selected.
|
|
960
|
+
@worksheets.first.select if @activesheet == 0
|
|
961
|
+
|
|
962
|
+
# Set the active sheet.
|
|
963
|
+
@worksheets.each { |sheet| sheet.activate if sheet.index == @activesheet }
|
|
964
|
+
|
|
965
|
+
# Convert the SST strings data structure.
|
|
966
|
+
prepare_sst_string_data
|
|
967
|
+
|
|
968
|
+
# Prepare the worksheet cell comments.
|
|
969
|
+
prepare_comments
|
|
970
|
+
|
|
971
|
+
# Set the defined names for the worksheets such as Print Titles.
|
|
972
|
+
prepare_defined_names
|
|
973
|
+
|
|
974
|
+
# Prepare the drawings, charts and images.
|
|
975
|
+
prepare_drawings
|
|
976
|
+
|
|
977
|
+
# Add cached data to charts.
|
|
978
|
+
add_chart_data
|
|
979
|
+
|
|
980
|
+
# Package the workbook.
|
|
981
|
+
packager.add_workbook(self)
|
|
982
|
+
packager.set_package_dir(@tempdir)
|
|
983
|
+
packager.create_package
|
|
984
|
+
|
|
985
|
+
# Free up the Packager object.
|
|
986
|
+
packager = nil
|
|
987
|
+
|
|
988
|
+
# Store the xlsx component files with the temp dir name removed.
|
|
989
|
+
ZipFileUtils.zip("#{@tempdir}", @filename)
|
|
990
|
+
IO.copy_stream(@filename, @fileobj) if @fileobj
|
|
991
|
+
Writexlsx::Utility.delete_files(@tempdir)
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
#
|
|
995
|
+
# Convert the SST string data from a hash to an array.
|
|
996
|
+
#
|
|
997
|
+
def prepare_sst_string_data #:nodoc:
|
|
998
|
+
strings = []
|
|
999
|
+
|
|
1000
|
+
@str_table.each_key { |key| strings[@str_table[key]] = key }
|
|
1001
|
+
|
|
1002
|
+
# The SST data could be very large, free some memory (maybe).
|
|
1003
|
+
@str_table = nil
|
|
1004
|
+
@str_array = strings
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
#
|
|
1008
|
+
# Prepare all of the format properties prior to passing them to Styles.pm.
|
|
1009
|
+
#
|
|
1010
|
+
def prepare_format_properties #:nodoc:
|
|
1011
|
+
# Separate format objects into XF and DXF formats.
|
|
1012
|
+
prepare_formats
|
|
1013
|
+
|
|
1014
|
+
# Set the font index for the format objects.
|
|
1015
|
+
prepare_fonts
|
|
1016
|
+
|
|
1017
|
+
# Set the number format index for the format objects.
|
|
1018
|
+
prepare_num_formats
|
|
1019
|
+
|
|
1020
|
+
# Set the border index for the format objects.
|
|
1021
|
+
prepare_borders
|
|
1022
|
+
|
|
1023
|
+
# Set the fill index for the format objects.
|
|
1024
|
+
prepare_fills
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
#
|
|
1028
|
+
# Iterate through the XF Format objects and separate them into XF and DXF
|
|
1029
|
+
# formats.
|
|
1030
|
+
#
|
|
1031
|
+
def prepare_formats #:nodoc:
|
|
1032
|
+
@formats.each do |format|
|
|
1033
|
+
xf_index = format.xf_index
|
|
1034
|
+
dxf_index = format.dxf_index
|
|
1035
|
+
|
|
1036
|
+
@xf_formats[xf_index] = format if xf_index
|
|
1037
|
+
@dxf_formats[dxf_index] = format if dxf_index
|
|
1038
|
+
end
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
#
|
|
1042
|
+
# Set the default index for each format. This is mainly used for testing.
|
|
1043
|
+
#
|
|
1044
|
+
def set_default_xf_indices #:nodoc:
|
|
1045
|
+
@formats.each { |format| format.get_xf_index }
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
#
|
|
1049
|
+
# Iterate through the XF Format objects and give them an index to non-default
|
|
1050
|
+
# font elements.
|
|
1051
|
+
#
|
|
1052
|
+
def prepare_fonts #:nodoc:
|
|
1053
|
+
fonts = {}
|
|
1054
|
+
index = 0
|
|
1055
|
+
|
|
1056
|
+
@xf_formats.each do |format|
|
|
1057
|
+
key = format.get_font_key
|
|
1058
|
+
|
|
1059
|
+
if fonts[key]
|
|
1060
|
+
# Font has already been used.
|
|
1061
|
+
format.font_index = fonts[key]
|
|
1062
|
+
format.has_font = 0
|
|
1063
|
+
else
|
|
1064
|
+
# This is a new font.
|
|
1065
|
+
fonts[key] = index
|
|
1066
|
+
format.font_index = index
|
|
1067
|
+
format.has_font = 1
|
|
1068
|
+
index += 1
|
|
1069
|
+
end
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
@font_count = index
|
|
1073
|
+
|
|
1074
|
+
# For the DXF formats we only need to check if the properties have changed.
|
|
1075
|
+
@dxf_formats.each do |format|
|
|
1076
|
+
# The only font properties that can change for a DXF format are: color,
|
|
1077
|
+
# bold, italic, underline and strikethrough.
|
|
1078
|
+
if format.color || format.bold? || format.italic? || format.underline? || format.strikeout?
|
|
1079
|
+
format.has_dxf_font = 1
|
|
1080
|
+
end
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
#
|
|
1085
|
+
# Iterate through the XF Format objects and give them an index to non-default
|
|
1086
|
+
# number format elements.
|
|
1087
|
+
#
|
|
1088
|
+
# User defined records start from index 0xA4.
|
|
1089
|
+
#
|
|
1090
|
+
def prepare_num_formats #:nodoc:
|
|
1091
|
+
num_formats = {}
|
|
1092
|
+
index = 164
|
|
1093
|
+
num_format_count = 0
|
|
1094
|
+
|
|
1095
|
+
(@xf_formats + @dxf_formats).each do |format|
|
|
1096
|
+
num_format = format.num_format
|
|
1097
|
+
|
|
1098
|
+
# Check if num_format is an index to a built-in number format.
|
|
1099
|
+
# Also check for a string of zeros, which is a valid number format
|
|
1100
|
+
# string but would evaluate to zero.
|
|
1101
|
+
#
|
|
1102
|
+
if num_format.to_s =~ /^\d+$/ && num_format.to_s !~ /^0+\d/
|
|
1103
|
+
# Index to a built-in number format.
|
|
1104
|
+
format.num_format_index = num_format
|
|
1105
|
+
next
|
|
1106
|
+
end
|
|
1107
|
+
|
|
1108
|
+
if num_formats[num_format]
|
|
1109
|
+
# Number format has already been used.
|
|
1110
|
+
format.num_format_index = num_formats[num_format]
|
|
1111
|
+
else
|
|
1112
|
+
# Add a new number format.
|
|
1113
|
+
num_formats[num_format] = index
|
|
1114
|
+
format.num_format_index = index
|
|
1115
|
+
index += 1
|
|
1116
|
+
|
|
1117
|
+
# Only increase font count for XF formats (not for DXF formats).
|
|
1118
|
+
num_format_count += 1 unless format.xf_index == 0
|
|
1119
|
+
end
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
@num_format_count = num_format_count
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
#
|
|
1126
|
+
# Iterate through the XF Format objects and give them an index to non-default
|
|
1127
|
+
# border elements.
|
|
1128
|
+
#
|
|
1129
|
+
def prepare_borders #:nodoc:
|
|
1130
|
+
borders = {}
|
|
1131
|
+
index = 0
|
|
1132
|
+
|
|
1133
|
+
@xf_formats.each do |format|
|
|
1134
|
+
key = format.get_border_key
|
|
1135
|
+
|
|
1136
|
+
if borders[key]
|
|
1137
|
+
# Border has already been used.
|
|
1138
|
+
format.border_index = borders[key]
|
|
1139
|
+
format.has_border = 0
|
|
1140
|
+
else
|
|
1141
|
+
# This is a new border.
|
|
1142
|
+
borders[key] = index
|
|
1143
|
+
format.border_index = index
|
|
1144
|
+
format.has_border = 1
|
|
1145
|
+
index += 1
|
|
1146
|
+
end
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
@border_count = index
|
|
1150
|
+
|
|
1151
|
+
# For the DXF formats we only need to check if the properties have changed.
|
|
1152
|
+
@dxf_formats.each do |format|
|
|
1153
|
+
key = format.get_border_key
|
|
1154
|
+
format.has_dxf_border = 1 if key =~ /[^0:]/
|
|
1155
|
+
end
|
|
1156
|
+
end
|
|
1157
|
+
|
|
1158
|
+
#
|
|
1159
|
+
# Iterate through the XF Format objects and give them an index to non-default
|
|
1160
|
+
# fill elements.
|
|
1161
|
+
#
|
|
1162
|
+
# The user defined fill properties start from 2 since there are 2 default
|
|
1163
|
+
# fills: patternType="none" and patternType="gray125".
|
|
1164
|
+
#
|
|
1165
|
+
def prepare_fills #:nodoc:
|
|
1166
|
+
fills = {}
|
|
1167
|
+
index = 2 # Start from 2. See above.
|
|
1168
|
+
|
|
1169
|
+
# Add the default fills.
|
|
1170
|
+
fills['0:0:0'] = 0
|
|
1171
|
+
fills['17:0:0'] = 1
|
|
1172
|
+
|
|
1173
|
+
@xf_formats.each do |format|
|
|
1174
|
+
# The following logical statements jointly take care of special cases
|
|
1175
|
+
# in relation to cell colours and patterns:
|
|
1176
|
+
# 1. For a solid fill (_pattern == 1) Excel reverses the role of
|
|
1177
|
+
# foreground and background colours, and
|
|
1178
|
+
# 2. If the user specifies a foreground or background colour without
|
|
1179
|
+
# a pattern they probably wanted a solid fill, so we fill in the
|
|
1180
|
+
# defaults.
|
|
1181
|
+
#
|
|
1182
|
+
if format.pattern <= 1 && format.bg_color != 0 && format.fg_color == 0
|
|
1183
|
+
format.fg_color = format.bg_color
|
|
1184
|
+
format.bg_color = 0
|
|
1185
|
+
format.pattern = 1
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
if format.pattern <= 1 && format.bg_color == 0 && format.fg_color != 0
|
|
1189
|
+
format.bg_color = 0
|
|
1190
|
+
format.pattern = 1
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
key = format.get_fill_key
|
|
1194
|
+
|
|
1195
|
+
if fills[key]
|
|
1196
|
+
# Fill has already been used.
|
|
1197
|
+
format.fill_index = fills[key]
|
|
1198
|
+
format.has_fill = 0
|
|
1199
|
+
else
|
|
1200
|
+
# This is a new fill.
|
|
1201
|
+
fills[key] = index
|
|
1202
|
+
format.fill_index = index
|
|
1203
|
+
format.has_fill = 1
|
|
1204
|
+
index += 1
|
|
1205
|
+
end
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
@fill_count = index
|
|
1209
|
+
|
|
1210
|
+
# For the DXF formats we only need to check if the properties have changed.
|
|
1211
|
+
@dxf_formats.each do |format|
|
|
1212
|
+
format.has_dxf_fill = 1 if format.pattern || format.bg_color || format.fg_color
|
|
1213
|
+
end
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
#
|
|
1217
|
+
# Iterate through the worksheets and store any defined names in addition to
|
|
1218
|
+
# any user defined names. Stores the defined names for the Workbook.xml and
|
|
1219
|
+
# the named ranges for App.xml.
|
|
1220
|
+
#
|
|
1221
|
+
def prepare_defined_names #:nodoc:
|
|
1222
|
+
defined_names = @defined_names
|
|
1223
|
+
|
|
1224
|
+
@worksheets.each do |sheet|
|
|
1225
|
+
# Check for Print Area settings.
|
|
1226
|
+
if sheet.autofilter_area
|
|
1227
|
+
range = sheet.autofilter_area
|
|
1228
|
+
hidden = 1
|
|
1229
|
+
|
|
1230
|
+
# Store the defined names.
|
|
1231
|
+
defined_names << ['_xlnm._FilterDatabase', sheet.index, range, hidden]
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
# Check for Print Area settings.
|
|
1235
|
+
if !sheet.print_area.empty?
|
|
1236
|
+
range = sheet.print_area
|
|
1237
|
+
|
|
1238
|
+
# Store the defined names.
|
|
1239
|
+
defined_names << ['_xlnm.Print_Area', sheet.index, range]
|
|
1240
|
+
end
|
|
1241
|
+
|
|
1242
|
+
# Check for repeat rows/cols. aka, Print Titles.
|
|
1243
|
+
if !sheet.print_repeat_cols.empty? || !sheet.print_repeat_rows.empty?
|
|
1244
|
+
range = ''
|
|
1245
|
+
|
|
1246
|
+
if !sheet.print_repeat_cols.empty? && !sheet.print_repeat_rows.empty?
|
|
1247
|
+
range = sheet.print_repeat_cols + ',' + sheet.print_repeat_rows
|
|
1248
|
+
else
|
|
1249
|
+
range = sheet.print_repeat_cols + sheet.print_repeat_rows
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
# Store the defined names.
|
|
1253
|
+
defined_names << ['_xlnm.Print_Titles', sheet.index, range]
|
|
1254
|
+
end
|
|
1255
|
+
end
|
|
1256
|
+
|
|
1257
|
+
defined_names = sort_defined_names(defined_names)
|
|
1258
|
+
@defined_names = defined_names
|
|
1259
|
+
@named_ranges = extract_named_ranges(defined_names)
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
#
|
|
1263
|
+
# Iterate through the worksheets and set up the comment data.
|
|
1264
|
+
#
|
|
1265
|
+
def prepare_comments #:nodoc:
|
|
1266
|
+
comment_id = 0
|
|
1267
|
+
vml_data_id = 1
|
|
1268
|
+
vml_shape_id = 1024
|
|
1269
|
+
|
|
1270
|
+
@worksheets.each do |sheet|
|
|
1271
|
+
next unless sheet.has_comments?
|
|
1272
|
+
|
|
1273
|
+
comment_id += 1
|
|
1274
|
+
count = sheet.prepare_comments( vml_data_id, vml_shape_id, comment_id)
|
|
1275
|
+
|
|
1276
|
+
# Each VML file should start with a shape id incremented by 1024.
|
|
1277
|
+
vml_data_id += 1 * ( ( 1024 + count ) / 1024.0 ).to_i
|
|
1278
|
+
vml_shape_id += 1024 * ( ( 1024 + count ) / 1024.0 ).to_i
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1281
|
+
@num_comment_files = comment_id
|
|
1282
|
+
|
|
1283
|
+
# Add a font format for cell comments.
|
|
1284
|
+
if comment_id > 0
|
|
1285
|
+
format = Format.new(
|
|
1286
|
+
@xf_format_indices,
|
|
1287
|
+
@dxf_format_indices,
|
|
1288
|
+
:font => 'Tahoma',
|
|
1289
|
+
:size => 8,
|
|
1290
|
+
:color_indexed => 81,
|
|
1291
|
+
:font_only => 1
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
format.get_xf_index
|
|
1295
|
+
|
|
1296
|
+
@formats << format
|
|
1297
|
+
end
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
#
|
|
1301
|
+
# Add "cached" data to charts to provide the numCache and strCache data for
|
|
1302
|
+
# series and title/axis ranges.
|
|
1303
|
+
#
|
|
1304
|
+
def add_chart_data #:nodoc:
|
|
1305
|
+
worksheets = {}
|
|
1306
|
+
seen_ranges = {}
|
|
1307
|
+
|
|
1308
|
+
# Map worksheet names to worksheet objects.
|
|
1309
|
+
@worksheets.each { |worksheet| worksheets[worksheet.name] = worksheet }
|
|
1310
|
+
|
|
1311
|
+
@charts.each do |chart|
|
|
1312
|
+
chart.formula_ids.each do |range, id|
|
|
1313
|
+
# Skip if the series has user defined data.
|
|
1314
|
+
if chart.formula_data[id]
|
|
1315
|
+
if !seen_ranges.has_key?(range) || seen_ranges[range]
|
|
1316
|
+
data = chart.formula_data[id]
|
|
1317
|
+
seen_ranges[range] = data
|
|
1318
|
+
end
|
|
1319
|
+
next
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
# Check to see if the data is already cached locally.
|
|
1323
|
+
if seen_ranges.has_key?(range)
|
|
1324
|
+
chart.formula_data[id] = seen_ranges[range]
|
|
1325
|
+
next
|
|
1326
|
+
end
|
|
1327
|
+
|
|
1328
|
+
# Convert the range formula to a sheet name and cell range.
|
|
1329
|
+
sheetname, *cells = get_chart_range(range)
|
|
1330
|
+
|
|
1331
|
+
# Skip if we couldn't parse the formula.
|
|
1332
|
+
next unless sheetname
|
|
1333
|
+
|
|
1334
|
+
# Skip if the name is unknown. Probably should throw exception.
|
|
1335
|
+
next unless worksheets[sheetname]
|
|
1336
|
+
|
|
1337
|
+
# Find the worksheet object based on the sheet name.
|
|
1338
|
+
worksheet = worksheets[sheetname]
|
|
1339
|
+
|
|
1340
|
+
# Get the data from the worksheet table.
|
|
1341
|
+
data = worksheet.get_range_data(*cells)
|
|
1342
|
+
|
|
1343
|
+
# Convert shared string indexes to strings.
|
|
1344
|
+
data.collect! do |token|
|
|
1345
|
+
if token.kind_of?(Hash)
|
|
1346
|
+
token = @str_array[token[:sst_id]]
|
|
1347
|
+
|
|
1348
|
+
# Ignore rich strings for now. Deparse later if necessary.
|
|
1349
|
+
token = '' if token =~ %r!^<r>! && token =~ %r!</r>$!
|
|
1350
|
+
end
|
|
1351
|
+
token
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
# Add the data to the chart.
|
|
1355
|
+
chart.formula_data[id] = data
|
|
1356
|
+
|
|
1357
|
+
# Store range data locally to avoid lookup if seen again.
|
|
1358
|
+
seen_ranges[range] = data
|
|
1359
|
+
end
|
|
1360
|
+
end
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
#
|
|
1364
|
+
# Sort internal and user defined names in the same order as used by Excel.
|
|
1365
|
+
# This may not be strictly necessary but unsorted elements caused a lot of
|
|
1366
|
+
# issues in the the Spreadsheet::WriteExcel binary version. Also makes
|
|
1367
|
+
# comparison testing easier.
|
|
1368
|
+
#
|
|
1369
|
+
def sort_defined_names(names) #:nodoc:
|
|
1370
|
+
names.sort do |a, b|
|
|
1371
|
+
name_a = normalise_defined_name(a[0])
|
|
1372
|
+
name_b = normalise_defined_name(b[0])
|
|
1373
|
+
sheet_a = normalise_sheet_name(a[2])
|
|
1374
|
+
sheet_b = normalise_sheet_name(b[2])
|
|
1375
|
+
# Primary sort based on the defined name.
|
|
1376
|
+
if name_a > name_b
|
|
1377
|
+
1
|
|
1378
|
+
elsif name_a < name_b
|
|
1379
|
+
-1
|
|
1380
|
+
else # name_a == name_b
|
|
1381
|
+
# Secondary sort based on the sheet name.
|
|
1382
|
+
if sheet_a >= sheet_b
|
|
1383
|
+
1
|
|
1384
|
+
else
|
|
1385
|
+
-1
|
|
1386
|
+
end
|
|
1387
|
+
end
|
|
1388
|
+
end
|
|
1389
|
+
end
|
|
1390
|
+
|
|
1391
|
+
# Used in the above sort routine to normalise the defined names. Removes any
|
|
1392
|
+
# leading '_xmln.' from internal names and lowercases the strings.
|
|
1393
|
+
def normalise_defined_name(name) #:nodoc:
|
|
1394
|
+
name.sub(/^_xlnm./, '').downcase
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
# Used in the above sort routine to normalise the worksheet names for the
|
|
1398
|
+
# secondary sort. Removes leading quote and lowercases the strings.
|
|
1399
|
+
def normalise_sheet_name(name) #:nodoc:
|
|
1400
|
+
name.sub(/^'/, '').downcase
|
|
1401
|
+
end
|
|
1402
|
+
|
|
1403
|
+
#
|
|
1404
|
+
# Extract the named ranges from the sorted list of defined names. These are
|
|
1405
|
+
# used in the App.xml file.
|
|
1406
|
+
#
|
|
1407
|
+
def extract_named_ranges(defined_names) #:nodoc:
|
|
1408
|
+
named_ranges = []
|
|
1409
|
+
|
|
1410
|
+
defined_names.each do |defined_name|
|
|
1411
|
+
name, index, range = defined_name
|
|
1412
|
+
|
|
1413
|
+
# Skip autoFilter ranges.
|
|
1414
|
+
next if name == '_xlnm._FilterDatabase'
|
|
1415
|
+
|
|
1416
|
+
# We are only interested in defined names with ranges.
|
|
1417
|
+
if range =~ /^([^!]+)!/
|
|
1418
|
+
sheet_name = $1
|
|
1419
|
+
|
|
1420
|
+
# Match Print_Area and Print_Titles xlnm types.
|
|
1421
|
+
if name =~ /^_xlnm\.(.*)$/
|
|
1422
|
+
xlnm_type = $1
|
|
1423
|
+
name = sheet_name + '!' + xlnm_type
|
|
1424
|
+
elsif index != -1
|
|
1425
|
+
name = sheet_name + '!' + name
|
|
1426
|
+
end
|
|
1427
|
+
|
|
1428
|
+
named_ranges << name
|
|
1429
|
+
end
|
|
1430
|
+
end
|
|
1431
|
+
|
|
1432
|
+
named_ranges
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
#
|
|
1436
|
+
# Iterate through the worksheets and set up any chart or image drawings.
|
|
1437
|
+
#
|
|
1438
|
+
def prepare_drawings #:nodoc:
|
|
1439
|
+
chart_ref_id = 0
|
|
1440
|
+
image_ref_id = 0
|
|
1441
|
+
drawing_id = 0
|
|
1442
|
+
@worksheets.each do |sheet|
|
|
1443
|
+
chart_count = sheet.charts.size
|
|
1444
|
+
image_count = sheet.images.size
|
|
1445
|
+
next if chart_count + image_count == 0
|
|
1446
|
+
|
|
1447
|
+
drawing_id += 1
|
|
1448
|
+
|
|
1449
|
+
(0 .. chart_count - 1).each do |index|
|
|
1450
|
+
chart_ref_id += 1
|
|
1451
|
+
sheet.prepare_chart(index, chart_ref_id, drawing_id)
|
|
1452
|
+
end
|
|
1453
|
+
|
|
1454
|
+
(0 .. image_count - 1).each do |index|
|
|
1455
|
+
filename = sheet.images[index][2]
|
|
1456
|
+
|
|
1457
|
+
image_id, type, width, height, name = get_image_properties(filename)
|
|
1458
|
+
|
|
1459
|
+
image_ref_id += 1
|
|
1460
|
+
|
|
1461
|
+
sheet.prepare_image(index, image_ref_id, drawing_id, width, height, name, type)
|
|
1462
|
+
end
|
|
1463
|
+
|
|
1464
|
+
drawing = sheet.drawing
|
|
1465
|
+
@drawings << drawing
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
@drawing_count = drawing_id
|
|
1469
|
+
end
|
|
1470
|
+
|
|
1471
|
+
#
|
|
1472
|
+
# Convert a sheet name to its index. Return undef otherwise.
|
|
1473
|
+
#
|
|
1474
|
+
def get_sheet_index(sheetname) #:nodoc:
|
|
1475
|
+
sheet_count = @sheetnames.size
|
|
1476
|
+
sheet_index = nil
|
|
1477
|
+
|
|
1478
|
+
sheetname.sub!(/^'/, '')
|
|
1479
|
+
sheetname.sub!(/'$/, '')
|
|
1480
|
+
|
|
1481
|
+
( 0 .. sheet_count - 1 ).each do |i|
|
|
1482
|
+
sheet_index = i if sheetname == @sheetnames[i]
|
|
1483
|
+
end
|
|
1484
|
+
|
|
1485
|
+
sheet_index
|
|
1486
|
+
end
|
|
1487
|
+
end
|
|
1488
|
+
end
|