xlsx_writer 0.1.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.
Files changed (35) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +74 -0
  3. data/Rakefile +13 -0
  4. data/lib/xlsx_writer.rb +32 -0
  5. data/lib/xlsx_writer/autofilter.rb +7 -0
  6. data/lib/xlsx_writer/cell.rb +173 -0
  7. data/lib/xlsx_writer/document.rb +82 -0
  8. data/lib/xlsx_writer/generators/app.erb +13 -0
  9. data/lib/xlsx_writer/generators/app.rb +7 -0
  10. data/lib/xlsx_writer/generators/content_types.erb +14 -0
  11. data/lib/xlsx_writer/generators/content_types.rb +7 -0
  12. data/lib/xlsx_writer/generators/doc_props.erb +3 -0
  13. data/lib/xlsx_writer/generators/doc_props.rb +7 -0
  14. data/lib/xlsx_writer/generators/image.rb +68 -0
  15. data/lib/xlsx_writer/generators/rels.erb +6 -0
  16. data/lib/xlsx_writer/generators/rels.rb +7 -0
  17. data/lib/xlsx_writer/generators/sheet.rb +105 -0
  18. data/lib/xlsx_writer/generators/sheet_rels.erb +4 -0
  19. data/lib/xlsx_writer/generators/sheet_rels.rb +17 -0
  20. data/lib/xlsx_writer/generators/styles.erb +43 -0
  21. data/lib/xlsx_writer/generators/styles.rb +7 -0
  22. data/lib/xlsx_writer/generators/vml_drawing.erb +6 -0
  23. data/lib/xlsx_writer/generators/vml_drawing.rb +7 -0
  24. data/lib/xlsx_writer/generators/vml_drawing_rels.erb +6 -0
  25. data/lib/xlsx_writer/generators/vml_drawing_rels.rb +7 -0
  26. data/lib/xlsx_writer/generators/workbook.erb +12 -0
  27. data/lib/xlsx_writer/generators/workbook.rb +7 -0
  28. data/lib/xlsx_writer/generators/workbook_rels.erb +7 -0
  29. data/lib/xlsx_writer/generators/workbook_rels.rb +7 -0
  30. data/lib/xlsx_writer/header_footer.rb +116 -0
  31. data/lib/xlsx_writer/page_setup.rb +43 -0
  32. data/lib/xlsx_writer/row.rb +42 -0
  33. data/lib/xlsx_writer/utils.rb +34 -0
  34. data/lib/xlsx_writer/xml.rb +44 -0
  35. metadata +116 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Dee Zsombor, Justin Beck, Seamus Abshere
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,74 @@
1
+ # xlsx_writer
2
+
3
+ Writes (doesn't read or modify) XLSX files.
4
+
5
+ ## Credit
6
+
7
+ Based on the [original simple\_xlsx\_writer gem](https://github.com/harvesthq/simple_xlsx_writer) and [patches by mumboe](https://github.com/mumboe/simple_xlsx_writer)
8
+
9
+ Then I tore it down and rebuilt it:
10
+
11
+ * no longer constructs everything in a single zipstream... instead writes the individual files to /tmp and then zips them together
12
+ * absolute minimum XML - went through every line, testing to see if I could remove it
13
+ * no more block format - this was more appropriate when it was constructed as a zipstream
14
+
15
+ Features not present in simple_xlsx_writer:
16
+
17
+ * opinionated, non-customizable styles - Arial 10pt, left-aligned text and dates, right-aligned numbers and currency
18
+ * autofilter based on a cell range
19
+ * header and footer, with support for images (.emf only) and page numbers
20
+ * fits columns to text
21
+
22
+ ## Example
23
+
24
+ require 'xlsx_writer'
25
+
26
+ doc = XlsxWriter::Document.new
27
+
28
+ sheet1 = doc.add_sheet("People")
29
+
30
+ # DATA
31
+
32
+ sheet1.add_row([
33
+ "DoB",
34
+ "Name",
35
+ "Occupation",
36
+ "Salary",
37
+ "Citations"
38
+ ])
39
+ sheet1.add_row([
40
+ Date.parse("July 31, 1912"),
41
+ "Milton Friedman",
42
+ "Economist / Statistician",
43
+ {:type => :Currency, :value => 10_000},
44
+ 500_000
45
+ ])
46
+ sheet1.add_autofilter 'A1:E1'
47
+
48
+ # FORMATTING
49
+
50
+ doc.page_setup.top = 1.5
51
+ doc.header.right.contents = 'Corporate Reporting'
52
+ doc.footer.left.contents = 'Confidential'
53
+ doc.footer.right.contents = :page_x_of_y
54
+
55
+ # if you really need images in header/footer: do it in Excel, save, unzip the xlsx... get the .emf files, "cropleft" (if necessary), etc. from there
56
+
57
+ left_header_image = doc.add_image('image1.emf', 118, 107)
58
+ left_header_image.croptop = '11025f'
59
+ left_header_image.cropleft = '9997f'
60
+ center_footer_image = doc.add_image('image2.emf', 116, 36)
61
+ doc.header.left.contents = left_header_image
62
+ doc.footer.center.contents = [ 'Powered by ', center_footer_image ]
63
+ doc.page_setup.header = 0
64
+ doc.page_setup.footer = 0
65
+
66
+ # OUTPUT
67
+
68
+ # You should move the file to where you want it
69
+ require 'fileutils'
70
+ ::FileUtils.mv doc.path, 'myfile.xlsx'
71
+
72
+ # don't forget
73
+ doc.cleanup
74
+
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ task :default => [:test]
6
+
7
+ Rake::TestTask.new do |test|
8
+ test.libs << "test"
9
+ test.test_files = Dir['test/**/*_test.rb'].sort
10
+ test.verbose = true
11
+ end
12
+
13
+
@@ -0,0 +1,32 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module XlsxWriter
4
+ def self.gem_dir
5
+ ::File.join ::File.dirname(__FILE__), 'xlsx_writer'
6
+ end
7
+
8
+ autoload :Cell, "#{gem_dir}/cell"
9
+ autoload :Document, "#{gem_dir}/document"
10
+ autoload :Row, "#{gem_dir}/row"
11
+ autoload :Utils, "#{gem_dir}/utils"
12
+ autoload :Xml, "#{gem_dir}/xml"
13
+ autoload :HeaderFooter, "#{gem_dir}/header_footer"
14
+ autoload :Autofilter, "#{gem_dir}/autofilter"
15
+ autoload :PageSetup, "#{gem_dir}/page_setup"
16
+
17
+ # manual
18
+ autoload :Sheet, "#{gem_dir}/generators/sheet"
19
+ autoload :SheetRels, "#{gem_dir}/generators/sheet_rels"
20
+ autoload :Image, "#{gem_dir}/generators/image"
21
+
22
+ # generators
23
+ autoload :App, "#{gem_dir}/generators/app"
24
+ autoload :ContentTypes, "#{gem_dir}/generators/content_types"
25
+ autoload :DocProps, "#{gem_dir}/generators/doc_props"
26
+ autoload :Rels, "#{gem_dir}/generators/rels"
27
+ autoload :Styles, "#{gem_dir}/generators/styles"
28
+ autoload :Workbook, "#{gem_dir}/generators/workbook"
29
+ autoload :WorkbookRels, "#{gem_dir}/generators/workbook_rels"
30
+ autoload :VmlDrawing, "#{gem_dir}/generators/vml_drawing"
31
+ autoload :VmlDrawingRels, "#{gem_dir}/generators/vml_drawing_rels"
32
+ end
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class Autofilter < ::Struct.new(:range)
3
+ def to_xml
4
+ %{<autoFilter ref="#{range}" />}
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,173 @@
1
+ require 'fast_xs'
2
+
3
+ module XlsxWriter
4
+ class Cell
5
+ class << self
6
+ # TODO make a class for this
7
+ def excel_type(calculated_type)
8
+ case calculated_type
9
+ when :String
10
+ :inlineStr
11
+ when :Number, :Date, :Currency
12
+ :n
13
+ when :Boolean
14
+ :b
15
+ else
16
+ raise ::ArgumentError, "Unknown cell type #{k}"
17
+ end
18
+ end
19
+
20
+ # TODO make a class for this
21
+ def excel_style_number(calculated_type)
22
+ case calculated_type
23
+ when :String
24
+ 0
25
+ when :Number
26
+ 3
27
+ when :Currency
28
+ 1
29
+ when :Date
30
+ 2
31
+ when :Boolean
32
+ 0 # todo
33
+ else
34
+ raise ::ArgumentError, "Unknown cell type #{k}"
35
+ end
36
+ end
37
+
38
+ def excel_column_letter(i)
39
+ result = []
40
+ while i >= 26 do
41
+ result << ABC[i % 26]
42
+ i /= 26
43
+ end
44
+ result << ABC[result.empty? ? i : i - 1]
45
+ result.reverse.join
46
+ end
47
+
48
+ def excel_string(value)
49
+ value.to_s.fast_xs
50
+ end
51
+
52
+ def excel_number(value)
53
+ str = value.to_s.dup
54
+ unless str =~ /\A[0-9\.\-]*\z/
55
+ raise ::ArgumentError, %{Bad value "#{value}" Only numbers and dots (.) allowed in number fields}
56
+ end
57
+ str.fast_xs
58
+ end
59
+
60
+ alias :excel_currency :excel_number
61
+
62
+ # doesn't necessarily work for times yet
63
+ JAN_1_1900 = ::Time.parse('1900-01-01')
64
+ def excel_date(value)
65
+ if value.is_a?(::String)
66
+ ((::Time.parse(str) - JAN_1_1900) / 86_400).round
67
+ elsif value.respond_to?(:to_date)
68
+ (value.to_date - JAN_1_1900.to_date).to_i
69
+ end
70
+ end
71
+
72
+ def excel_boolean(value)
73
+ value ? 1 : 0
74
+ end
75
+ end
76
+
77
+ ABC = ('A'..'Z').to_a
78
+
79
+ attr_reader :row
80
+ attr_reader :data
81
+
82
+ def initialize(row, data)
83
+ @row = row
84
+ @data = data.is_a?(::Hash) ? data.symbolize_keys : data
85
+ end
86
+
87
+ # width = Truncate([{Number of Characters} * {Maximum Digit Width} + {5 pixel padding}]/{Maximum Digit Width}*256)/256
88
+ # Using the Calibri font as an example, the maximum digit width of 11 point font size is 7 pixels (at 96 dpi). In fact, each digit is the same width for this font. Therefore if the cell width is 8 characters wide, the value of this attribute shall be Truncate([8*7+5]/7*256)/256 = 8.7109375.
89
+ MAX_DIGIT_WIDTH = 5
90
+ MAX_REASONABLE_WIDTH = 75
91
+ def pixel_width
92
+ @pixel_width ||= [
93
+ ((character_width.to_f*MAX_DIGIT_WIDTH+5)/MAX_DIGIT_WIDTH*256)/256,
94
+ MAX_REASONABLE_WIDTH
95
+ ].min
96
+ end
97
+
98
+ DATE_LENGTH = 'YYYY-MM-DD'.length
99
+ BOOLEAN_LENGTH = 'FALSE'.length
100
+ def character_width
101
+ @character_width ||= case calculated_type
102
+ when :String
103
+ value.to_s.length
104
+ when :Number
105
+ # -1000000.5
106
+ len = value.round(2).to_s.length
107
+ len += 1 if value < 0
108
+ len
109
+ when :Currency
110
+ # (1,000,000.50)
111
+ len = value.round(2).to_s.length + ::Math.log(value.abs, 1_000).floor
112
+ len += 2 if value < 0
113
+ len
114
+ when :Date
115
+ DATE_LENGTH
116
+ when :Boolean
117
+ BOOLEAN_LENGTH
118
+ end
119
+ end
120
+
121
+ def unstyled?
122
+ !styled?
123
+ end
124
+
125
+ def styled?
126
+ data.is_a?(::Hash)
127
+ end
128
+
129
+ def to_xml
130
+ if value.blank?
131
+ %{<c r="#{excel_column_letter}#{row.ndx}" s="0" t="inlineStr" />}
132
+ elsif excel_type == :inlineStr
133
+ %{<c r="#{excel_column_letter}#{row.ndx}" s="#{excel_style_number}" t="#{excel_type}"><is><t>#{excel_value}</t></is></c>}
134
+ else
135
+ %{<c r="#{excel_column_letter}#{row.ndx}" s="#{excel_style_number}" t="#{excel_type}"><v>#{excel_value}</v></c>}
136
+ end
137
+ end
138
+
139
+ # 0 -> A (zero based!)
140
+ def excel_column_letter
141
+ Cell.excel_column_letter row.cells.index(self)
142
+ end
143
+
144
+ # detect dates here, even if we're not styled
145
+ def excel_type
146
+ Cell.excel_type calculated_type
147
+ end
148
+
149
+ def excel_style_number
150
+ Cell.excel_style_number calculated_type
151
+ end
152
+
153
+ def calculated_type
154
+ @calculated_type ||= if styled?
155
+ data[:type]
156
+ elsif value.is_a?(::Date)
157
+ :Date
158
+ elsif value.is_a?(::Numeric)
159
+ :Number
160
+ else
161
+ :String
162
+ end
163
+ end
164
+
165
+ def value
166
+ styled? ? data[:value] : data
167
+ end
168
+
169
+ def excel_value
170
+ Cell.send "excel_#{calculated_type.to_s.underscore}", value
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,82 @@
1
+ require 'fileutils'
2
+
3
+ module XlsxWriter
4
+ class Document
5
+ class << self
6
+ def auto
7
+ ::Dir[::File.expand_path('../generators/*.rb', __FILE__)].map do |path|
8
+ XlsxWriter.const_get ::File.basename(path, '.rb').camelcase
9
+ end.reject do |klass|
10
+ klass.const_defined?(:AUTO) and klass.const_get(:AUTO) == false
11
+ end
12
+ end
13
+ end
14
+
15
+ def add_sheet(name)
16
+ raise ::RuntimeError, "Can't add sheet, already generated!" if generated?
17
+ sheet = Sheet.new self, name
18
+ sheets << sheet
19
+ sheet
20
+ end
21
+
22
+ def page_setup
23
+ @page_setup ||= PageSetup.new
24
+ end
25
+
26
+ def header_footer
27
+ @header_footer ||= HeaderFooter.new self
28
+ end
29
+
30
+ delegate :header, :footer, :to => :header_footer
31
+
32
+ def add_image(path, width, height)
33
+ raise ::RuntimeError, "Can't add image, already generated!" if generated?
34
+ image = Image.new self, path, width, height
35
+ images << image
36
+ image
37
+ end
38
+
39
+ def path
40
+ generate unless generated?
41
+ @path
42
+ end
43
+
44
+ def cleanup
45
+ ::File.unlink(@path) if ::File.exist?(@path)
46
+ ::FileUtils.rm_rf(@staging_dir) if ::File.exist?(@staging_dir)
47
+ @path = nil
48
+ @staging_dir = nil
49
+ @generated = false
50
+ end
51
+
52
+ def sheets #:nodoc:
53
+ @sheets ||= []
54
+ end
55
+
56
+ def images
57
+ @images ||= []
58
+ end
59
+
60
+ def staging_dir
61
+ @staging_dir ||= Utils.tmp_path
62
+ ::FileUtils.mkdir_p @staging_dir
63
+ @staging_dir
64
+ end
65
+
66
+ private
67
+
68
+ def generate
69
+ sheets.each(&:generate)
70
+ images.each(&:generate)
71
+ Document.auto.each do |part|
72
+ part.new(self).generate
73
+ end
74
+ @path = Utils.zip staging_dir
75
+ @generated = true
76
+ end
77
+
78
+ def generated?
79
+ @generated == true
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
3
+ <HeadingPairs>
4
+ <vt:vector size="2" baseType="variant">
5
+ <vt:variant>
6
+ <vt:lpstr>Worksheets</vt:lpstr>
7
+ </vt:variant>
8
+ <vt:variant>
9
+ <vt:i4>1</vt:i4>
10
+ </vt:variant>
11
+ </vt:vector>
12
+ </HeadingPairs>
13
+ </Properties>
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class App < Xml
3
+ def relative_path
4
+ "docProps/app.xml"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
3
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
4
+ <Default Extension="xml" ContentType="application/xml"/>
5
+ <Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>
6
+ <Default Extension="emf" ContentType="image/x-emf"/>
7
+ <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
8
+ <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
9
+ <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
10
+ <% document.sheets.each do |sheet| %>
11
+ <Override PartName="<%= sheet.absolute_path %>" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
12
+ <% end %>
13
+ <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
14
+ </Types>
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class ContentTypes < Xml
3
+ def relative_path
4
+ "[Content_Types].xml"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
3
+ </cp:coreProperties>
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class DocProps < Xml
3
+ def relative_path
4
+ "docProps/core.xml"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,68 @@
1
+ require 'fileutils'
2
+ module XlsxWriter
3
+ class Image < ::Struct.new(:document, :original_path, :width, :height, :lcr, :croptop, :cropleft)
4
+
5
+ AUTO = false
6
+
7
+ def to_xml
8
+ <<-EOS
9
+ <v:shape id="#{id}" o:spid="#{o_spid}" type="#_x0000_t75" style="position:absolute;margin-left:0;margin-top:0;width:#{width}pt;height:#{height}pt;z-index:1">
10
+ <v:imagedata o:relid="#{rid}" o:title="#{o_title}" croptop=#{croptop} cropleft=#{cropleft}/>
11
+ <o:lock v:ext="edit" rotation="t"/>
12
+ </v:shape>
13
+ EOS
14
+ end
15
+
16
+ def croptop
17
+ self[:croptop] || 0
18
+ end
19
+
20
+ def cropleft
21
+ self[:cropleft] || 0
22
+ end
23
+
24
+ def id
25
+ if lcr
26
+ lcr.image_id
27
+ else
28
+ o_spid #?
29
+ end
30
+ end
31
+
32
+ def generate
33
+ ::FileUtils.cp original_path, staging_path
34
+ end
35
+
36
+ def ndx
37
+ document.images.index(self) + 1
38
+ end
39
+
40
+ def rid
41
+ "rId#{ndx}"
42
+ end
43
+
44
+ def o_title
45
+ ::File.basename(original_path)
46
+ end
47
+
48
+ def o_spid
49
+ "_x0000_s#{1025+ndx}"
50
+ end
51
+
52
+ def absolute_path
53
+ "/#{relative_path}"
54
+ end
55
+
56
+ private
57
+
58
+ def relative_path
59
+ "xl/media/image#{ndx}.emf"
60
+ end
61
+
62
+ def staging_path
63
+ p = ::File.join document.staging_dir, relative_path
64
+ ::FileUtils.mkdir_p ::File.dirname(p)
65
+ p
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="/xl/workbook.xml"/>
4
+ <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="/docProps/core.xml"/>
5
+ <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="/docProps/app.xml"/>
6
+ </Relationships>
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class Rels < Xml
3
+ def relative_path
4
+ "_rels/.rels"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,105 @@
1
+ require 'fast_xs'
2
+
3
+ module XlsxWriter
4
+ class Sheet < Xml
5
+ class << self
6
+ def excel_name(value)
7
+ str = value.to_s.dup
8
+ str.gsub! '/', '' # remove forward slashes
9
+ str.gsub! /\s+/, '' # compress "inner" whitespace
10
+ str.strip! # trim whitespace from ends
11
+ str.fast_xs
12
+ end
13
+ end
14
+
15
+ AUTO = false
16
+
17
+ attr_reader :name
18
+
19
+ def initialize(document, name)
20
+ @document = document
21
+ @name = Sheet.excel_name name
22
+ end
23
+
24
+ def ndx
25
+ document.sheets.index(self) + 1
26
+ end
27
+
28
+ # +1 because styles.xml occupies the first spot
29
+ def rid
30
+ "rId#{ndx + 1}"
31
+ end
32
+
33
+ def relative_path
34
+ "xl/worksheets/sheet#{ndx}.xml"
35
+ end
36
+
37
+ def absolute_path
38
+ "/#{relative_path}"
39
+ end
40
+
41
+ def autofilters
42
+ @autofilters ||= []
43
+ end
44
+
45
+ # specify range like "A1:C1"
46
+ def add_autofilter(range)
47
+ raise ::RuntimeError, "Can't add autofilter, already generated!" if generated?
48
+ autofilters << Autofilter.new(range)
49
+ end
50
+
51
+ def rows
52
+ @rows ||= []
53
+ end
54
+
55
+ def add_row(data)
56
+ raise ::RuntimeError, "Can't add row, already generated!" if generated?
57
+ row = Row.new self, data
58
+ rows << row
59
+ row
60
+ end
61
+
62
+ # override Xml method to save memory
63
+ def generate
64
+ @path = staging_path
65
+ ::File.open(@path, 'wb') do |out|
66
+ to_file out
67
+ end
68
+ Utils.unix2dos @path
69
+ SheetRels.new(document, self).generate
70
+ @generated = true
71
+ end
72
+
73
+ delegate :header_footer, :page_setup, :to => :document
74
+
75
+ private
76
+
77
+ # not using ERB to save memory
78
+ def to_file(f)
79
+ f.puts <<-EOS
80
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
81
+ <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
82
+ <cols>
83
+ EOS
84
+ (0..max_length-1).each do |x|
85
+ f.puts %{<col min="#{x+1}" max="#{x+1}" width="#{max_cell_width(x)}" bestFit="1" customWidth="1" />}
86
+ end
87
+ f.puts %{</cols>}
88
+ f.puts %{<sheetData>}
89
+ rows.each { |row| f.puts row.to_xml }
90
+ f.puts %{</sheetData>}
91
+ autofilters.each { |autofilter| f.puts autofilter.to_xml }
92
+ f.puts page_setup.to_xml
93
+ f.puts header_footer.to_xml
94
+ f.puts %{</worksheet>}
95
+ end
96
+
97
+ def max_length
98
+ rows.max_by { |row| row.length }.length
99
+ end
100
+
101
+ def max_cell_width(x)
102
+ rows.max_by { |row| row.cell_width(x) }.cell_width(x)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="/xl/drawings/vmlDrawing1.vml"/>
4
+ </Relationships>
@@ -0,0 +1,17 @@
1
+ module XlsxWriter
2
+ class SheetRels < Xml
3
+
4
+ AUTO = false
5
+
6
+ attr_reader :sheet
7
+
8
+ def initialize(document, sheet)
9
+ @document = document
10
+ @sheet = sheet
11
+ end
12
+
13
+ def relative_path
14
+ "xl/worksheets/_rels/sheet#{sheet.ndx}.xml.rels"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
3
+ <fonts count="1">
4
+ <font>
5
+ <sz val="10"/>
6
+ <name val="Arial"/>
7
+ </font>
8
+ </fonts>
9
+
10
+ <fills count="1">
11
+ <fill />
12
+ </fills>
13
+
14
+ <borders count="1">
15
+ <border />
16
+ </borders>
17
+
18
+ <cellStyleXfs count="2">
19
+ <!-- general -->
20
+ <xf />
21
+ <!-- currency -->
22
+ <xf builtinId="4" />
23
+ </cellStyleXfs>
24
+
25
+ <cellXfs count="4">
26
+ <!-- general -->
27
+ <xf numFmtId="0" fontId="0">
28
+ <alignment vertical="top" horizontal="left" />
29
+ </xf>
30
+ <!-- currency (really accounting) -->
31
+ <xf numFmtId="39" fontId="0" xfId="1">
32
+ <alignment vertical="top" horizontal="right" />
33
+ </xf>
34
+ <!-- date -->
35
+ <xf numFmtId="14" fontId="0">
36
+ <alignment vertical="top" horizontal="left" />
37
+ </xf>
38
+ <!-- number -->
39
+ <xf numFmtId="0" fontId="0">
40
+ <alignment vertical="top" horizontal="right" />
41
+ </xf>
42
+ </cellXfs>
43
+ </styleSheet>
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class Styles < Xml
3
+ def relative_path
4
+ "xl/styles.xml"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:mv="http://macVmlSchemaUri">
3
+ <% document.images.each do |image| %>
4
+ <%= image.to_xml %>
5
+ <% end %>
6
+ </xml>
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class VmlDrawing < Xml
3
+ def relative_path
4
+ "xl/drawings/vmlDrawing1.vml"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3
+ <% document.images.each do |image| %>
4
+ <Relationship Id="<%= image.rid %>" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="<%= image.absolute_path %>"/>
5
+ <% end %>
6
+ </Relationships>
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class VmlDrawingRels < Xml
3
+ def relative_path
4
+ "xl/drawings/_rels/vmlDrawing1.vml.rels"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
3
+ <workbookPr date1904="0" />
4
+ <bookViews>
5
+ <workbookView xWindow="0" yWindow="0" windowWidth="22667" windowHeight="17000" tabRatio="500"/>
6
+ </bookViews>
7
+ <sheets>
8
+ <% document.sheets.each do |sheet| %>
9
+ <sheet name="<%= sheet.name %>" sheetId="<%= sheet.ndx %>" r:id="<%= sheet.rid %>"/>
10
+ <% end %>
11
+ </sheets>
12
+ </workbook>
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class Workbook < Xml
3
+ def relative_path
4
+ "xl/workbook.xml"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="/xl/styles.xml"/>
4
+ <% document.sheets.each do |sheet| %>
5
+ <Relationship Id="<%= sheet.rid %>" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="<%= sheet.absolute_path %>"/>
6
+ <% end %>
7
+ </Relationships>
@@ -0,0 +1,7 @@
1
+ module XlsxWriter
2
+ class WorkbookRels < Xml
3
+ def relative_path
4
+ "xl/_rels/workbook.xml.rels"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,116 @@
1
+ module XlsxWriter
2
+ class HeaderFooter < ::Struct.new(:document, :header, :footer)
3
+ def header
4
+ self[:header] ||= H.new self
5
+ end
6
+
7
+ def footer
8
+ self[:footer] ||= F.new self
9
+ end
10
+
11
+ def to_xml
12
+ lines = []
13
+ lines << %{<headerFooter>}
14
+ lines << header.to_xml
15
+ lines << footer.to_xml
16
+ lines << %{</headerFooter>}
17
+ if header.has_image? or footer.has_image?
18
+ lines << %{<legacyDrawingHF r:id="rId1"/>}
19
+ end
20
+ lines.join("\n")
21
+ end
22
+
23
+ class HF < ::Struct.new(:header_footer, :left, :center, :right)
24
+ def left
25
+ self[:left] ||= L.new self
26
+ end
27
+
28
+ def center
29
+ self[:center] ||= C.new self
30
+ end
31
+
32
+ def right
33
+ self[:right] ||= R.new self
34
+ end
35
+
36
+ def hf
37
+ self.class.name.demodulize
38
+ end
39
+
40
+ def to_xml
41
+ %{<#{tag}>#{parts.map(&:to_s).join}</#{tag}>}
42
+ end
43
+
44
+ def parts
45
+ [left,center,right].select(&:present?)
46
+ end
47
+
48
+ def has_image?
49
+ parts.any?(&:has_image?)
50
+ end
51
+
52
+ class LCR < ::Struct.new(:hf, :contents)
53
+ FONT = %{"Arial,Regular"}
54
+ SIZE = 10
55
+
56
+ def present?
57
+ contents.present?
58
+ end
59
+
60
+ def has_image?
61
+ ::Array.wrap(contents).any? { |v| v.is_a?(XlsxWriter::Image) }
62
+ end
63
+
64
+ def lcr
65
+ self.class.name.demodulize
66
+ end
67
+
68
+ def image_id
69
+ [ lcr, hf.hf ].join
70
+ end
71
+
72
+ def render
73
+ out = case contents
74
+ when :page_x_of_y
75
+ 'Page &amp;P of &amp;N'
76
+ when ::Array
77
+ contents.map do |v|
78
+ case v
79
+ when XlsxWriter::Image
80
+ v.lcr = self
81
+ '&amp;G'
82
+ else
83
+ v
84
+ end
85
+ end.join
86
+ when XlsxWriter::Image
87
+ contents.lcr = self
88
+ '&amp;G'
89
+ else
90
+ contents
91
+ end
92
+ "K000000#{out}"
93
+ end
94
+
95
+ def to_s
96
+ [ '', lcr, FONT, SIZE, render ].join('&amp;')
97
+ end
98
+ end
99
+
100
+ class L < LCR; end
101
+ class C < LCR; end
102
+ class R < LCR; end
103
+ end
104
+
105
+ class H < HF
106
+ def tag
107
+ 'oddHeader'
108
+ end
109
+ end
110
+ class F < HF
111
+ def tag
112
+ 'oddFooter'
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,43 @@
1
+ module XlsxWriter
2
+ class PageSetup < ::Struct.new(:top, :right, :bottom, :left, :header, :footer, :orientation, :vertical_dpi, :horizontal_dpi)
3
+ def top
4
+ self[:top] || 1.0
5
+ end
6
+
7
+ def right
8
+ self[:right] || 0.75
9
+ end
10
+
11
+ def bottom
12
+ self[:bottom] || 1.0
13
+ end
14
+
15
+ def left
16
+ self[:left] || 0.75
17
+ end
18
+
19
+ def header
20
+ self[:header] || 0.5
21
+ end
22
+
23
+ def footer
24
+ self[:footer] || 0.5
25
+ end
26
+
27
+ def orientation
28
+ self[:orientation] || 'landscape'
29
+ end
30
+
31
+ def vertical_dpi
32
+ self[:vertical_dpi] || 4294967292
33
+ end
34
+
35
+ def horizontal_dpi
36
+ self[:horizontal_dpi] || 4294967292
37
+ end
38
+
39
+ def to_xml
40
+ %{<pageMargins left="#{left}" right="#{right}" top="#{top}" bottom="#{bottom}" header="#{header}" footer="#{footer}"/><pageSetup orientation="#{orientation}" horizontalDpi="#{horizontal_dpi}" verticalDpi="#{vertical_dpi}"/>}
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ module XlsxWriter
2
+ class Row
3
+ attr_reader :sheet
4
+ attr_reader :cells
5
+
6
+ def initialize(sheet, columns)
7
+ @sheet = sheet
8
+ @cells = columns.map do |column|
9
+ Cell.new self, column
10
+ end
11
+ end
12
+
13
+ def ndx
14
+ sheet.rows.index(self) + 1
15
+ end
16
+
17
+ def length
18
+ cells.length
19
+ end
20
+
21
+ def cell_width(x)
22
+ if cell = cells[x]
23
+ cell.pixel_width
24
+ else
25
+ 0
26
+ end
27
+ end
28
+
29
+ def to_xml
30
+ ary = []
31
+ ary << %{<row r="#{ndx}">}
32
+ cells.each do |cell|
33
+ ary << cell.to_xml
34
+ end
35
+ ary << %{</row>}
36
+ ary.join
37
+ end
38
+
39
+ extend ::ActiveSupport::Memoizable
40
+ memoize :cell_width
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ require 'fileutils'
2
+ require 'tmpdir'
3
+ require 'posix/spawn'
4
+
5
+ module XlsxWriter
6
+ module Utils
7
+ def self.tmp_path(basename = nil, extname = nil)
8
+ ::Kernel.srand
9
+ ::File.join ::Dir.tmpdir, "XlsxWriter-#{basename}#{::Kernel.rand(99999999)}#{extname ? ".#{extname}" : ''}"
10
+ end
11
+
12
+ # zip -r -q #{filename} .
13
+ def self.zip(src_dir)
14
+ out_path = tmp_path('zip', 'zip')
15
+ child = ::POSIX::Spawn::Child.new 'zip', '--recurse-paths', out_path, '.', :chdir => src_dir
16
+ if child.success?
17
+ out_path
18
+ else
19
+ raise ::RuntimeError, child.err
20
+ end
21
+ end
22
+
23
+ # use awk to convert [CR]LF to CRLF
24
+ def self.unix2dos(path)
25
+ out_path = tmp_path
26
+ ::File.open(out_path, 'wb') do |out|
27
+ pid = ::POSIX::Spawn.spawn 'awk', '{ sub(/\r?$/,"\r"); print }', path, :out => out
28
+ ::Process.waitpid pid
29
+ end
30
+ ::FileUtils.mv out_path, path
31
+ path
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ require 'erb'
2
+ require 'fileutils'
3
+
4
+ module XlsxWriter
5
+ class Xml
6
+ attr_reader :document
7
+
8
+ def initialize(document)
9
+ @document = document
10
+ end
11
+
12
+ def path
13
+ generate unless generated?
14
+ @path
15
+ end
16
+
17
+ def generated?
18
+ @generated == true
19
+ end
20
+
21
+ def staging_path
22
+ p = ::File.join document.staging_dir, relative_path
23
+ ::FileUtils.mkdir_p ::File.dirname(p)
24
+ p
25
+ end
26
+
27
+ def template_path
28
+ ::File.expand_path "../generators/#{self.class.name.demodulize.underscore}.erb", __FILE__
29
+ end
30
+
31
+ def render
32
+ ::ERB.new(::File.read(template_path), nil, '<>').result(binding)
33
+ end
34
+
35
+ def generate
36
+ @path = staging_path
37
+ ::File.open(@path, 'wb') do |out|
38
+ out.write render
39
+ end
40
+ Utils.unix2dos @path
41
+ @generated = true
42
+ end
43
+ end
44
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xlsx_writer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dee Zsombor
9
+ - Justin Beck
10
+ - Seamus Abshere
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2011-12-06 00:00:00.000000000Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: activesupport
18
+ requirement: &2166102660 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '0'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: *2166102660
27
+ - !ruby/object:Gem::Dependency
28
+ name: fast_xs
29
+ requirement: &2166101620 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: 0.7.3
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: *2166101620
38
+ - !ruby/object:Gem::Dependency
39
+ name: posix-spawn
40
+ requirement: &2166100780 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: *2166100780
49
+ description: Writes XLSX files. Minimal XML and style. Supports autofilters and headers/footers
50
+ with images and page numbers.
51
+ email:
52
+ - seamus@abshere.net
53
+ executables: []
54
+ extensions: []
55
+ extra_rdoc_files: []
56
+ files:
57
+ - lib/xlsx_writer/autofilter.rb
58
+ - lib/xlsx_writer/cell.rb
59
+ - lib/xlsx_writer/document.rb
60
+ - lib/xlsx_writer/generators/app.erb
61
+ - lib/xlsx_writer/generators/app.rb
62
+ - lib/xlsx_writer/generators/content_types.erb
63
+ - lib/xlsx_writer/generators/content_types.rb
64
+ - lib/xlsx_writer/generators/doc_props.erb
65
+ - lib/xlsx_writer/generators/doc_props.rb
66
+ - lib/xlsx_writer/generators/image.rb
67
+ - lib/xlsx_writer/generators/rels.erb
68
+ - lib/xlsx_writer/generators/rels.rb
69
+ - lib/xlsx_writer/generators/sheet.rb
70
+ - lib/xlsx_writer/generators/sheet_rels.erb
71
+ - lib/xlsx_writer/generators/sheet_rels.rb
72
+ - lib/xlsx_writer/generators/styles.erb
73
+ - lib/xlsx_writer/generators/styles.rb
74
+ - lib/xlsx_writer/generators/vml_drawing.erb
75
+ - lib/xlsx_writer/generators/vml_drawing.rb
76
+ - lib/xlsx_writer/generators/vml_drawing_rels.erb
77
+ - lib/xlsx_writer/generators/vml_drawing_rels.rb
78
+ - lib/xlsx_writer/generators/workbook.erb
79
+ - lib/xlsx_writer/generators/workbook.rb
80
+ - lib/xlsx_writer/generators/workbook_rels.erb
81
+ - lib/xlsx_writer/generators/workbook_rels.rb
82
+ - lib/xlsx_writer/header_footer.rb
83
+ - lib/xlsx_writer/page_setup.rb
84
+ - lib/xlsx_writer/row.rb
85
+ - lib/xlsx_writer/utils.rb
86
+ - lib/xlsx_writer/xml.rb
87
+ - lib/xlsx_writer.rb
88
+ - LICENSE
89
+ - README.markdown
90
+ - Rakefile
91
+ homepage: http://github.com/seamusabshere/xlsx_writer
92
+ licenses: []
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 1.8.10
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: Writes XLSX files. Minimal XML and style. Supports autofilters and headers/footers
115
+ with images and page numbers.
116
+ test_files: []