001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.io.euclidean.threed.stl;
018
019import java.io.InputStream;
020import java.nio.ByteBuffer;
021import java.nio.charset.Charset;
022import java.util.Arrays;
023
024import org.apache.commons.geometry.euclidean.threed.Vector3D;
025import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
026import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
027
028/** Class used to read the binary form of the STL file format.
029 * @see <a href="https://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL">Binary STL</a>
030 */
031public class BinaryStlFacetDefinitionReader implements FacetDefinitionReader {
032
033    /** Input stream to read from. */
034    private final InputStream in;
035
036    /** Buffer used to read triangle definitions. */
037    private final ByteBuffer triangleBuffer = StlUtils.byteBuffer(StlConstants.BINARY_TRIANGLE_BYTES);
038
039    /** Header content. */
040    private ByteBuffer header = StlUtils.byteBuffer(StlConstants.BINARY_HEADER_BYTES);
041
042    /** Total number of triangles declared to be present in the input. */
043    private long triangleTotal;
044
045    /** Number of triangles read so far. */
046    private long trianglesRead;
047
048    /** True when the header content has been read. */
049    private boolean hasReadHeader;
050
051    /** Construct a new instance that reads from the given input stream.
052     * @param in input stream to read from.
053     */
054    public BinaryStlFacetDefinitionReader(final InputStream in) {
055        this.in = in;
056    }
057
058    /** Get a read-only buffer containing the 80 bytes of the STL header. The header does not
059     * include the 4-byte value indicating the total number of triangles in the STL file.
060     * @return the STL header content
061     * @throws java.io.UncheckedIOException if an I/O error occurs
062     */
063    public ByteBuffer getHeader() {
064        beginRead();
065        return ByteBuffer.wrap(header.array().clone());
066    }
067
068    /** Return the header content as a string decoded using the UTF-8 charset. Control
069     * characters (such as '\0') are not included in the result.
070     * @return the header content decoded as a UTF-8 string
071     * @throws java.io.UncheckedIOException if an I/O error occurs
072     */
073    public String getHeaderAsString() {
074        return getHeaderAsString(StlConstants.DEFAULT_CHARSET);
075    }
076
077    /** Return the header content as a string decoded using the given charset. Control
078     * characters (such as '\0') are not included in the result.
079     * @param charset charset to decode the header with
080     * @return the header content decoded as a string
081     * @throws java.io.UncheckedIOException if an I/O error occurs
082     */
083    public String getHeaderAsString(final Charset charset) {
084        // decode the entire header as characters in the given charset
085        final String raw = charset.decode(getHeader()).toString();
086
087        // strip out any control characters, such as '\0'
088        final StringBuilder sb = new StringBuilder();
089        for (char c : raw.toCharArray()) {
090            if (!Character.isISOControl(c)) {
091                sb.append(c);
092            }
093        }
094
095        return sb.toString();
096    }
097
098    /** Get the total number of triangles (i.e. facets) declared to be present in the input.
099     * @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}