class Document

Constants

MODEL_TYPE

Relationship schemas

RELATIONSHIP_TYPES

Relationship Type => Class validating relationship type

TEXTURE_TYPE
TEXTURE_TYPES
THUMBNAIL_TYPE
THUMBNAIL_TYPES

Image Content Types

Attributes

models[RW]
objects[RW]
parts[RW]
relationships[RW]
textures[RW]
thumbnails[RW]
types[RW]
zip_filename[RW]

Public Class Methods

new(zip_filename) click to toggle source
# File lib/ruby3mf/document.rb, line 30
def initialize(zip_filename)
  self.models=[]
  self.thumbnails=[]
  self.textures=[]
  self.objects={}
  self.relationships={}
  self.types=nil
  self.parts=[]
  @zip_filename = zip_filename
end
read(input_file) click to toggle source
# File lib/ruby3mf/document.rb, line 56
def self.read(input_file)

  m = new(input_file)
  begin
    Log3mf.context 'zip' do |l|
      begin
        Zip.warn_invalid_date = false
        Zip.unicode_names = true

        # check for the general purpose flag set - if so, warn that 3mf may not work on some systems
        File.open(input_file, "r") do |file|
          if file.read[6] == "\b"
            l.warning 'File format: this file may not open on all systems'
          end
        end

        Zip::File.open(input_file) do |zip_file|

          l.info 'Zip file is valid'

          # check for valid, absolute URI's for each path name

          zip_file.each do |part|
            l.context "part names /#{part.name}" do |l|
              unless part.name.end_with? '[Content_Types].xml'
                next unless u = parse_uri(l, part.name)

                u.path.split('/').each do |segment|
                  l.error :err_uri_hidden_file if (segment.start_with? '.') && !(segment.end_with? '.rels')
                end
                m.parts << '/' + part.name unless part.directory?
              end
            end
          end

          l.context 'content types' do |l|
            content_type_match = zip_file.glob('\[Content_Types\].xml').first
            if content_type_match
              m.types = ContentTypes.parse(content_type_match)
            else
              l.fatal_error :missing_content_types
            end
          end

          l.context 'relationships' do |l|
            rel_file = zip_file.glob('_rels/.rels').first
            l.fatal_error :missing_dot_rels_file unless rel_file

            zip_file.glob('**/*.rels').each do |rel|
              m.relationships[rel.name] = Relationships.parse(rel)
            end

            root_rels = m.relationships['_rels/.rels']
            unless root_rels.nil?
              start_part_rel = root_rels.select { |rel| rel[:type] == Document::MODEL_TYPE }.first
              if start_part_rel
                start_part_target = start_part_rel[:target]
                start_part_types = m.relationships.flat_map { |k, v| v }.select { |rel| rel[:type] == Document::MODEL_TYPE && rel[:target] == start_part_target }
                l.error :invalid_startpart_target, :target => start_part_target if start_part_types.size > 1
              end
            end

          end

          l.context "print tickets" do |l|
            print_ticket_types = m.relationships.flat_map { |k, v| v }.select { |rel| rel[:type] == PRINT_TICKET_TYPE }
            l.error :multiple_print_tickets if print_ticket_types.size > 1
          end

          l.context "relationship elements" do |l|
            m.relationships.each do |file_name, rels|
              rels.each do |rel|
                l.context rel[:target] do |l|
                  next unless u = parse_uri(l, rel[:target])

                  l.error :err_uri_relative_path unless u.to_s.start_with? '/'

                  target = rel[:target].gsub(/^\//, "")
                  l.error :err_uri_empty_segment if target.end_with? '/' or target.include? '//'
                  l.error :err_uri_relative_path if target.include? '/../'

                  # necessary since it has been observed that rubyzip treats all zip entry names
                  # as ASCII-8BIT regardless if they really contain unicode chars. Without forcing
                  # the encoding we are unable to find the target within the zip if the target contains
                  # unicode chars.
                  zip_target = target
                  zip_target.force_encoding('ASCII-8BIT')
                  relationship_file = zip_file.glob(zip_target).first

                  rel_type = rel[:type]
                  if relationship_file
                    relationship_type = RELATIONSHIP_TYPES[rel_type]
                    if relationship_type.nil?
                      l.warning :unsupported_relationship_type, type: rel[:type], target: rel[:target]
                    else
                      # check that relationships are valid; extensions and relationship types must jive
                      content_type = m.types.get_type(target)
                      expected_content_type = relationship_type[:valid_types]

                      if (expected_content_type)
                        l.error :missing_content_type, part: target unless content_type
                        l.error :resource_contentype_invalid, bt: content_type, rt: rel[:target] unless (content_type.nil? || expected_content_type.include?(content_type))
                      else
                        l.info "found unrecognized relationship type: #{rel_type}"
                      end

                      unless relationship_type[:klass].nil?
                        m.send(relationship_type[:collection]) << {
                          rel_id: rel[:id],
                          target: rel[:target],
                          object: Object.const_get(relationship_type[:klass]).parse(m, relationship_file)
                        }
                      end
                    end
                  else
                    l.error :rel_file_not_found, mf: "#{rel[:target]}"
                  end
                end
              end
            end
          end

          validate_texture_parts(m, l)
        end

        return m
      rescue Zip::Error
        l.fatal_error :not_a_zip
      end
    end
  rescue Log3mf::FatalError
    #puts "HALTING PROCESSING DUE TO FATAL ERROR"
    return nil
  end
end
validate_texture_parts(document, log) click to toggle source

verify that each texture part in the 3MF is related to the model through a texture relationship in a rels file

# File lib/ruby3mf/document.rb, line 42
def self.validate_texture_parts(document, log)
  unless document.types.empty?
    document.parts.select { |part| TEXTURE_TYPES.include?(document.types.get_type(part)) }.each do |tfile|
      if document.textures.select { |f| f[:target] == tfile }.size == 0
        if document.thumbnails.select { |t| t[:target] == tfile }.size == 0
          log.context "part names" do |l|
            l.warning "#{tfile} appears to be a texture file but no rels file declares any relationship to the model", page: 13
          end
        end
      end
    end
  end
end

Private Class Methods

parse_uri(logger, uri) click to toggle source
# File lib/ruby3mf/document.rb, line 226
def self.parse_uri(logger, uri)
  begin
    reserved_chars = /[\[\]]/

    if uri =~ reserved_chars
      logger.error :err_uri_bad
      return nil
    end

    u = Addressable::URI.parse uri
  rescue ArgumentError, Addressable::URI::InvalidURIError
    logger.error :err_uri_bad
  end
  u
end

Public Instance Methods

contents_for(path) click to toggle source
# File lib/ruby3mf/document.rb, line 218
def contents_for(path)
  Zip::File.open(zip_filename) do |zip_file|
    zip_file.glob(path).first.get_input_stream.read
  end
end
write(output_file = nil) click to toggle source
# File lib/ruby3mf/document.rb, line 192
def write(output_file = nil)
  output_file = zip_filename if output_file.nil?

  Zip::File.open(zip_filename) do |input_zip_file|

    buffer = Zip::OutputStream.write_buffer do |out|
      input_zip_file.entries.each do |e|
        if e.directory?
          out.copy_raw_entry(e)
        else
          out.put_next_entry(e.name)
          if objects[e.name]
            out.write objects[e.name]
          else
            out.write e.get_input_stream.read
          end
        end
      end
    end

    File.open(output_file, 'wb') { |f| f.write(buffer.string) }

  end

end