xlsx_writer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []