Coverage for tatlin/lib/model/stl/parser.py: 89%

171 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-20 05:56 +0000

1# -*- coding: utf-8 -*- 

2# Copyright (C) 2011 Denis Kobozev 

3# 

4# This program is free software; you can redistribute it and/or modify 

5# it under the terms of the GNU General Public License as published by 

6# the Free Software Foundation; either version 2 of the License, or 

7# (at your option) any later version. 

8# 

9# This program is distributed in the hope that it will be useful, 

10# but WITHOUT ANY WARRANTY; without even the implied warranty of 

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

12# GNU General Public License for more details. 

13# 

14# You should have received a copy of the GNU General Public License 

15# along with this program; if not, write to the Free Software Foundation, 

16# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

17 

18 

19""" 

20Parser for STL (stereolithography) files. 

21""" 

22import struct 

23import time 

24import logging 

25import math 

26from io import StringIO 

27 

28 

29ENCODING = "utf-8" 

30 

31 

32class StlParseError(Exception): 

33 pass 

34 

35 

36class InvalidTokenError(StlParseError): 

37 def __init__(self, line_no, msg): 

38 full_msg = "parse error on line %d: %s" % (line_no, msg) 

39 StlParseError.__init__(self, full_msg) 

40 

41 

42class ParseEOF(StlParseError): 

43 pass 

44 

45 

46class StlAsciiParser(object): 

47 """ 

48 Parse for ASCII STL files. 

49 

50 Points of interest are: 

51 * create normal in _facet() method 

52 * create vertex in _vertex() method 

53 * create facet in _endfacet() method 

54 

55 The rest is boring parser stuff. 

56 """ 

57 

58 def __init__(self): 

59 self.line_count = 0 

60 self.line_no = 0 

61 self.tokenized_peek_line = None 

62 

63 def load(self, stl): 

64 if hasattr(stl, "read"): 

65 for line in stl: 

66 self.line_count += 1 

67 stl.seek(0) 

68 else: 

69 stl = stl.split("\n") 

70 self.line_count = len(stl) 

71 

72 self.stl = iter(stl) 

73 

74 def readline(self): 

75 line = next(self.stl) 

76 if line == "": 

77 raise ParseEOF 

78 return line.decode(ENCODING) 

79 

80 def next_line(self): 

81 next_line = self.peek_line() 

82 

83 self.tokenized_peek_line = None # force peak line read on next call 

84 self.line_no += 1 

85 

86 return next_line 

87 

88 def peek_line(self): 

89 if self.tokenized_peek_line is None: 

90 while True: 

91 line = self.readline() 

92 self.tokenized_peek_line = self._tokenize(line) 

93 if len(self.tokenized_peek_line) > 0: 

94 break 

95 

96 return self.tokenized_peek_line 

97 

98 def _tokenize(self, line): 

99 line = line.strip().split() 

100 return line 

101 

102 def parse(self, callback=None): 

103 """ 

104 Parse the file into a tuple of normal and facet lists. 

105 """ 

106 t_start = time.time() 

107 

108 self.callback = callback 

109 self.callback_every = self.line_count // 50 # every 2 percent 

110 self.callback_next = self.callback_every 

111 

112 self._solid() 

113 

114 if self.callback: 

115 self.callback(self.line_no, self.line_count) 

116 

117 t_end = time.time() 

118 logging.info("Parsed STL ASCII file in %.2f seconds" % (t_end - t_start)) 

119 

120 return self.facet_list, self.normal_list 

121 

122 def _solid(self): 

123 line = self.next_line() 

124 if line[0] != "solid": 

125 raise InvalidTokenError( 

126 self.line_no, 'expected "%s", got "%s"' % ("solid", line[0]) 

127 ) 

128 

129 self._facets() 

130 self._endsolid() 

131 

132 def _endsolid(self): 

133 line = self.next_line() 

134 if line[0] != "endsolid": 

135 raise InvalidTokenError( 

136 self.line_no, 'expected "%s", got "%s"' % ("endsolid", line[0]) 

137 ) 

138 

139 def _facets(self): 

140 self.facet_list = [] 

141 self.normal_list = [] 

142 peek = self.peek_line() 

143 while peek[0] != "endsolid": 

144 self._facet() 

145 peek = self.peek_line() 

146 

147 def _facet(self): 

148 line = self.next_line() 

149 if line[0] != "facet": 

150 raise InvalidTokenError( 

151 self.line_no, 'expected "%s", got "%s"' % ("facet", line[0]) 

152 ) 

153 

154 if line[1] == "normal": 

155 self.facet_normal = [float(line[2]), float(line[3]), float(line[4])] 

156 else: 

157 raise InvalidTokenError( 

158 self.line_no, 'expected "%s", got "%s"' % ("normal", line[1]) 

159 ) 

160 

161 self._outer_loop() 

