dxf_io 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ module DxfIO
2
+ module Entity
3
+ module Support
4
+ extend ActiveSupport::Autoload
5
+
6
+ autoload :Point
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,114 @@
1
+ module DxfIO
2
+ module Entity
3
+ module Support
4
+ class Point
5
+
6
+ START_POINT_GROUP_NUMS = DxfIO::Constants::START_POINT_GROUP_NUMS
7
+ END_POINT_GROUP_NUMS = DxfIO::Constants::END_POINT_GROUP_NUMS
8
+ TYPES = %i(start end).freeze
9
+
10
+ attr_reader :x, :y, :type
11
+
12
+ def initialize(x, y, options = {})
13
+ @x, @y = x, y
14
+ @type = TYPES.include?(options[:type]) ? options[:type] : TYPES.first
15
+ end
16
+
17
+ def start?
18
+ @type == :start
19
+ end
20
+
21
+ def end?
22
+ @type == :end
23
+ end
24
+
25
+ def to_a
26
+ [@x, @y]
27
+ end
28
+
29
+ def to_h
30
+ {x: @x, y: @y}
31
+ end
32
+
33
+ def to_dxf_array
34
+ if start?
35
+ [{START_POINT_GROUP_NUMS.first => @x}, {START_POINT_GROUP_NUMS.last => @y}]
36
+ elsif end?
37
+ [{END_POINT_GROUP_NUMS.first => @x}, {END_POINT_GROUP_NUMS.last => @y}]
38
+ end
39
+ end
40
+
41
+ # eq operations
42
+
43
+ def ==(point)
44
+ to_a == point.to_a
45
+ end
46
+
47
+ # math operations
48
+
49
+ # unary
50
+
51
+ def -@
52
+ self.class.new(-@x, -@y, type: @type)
53
+ end
54
+
55
+ # binary
56
+
57
+ def +(point)
58
+ if point.is_a? self.class
59
+ self.class.new(@x + point.x,
60
+ @y + point.y,
61
+ type: @type == point.type ? @type : :start)
62
+ elsif point.is_a? Array
63
+ self.class.new(@x + point[0],
64
+ @y + point[1],
65
+ type: @type)
66
+ else
67
+ raise ArgumentError, 'point must be an Array or a DxfIO::Entity::Support::Point'
68
+ end
69
+ end
70
+
71
+ def -(point)
72
+ if point.is_a? self.class
73
+ self - point
74
+ elsif point.is_a? Array
75
+ self + point.map(&:-@)
76
+ else
77
+ raise ArgumentError, 'point must be an Array or a DxfIO::Entity::Support::Point'
78
+ end
79
+ end
80
+
81
+ def *(num)
82
+ if num.is_a? Numeric
83
+ self.class.new(@x * num,
84
+ @y * num,
85
+ type: @type)
86
+ else
87
+ raise ArgumentError, 'argument must be Numeric'
88
+ end
89
+ end
90
+
91
+ def /(num)
92
+ if num.is_a? Numeric
93
+ self.class.new(@x / num,
94
+ @y / num,
95
+ type: @type)
96
+ else
97
+ raise ArgumentError, 'argument must be Numeric'
98
+ end
99
+ end
100
+
101
+ # geometrical operation (supposed what point is a vector from zero)
102
+
103
+ def rotate_90
104
+ self.class.new(@y, -@x, type: @type)
105
+ end
106
+
107
+ def rotate_180
108
+ -self
109
+ end
110
+
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,14 @@
1
+ module DxfIO
2
+ module Entity
3
+ class Text < Other
4
+
5
+ protected
6
+
7
+ def initialize(groups, representation_hash)
8
+ @representation_hash = representation_hash
9
+ super(groups)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,139 @@
1
+ module DxfIO
2
+ # based on DXF AutoCAD 2008 documentation (http://images.autodesk.com/adsk/files/acad_dxf0.pdf)
3
+ class Reader
4
+
5
+ SECTIONS_LIST = DxfIO::Constants::SECTIONS_LIST
6
+ HEADER_NAME = DxfIO::Constants::HEADER_NAME
7
+
8
+ def initialize(options)
9
+ if options.is_a? String
10
+ @filename = options
11
+ elsif options.is_a? Hash
12
+ if options[:path].present?
13
+ @filename = options[:path]
14
+ else
15
+ raise ArgumentError, 'options must contain a :path key'
16
+ end
17
+ @encoding = options[:encoding] || 'Windows-1251'
18
+ end
19
+ end
20
+
21
+ class << self
22
+ def open(options)
23
+ self.new(options).tap do |reader_instance|
24
+ if block_given?
25
+ yield reader_instance
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ([HEADER_NAME] + SECTIONS_LIST).each do |method|
32
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
33
+ def #{method.downcase} # def classes
34
+ run['#{method}'] # run['CLASSES']
35
+ end # end
36
+ EOT
37
+ end
38
+
39
+ def run
40
+ @result_hash ||= parse
41
+ end
42
+
43
+ alias to_hash run
44
+ alias to_h run
45
+
46
+ def rerun
47
+ @result_hash = parse
48
+ end
49
+
50
+ def parse(filename = @filename, encoding = @encoding)
51
+ read_flag = "r:#{encoding}:UTF-8"
52
+ fp = File.open(filename, read_flag)
53
+ dxf = {HEADER_NAME => {}}
54
+ SECTIONS_LIST.each do |section_name|
55
+ dxf[section_name] = []
56
+ end
57
+ #
58
+ # main loop
59
+ #
60
+ begin
61
+ while true
62
+ c, v = read_codes(fp)
63
+ break if v == 'EOF'
64
+ if v == 'SECTION'
65
+ c, v = read_codes(fp)
66
+ if v == HEADER_NAME
67
+ hdr = dxf[HEADER_NAME]
68
+ while true
69
+ c, v = read_codes(fp)
70
+ break if v == 'ENDSEC' # or v == "BLOCKS" or v == "ENTITIES" or v == "EOF"
71
+ if c == 9
72
+ key = v
73
+ hdr[key] = {}
74
+ else
75
+ add_att(hdr[key], c, v)
76
+ end
77
+ end # while
78
+ elsif SECTIONS_LIST.include?(v)
79
+ section = dxf[v]
80
+ parse_entities(section, fp)
81
+ end
82
+ end # if in SECTION
83
+ end # main loop
84
+ ensure
85
+ fp.close unless fp.nil?
86
+ end
87
+
88
+ dxf
89
+ end
90
+
91
+ private
92
+
93
+ def parse_entities(section, fp)
94
+ while true
95
+ c, v = read_codes(fp)
96
+ break if v == 'ENDSEC' || v == 'EOF'
97
+ next if c == 999
98
+
99
+ if c == 0
100
+ section << [c => v]
101
+ else
102
+ section[-1] << {c => v}
103
+ end
104
+ end # while
105
+ end
106
+
107
+ def read_codes(fp)
108
+ c = fp.gets
109
+ return [0, 'EOF'] if c.nil?
110
+ v = fp.gets
111
+ return [0, 'EOF'] if v.nil?
112
+ c = c.to_i
113
+ v.strip!
114
+ v.upcase! if c == 0
115
+ case c
116
+ when 10..59, 110..119, 120..129, 130..139, 140..149, 140..147, 210..239, 460..469, 1010..1059
117
+ v = v.to_f
118
+ when 60..79, 90..99, 170..175, 280..289, 370..379, 380..389, 400..409, 420..429, 440..449, 450..459, 500..409, 1060..1070, 1071
119
+ v = v.to_i
120
+ end
121
+
122
+ [c, v]
123
+ end
124
+
125
+ def add_att(ent, code, value)
126
+ if ent[code].nil?
127
+ ent[code] = value
128
+ elsif ent[code].is_a? Array
129
+ ent[code] << value
130
+ else
131
+ t = ent[code]
132
+ ent[code] = []
133
+ ent[code] << t
134
+ ent[code] << value
135
+ end
136
+ end
137
+
138
+ end
139
+ end
@@ -0,0 +1,3 @@
1
+ module DxfIO
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,45 @@
1
+ module DxfIO
2
+ class Wrapper
3
+
4
+ SECTIONS_LIST = DxfIO::Constants::SECTIONS_LIST
5
+ HEADER_NAME = DxfIO::Constants::HEADER_NAME
6
+ ENTITIES_TYPE_NAME_VALUE_MAPPING = DxfIO::Constants::ENTITIES_TYPE_NAME_VALUE_MAPPING
7
+
8
+ def initialize(options)
9
+ if options.is_a? Hash
10
+ if options[:dxf_hash].nil?
11
+ @dxf_hash = options
12
+ else
13
+ @dxf_hash = options[:dxf_hash]
14
+ end
15
+ else
16
+ raise ArgumentError, 'options must be a Hash'
17
+ end
18
+ end
19
+
20
+ SECTIONS_LIST.each do |method|
21
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
22
+ def #{method.downcase} # def classes
23
+ fetch_entities('#{method}') # fetch_entities('CLASSES')
24
+ end # end
25
+ EOT
26
+ end
27
+
28
+ def fetch_entities(group_name)
29
+ @dxf_hash[group_name.upcase].collect do |entity_groups|
30
+ to_proper_class(DxfIO::Entity::Other.new(entity_groups))
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def to_proper_class(entity)
37
+ type_name = ENTITIES_TYPE_NAME_VALUE_MAPPING.invert[entity.type.upcase]
38
+ if type_name.nil? || !DxfIO::Entity.constants.include?(type_name.capitalize.to_sym)
39
+ entity
40
+ else
41
+ entity.public_send("to_#{type_name}")
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,215 @@
1
+ module DxfIO
2
+ class Writer
3
+ require 'fileutils'
4
+
5
+ SECTIONS_LIST = DxfIO::Constants::SECTIONS_LIST
6
+ HEADER_NAME = DxfIO::Constants::HEADER_NAME
7
+ STRATEGY = DxfIO::Constants::WRITER_STRATEGY
8
+
9
+ def initialize(options)
10
+ # TODO: replace instance variables to hash with options
11
+ # default options
12
+ @encoding = 'Windows-1251'
13
+ @delimiter = "\r\n"
14
+ @strategy = STRATEGY.first
15
+ @dxf_hash = {}
16
+
17
+ if options.is_a? String
18
+ @filename = options
19
+ elsif options.is_a? Hash
20
+ @dxf_hash = options[:dxf_hash] if options.has_key? :dxf_hash
21
+ @filename = options[:path] if options.has_key? :path
22
+ @encoding = options[:encoding] if options.has_key? :encoding
23
+ @delimiter = options[:delimiter] if options.has_key? :delimiter
24
+ @strategy = options[:strategy] if options.has_key? :strategy && STRATEGY.include?(options[:strategy])
25
+ else
26
+ raise ArgumentError, 'options must be String or Hash with :path key'
27
+ end
28
+ end
29
+
30
+ class << self
31
+ def open(options)
32
+ self.new(options).tap do |writer_instance|
33
+ if block_given?
34
+ yield writer_instance
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ # construct dxf content in memory and write all in file at once
41
+ def write_through_memory(dxf_hash = @dxf_hash)
42
+ file_stream do |fp|
43
+ fp.write(
44
+ file_content do
45
+ dxf_hash.inject('') do |sections_content, (section_name, section_content)|
46
+ sections_content << section_wrapper_content(section_name) do
47
+ if header_section?(section_name)
48
+ header_content(section_content)
49
+ else
50
+ other_section_content(section_content)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ )
56
+ end
57
+ end
58
+
59
+ # write dxf file directly on disk without temporary usage of memory for content
60
+ def write_through_disk(dxf_hash = @dxf_hash)
61
+ file_stream do |fp|
62
+ file_wrap(fp) do
63
+ dxf_hash.each_pair do |section_name, section_content|
64
+ section_wrap(fp, section_name) do
65
+ if header_section?(section_name)
66
+ header_wrap(fp, section_content)
67
+ else
68
+ other_section_wrap(fp, section_content)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def run(dxf_hash = @dxf_hash)
77
+ if @strategy == :memory
78
+ write_through_memory(dxf_hash)
79
+ elsif @strategy == :disk
80
+ write_through_disk(dxf_hash)
81
+ else
82
+ raise ArgumentError, ':strategy has invalid value; allowed only [:memory, :disk]'
83
+ end
84
+ end
85
+
86
+ alias write_hash run
87
+
88
+ private
89
+
90
+ # work with file
91
+
92
+ def file_stream(&block)
93
+ folder_path = @filename.split('/')[0..-2].join('/')
94
+ FileUtils.mkdir_p(folder_path)
95
+ fp = File.open(@filename, "w:#{@encoding}")
96
+ begin
97
+ block.call(fp)
98
+ ensure
99
+ fp.close unless fp.nil?
100
+ end
101
+ end
102
+
103
+ # helpers
104
+
105
+ def header_section?(section_name)
106
+ section_name.upcase == HEADER_NAME
107
+ end
108
+
109
+ # wrappers
110
+
111
+ # file format:
112
+ # ... file content ...
113
+ # 0
114
+ # EOF
115
+ def file_wrap(fp, &block)
116
+ file_end = "0#{@delimiter}EOF#{@delimiter}"
117
+
118
+ block.call
119
+ fp.write(file_end)
120
+ end
121
+
122
+ # section format:
123
+ # 0
124
+ # SECTION
125
+ # 2
126
+ # <section_name>
127
+ # ... section content ...
128
+ # 0
129
+ # ENDSEC
130
+ def section_wrap(fp, section_name, &block)
131
+ section_begin = "0#{@delimiter}SECTION#{@delimiter}2#{@delimiter}#{section_name.upcase}#{@delimiter}"
132
+ section_end = "0#{@delimiter}ENDSEC#{@delimiter}"
133
+
134
+ fp.write(section_begin)
135
+ block.call
136
+ fp.write(section_end)
137
+ end
138
+
139
+ # header format:
140
+ # 9
141
+ # $<variable>
142
+ # <group code>
143
+ # <value>
144
+ def header_wrap(fp, variables)
145
+ variables.each_pair do |variable, groups|
146
+ fp.write("9#{@delimiter}#{'$' if variable[0] != '$'}#{variable}#{@delimiter}")
147
+ groups.each_pair do |group_code, value|
148
+ fp.write("#{group_code}#{@delimiter}#{try_to_upcase_exponent(value)}#{@delimiter}")
149
+ end
150
+ end
151
+ end
152
+
153
+ # other section format:
154
+ # <group code>
155
+ # <value>
156
+ def other_section_wrap(fp, variables)
157
+ variables.each do |groups|
158
+ groups.each do |group|
159
+ fp.write("#{group.keys.first}#{@delimiter}#{try_to_upcase_exponent(group.values.first)}#{@delimiter}")
160
+ end
161
+ end
162
+ end
163
+
164
+ # content constructors
165
+
166
+ def file_content(&block)
167
+ file_end = "0#{@delimiter}EOF#{@delimiter}"
168
+
169
+ "#{block.call}#{file_end}"
170
+ end
171
+
172
+ def section_wrapper_content(section_name, &block)
173
+ section_begin = "0#{@delimiter}SECTION#{@delimiter}2#{@delimiter}#{section_name.upcase}#{@delimiter}"
174
+ section_end = "0#{@delimiter}ENDSEC#{@delimiter}"
175
+
176
+ "#{section_begin}#{block.call}#{section_end}"
177
+ end
178
+
179
+ def header_content(variables)
180
+ variables.inject('') do |result, (variable, groups)|
181
+ variable_part = "9#{@delimiter}#{'$' if variable[0] != '$'}#{variable}#{@delimiter}"
182
+ result << groups.inject(variable_part) do |group_result, (group_code, value)|
183
+ group_result << "#{group_code}#{@delimiter}#{try_to_upcase_exponent(value)}#{@delimiter}"
184
+ end
185
+ end
186
+ end
187
+
188
+ def other_section_content(variables)
189
+ variables.inject('') do |result, groups|
190
+ result << groups.inject('') do |group_result, group|
191
+ group_result << "#{group.keys.first}#{@delimiter}#{try_to_upcase_exponent(group.values.first)}#{@delimiter}"
192
+ end
193
+ end
194
+ end
195
+
196
+ # formatting
197
+
198
+ # replace exponential notation by decimal notation and remove redundant zeros in the end
199
+ def try_to_decimal_fraction(num)
200
+ if num.is_a? Float
201
+ ('%.25f' % num).to_s.sub(/0+$/, '')
202
+ else
203
+ num
204
+ end
205
+ end
206
+
207
+ def try_to_upcase_exponent(num)
208
+ if num.is_a? Float
209
+ num.to_s.sub('e', 'E')
210
+ else
211
+ num
212
+ end
213
+ end
214
+ end
215
+ end