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 }