class Spreet::Handlers::OpenDocument

Constants

DATE_ELEMENTS
DATE_REGEXP
MIME
XMLNS

Public Class Methods

add_attr(node, name, value, ns=nil) click to toggle source
# File lib/spreet/handlers/open_document.rb, line 60
def self.add_attr(node, name, value, ns=nil)
  attr = LibXML::XML::Attr.new(node, name, value.to_s)
  attr.namespaces.namespace = ns if ns
  return attr
end
read(file, options={}) click to toggle source
# File lib/spreet/handlers/open_document.rb, line 66
def self.read(file, options={})
  spreet = nil
  Zip::File.open(file) do |zile|
    # Check mime_type
    entry = zile.find_entry "mimetype"
    if entry.nil?
      raise StandardError.new("First element in archive must be a non-compressed 'mimetype'-named file.")
    else
      mime_type = zile.read(entry)
      unless mime_type == MIME_ODS
        raise StandardError.new("Mimetype mismatch")
      end
    end
    
    # Get manifest
    entry, files = zile.find_entry("META-INF/manifest.xml"), {}
    if entry.nil?
      raise StandardError.new("Second element in archive must be a 'META-INF/manifest.xml'-named file.")
    else
      doc = LibXML::XML::Parser.string(zile.read(entry)).parse
      for child in doc.root.children
        if child.name == 'file-entry'
          files[child["full-path"]] = child
        end
      end
    end
    if files["/"]["media-type"] != MIME_ODS
      raise StandardError.new("Mimetype difference")
    end

    # Get content
    if files["content.xml"] and entry = zile.find_entry("content.xml")
      doc = LibXML::XML::Parser.string(zile.read(entry)).parse
      unless doc.root.name == 'document-content'
        raise StandardError.new("<document-content> element expected at root of content.xml")
      end
      if spreadsheet = doc.root.find('./office:body/office:spreadsheet', XMLNS_OFFICE).first
        spreet = Spreet::Document.new()
        for table in spreadsheet.find('./table:table', XMLNS_TABLE)
          sheet = spreet.sheets.add(table["name"])
          # Ignore table-column for now

          rows = table.find("./table:table-rows").first || table
          # # Expand rows and cells
          # array = []
          # for row in table.find('./table:table-row', XMLNS_TABLE)
          #   line = []
          #   for cell in row.find('./table:table-cell', XMLNS_TABLE)
          #     (cell["number-columns-repeated"]||'1').to_i.times do
          #       line << cell
          #     end
          #   end
          #   (row["number-rows-repeated"]||'1').to_i.times do
          #     array << line
          #   end
          # end
          # Fill sheet
          row_offset = 0
          rows.find('./table:table-row', XMLNS_TABLE).each_with_index do |row, y|
            row_content, cell_offset = false, 0
            row.find('./table:table-cell|./table:covered-table-cell', XMLNS_TABLE).each_with_index do |cell, x|
              x += cell_offset
              cell_content = false
              if cell.name == "covered-table-cell"
                # puts "covered-table-cell"
              else
                if value_type = cell.attributes.get_attribute_ns(XMLNS_OFFICE, "value-type")
                  value_type = value_type.value.to_sym
                  p = cell.find('./text:p', XMLNS_TEXT).first
                  if [:float, :percentage].include?(value_type)
                    value = cell.attributes.get_attribute_ns(XMLNS_OFFICE, "value").value
                    sheet[x,y] = value.to_f
                  elsif value_type == :currency
                    value = cell.attributes.get_attribute_ns(XMLNS_OFFICE, "value").value
                    currency = Money::Currency.new(cell.attributes.get_attribute_ns(XMLNS_OFFICE, "currency").value)
                    sheet[x,y] = Money.new(value.to_f * currency.subunit_to_unit, currency)
                  elsif value_type == :date
                    value = cell.attributes.get_attribute_ns(XMLNS_OFFICE, "date-value").value
                    if value.match(/\d{1,8}-\d{1,2}-\d{1,2}/)
                      value = Date.civil(*value.split(/[\-]+/).collect{|v| v.to_f})
                    elsif value.match(/\d{1,8}-\d{1,2}-\d{1,2}T\d{1,2}\:\d{1,2}\:\d{1,2}(\.\d+)?/)
                      value = Time.new(*value.split(/[\-\:\.\T]+/).collect{|v| v.to_f})
                    else
                      raise Exception.new("Bad date format")
                    end
                    sheet[x,y] = value
                  elsif value_type == :time
                    value = cell.attributes.get_attribute_ns(XMLNS_OFFICE, "time-value").value
                    sheet[x,y] = Duration.new(value)
                  elsif value_type == :boolean
                    value = cell.attributes.get_attribute_ns(XMLNS_OFFICE, "boolean-value").value
                    sheet[x,y] = (value == "true" ? true : false)
                  elsif value_type == :string
                    sheet[x,y] = p.content.to_s if p
                  end
                  sheet[x,y].text = p.content.to_s if p
                  cell_content = true
                end
                if annotation = cell.find("./office:annotation", XMLNS_OFFICE).first
                  if text = annotation.find("./text:p", XMLNS_TEXT).first
                    sheet[x,y].annotation = text.content.to_s
                    cell_content = true
                  end
                end
              end
              repeated = (cell["number-columns-repeated"]||'1').to_i - 1
              if repeated > 0
                repeated.times do |i|
                  sheet[x+i+1,y] = sheet[x,y]
                end if cell_content
                cell_offset += repeated
              end
              row_content = true if cell_content
            end

            repeated = (row["number-rows-repeated"]||'1').to_i - 1
            if repeated > 0
              repeated.times do |i|
                sheet.row(sheet.rows(y), :row=>(y+i+1))
              end if row_content
              row_offset += repeated
            end
            
          end
          # What else ?
        end
      end
    end

    if spreet.nil?
      raise StandardError.new("Missing or bad content.xml")
    end
  end        
  return spreet
