BinaryStlFacetDefinitionReader.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.io.euclidean.threed.stl;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;
/** Class used to read the binary form of the STL file format.
* @see <a href="https://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL">Binary STL</a>
*/
public class BinaryStlFacetDefinitionReader implements FacetDefinitionReader {
/** Input stream to read from. */
private final InputStream in;
/** Buffer used to read triangle definitions. */
private final ByteBuffer triangleBuffer = StlUtils.byteBuffer(StlConstants.BINARY_TRIANGLE_BYTES);
/** Header content. */
private ByteBuffer header = StlUtils.byteBuffer(StlConstants.BINARY_HEADER_BYTES);
/** Total number of triangles declared to be present in the input. */
private long triangleTotal;
/** Number of triangles read so far. */
private long trianglesRead;
/** True when the header content has been read. */
private boolean hasReadHeader;
/** Construct a new instance that reads from the given input stream.
* @param in input stream to read from.
*/
public BinaryStlFacetDefinitionReader(final InputStream in) {
this.in = in;
}
/** Get a read-only buffer containing the 80 bytes of the STL header. The header does not
* include the 4-byte value indicating the total number of triangles in the STL file.
* @return the STL header content
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public ByteBuffer getHeader() {
beginRead();
return ByteBuffer.wrap(header.array().clone());
}
/** Return the header content as a string decoded using the UTF-8 charset. Control
* characters (such as '\0') are not included in the result.
* @return the header content decoded as a UTF-8 string
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public String getHeaderAsString() {
return getHeaderAsString(StlConstants.DEFAULT_CHARSET);
}
/** Return the header content as a string decoded using the given charset. Control
* characters (such as '\0') are not included in the result.
* @param charset charset to decode the header with
* @return the header content decoded as a string
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public String getHeaderAsString(final Charset charset) {
// decode the entire header as characters in the given charset
final String raw = charset.decode(getHeader()).toString();
// strip out any control characters, such as '\0'
final StringBuilder sb = new StringBuilder();
for (char c : raw.toCharArray()) {
if (!Character.isISOControl(c)) {
sb.append(c);
}
}
return sb.toString();
}
/** Get the total number of triangles (i.e. facets) declared to be present in the input.
* @return total number of triangle in the input
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
public long getNumTriangles() {
beginRead();
return triangleTotal;
}
/** {@inheritDoc} */
@Override
public BinaryStlFacetDefinition readFacet() {
beginRead();
BinaryStlFacetDefinition facet = null;
if (trianglesRead < triangleTotal) {
facet = readFacetInternal();
++trianglesRead;
}
return facet;
}
/** {@inheritDoc} */
@Override
public void close() {
GeometryIOUtils.closeUnchecked(in);
}
/** Read the file header content and triangle count.
* @throws IllegalStateException is a parse error occurs
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
private void beginRead() {
if (!hasReadHeader) {
// read header content
final int headerBytesRead = GeometryIOUtils.applyAsIntUnchecked(in::read, header.array());
if (headerBytesRead < StlConstants.BINARY_HEADER_BYTES) {
throw dataNotAvailable("header");
}
header.rewind();
// read the triangle total
final ByteBuffer triangleBuf = StlUtils.byteBuffer(Integer.BYTES);
if (fill(triangleBuf) < triangleBuf.capacity()) {
throw dataNotAvailable("triangle count");
}
triangleTotal = Integer.toUnsignedLong(triangleBuf.getInt());
hasReadHeader = true;
}
}
/** Internal method to read a single facet from the input.
* @return facet read from the input
*/
private BinaryStlFacetDefinition readFacetInternal() {
if (fill(triangleBuffer) < triangleBuffer.capacity()) {
throw dataNotAvailable("triangle at index " + trianglesRead);
}
final Vector3D normal = readVector(triangleBuffer);
final Vector3D p1 = readVector(triangleBuffer);
final Vector3D p2 = readVector(triangleBuffer);
final Vector3D p3 = readVector(triangleBuffer);
final int attr = Short.toUnsignedInt(triangleBuffer.getShort());
return new BinaryStlFacetDefinition(Arrays.asList(p1, p2, p3), normal, attr);
}
/** Fill the buffer with data from the input stream. The buffer is then flipped and
* made ready for reading.
* @param buf buffer to fill
* @return number of bytes read
* @throws java.io.UncheckedIOException if an I/O error occurs
*/
private int fill(final ByteBuffer buf) {
int read = GeometryIOUtils.applyAsIntUnchecked(in::read, buf.array());
buf.rewind();
return read;
}
/** Read a vector from the given byte buffer.
* @param buf buffer to read from
* @return vector containing the next 3 double values from the
* given buffer
*/
private Vector3D readVector(final ByteBuffer buf) {
final double x = buf.getFloat();
final double y = buf.getFloat();
final double z = buf.getFloat();
return Vector3D.of(x, y, z);
}
/** Return an exception stating that data is not available for the file
* component with the given name.
* @param name name of the file component missing data
* @return exception instance
*/
private static IllegalStateException dataNotAvailable(final String name) {
return GeometryIOUtils.parseError("Failed to read STL " + name + ": data not available");
}
}