ruby3mf 0.1.15 → 0.1.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/batch.rb +20 -0
- data/bin/cli.rb +3 -3
- data/bin/folder_test.sh +13 -0
- data/lib/ruby3mf.rb +1 -1
- data/lib/ruby3mf/content_types.rb +1 -1
- data/lib/ruby3mf/document.rb +46 -9
- data/lib/ruby3mf/errors.yml +17 -2
- data/lib/ruby3mf/model3mf.rb +8 -1
- data/lib/ruby3mf/relationships.rb +2 -6
- data/lib/ruby3mf/thumbnail3mf.rb +7 -9
- data/lib/ruby3mf/version.rb +1 -1
- data/lib/ruby3mf/xml_val.rb +47 -0
- metadata +5 -3
- data/lib/ruby3mf/global_xml_validations.rb +0 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 152e1cd32d11dadcadb62904f2942100278d059c
|
4
|
+
data.tar.gz: 3359551e080d311dbdbe6a8607f0336cbfedf298
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 670d9a203081c4da7d93cdb8dc81e8db53352a5bd6635511f028a29e073bc8ffb5325365f7539d9718227bd6646fb2d5b00b5c6b1cc3161dfe77528cea6ae242
|
7
|
+
data.tar.gz: 48bf7933386810d11e503953607545b3db168949c97123836fadec03fb10392530732b11b02b40d02c761eccac96fca9e62bb5716c2d289da3c32c46a5026bdb
|
data/bin/batch.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/ruby3mf'
|
4
|
+
|
5
|
+
files = Dir["spec/ruby3mf-testfiles/#{ARGV[0] || "failing_cases"}/*.#{ARGV[1] || '3mf'}"]
|
6
|
+
|
7
|
+
files.each do |file|
|
8
|
+
|
9
|
+
Log3mf.reset_log
|
10
|
+
doc = Document.read(file)
|
11
|
+
errors = Log3mf.entries(:error, :fatal_error)
|
12
|
+
|
13
|
+
puts "=" * 100
|
14
|
+
puts "Validating file: #{file}"
|
15
|
+
errors.each do |ent|
|
16
|
+
h = { context: ent[0], severity: ent[1], message: ent[2] }
|
17
|
+
puts h
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
data/bin/cli.rb
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
require_relative '../lib/ruby3mf'
|
4
4
|
|
5
|
-
|
5
|
+
filename = ARGV.first
|
6
|
+
doc = Document.read(filename)
|
6
7
|
|
7
8
|
errors = Log3mf.entries(:error, :fatal_error)
|
8
|
-
puts "Validating file: #{ARGV.first}"
|
9
9
|
errors.each do |ent|
|
10
|
-
h = { context: ent[0], severity: ent[1], message: ent[2] }
|
10
|
+
h = { file: filename, context: ent[0], severity: ent[1], message: ent[2] }
|
11
11
|
puts h
|
12
12
|
end
|
data/bin/folder_test.sh
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
NAME=$1
|
4
|
+
echo "input: ${NAME}"
|
5
|
+
echo "Starting test on: ${NAME}"
|
6
|
+
echo "Starting test on: ${NAME}" >> ${NAME}_test.json
|
7
|
+
|
8
|
+
for filename in ${NAME}/*.3mf; do
|
9
|
+
echo "Validating ${filename}"
|
10
|
+
~/src/ruby3mf/bin/cli.rb ${filename} >> ${NAME}_test.json
|
11
|
+
done
|
12
|
+
|
13
|
+
echo "All Done."
|
data/lib/ruby3mf.rb
CHANGED
@@ -7,7 +7,7 @@ require_relative "ruby3mf/model3mf"
|
|
7
7
|
require_relative "ruby3mf/relationships"
|
8
8
|
require_relative "ruby3mf/thumbnail3mf"
|
9
9
|
require_relative "ruby3mf/texture3mf"
|
10
|
-
require_relative "ruby3mf/
|
10
|
+
require_relative "ruby3mf/xml_val"
|
11
11
|
|
12
12
|
require 'zip'
|
13
13
|
require 'nokogiri'
|
@@ -6,7 +6,7 @@ class ContentTypes
|
|
6
6
|
Log3mf.context "parse" do |l|
|
7
7
|
begin
|
8
8
|
|
9
|
-
doc =
|
9
|
+
doc = XmlVal.validate_parse(zip_entry)
|
10
10
|
|
11
11
|
l.warning '[Content_Types].xml must contain exactly one root node' unless doc.children.size == 1
|
12
12
|
l.warning '[Content_Types].xml must contain root name Types' unless doc.children.first.name == "Types"
|
data/lib/ruby3mf/document.rb
CHANGED
@@ -6,6 +6,7 @@ class Document
|
|
6
6
|
attr_accessor :thumbnails
|
7
7
|
attr_accessor :textures
|
8
8
|
attr_accessor :objects
|
9
|
+
attr_accessor :parts
|
9
10
|
attr_accessor :zip_filename
|
10
11
|
|
11
12
|
# Relationship schemas
|
@@ -22,6 +23,8 @@ class Document
|
|
22
23
|
PRINT_TICKET_TYPE => {}
|
23
24
|
}
|
24
25
|
|
26
|
+
TEXTURE_TYPES = %w[image/jpeg image/png application/vnd.ms-package.3dmanufacturing-3dmodeltexture]
|
27
|
+
|
25
28
|
def initialize(zip_filename)
|
26
29
|
self.models=[]
|
27
30
|
self.thumbnails=[]
|
@@ -29,25 +32,56 @@ class Document
|
|
29
32
|
self.objects={}
|
30
33
|
self.relationships=[]
|
31
34
|
self.types=[]
|
35
|
+
self.parts=[]
|
32
36
|
@zip_filename = zip_filename
|
33
37
|
end
|
34
38
|
|
39
|
+
#verify that each texture part in the 3MF is related to the model through a texture relationship in a rels file
|
40
|
+
def self.validate_texture_parts(document, log)
|
41
|
+
|
42
|
+
return unless document.types.size > 0
|
43
|
+
document.parts.each do |filename|
|
44
|
+
ext = File::extname(filename).delete '.'
|
45
|
+
content_type = document.types[ext]
|
46
|
+
next unless TEXTURE_TYPES.include?(content_type)
|
47
|
+
|
48
|
+
has_relationship = false
|
49
|
+
document.textures.each do |texture_file|
|
50
|
+
if texture_file[:target] == filename
|
51
|
+
has_relationship = true
|
52
|
+
break
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
next unless !has_relationship
|
57
|
+
document.thumbnails.each do |thumbnail_file|
|
58
|
+
if thumbnail_file[:target] == filename
|
59
|
+
has_relationship = true
|
60
|
+
break
|
61
|
+
end
|
62
|
+
end
|
63
|
+
log.context "part names /#{filename}" do |l|
|
64
|
+
l.error :texture_without_relationship, name: filename unless has_relationship
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
35
69
|
def self.read(input_file)
|
36
70
|
|
37
71
|
m = new(input_file)
|
38
72
|
begin
|
39
|
-
Log3mf.context
|
73
|
+
Log3mf.context 'zip' do |l|
|
40
74
|
begin
|
41
75
|
Zip.warn_invalid_date = false
|
42
76
|
|
43
77
|
# check for the general purpose flag set - if so, warn that 3mf may not work on some systems
|
44
78
|
if File.read(input_file)[6] == "\b"
|
45
|
-
l.warning
|
79
|
+
l.warning 'File format: this file may not open on all systems'
|
46
80
|
end
|
47
81
|
|
48
82
|
Zip::File.open(input_file) do |zip_file|
|
49
83
|
|
50
|
-
l.info
|
84
|
+
l.info 'Zip file is valid'
|
51
85
|
|
52
86
|
# check for valid, absolute URI's for each path name
|
53
87
|
|
@@ -56,7 +90,7 @@ class Document
|
|
56
90
|
unless part.name.end_with? '[Content_Types].xml'
|
57
91
|
begin
|
58
92
|
u = URI part.name
|
59
|
-
rescue ArgumentError
|
93
|
+
rescue ArgumentError, URI::InvalidURIError
|
60
94
|
l.error :err_uri_bad
|
61
95
|
next
|
62
96
|
end
|
@@ -64,28 +98,29 @@ class Document
|
|
64
98
|
u.path.split('/').each do |segment|
|
65
99
|
l.error :err_uri_hidden_file if (segment.start_with? '.') && !(segment.end_with? '.rels')
|
66
100
|
end
|
101
|
+
m.parts << '/' + part.name unless part.directory?
|
67
102
|
end
|
68
103
|
end
|
69
104
|
end
|
70
105
|
|
71
|
-
l.context
|
106
|
+
l.context 'content types' do |l|
|
72
107
|
# 1. Get Content Types
|
73
108
|
content_type_match = zip_file.glob('\[Content_Types\].xml').first
|
74
109
|
if content_type_match
|
75
110
|
m.types = ContentTypes.parse(content_type_match)
|
76
111
|
else
|
77
|
-
l.error
|
112
|
+
l.error 'Missing required file: [Content_Types].xml', page: 4
|
78
113
|
end
|
79
114
|
end
|
80
115
|
|
81
|
-
l.context
|
116
|
+
l.context 'relationships' do |l|
|
82
117
|
# 2. Get Relationships
|
83
118
|
# rel_folders = zip_file.glob('**/_rels')
|
84
119
|
# l.fatal_error "Missing any _rels folder", page: 4 unless rel_folders.size>0
|
85
120
|
|
86
121
|
# 2.1 Validate that the top level _rels/.rel file exists
|
87
122
|
rel_file = zip_file.glob('_rels/.rels').first
|
88
|
-
l.fatal_error
|
123
|
+
l.fatal_error 'Missing required file _rels/.rels', page: 4 unless rel_file
|
89
124
|
|
90
125
|
zip_file.glob('**/*.rels').each do |rel|
|
91
126
|
m.relationships += Relationships.parse(rel)
|
@@ -135,6 +170,8 @@ class Document
|
|
135
170
|
end
|
136
171
|
end
|
137
172
|
end
|
173
|
+
|
174
|
+
validate_texture_parts(m, l)
|
138
175
|
end
|
139
176
|
|
140
177
|
return m
|
@@ -168,7 +205,7 @@ class Document
|
|
168
205
|
end
|
169
206
|
end
|
170
207
|
|
171
|
-
File.open(output_file,
|
208
|
+
File.open(output_file, 'wb') { |f| f.write(buffer.string) }
|
172
209
|
|
173
210
|
end
|
174
211
|
|
data/lib/ruby3mf/errors.yml
CHANGED
@@ -52,11 +52,14 @@ invalid_startpart_type:
|
|
52
52
|
invalid_relationship_type:
|
53
53
|
msg: "Invalid relationship type '%{type}' specified in .rels relationship file. Parts in the 3D payload MUST use one of the appropriate relationship types to establish that relationship between two parts in the payload."
|
54
54
|
page: 10
|
55
|
+
invalid_thumbnail_file:
|
56
|
+
msg: "thumbnail file must be valid image file"
|
57
|
+
page: 10
|
55
58
|
invalid_thumbnail_colorspace:
|
56
59
|
msg: "CMYK JPEG images must not be used for the thumbnail"
|
57
60
|
page: 36
|
58
61
|
invalid_thumbnail_file_type:
|
59
|
-
msg: "Expected a png or jpeg thumbnail but the thumbnail was of type
|
62
|
+
msg: "Expected a png or jpeg thumbnail but the thumbnail was of type %{type}"
|
60
63
|
page: 36
|
61
64
|
invalid_texture_file_type:
|
62
65
|
msg: "Expected a png or jpeg texture but the texture was of type image/gif"
|
@@ -117,4 +120,16 @@ unknown_required_extension:
|
|
117
120
|
page: 14
|
118
121
|
missing_extension_namespace_uri:
|
119
122
|
msg: "Required extension '%{ns}' MUST refer to namespace with URI"
|
120
|
-
page: 14
|
123
|
+
page: 14
|
124
|
+
texture_without_relationship:
|
125
|
+
msg: "%{name} appears to be a texture file but no rels file declares any relationship to the model"
|
126
|
+
page: 13
|
127
|
+
invalid_metadata_under_defaultns:
|
128
|
+
msg: "Metadata without a namespace name must only contain allowed name values"
|
129
|
+
page: 21
|
130
|
+
has_commas_for_floats:
|
131
|
+
msg: "numbers not formatted for the en-US locale"
|
132
|
+
page: 15
|
133
|
+
invalid_language_locale:
|
134
|
+
msg: "locale should be en-US"
|
135
|
+
page: 15
|
data/lib/ruby3mf/model3mf.rb
CHANGED
@@ -14,7 +14,7 @@ class Model3mf
|
|
14
14
|
|
15
15
|
Log3mf.context "parsing model" do |l|
|
16
16
|
begin
|
17
|
-
model_doc =
|
17
|
+
model_doc = XmlVal.validate_parse(zip_entry)
|
18
18
|
rescue Nokogiri::XML::SyntaxError => e
|
19
19
|
l.fatal_error "Model file invalid XML. Exception #{e}"
|
20
20
|
end
|
@@ -108,6 +108,13 @@ class Model3mf
|
|
108
108
|
metadata = model_doc.root.css("metadata")
|
109
109
|
metadata_names = metadata.map { |met| met['name'] }
|
110
110
|
l.error :metadata_elements_with_same_name unless metadata_names.uniq!.nil?
|
111
|
+
|
112
|
+
# metadata values allowed under default namespace (xmlns):
|
113
|
+
metadata_values = ['Title', 'Designer', 'Description', 'Copyright', 'LicenseTerms', 'Rating', 'CreationDate', 'ModificationDate' ]
|
114
|
+
|
115
|
+
unless model_doc.root.namespace.href.nil? || model_doc.root.namespace_definitions.count > 1
|
116
|
+
l.error :invalid_metadata_under_defaultns unless (metadata_names - metadata_values).empty?
|
117
|
+
end
|
111
118
|
end
|
112
119
|
|
113
120
|
MeshAnalyzer.validate(model_doc)
|
@@ -5,7 +5,7 @@ class Relationships
|
|
5
5
|
Log3mf.context "parsing relationships" do |l|
|
6
6
|
begin
|
7
7
|
# Parse Relationships XML
|
8
|
-
doc =
|
8
|
+
doc = XmlVal.validate_parse(zip_entry)
|
9
9
|
|
10
10
|
# Verify <Relationships><Relationship/></Relationships>
|
11
11
|
root_element = doc.children[0]
|
@@ -14,11 +14,7 @@ class Relationships
|
|
14
14
|
if relationship_elements.size > 0
|
15
15
|
relationship_elements.each do |node|
|
16
16
|
if node.is_a?(Nokogiri::XML::Element) && node.name == "Relationship"
|
17
|
-
relationships.
|
18
|
-
if previous_rel[:target] == node['Target'] && previous_rel[:type] == node['Type']
|
19
|
-
l.error :multiple_relationships
|
20
|
-
end
|
21
|
-
end
|
17
|
+
l.error :multiple_relationships if (relationships.select { |r| r[:target] == node['Target'] && r[:type] == node['Type'] }.size > 0)
|
22
18
|
relationships << {target: node['Target'], type: node['Type'], id: node['Id']}
|
23
19
|
l.info "adding relationship: #{relationships.last.inspect}"
|
24
20
|
else
|
data/lib/ruby3mf/thumbnail3mf.rb
CHANGED
@@ -3,16 +3,14 @@ require 'mini_magick'
|
|
3
3
|
class Thumbnail3mf
|
4
4
|
|
5
5
|
def self.parse(doc, relationship_file)
|
6
|
-
img_type = MimeMagic.by_magic(relationship_file.get_input_stream)
|
7
|
-
img_colorspace = MiniMagick::Image.read(relationship_file.get_input_stream).colorspace
|
8
|
-
|
9
6
|
Log3mf.context "Thumbnail3mf" do |l|
|
10
|
-
l.fatal_error "thumbnail file must be valid image file", page: 10 unless img_type
|
11
|
-
l.fatal_error "CMYK JPEG images must not be used for the thumbnail", page: 36 if img_colorspace.include? "CMYK"
|
12
|
-
l.debug "thumbnail is of type: #{img_type}"
|
13
|
-
l.error "Expected a png or jpeg thumbnail but the thumbnail was of type #{img_type}", page: 12 unless ['image/png', 'image/jpeg'].include? img_type.type
|
14
|
-
end
|
15
7
|
|
16
|
-
|
8
|
+
img_type = MimeMagic.by_magic(relationship_file.get_input_stream)
|
9
|
+
l.fatal_error :invalid_thumbnail_file unless img_type
|
10
|
+
l.error(:invalid_thumbnail_file_type, type: img_type) unless ['image/png', 'image/jpeg'].include? img_type.type
|
17
11
|
|
12
|
+
img_colorspace = MiniMagick::Image.read(relationship_file.get_input_stream).colorspace
|
13
|
+
l.fatal_error :invalid_thumbnail_colorspace if img_colorspace.include? "CMYK"
|
14
|
+
end
|
15
|
+
end
|
18
16
|
end
|
data/lib/ruby3mf/version.rb
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class XmlVal
|
4
|
+
|
5
|
+
def self.validate_parse(file)
|
6
|
+
doc = Nokogiri::XML(file.get_input_stream) do |config|
|
7
|
+
config.strict.nonet.noblanks
|
8
|
+
end
|
9
|
+
validate(file, doc)
|
10
|
+
doc
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.validate(file, document)
|
14
|
+
Log3mf.context "validations" do |l|
|
15
|
+
l.error :invalid_language_locale if invalid_locale?(document)
|
16
|
+
l.error :has_xml_space_attribute if space_attribute_exists?(document)
|
17
|
+
l.error :wrong_encoding if xml_not_utf8_encoded?(document)
|
18
|
+
l.error :dtd_not_allowed if dtd_exists?(file)
|
19
|
+
l.error :has_commas_for_floats if bad_floating_numbers?(document)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.invalid_locale?(document)
|
24
|
+
!document.xpath('//@xml:lang').empty? && document.xpath('//@xml:lang').text != "en-US"
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.bad_floating_numbers?(document)
|
28
|
+
!document.xpath('.//*[find_with_regex(., "[0-9]+\,[0-9]+")]', Class.new {
|
29
|
+
def find_with_regex node_set, regex
|
30
|
+
node_set.find_all { |node| node.values.any? {|v| v =~ /#{regex}/ } }
|
31
|
+
end
|
32
|
+
}.new).empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.space_attribute_exists?(document)
|
36
|
+
!(document.xpath('//*[@xml:space]').empty?)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.xml_not_utf8_encoded?(document)
|
40
|
+
!document.encoding.nil? && (document.encoding.to_s.downcase != 'utf-8')
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.dtd_exists?(file)
|
44
|
+
found = file.get_input_stream.find { |line| line =~ /(!DOCTYPE\b)|(!ELEMENT\b)|(!ENTITY\b)|(!NOTATION\b)|(!ATTLIST\b)/ }
|
45
|
+
!found.nil?
|
46
|
+
end
|
47
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby3mf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.17
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Whitmarsh, Jeff Porter, and William Hertling
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-01-
|
11
|
+
date: 2017-01-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -141,15 +141,16 @@ files:
|
|
141
141
|
- LICENSE.txt
|
142
142
|
- README.md
|
143
143
|
- Rakefile
|
144
|
+
- bin/batch.rb
|
144
145
|
- bin/cli.rb
|
145
146
|
- bin/console
|
147
|
+
- bin/folder_test.sh
|
146
148
|
- bin/setup
|
147
149
|
- lib/ruby3mf.rb
|
148
150
|
- lib/ruby3mf/content_types.rb
|
149
151
|
- lib/ruby3mf/document.rb
|
150
152
|
- lib/ruby3mf/edge_list.rb
|
151
153
|
- lib/ruby3mf/errors.yml
|
152
|
-
- lib/ruby3mf/global_xml_validations.rb
|
153
154
|
- lib/ruby3mf/interpolation.rb
|
154
155
|
- lib/ruby3mf/log3mf.rb
|
155
156
|
- lib/ruby3mf/mesh_analyzer.rb
|
@@ -158,6 +159,7 @@ files:
|
|
158
159
|
- lib/ruby3mf/texture3mf.rb
|
159
160
|
- lib/ruby3mf/thumbnail3mf.rb
|
160
161
|
- lib/ruby3mf/version.rb
|
162
|
+
- lib/ruby3mf/xml_val.rb
|
161
163
|
- ruby3mf.gemspec
|
162
164
|
homepage: https://github.com/IPGPTP/ruby3mf
|
163
165
|
licenses:
|
@@ -1,36 +0,0 @@
|
|
1
|
-
class GlobalXMLValidations
|
2
|
-
|
3
|
-
def self.validate_parse(file)
|
4
|
-
doc = Nokogiri::XML(file.get_input_stream) do |config|
|
5
|
-
config.strict.nonet.noblanks
|
6
|
-
end
|
7
|
-
validate(file, doc)
|
8
|
-
doc
|
9
|
-
end
|
10
|
-
|
11
|
-
def self.validate(file, document)
|
12
|
-
Log3mf.context "global xml validations" do |l|
|
13
|
-
l.error :has_xml_space_attribute if space_attribute_exists?(document)
|
14
|
-
l.error :wrong_encoding if xml_not_utf8_encoded?(document)
|
15
|
-
l.error :dtd_not_allowed if dtd_exists?(file)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.space_attribute_exists?(document)
|
20
|
-
!(document.xpath('//*[@xml:space]').empty?)
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.xml_not_utf8_encoded?(document)
|
24
|
-
!(document.encoding == 'UTF-8' || document.encoding == 'utf-8')
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.dtd_exists?(file)
|
28
|
-
found = file.get_input_stream.find { |line| line =~ /(!DOCTYPE\b)|(!ELEMENT\b)|(!ENTITY\b)|(!NOTATION\b)|(!ATTLIST\b)/ }
|
29
|
-
!found.nil?
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|