end
write(spreet, file, options={}) click to toggle source
# File lib/spreet/handlers/open_document.rb, line 203
def self.write(spreet, file, options={})
  xml_escape = "to_s.gsub('&', '&amp;').gsub('\\'', '&apos;').gsub('<', '&lt;').gsub('>', '&gt;')"
  xml_escape << ".force_encoding('US-ASCII')" if xml_escape.respond_to?(:force_encoding)
  mime_type = MIME_ODS
  # name = #{table.model.name}.model_name.human.gsub(/[^a-z0-9]/i,'_')
  Zip::OutputStream.open(file) do |zile|
    # MimeType in first place
    zile.put_next_entry('mimetype', nil, nil, Zip::Entry::STORED)
    zile << mime_type
    
    # Manifest
    doc = LibXML::XML::Document.new
    doc.root = LibXML::XML::Node.new('manifest')
    ns = LibXML::XML::Namespace.new(doc.root, 'manifest', XMLNS_MANIFEST)
    doc.root.namespaces.namespace = ns
    files = {
      "/" => {"media-type" => mime_type},
      "content.xml" => {"media-type" => MIME_XML}
    }
    for path, attributes in files
      doc.root << entry = LibXML::XML::Node.new('file-entry', nil, ns)
      attributes['full-path'] = path
      for name, value in attributes.sort
        self.add_attr(entry, name, value, ns)
      end
    end
    zile.put_next_entry('META-INF/manifest.xml')
    xml = doc.to_s(:indent=>false)
    xml.force_encoding('US-ASCII') if xml.respond_to? :force_encoding
    zile << xml

    # Content
    doc = LibXML::XML::Document.new
    doc.root = LibXML::XML::Node.new('document-content')
    nss = {}
    for prefix, ns in XMLNS.select{|k,v| k != :manifest}
      nss[prefix] = LibXML::XML::Namespace.new(doc.root, prefix.to_s, ns)
    end
    doc.root.namespaces.namespace = nss[:office]
    add_attr(doc.root, "version", "1.1", nss[:office])

    doc.root << automatic_styles = LibXML::XML::Node.new("automatic-styles", nil, nss[:office])

    automatic_styles << style = LibXML::XML::Node.new("date-style", nil, nss[:number])
    add_attr(style, "name", "DMY", nss[:style])
    add_attr(style, "automatic-order", "true", nss[:number])
    style << token = LibXML::XML::Node.new("day", nil, nss[:number])
    add_attr(token, "style", "long", nss[:number])
    style << token = LibXML::XML::Node.new("text", "/", nss[:number])
    style << token = LibXML::XML::Node.new("month", nil, nss[:number])
    add_attr(token, "style", "long", nss[:number])
    style << token = LibXML::XML::Node.new("text", "/", nss[:number])
    style << token = LibXML::XML::Node.new("year", nil, nss[:number])

    automatic_styles << style = LibXML::XML::Node.new("style", nil, nss[:style])
    add_attr(style, "name", "CE1", nss[:style])
    add_attr(style, "family", "table-cell", nss[:style])
    add_attr(style, "data-style-name", "DMY", nss[:style])

    automatic_styles << style = LibXML::XML::Node.new("style", nil, nss[:number])
    add_attr(style, "name", "COL", nss[:style])
    add_attr(style, "family", "table-column", nss[:number])
    style << token = LibXML::XML::Node.new("table-column-properties", nil, nss[:style])
    add_attr(token, "break-before", "auto", nss[:fo])
    add_attr(token, "use-optimal-column-width", "true", nss[:style])

    doc.root << body = LibXML::XML::Node.new("body", nil, nss[:office])
    body << spreadsheet = LibXML::XML::Node.new("spreadsheet", nil, nss[:office])
    for sheet in spreet.sheets
      spreadsheet << table = LibXML::XML::Node.new("table", nil, nss[:table])
      add_attr(table, "name", sheet.name, nss[:table])
      table << table_columns = LibXML::XML::Node.new("table-columns", nil, nss[:table])
      for x in 0..sheet.bound.x
        table_columns << table_column = LibXML::XML::Node.new("table-column", nil, nss[:table])
        add_attr(table_column, "style-name", "COL", nss[:table])
      end
      table << table_rows = LibXML::XML::Node.new("table-rows", nil, nss[:table])
      sheet.each_row do |row| # #{record} in #{table.records_variable_name}\n"
        table_rows << table_row = LibXML::XML::Node.new("table-row", nil, nss[:table])
        for cell in row
          table_row << table_cell = LibXML::XML::Node.new("table-cell", nil, nss[:table])
          unless cell.empty?
            add_attr(table_cell, "value-type", cell.type, nss[:office])
            if cell.type == :float # or percentage
              add_attr(table_cell, "value", cell.value, nss[:office])
            elsif cell.type == :currency
              add_attr(table_cell, "value", cell.value.to_f, nss[:office])
              add_attr(table_cell, "currency", cell.value.currency_as_string, nss[:office])
            elsif cell.type == :date
              if cell.value.is_a? Date
                add_attr(table_cell, "date-value", cell.value.to_s, nss[:office])
                add_attr(table_cell, "style-name", "CE1", nss[:table])
              elsif cell.value.is_a?(DateTime) or cell.value.is_a?(Time) 
                add_attr(table_cell, "datetime-value", cell.value.to_xsd, nss[:office])
              end
            elsif cell.type == :time
              add_attr(table_cell, "time-value", cell.value.to_s, nss[:office])
            elsif cell.type == :boolean
              add_attr(table_cell, "boolean-value", cell.value.to_s, nss[:office])
            end
            table_cell << LibXML::XML::Node.new("p", cell.text, nss[:text])
          end
          unless cell.annotation.nil?
            table_cell << annotation = LibXML::XML::Node.new("annotation", nil, nss[:office])
            annotation << LibXML::XML::Node.new("p", cell.annotation, nss[:text])
          end
        end
      end
    end

    zile.put_next_entry('content.xml')        
    xml = doc.to_s(:indent=>false)
    xml.force_encoding('US-ASCII') if xml.respond_to? :force_encoding
    zile << xml

    #   zile.put_next_entry('content.xml')
    #   zile << ("<?xml version=\"1.0\" encoding=\"UTF-8\"?><office:document-content xmlns:office=\"urn:oasis:names:tc:opendocument:xmlns:office:1.0\" xmlns:style=\"urn:oasis:names:tc:opendocument:xmlns:style:1.0\" xmlns:text=\"urn:oasis:names:tc:opendocument:xmlns:text:1.0\" xmlns:table=\"urn:oasis:names:tc:opendocument:xmlns:table:1.0\" xmlns:draw=\"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0\" xmlns:fo=\"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:meta=\"urn:oasis:names:tc:opendocument:xmlns:meta:1.0\" xmlns:number=\"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0\" xmlns:presentation=\"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0\" xmlns:svg=\"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0\" xmlns:chart=\"urn:oasis:names:tc:opendocument:xmlns:chart:1.0\" xmlns:dr3d=\"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0\" xmlns:math=\"http://www.w3.org/1998/Math/MathML\" xmlns:form=\"urn:oasis:names:tc:opendocument:xmlns:form:1.0\" xmlns:script=\"urn:oasis:names:tc:opendocument:xmlns:script:1.0\" xmlns:ooo=\"http://openoffice.org/2004/office\" xmlns:ooow=\"http://openoffice.org/2004/writer\" xmlns:oooc=\"http://openoffice.org/2004/calc\" xmlns:dom=\"http://www.w3.org/2001/xml-events\" xmlns:xforms=\"http://www.w3.org/2002/xforms\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:field=\"urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:field:1.0\" office:version=\"1.1\"><office:scripts/>")
    #   # Styles
    #   default_date_format = '%d/%m%Y' # ::I18n.translate("date.formats.default")
    #   zile << ("<office:automatic-styles><style:style style:name=\"co1\" style:family=\"table-column\"><style:table-column-properties fo:break-before=\"auto\" style:use-optimal-column-width=\"true\"/></style:style><style:style style:name=\"header\" style:family=\"table-cell\"><style:text-properties fo:font-weight=\"bold\" style:font-weight-asian=\"bold\" style:font-weight-complex=\"bold\"/></style:style><number:date-style style:name=\"K4D\" number:automatic-order=\"true\"><number:text>"+default_date_format.gsub(DATE_REGEXP){|x| "</number:text>"+DATE_ELEMENTS[x[1..1]]+"<number:text>"} +"</number:text></number:date-style><style:style style:name=\"ce1\" style:family=\"table-cell\" style:data-style-name=\"K4D\"/></office:automatic-styles>")
    
    #   zile << ("<office:body><office:spreadsheet>")
    #   # Tables
    #   for sheet in spreet.sheets
    #     zile << ("<table:table table:name=\"#{xmlec(sheet.name)}\">")
    #     zile << ("<table:table-column table:number-columns-repeated=\"#{sheet.bound.x+1}\"/>")
    #     # zile << ("<table:table-header-rows><table:table-row>"+columns_headers(table).collect{|h| "<table:table-cell table:style-name=\"header\" office:value-type=\"string\"><text:p>'+(#{h}).#{xml_escape}+'</text:p></table:table-cell>"}.join+"</table:table-row></table:table-header-rows>")
    #     sheet.each_row do |row| # #{record} in #{table.records_variable_name}\n"
    #       zile << "<table:table-row>"
    #       for cell in row
    #         if cell.empty?
    #           zile << "<table:table-cell/>"
    #         else
    #           zile << "<table:table-cell"+(if cell.type == :decimal
    #                                          " office:value-type=\"float\" office:value=\"#{xmlec(cell.value)}\""
    #                                        elsif cell.type == :boolean
    #                                          " office:value-type=\"boolean\" office:boolean-value=\"#{xmlec(cell.value ? 'true' : 'false')}\""
    #                                        elsif cell.type == :date
    #                                          " office:value-type=\"date\" table:style-name=\"ce1\" office:date-value=\"#{xmlec(cell.value)}\""
    #                                        else
    #                                          " office:value-type=\"string\""
    #                                        end)+"><text:p>"+xmlec(cell.text)+"</text:p></table:table-cell>"
    #         end
    #       end
    #       zile << "</table:table-row>"
    #     end
    #     zile << ("</table:table>")
    #   end
    #   zile << ("</office:spreadsheet></office:body></office:document-content>")
  end
  # Zile is finished
end
xmlec(string) click to toggle source
# File lib/spreet/handlers/open_document.rb, line 54
def self.xmlec(string)
  zs = string.to_s.gsub('&', '&amp;').gsub('\'', '&apos;').gsub('<', '&lt;').gsub('>', '&gt;')
  zs.force_encoding('US-ASCII') if zs.respond_to?(:force_encoding)
  return zs
end