162 self._endfacet() 

163 

164 def _endfacet(self): 

165 line = self.next_line() 

166 if line[0] != "endfacet": 

167 raise InvalidTokenError( 

168 self.line_no, 'expected "%s", got "%s"' % ("endfacet", line[0]) 

169 ) 

170 

171 self.facet_list.extend(self.vertex_list) 

172 self.normal_list.extend([self.facet_normal] * len(self.vertex_list)) 

173 

174 if self.callback and self.line_no >= self.callback_next: 

175 self.callback_next += self.callback_every 

176 self.callback(self.line_no, self.line_count) 

177 

178 def _outer_loop(self): 

179 line = self.next_line() 

180 if " ".join(line) != "outer loop": 

181 raise InvalidTokenError( 

182 self.line_no, 'expected "%s", got "%s"' % ("outer loop", " ".join(line)) 

183 ) 

184 

185 self._vertices() 

186 self._endloop() 

187 

188 def _endloop(self): 

189 line = self.next_line() 

190 if line[0] != "endloop": 

191 raise InvalidTokenError( 

192 self.line_no, 'expected "%s", got "%s"' % ("endloop", line[0]) 

193 ) 

194 

195 def _vertices(self): 

196 self.vertex_list = [] 

197 peek = self.peek_line() 

198 while peek[0] != "endloop": 

199 self._vertex() 

200 peek = self.peek_line() 

201 

202 def _vertex(self): 

203 line = self.next_line() 

204 if line[0] != "vertex": 

205 raise InvalidTokenError( 

206 self.line_no, 'expected "%s", got "%s"' % ("vertex", line[0]) 

207 ) 

208 vertex = [float(line[1]), float(line[2]), float(line[3])] 

209 self.vertex_list.append(vertex) 

210 

211 

212class StlBinaryParser(object): 

213 """ 

214 Read data from a binary STL file. 

215 """ 

216 

217 HEADER_LEN = 80 

218 FACET_COUNT_LEN = 4 # one 32-bit unsigned int 

219 FACET_LEN = 50 # twelve 32-bit floats + one 16-bit short unsigned int 

220 

221 def load(self, stl): 

222 if not hasattr(stl, "read"): 

223 stl = StringIO(stl) 

224 self.stl = stl 

225 

226 def parse(self, callback=None): 

227 """ 

228 Parse the file into a tuple of normal and facet lists. 

229 """ 

230 t_start = time.time() 

231 

232 normal_list = [] 

233 facet_list = [] 

234 

235 self._skip_header(self.stl) 

236 fcount = self._facet_count(self.stl) 

237 callback_every = max(1, int(math.floor(fcount / 100))) 

238 for facet_idx in range(fcount): 

239 vertices, normal = self._parse_facet(self.stl) 

240 facet_list.extend(vertices) 

241 normal_list.extend([normal] * len(vertices)) # one normal per vertex 

242 

243 if callback and (facet_idx + 1) % callback_every == 0: 

244 callback(facet_idx + 1, fcount) 

245 

246 if callback: 

247 callback(facet_idx + 1, fcount) 

248 

249 t_end = time.time() 

250 logging.info("Parsed STL binary file in %.2f seconds" % (t_end - t_start)) 

251 

252 return facet_list, normal_list 

253 

254 def _skip_header(self, fp): 

255 fp.seek(self.HEADER_LEN) 

256 

257 def _facet_count(self, fp): 

258 raw = fp.read(self.FACET_COUNT_LEN) 

259 try: 

260 (count,) = struct.unpack("<I", raw) 

261 return count 

262 except struct.error: 

263 raise StlParseError("Error unpacking binary STL data") 

264 

265 def _parse_facet(self, fp): 

266 raw = fp.read(self.FACET_LEN) 

267 try: 

268 vertex_data = struct.unpack("<ffffffffffffH", raw) 

269 normal = [vertex_data[0], vertex_data[1], vertex_data[2]] 

270 vertices = [] 

271 for i in range(3, 12, 3): 

272 vertices.append( 

273 [vertex_data[i], vertex_data[i + 1], vertex_data[i + 2]] 

274 ) 

275 # ignore the attribute byte count... 

276 return vertices, normal 

277 except struct.error: 

278 raise StlParseError("Error unpacking binary STL data") 

279 

280 

281def is_stl_ascii(fp): 

282 """ 

283 Guess whether file with the given name is plain ASCII STL file. 

284 """ 

285 first_line = fp.readline().strip() 

286 is_ascii = first_line.startswith(b"solid") 

287 fp.seek(0) 

288 return is_ascii 

289 

290 

291def StlParser(fp): 

292 """ 

293 STL parser that handles both ASCII and binary formats. 

294 """ 

295 parser = StlAsciiParser if is_stl_ascii(fp) else StlBinaryParser 

296 return parser()