module XLSX class Formatter # :nodoc: def self.build(workbook) self.new(workbook).build_workbook end def self.dump(workbook, output_path=nil) self.new(workbook, output_path).dump_workbook end attr_accessor :output_path attr_accessor :tmp_path attr_accessor :workbook attr_accessor :zip_file def initialize(workbook, output_path=nil) @workbook, @output_path = workbook, output_path end def build_workbook data = File.open(dump_workbook).read File.unlink(@output_path) data end def dump_workbook in_zip_file do mkdir("_rels") mkdir("docProps") mkdir("xl") mkdir("xl/_rels") mkdir("xl/worksheets") build_content_types_xml_file build_docProps_app_xml_file build_docProps_core_xml_file build_xl__rels_workbook_xml_rels_file build_xl_sharedStrings_xml_file build_xl_styles_xml_file build_xl_workbook_xml_file build_xl_worksheets_sheet_xml_files end end def build_xl_worksheets_sheet_xml_files @workbook.sheets.each do |sheet| dump_xml("xl/worksheets/sheet#{sheet.id}.xml") do |xml| rows = sheet.data.values.sort.inject([]) do |rows, cell| if rows.last.nil? or (rows.last.first != cell.row) rows.push([cell.row, []]) end rows.last.last.push(cell) rows end column_counts = rows.collect { |row| row.last.size } max_column_count = column_counts.max xml.worksheet({'xmlns' => "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}) do xml.sheetViews do xml.sheetView({ 'workbookViewId' => "0", 'zoomScale' => "100" }) do xml.selection({ 'activeCell' => 'A1', 'sqref' => 'A1' }) end end xml.sheetFormatPr({ 'defaultColWidth' => "13.2307692307692", 'defaultRowHeight' => "13", 'customHeight' => "true" }) xml.cols do xml.col({ 'min' => '1', 'max' => max_column_count.to_s, 'width' => "13.2307692307692", 'customWidth' => "1" }) end xml.sheetData do rows.each do |row| xml.row({ 'r' => row.first.to_s, 'ht' => '12.1' }) do row.last.each do |cell| xml.c({ 'r' => "#{letter_column_name(cell.column+1)}#{row.first + 1}", 't' => cell.type, 's' => cell.style }) do xml.v(cell.value.to_s) end end end end end xml.headerFooter do xml.oddHeader("&C&A", { 'xml:space' => "preserve" }) xml.oddHeader("&C&\"Arial\"&10Page &P", { 'xml:space' => "preserve" }) end end end end end def build_content_types_xml_file(sheets=[]) dump_xml("[Content_Types].xml") do |xml| xml.Types({'xmlns' => "http://schemas.openxmlformats.org/package/2006/content-types"}) do xml.Default({'Extension' => 'jpeg', 'ContentType' => 'image/jpeg'}) xml.Default({'Extension' => 'jpg', 'ContentType' => 'image/jpeg'}) xml.Default({'Extension' => 'gif', 'ContentType' => 'image/gif'}) xml.Default({'Extension' => 'png', 'ContentType' => 'image/png'}) xml.Default({'Extension' => 'wmf', 'ContentType' => 'image/x-emf'}) xml.Default({'Extension' => 'emf', 'ContentType' => 'image/x-emf'}) xml.Default({'Extension' => 'tif', 'ContentType' => 'image/tiff'}) xml.Default({'Extension' => 'tiff', 'ContentType' => 'image/tiff'}) xml.Default({'Extension' => 'bin', 'ContentType' => 'application/vnd.openxmlformats-officedocument.oleObject'}) xml.Default({'Extension' => 'rels', 'ContentType' => 'application/vnd.openxmlformats-package.relationships+xml'}) xml.Default({'Extension' => 'xml', 'ContentType' => 'application/xml'}) xml.Default({'Extension' => 'vml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.vmlDrawing'}) xml.Override({'PartName' => '/xl/theme/theme1.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.theme+xml'}) xml.Override({'PartName' => '/xl/workbook.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'}) xml.Override({'PartName' => '/xl/sharedStrings.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'}) xml.Override({'PartName' => '/docProps/core.xml', 'ContentType' => 'application/vnd.openxmlformats-package.core-properties+xml'}) xml.Override({'PartName' => '/docProps/app.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.extended-properties+xml'}) xml.Override({'PartName' => '/docProps/custom.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.custom-properties+xml'}) xml.Override({'PartName' => '/xl/styles.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'}) xml.Override({'PartName' => '/xl/connections.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml'}) @workbook.sheets.each do |sheet| xml.Override({'PartName' => "/xl/worksheets/sheet#{sheet.id}.xml", 'ContentType' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'}) end end end end def build_docProps_app_xml_file dump_xml("docProps/app.xml") do |xml| xml.Properties({ 'xmlns' => 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties', 'xmlns:vt' => 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes', 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/' }) do xml.DocSecurity("1") xml.Application("RbXLSX") xml.TotalTime("1") end end end def build_docProps_core_xml_file dump_xml("docProps/core.xml") do |xml| xml.cp :coreProperties, { 'xmlns:cp' => 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties', 'xmlns:dcmitype' => 'http://purl.org/dc/dcmitype/' } do xml.dcterms :created, '2008-08-25T14:50:39Z', { 'xmlns:dcterms' => 'http://purl.org/dc/terms/', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:type' => 'dcterms:W3CDTF' } xml.dcterms :modified, '2008-08-25T14:51:32Z', { 'xmlns:dcterms' => 'http://purl.org/dc/terms/', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:type' => 'dcterms:W3CDTF' } xml.dc :language, 'en', { 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/' } xml.cp :revision, '2' end end end def build_xl_sharedStrings_xml_file dump_xml("xl/sharedStrings.xml") do |xml| xml.sst({ 'xmlns' => 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'count' => @workbook.strings.size.to_s, 'uniqueCount' => @workbook.strings.size.to_s }) do @workbook.strings.each do |string| xml.si { xml.t string.to_s, { 'xml:space' => 'preserve' } } end end end end def build_xl_styles_xml_file dump_xml("xl/styles.xml") do |xml| xml.styleSheet({ 'xmlns' => 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' }) do xml.numFmts({ 'count' => '3' }) do xml.numFmt({ 'numFmtId' => '1', 'formatCode' => '0.##' }) xml.numFmt({ 'numFmtId' => '3', 'formatCode' => '[$$-409]#,##0.00;[$$-409][Red]-#,##0.00' }) end xml.fonts({ 'count' => '1' }) do xml.font do xml.sz({ 'val' => '10' }) xml.name({ 'val' => 'Arial' }) end xml.font do xml.b xml.i xml.u({ 'val' => 'single' }) end xml.font do xml.b xml.i xml.sz({ 'val' => '16' }) end end xml.fills do xml.fill do xml.patternFill({ 'patternType' => 'none' }) end xml.fill do xml.patternFill({ 'patternType' => 'gray125' }) end end xml.borders do xml.border do xml.left xml.right xml.top xml.bottom xml.diagonal end end xml.cellStyleXfs({ 'count' => '5' }) do xml.xf({ 'numFmtId' => '0', 'fontId' => '0', 'fillId' => '0', 'borderId' => '0' }) xml.xf({ 'numFmtId' => '0', 'fillId' => '0', 'borderId' => '0', 'applyFont' => '1', 'ZMIENNA_contentFontsCount' => '0', 'ZMIENNA_styleFontsCount' => '2', 'fontId' => '2' }) xml.xf({ 'numFmtId' => '3', 'fillId' => '0', 'borderId' => '0', 'applyFont' => '1', 'ZMIENNA_contentFontsCount' => '0', 'ZMIENNA_styleFontsCount' => '2', 'fontId' => '2' }) xml.xf({ 'numFmtId' => '0', 'fillId' => '0', 'borderId' => '0', 'applyFont' => '1', 'ZMIENNA_contentFontsCount' => '0', 'ZMIENNA_styleFontsCount' => '2', 'fontId' => '3', 'applyAlignment' => '1' }) do xml.alignment({ 'horizontal' => 'center' }) end xml.xf({ 'numFmtId' => '0', 'fillId' => '0', 'borderId' => '0', 'applyFont' => '1', 'ZMIENNA_contentFontsCount' => '0', 'ZMIENNA_styleFontsCount' => '2', 'fontId' => '3', 'applyAlignment' => '1' }) do xml.alignment({ 'horizontal' => 'center', 'textRotation' => '90' }) end end xml.cellXfs({ 'count' => '1' }) do xml.xf({ 'numFmtId' => '0', 'fontId' => '0', 'fillId' => '0', 'borderId' => '0', 'xfId' => '1' }) xml.xf({ 'numFmtId' => '0', 'fillId' => '0', 'borderId' => '0', 'xfId' => '2', 'applyFont' => '1', 'ZMIENNA_contentFontsCount' => '0', 'ZMIENNA_styleFontsCount' => '2', 'fontId' => '2' }) xml.xf({ 'numFmtId' => '3', 'fillId' => '0', 'borderId' => '0', 'xfId' => '3', 'applyFont' => '1', 'ZMIENNA_contentFontsCount' => '0', 'ZMIENNA_styleFontsCount' => '2', 'fontId' => '2' }) xml.xf({ 'numFmtId' => '0', 'fillId' => '0', 'borderId' => '0', 'xfId' => '4', 'applyFont' => '1', 'ZMIENNA_contentFontsCount' => '0', 'ZMIENNA_styleFontsCount' => '2', 'fontId' => '3', 'applyAlignment' => '1' }) do xml.alignment({ 'horizontal' => 'center' }) end xml.xf({ 'numFmtId' => '0', 'fillId' => '0', 'borderId' => '0', 'xfId' => '5', 'applyFont' => '1', 'ZMIENNA_contentFontsCount' => '0', 'ZMIENNA_styleFontsCount' => '2', 'fontId' => '3', 'applyAlignment' => '1' }) do xml.alignment({ 'horizontal' => 'center', 'textRotation' => '90' }) end end xml.cellStyles do xml.cellStyle({ 'xfId' => '1', 'name' => 'Default' }) xml.cellStyle({ 'xfId' => '2', 'name' => 'Result' }) xml.cellStyle({ 'xfId' => '3', 'name' => 'Result2' }) xml.cellStyle({ 'xfId' => '4', 'name' => 'Heading' }) xml.cellStyle({ 'xfId' => '5', 'name' => 'Heading1' }) end xml.dxfs({ 'xmlns:config' => 'urn:oasis:names:tc:opendocument:xmlns:config:1.0', 'xmlns:fo' => 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0', 'count' => '3' }) do xml.dxf do xml.font do xml.condense({ 'val' => '0' }) xml.extend({ 'val' => '0' }) xml.color({ 'rgb' => 'FF9C0006' }) end xml.fill do xml.patternFill do xml.bgColor({ 'rgb' => 'FFFFC7CE' }) end end end end xml.tableStyles({ 'count' => '0', 'defaultTableStyle' => 'TableStyleMedium9', 'defaultPivotStyle' => 'PivotStyleLight16' }) end end end def build_xl_workbook_xml_file(sheets=[]) dump_xml("xl/workbook.xml") do |xml| xml.workbook({ 'xmlns' => 'http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'xmlns:r' => 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' }) do xml.bookViews do xml.workbookView({ 'activeTab' => '0' }) end xml.sheets do @workbook.sheets.each do |sheet| xml.sheet({ 'name' => sheet.name, 'sheetId' => (sheet.id).to_s,'r:id' => sheet.hash }) end end end end end def build_xl__rels_workbook_xml_rels_file dump_xml("xl/_rels/workbook.xml.rels") do |xml| xml.Relationships({ 'xmlns' => 'http://schemas.openxmlformats.org/package/2006/relationships' }) do xml.Relationship({ 'Id' => 'rId1', 'Target' => 'styles.xml', 'Type' => 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles' }) xml.Relationship({ 'Id' => 'rId2', 'Target' => 'sharedStrings.xml', 'Type' => 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings' }) xml.Relationship({ 'Id' => 'rId3', 'Target' => 'connections.xml', 'Type' => 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections' }) @workbook.sheets.each do |sheet| xml.Relationship({ 'Id' => sheet.hash, 'Type' => 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet', 'Target' => "worksheets/sheet#{sheet.id}.xml" }) end end end end private def letter_column_name(i) s = "" i = i.to_i while i > 26 r = i % 27 i = i / 27 s = (r == 0 ? s : (r + 64).chr + s) end (i == 0 ? s : (i + 64).chr + s) end def build_xml(options={}) xml= Builder::XmlMarkup.new(options) xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8" yield(xml) xml.target! end def dump_xml(path, &block) @zip_file.file.open(path, "w") do |f| f.write(build_xml(&block)) end end def in_zip_file @output_path = File.join(Dir.tmpdir, "xlsx_#{Time.now.to_i}.xlsx") if @output_path.nil? File.unlink(@output_path) if File.exist?(@output_path) Zip::ZipFile.open(@output_path, Zip::ZipFile::CREATE) do |zip_file| @zip_file = zip_file yield @zip_file = nil end @output_path end def mkdir(path) @zip_file.dir.mkdir(path) end end end