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 }) 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 xml.fill do xml.patternFill({ 'patternType' => 'none' }) end xml << "$-$" xml.fill do xml.patternFill({ 'patternType' => 'none' }) end xml.fill do xml.patternFill({ 'patternType' => 'none' }) end end xml.borders do xml.border do xml.left xml.right xml.top xml.bottom xml.diagonal end xml.border do xml.left xml.right xml.top xml.bottom xml.diagonal end xml.border do xml.left xml.right xml.top xml.bottom xml.diagonal end xml.border do xml.left xml.right xml.top xml.bottom xml.diagonal end end xml.cellStyleXfs({ 'count' => '6' }) do xml.xf({ 'numFmtId' => '0', 'fontId' => '0', 'fillId' => '0', 'borderId' => '0' }) 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' => '0' }) 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