0032441: Draw Harness - command readstl doesn't print error message on failure
[occt.git] / src / RWStl / RWStl_Reader.cxx
1 // Created: 2016-05-01
2 // Author: Andrey Betenev
3 // Copyright: Open CASCADE 2016
4 //
5 // This file is part of Open CASCADE Technology software library.
6 //
7 // This library is free software; you can redistribute it and/or modify it under
8 // the terms of the GNU Lesser General Public License version 2.1 as published
9 // by the Free Software Foundation, with special exception defined in the file
10 // OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT
11 // distribution for complete text of the license and disclaimer of any warranty.
12 //
13 // Alternatively, this file may be used under the terms of Open CASCADE
14 // commercial license or contractual agreement.
15
16 #include <RWStl_Reader.hxx>
17
18 #include <gp_XY.hxx>
19 #include <Message.hxx>
20 #include <Message_Messenger.hxx>
21 #include <Message_ProgressScope.hxx>
22 #include <NCollection_DataMap.hxx>
23 #include <NCollection_IncAllocator.hxx>
24 #include <FSD_BinaryFile.hxx>
25 #include <OSD_OpenFile.hxx>
26 #include <OSD_Timer.hxx>
27 #include <Precision.hxx>
28 #include <Standard_CLocaleSentry.hxx>
29
30 #include <algorithm>
31 #include <limits>
32
33 IMPLEMENT_STANDARD_RTTIEXT(RWStl_Reader, Standard_Transient)
34
35 namespace
36 {
37   // Binary STL sizes
38   static const size_t THE_STL_HEADER_SIZE   = 84;
39   static const size_t THE_STL_SIZEOF_FACET  = 50;
40   static const size_t THE_STL_MIN_FILE_SIZE = THE_STL_HEADER_SIZE + THE_STL_SIZEOF_FACET;
41
42   // The length of buffer to read (in bytes)
43   static const size_t THE_BUFFER_SIZE = 1024;
44
45   //! Auxiliary tool for merging nodes during STL reading.
46   class MergeNodeTool
47   {
48   public:
49
50     //! Constructor
51     MergeNodeTool (RWStl_Reader* theReader)
52     : myReader (theReader),
53       myMap (1024, new NCollection_IncAllocator (1024 * 1024))
54     {
55     }
56
57     //! Add new triangle
58     int AddNode (double theX, double theY, double theZ)
59     {
60       // use existing node if found at the same point
61       gp_XYZ aPnt (theX, theY, theZ);
62
63       Standard_Integer anIndex = -1;
64       if (myMap.Find (aPnt, anIndex))
65       {
66         return anIndex;
67       }
68
69       anIndex = myReader->AddNode (aPnt);
70       myMap.Bind (aPnt, anIndex);
71       return anIndex;
72     }
73
74   public:
75
76     static Standard_Boolean IsEqual (const gp_XYZ& thePnt1, const gp_XYZ& thePnt2)
77     {
78       return (thePnt1 - thePnt2).SquareModulus() < Precision::SquareConfusion();
79     }
80
81     //! Computes a hash code for the point, in the range [1, theUpperBound]
82     //! @param thePoint the point which hash code is to be computed
83     //! @param theUpperBound the upper bound of the range a computing hash code must be within
84     //! @return a computed hash code, in the range [1, theUpperBound]
85     static Standard_Integer HashCode (const gp_XYZ& thePoint, const Standard_Integer theUpperBound)
86     {
87       return ::HashCode (thePoint.X() * M_LN10 + thePoint.Y() * M_PI + thePoint.Z() * M_E, theUpperBound);
88     }
89
90   private:
91     RWStl_Reader* myReader;
92     NCollection_DataMap<gp_XYZ, Standard_Integer, MergeNodeTool> myMap;
93   };
94
95   //! Read a Little Endian 32 bits float
96   inline static float readStlFloat (const char* theData)
97   {
98   #if OCCT_BINARY_FILE_DO_INVERSE
99     // on big-endian platform, map values byte-per-byte
100     union
101     {
102       uint32_t i;
103       float    f;
104     } bidargum;
105     bidargum.i  =  theData[0] & 0xFF;
106     bidargum.i |= (theData[1] & 0xFF) << 0x08;
107     bidargum.i |= (theData[2] & 0xFF) << 0x10;
108     bidargum.i |= (theData[3] & 0xFF) << 0x18;
109     return bidargum.f;
110   #else
111     // on little-endian platform, use plain cast
112     return *reinterpret_cast<const float*>(theData);
113   #endif
114   }
115
116   //! Read a Little Endian 32 bits float
117   inline static gp_XYZ readStlFloatVec3 (const char* theData)
118   {
119     return gp_XYZ (readStlFloat (theData),
120                    readStlFloat (theData + sizeof(float)),
121                    readStlFloat (theData + sizeof(float) * 2));
122   }
123
124 }
125
126 //==============================================================================
127 //function : Read
128 //purpose  :
129 //==============================================================================
130
131 Standard_Boolean RWStl_Reader::Read (const char* theFile,
132                                      const Message_ProgressRange& theProgress)
133 {
134   std::filebuf aBuf;
135   OSD_OpenStream (aBuf, theFile, std::ios::in | std::ios::binary);
136   if (!aBuf.is_open())
137   {
138     Message::SendFail (TCollection_AsciiString("Error: file '") + theFile + "' is not found");
139     return Standard_False;
140   }
141
142   Standard_IStream aStream (&aBuf);
143
144   // get length of file to feed progress indicator in Ascii mode
145   aStream.seekg (0, aStream.end);
146   std::streampos theEnd = aStream.tellg();
147   aStream.seekg (0, aStream.beg);
148
149   // binary STL files cannot be shorter than 134 bytes 
150   // (80 bytes header + 4 bytes facet count + 50 bytes for one facet);
151   // thus assume files shorter than 134 as Ascii without probing
152   // (probing may bring stream to fail state if EOF is reached)
153   bool isAscii = ((size_t)theEnd < THE_STL_MIN_FILE_SIZE || IsAscii (aStream, true));
154
155   Standard_ReadLineBuffer aBuffer (THE_BUFFER_SIZE);
156
157   // Note: here we are trying to handle rare but realistic case of
158   // STL files which are composed of several STL data blocks
159   // running translation in cycle.
160   // For this reason use infinite (logarithmic) progress scale,
161   // but in special mode so that the first cycle will take ~ 70% of it
162   Message_ProgressScope aPS (theProgress, NULL, 1, true);
163   while (aStream.good())
164   {
165     if (isAscii)
166     {
167       if (!ReadAscii (aStream, aBuffer, theEnd, aPS.Next(2)))
168       {
169         break;
170       }
171     }
172     else
173     {
174       if (!ReadBinary (aStream, aPS.Next(2)))
175       {
176         break;
177       }
178     }
179     aStream >> std::ws; // skip any white spaces
180   }
181   return ! aStream.fail();
182 }
183
184 //==============================================================================
185 //function : IsAscii
186 //purpose  :
187 //==============================================================================
188
189 Standard_Boolean RWStl_Reader::IsAscii (Standard_IStream& theStream,
190                                         const bool        isSeekgAvailable)
191 {
192   // read first 134 bytes to detect file format
193   char aBuffer[THE_STL_MIN_FILE_SIZE];
194   std::streamsize aNbRead = theStream.read (aBuffer, THE_STL_MIN_FILE_SIZE).gcount();
195   if (! theStream)
196   {
197     Message::SendFail ("Error: Cannot read file");
198     return true;
199   }
200
201   if (isSeekgAvailable)
202   {
203     // get back to the beginning
204     theStream.seekg(0, theStream.beg);
205   }
206   else
207   {
208     // put back the read symbols
209     for (std::streamsize aByteIter = aNbRead; aByteIter > 0; --aByteIter)
210     {
211       theStream.unget();
212     }
213   }
214
215   // if file is shorter than size of binary file with 1 facet, it must be ascii
216   if (aNbRead < std::streamsize(THE_STL_MIN_FILE_SIZE))
217   {
218     return true;
219   }
220
221   // otherwise, detect binary format by presence of non-ascii symbols in first 128 bytes
222   // (note that binary STL file may start with the same bytes "solid " as Ascii one)
223   for (Standard_Integer aByteIter = 0; aByteIter < aNbRead; ++aByteIter)
224   {
225     if ((unsigned char )aBuffer[aByteIter] > (unsigned char )'~')
226     {
227       return false;
228     }
229   }
230   return true;
231 }
232
233 // adapted from Standard_CString.cxx
234 #ifdef __APPLE__
235   // There are a lot of *_l functions available on Mac OS X - we use them
236   #define SAVE_TL()
237 #elif defined(_MSC_VER)
238   // MSVCRT has equivalents with slightly different syntax
239   #define SAVE_TL()
240   #define sscanf_l(theBuffer, theLocale, theFormat, ...) _sscanf_s_l(theBuffer, theFormat, theLocale, __VA_ARGS__)
241 #else
242   // glibc provides only limited xlocale implementation:
243   // strtod_l/strtol_l/strtoll_l functions with explicitly specified locale
244   // and newlocale/uselocale/freelocale to switch locale within current thread only.
245   // So we switch to C locale temporarily
246   #define SAVE_TL() Standard_CLocaleSentry aLocaleSentry;
247   #define sscanf_l(theBuffer, theLocale, theFormat, ...) sscanf(theBuffer, theFormat, __VA_ARGS__)
248 #endif
249
250 // Macro to get 64-bit position of the file from std::streampos
251 #if defined(_MSC_VER) && _MSC_VER < 1700
252   // In MSVC 2010, cast of std::streampos to 64-bit int is implemented incorrectly;
253   // work-around (relevant for files larger than 4 GB) is to use internal function seekpos(). 
254   // Since MSVC 15.8, seekpos() is deprecated and is said to always return 0.
255   #define GETPOS(aPos) aPos.seekpos()
256 #else
257   #define GETPOS(aPos) ((int64_t)aPos)
258 #endif
259
260 static inline bool str_starts_with (const char* theStr, const char* theWord, int theN)
261 {
262   while (isspace (*theStr) && *theStr != '\0') theStr++;
263   return !strncasecmp (theStr, theWord, theN);
264 }
265
266 static bool ReadVertex (const char* theStr, double& theX, double& theY, double& theZ)
267 {
268   const char *aStr = theStr;
269
270   // skip 'vertex'
271   while (isspace ((unsigned char)*aStr) || isalpha ((unsigned char)*aStr)) 
272     ++aStr;
273
274   // read values
275   char *aEnd;
276   theX = Strtod (aStr, &aEnd);
277   theY = Strtod (aStr = aEnd, &aEnd);
278   theZ = Strtod (aStr = aEnd, &aEnd);
279
280   return aEnd != aStr;
281 }
282
283 //==============================================================================
284 //function : ReadAscii
285 //purpose  :
286 //==============================================================================
287 Standard_Boolean RWStl_Reader::ReadAscii (Standard_IStream& theStream,
288                                           Standard_ReadLineBuffer& theBuffer,
289                                           const std::streampos theUntilPos,
290                                           const Message_ProgressRange& theProgress)
291 {
292   // use method seekpos() to get true 64-bit offset to enable
293   // handling of large files (VS 2010 64-bit)
294   const int64_t aStartPos = GETPOS(theStream.tellg());
295   size_t aLineLen = 0;
296   const char* aLine;
297
298   // skip header "solid ..."
299   aLine = theBuffer.ReadLine (theStream, aLineLen);
300   if (aLine == NULL)
301   {
302     Message::SendFail ("Error: premature end of file");
303     return false;
304   }
305
306   MergeNodeTool aMergeTool (this);
307   Standard_CLocaleSentry::clocale_t aLocale = Standard_CLocaleSentry::GetCLocale();
308   (void)aLocale; // to avoid warning on GCC where it is actually not used
309   SAVE_TL() // for GCC only, set C locale globally
310
311   // report progress every 1 MiB of read data
312   const int aStepB = 1024 * 1024;
313   const Standard_Integer aNbSteps = 1 + Standard_Integer((GETPOS(theUntilPos) - aStartPos) / aStepB);
314   Message_ProgressScope aPS (theProgress, "Reading text STL file", aNbSteps);
315   int64_t aProgressPos = aStartPos + aStepB;
316   int aNbLine = 1;
317
318   while (aPS.More())
319   {
320     if (GETPOS(theStream.tellg()) > aProgressPos)
321     {
322       aPS.Next();
323       aProgressPos += aStepB;
324     }
325
326     aLine = theBuffer.ReadLine (theStream, aLineLen); // "facet normal nx ny nz"
327     if (aLine == NULL)
328     {
329       Message::SendFail ("Error: premature end of file");
330       return false;
331     }
332     if (str_starts_with (aLine, "endsolid", 8))
333     {
334       // end of STL code
335       break;
336     }
337     if (!str_starts_with (aLine, "facet", 5))
338     {
339       Message::SendFail (TCollection_AsciiString ("Error: unexpected format of facet at line ") + (aNbLine + 1));
340       return false;
341     }
342
343     aLine = theBuffer.ReadLine (theStream, aLineLen);  // "outer loop"
344     if (aLine == NULL || !str_starts_with (aLine, "outer", 5))
345     {
346       Message::SendFail (TCollection_AsciiString ("Error: unexpected format of facet at line ") + (aNbLine + 1));
347       return false;
348     }
349
350     gp_XYZ aVertex[3];
351     Standard_Boolean isEOF = false;
352     for (Standard_Integer i = 0; i < 3; i++)
353     {
354       aLine = theBuffer.ReadLine (theStream, aLineLen);
355       if (aLine == NULL)
356       {
357         isEOF = true;
358         break;
359       }
360       gp_XYZ aReadVertex;
361       if (!ReadVertex (aLine, aReadVertex.ChangeCoord (1), aReadVertex.ChangeCoord (2), aReadVertex.ChangeCoord (3)))
362       {
363         Message::SendFail (TCollection_AsciiString ("Error: cannot read vertex coordinates at line ") + aNbLine);
364         return false;
365       }
366       aVertex[i] = aReadVertex;
367     }
368
369     // stop reading if end of file is reached;
370     // note that well-formatted file never ends by the vertex line
371     if (isEOF)
372     {
373       break;
374     }
375
376     aNbLine += 5;
377
378     // add triangle
379     int n1 = aMergeTool.AddNode (aVertex[0].X(), aVertex[0].Y(), aVertex[0].Z());
380     int n2 = aMergeTool.AddNode (aVertex[1].X(), aVertex[1].Y(), aVertex[1].Z());
381     int n3 = aMergeTool.AddNode (aVertex[2].X(), aVertex[2].Y(), aVertex[2].Z());
382     if (n1 != n2 && n2 != n3 && n3 != n1)
383     {
384       AddTriangle (n1, n2, n3);
385     }
386
387     theBuffer.ReadLine (theStream, aLineLen); // skip "endloop"
388     theBuffer.ReadLine (theStream, aLineLen); // skip "endfacet"
389
390     aNbLine += 2;
391   }
392
393   return aPS.More();
394 }
395
396 //==============================================================================
397 //function : readStlBinary
398 //purpose  :
399 //==============================================================================
400
401 Standard_Boolean RWStl_Reader::ReadBinary (Standard_IStream& theStream,
402                                            const Message_ProgressRange& theProgress)
403 {
404 /*
405   // the size of the file (minus the header size)
406   // must be a multiple of SIZEOF_STL_FACET
407   if ((theFileLen - THE_STL_HEADER_SIZE) % THE_STL_SIZEOF_FACET != 0
408    || (theFileLen < THE_STL_MIN_FILE_SIZE))
409   {
410     Message::SendFail ("Error: Corrupted binary STL file (inconsistent file size)");
411     return Standard_False;
412   }
413   const Standard_Integer  aNbFacets = Standard_Integer((theFileLen - THE_STL_HEADER_SIZE) / THE_STL_SIZEOF_FACET);
414 */
415
416   // read file header at first
417   char aHeader[THE_STL_HEADER_SIZE + 1];
418   if (theStream.read (aHeader, THE_STL_HEADER_SIZE).gcount() != std::streamsize(THE_STL_HEADER_SIZE))
419   {
420     Message::SendFail ("Error: Corrupted binary STL file");
421     return false;
422   }
423
424   // number of facets is stored as 32-bit integer at position 80
425   const Standard_Integer aNbFacets = *(int32_t*)(aHeader + 80);
426
427   MergeNodeTool aMergeTool (this);
428
429   // don't trust the number of triangles which is coded in the file
430   // sometimes it is wrong, and with this technique we don't need to swap endians for integer
431   Message_ProgressScope  aPS (theProgress, "Reading binary STL file", aNbFacets);
432   Standard_Integer        aNbRead = 0;
433
434   // allocate buffer for 80 triangles
435   const int THE_CHUNK_NBFACETS = 80;
436   char aBuffer[THE_STL_SIZEOF_FACET * THE_CHUNK_NBFACETS];
437
438   // normal + 3 nodes + 2 extra bytes
439   const size_t aVec3Size    = sizeof(float) * 3;
440   const size_t aFaceDataLen = aVec3Size * 4 + 2;
441   const char*  aBufferPtr   = aBuffer;
442   Standard_Integer aNbFacesInBuffer = 0;
443   for (Standard_Integer aNbFacetRead = 0; aNbFacetRead < aNbFacets && aPS.More();
444        ++aNbFacetRead, ++aNbRead, --aNbFacesInBuffer, aBufferPtr += aFaceDataLen, aPS.Next())
445   {
446     // read more data
447     if (aNbFacesInBuffer <= 0)
448     {
449       aNbFacesInBuffer = Min (THE_CHUNK_NBFACETS, aNbFacets - aNbFacetRead);
450       const std::streamsize aDataToRead = aNbFacesInBuffer * aFaceDataLen;
451       if (theStream.read (aBuffer, aDataToRead).gcount() != aDataToRead)
452       {
453         Message::SendFail ("Error: binary STL read failed");
454         return false;
455       }
456       aBufferPtr = aBuffer;
457     }
458
459     // get points from buffer
460 //    readStlFloatVec3 (aBufferPtr); // skip normal
461     gp_XYZ aP1 = readStlFloatVec3 (aBufferPtr + aVec3Size);
462     gp_XYZ aP2 = readStlFloatVec3 (aBufferPtr + aVec3Size * 2);
463     gp_XYZ aP3 = readStlFloatVec3 (aBufferPtr + aVec3Size * 3);
464
465     // add triangle
466     int n1 = aMergeTool.AddNode (aP1.X(), aP1.Y(), aP1.Z());
467     int n2 = aMergeTool.AddNode (aP2.X(), aP2.Y(), aP2.Z());
468     int n3 = aMergeTool.AddNode (aP3.X(), aP3.Y(), aP3.Z());
469     if (n1 != n2 && n2 != n3 && n3 != n1)
470     {
471       AddTriangle (n1, n2, n3);
472     }
473   }
474
475   return aPS.More();
476 }