View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.geometry.io.euclidean.threed.stl;
18  
19  import java.io.InputStream;
20  import java.nio.ByteBuffer;
21  import java.nio.charset.Charset;
22  import java.util.Arrays;
23  
24  import org.apache.commons.geometry.euclidean.threed.Vector3D;
25  import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
26  import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
27  
28  /** Class used to read the binary form of the STL file format.
29   * @see <a href="https://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL">Binary STL</a>
30   */
31  public class BinaryStlFacetDefinitionReader implements FacetDefinitionReader {
32  
33      /** Input stream to read from. */
34      private final InputStream in;
35  
36      /** Buffer used to read triangle definitions. */
37      private final ByteBuffer triangleBuffer = StlUtils.byteBuffer(StlConstants.BINARY_TRIANGLE_BYTES);
38  
39      /** Header content. */
40      private ByteBuffer header = StlUtils.byteBuffer(StlConstants.BINARY_HEADER_BYTES);
41  
42      /** Total number of triangles declared to be present in the input. */
43      private long triangleTotal;
44  
45      /** Number of triangles read so far. */
46      private long trianglesRead;
47  
48      /** True when the header content has been read. */
49      private boolean hasReadHeader;
50  
51      /** Construct a new instance that reads from the given input stream.
52       * @param in input stream to read from.
53       */
54      public BinaryStlFacetDefinitionReader(final InputStream in) {
55          this.in = in;
56      }
57  
58      /** Get a read-only buffer containing the 80 bytes of the STL header. The header does not
59       * include the 4-byte value indicating the total number of triangles in the STL file.
60       * @return the STL header content
61       * @throws java.io.UncheckedIOException if an I/O error occurs
62       */
63      public ByteBuffer getHeader() {
64          beginRead();
65          return ByteBuffer.wrap(header.array().clone());
66      }
67  
68      /** Return the header content as a string decoded using the UTF-8 charset. Control
69       * characters (such as '\0') are not included in the result.
70       * @return the header content decoded as a UTF-8 string
71       * @throws java.io.UncheckedIOException if an I/O error occurs
72       */
73      public String getHeaderAsString() {
74          return getHeaderAsString(StlConstants.DEFAULT_CHARSET);
75      }
76  
77      /** Return the header content as a string decoded using the given charset. Control
78       * characters (such as '\0') are not included in the result.
79       * @param charset charset to decode the header with
80       * @return the header content decoded as a string
81       * @throws java.io.UncheckedIOException if an I/O error occurs
82       */
83      public String getHeaderAsString(final Charset charset) {
84          // decode the entire header as characters in the given charset
85          final String raw = charset.decode(getHeader()).toString();
86  
87          // strip out any control characters, such as '\0'
88          final StringBuilder sb = new StringBuilder();
89          for (char c : raw.toCharArray()) {
90              if (!Character.isISOControl(c)) {
91                  sb.append(c);
92              }
93          }
94  
95          return sb.toString();
96      }
97  
98      /** Get the total number of triangles (i.e. facets) declared to be present in the input.
99       * @return total number of triangle in the input
100      * @throws java.io.UncheckedIOException if an I/O error occurs
101      */
102     public long getNumTriangles() {
103         beginRead();
104         return triangleTotal;
105     }
106 
107     /** {@inheritDoc} */
108     @Override
109     public BinaryStlFacetDefinition readFacet() {
110         beginRead();
111 
112         BinaryStlFacetDefinition facet = null;
113 
114         if (trianglesRead < triangleTotal) {
115             facet = readFacetInternal();
116 
117             ++trianglesRead;
118         }
119 
120         return facet;
121     }
122 
123     /** {@inheritDoc} */
124     @Override
125     public void close() {
126         GeometryIOUtils.closeUnchecked(in);
127     }
128 
129     /** Read the file header content and triangle count.
130      * @throws IllegalStateException is a parse error occurs
131      * @throws java.io.UncheckedIOException if an I/O error occurs
132      */
133     private void beginRead() {
134         if (!hasReadHeader) {
135             // read header content
136             final int headerBytesRead = GeometryIOUtils.applyAsIntUnchecked(in::read, header.array());
137             if (headerBytesRead < StlConstants.BINARY_HEADER_BYTES) {
138                 throw dataNotAvailable("header");
139             }
140 
141             header.rewind();
142 
143             // read the triangle total
144             final ByteBuffer triangleBuf = StlUtils.byteBuffer(Integer.BYTES);
145 
146             if (fill(triangleBuf) < triangleBuf.capacity()) {
147                 throw dataNotAvailable("triangle count");
148             }
149 
150             triangleTotal = Integer.toUnsignedLong(triangleBuf.getInt());
151 
152             hasReadHeader = true;
153         }
154     }
155 
156     /** Internal method to read a single facet from the input.
157      * @return facet read from the input
158      */
159     private BinaryStlFacetDefinition readFacetInternal() {
160         if (fill(triangleBuffer) < triangleBuffer.capacity()) {
161             throw dataNotAvailable("triangle at index " + trianglesRead);
162         }
163 
164         final Vector3D normal = readVector(triangleBuffer);
165         final Vector3D p1 = readVector(triangleBuffer);
166         final Vector3D p2 = readVector(triangleBuffer);
167         final Vector3D p3 = readVector(triangleBuffer);
168 
169         final int attr = Short.toUnsignedInt(triangleBuffer.getShort());
170 
171         return new BinaryStlFacetDefinition(Arrays.asList(p1, p2, p3), normal, attr);
172     }
173 
174     /** Fill the buffer with data from the input stream. The buffer is then flipped and
175      * made ready for reading.
176      * @param buf buffer to fill
177      * @return number of bytes read
178      * @throws java.io.UncheckedIOException if an I/O error occurs
179      */
180     private int fill(final ByteBuffer buf) {
181         int read = GeometryIOUtils.applyAsIntUnchecked(in::read, buf.array());
182         buf.rewind();
183 
184         return read;
185     }
186 
187     /** Read a vector from the given byte buffer.
188      * @param buf buffer to read from
189      * @return vector containing the next 3 double values from the
190      *      given buffer
191      */
192     private Vector3D readVector(final ByteBuffer buf) {
193         final double x = buf.getFloat();
194         final double y = buf.getFloat();
195         final double z = buf.getFloat();
196 
197         return Vector3D.of(x, y, z);
198     }
199 
200     /** Return an exception stating that data is not available for the file
201      * component with the given name.
202      * @param name name of the file component missing data
203      * @return exception instance
204      */
205     private static IllegalStateException dataNotAvailable(final String name) {
206         return GeometryIOUtils.parseError("Failed to read STL " + name + ": data not available");
207     }
208 }