BinaryStlFacetDefinitionReader.java

  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. import java.io.InputStream;
  19. import java.nio.ByteBuffer;
  20. import java.nio.charset.Charset;
  21. import java.util.Arrays;

  22. import org.apache.commons.geometry.euclidean.threed.Vector3D;
  23. import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
  24. import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;

  25. /** Class used to read the binary form of the STL file format.
  26.  * @see <a href="https://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL">Binary STL</a>
  27.  */
  28. public class BinaryStlFacetDefinitionReader implements FacetDefinitionReader {

  29.     /** Input stream to read from. */
  30.     private final InputStream in;

  31.     /** Buffer used to read triangle definitions. */
  32.     private final ByteBuffer triangleBuffer = StlUtils.byteBuffer(StlConstants.BINARY_TRIANGLE_BYTES);

  33.     /** Header content. */
  34.     private ByteBuffer header = StlUtils.byteBuffer(StlConstants.BINARY_HEADER_BYTES);

  35.     /** Total number of triangles declared to be present in the input. */
  36.     private long triangleTotal;

  37.     /** Number of triangles read so far. */
  38.     private long trianglesRead;

  39.     /** True when the header content has been read. */
  40.     private boolean hasReadHeader;

  41.     /** Construct a new instance that reads from the given input stream.
  42.      * @param in input stream to read from.
  43.      */
  44.     public BinaryStlFacetDefinitionReader(final InputStream in) {
  45.         this.in = in;
  46.     }

  47.     /** Get a read-only buffer containing the 80 bytes of the STL header. The header does not
  48.      * include the 4-byte value indicating the total number of triangles in the STL file.
  49.      * @return the STL header content
  50.      * @throws java.io.UncheckedIOException if an I/O error occurs
  51.      */
  52.     public ByteBuffer getHeader() {
  53.         beginRead();
  54.         return ByteBuffer.wrap(header.array().clone());
  55.     }

  56.     /** Return the header content as a string decoded using the UTF-8 charset. Control
  57.      * characters (such as '\0') are not included in the result.
  58.      * @return the header content decoded as a UTF-8 string
  59.      * @throws java.io.UncheckedIOException if an I/O error occurs
  60.      */
  61.     public String getHeaderAsString() {
  62.         return getHeaderAsString(StlConstants.DEFAULT_CHARSET);
  63.     }

  64.     /** Return the header content as a string decoded using the given charset. Control
  65.      * characters (such as '\0') are not included in the result.
  66.      * @param charset charset to decode the header with
  67.      * @return the header content decoded as a string
  68.      * @throws java.io.UncheckedIOException if an I/O error occurs
  69.      */
  70.     public String getHeaderAsString(final Charset charset) {
  71.         // decode the entire header as characters in the given charset
  72.         final String raw = charset.decode(getHeader()).toString();

  73.         // strip out any control characters, such as '\0'
  74.         final StringBuilder sb = new StringBuilder();
  75.         for (char c : raw.toCharArray()) {
  76.             if (!Character.isISOControl(c)) {
  77.                 sb.append(c);
  78.             }
  79.         }

  80.         return sb.toString();
  81.     }

  82.     /** Get the total number of triangles (i.e. facets) declared to be present in the input.
  83.      * @return total number of triangle in the input
  84.      * @throws java.io.UncheckedIOException if an I/O error occurs
  85.      */
  86.     public long getNumTriangles() {
  87.         beginRead();
  88.         return triangleTotal;
  89.     }

  90.     /** {@inheritDoc} */
  91.     @Override
  92.     public BinaryStlFacetDefinition readFacet() {
  93.         beginRead();

  94.         BinaryStlFacetDefinition facet = null;

  95.         if (trianglesRead < triangleTotal) {
  96.             facet = readFacetInternal();

  97.             ++trianglesRead;
  98.         }

  99.         return facet;
  100.     }

  101.     /** {@inheritDoc} */
  102.     @Override
  103.     public void close() {
  104.         GeometryIOUtils.closeUnchecked(in);
  105.     }

  106.     /** Read the file header content and triangle count.
  107.      * @throws IllegalStateException is a parse error occurs
  108.      * @throws java.io.UncheckedIOException if an I/O error occurs
  109.      */
  110.     private void beginRead() {
  111.         if (!hasReadHeader) {
  112.             // read header content
  113.             final int headerBytesRead = GeometryIOUtils.applyAsIntUnchecked(in::read, header.array());
  114.             if (headerBytesRead < StlConstants.BINARY_HEADER_BYTES) {
  115.                 throw dataNotAvailable("header");
  116.             }

  117.             header.rewind();

  118.             // read the triangle total
  119.             final ByteBuffer triangleBuf = StlUtils.byteBuffer(Integer.BYTES);

  120.             if (fill(triangleBuf) < triangleBuf.capacity()) {
  121.                 throw dataNotAvailable("triangle count");
  122.             }

  123.             triangleTotal = Integer.toUnsignedLong(triangleBuf.getInt());

  124.             hasReadHeader = true;
  125.         }
  126.     }

  127.     /** Internal method to read a single facet from the input.
  128.      * @return facet read from the input
  129.      */
  130.     private BinaryStlFacetDefinition readFacetInternal() {
  131.         if (fill(triangleBuffer) < triangleBuffer.capacity()) {
  132.             throw dataNotAvailable("triangle at index " + trianglesRead);
  133.         }

  134.         final Vector3D normal = readVector(triangleBuffer);
  135.         final Vector3D p1 = readVector(triangleBuffer);
  136.         final Vector3D p2 = readVector(triangleBuffer);
  137.         final Vector3D p3 = readVector(triangleBuffer);

  138.         final int attr = Short.toUnsignedInt(triangleBuffer.getShort());

  139.         return new BinaryStlFacetDefinition(Arrays.asList(p1, p2, p3), normal, attr);
  140.     }

  141.     /** Fill the buffer with data from the input stream. The buffer is then flipped and
  142.      * made ready for reading.
  143.      * @param buf buffer to fill
  144.      * @return number of bytes read
  145.      * @throws java.io.UncheckedIOException if an I/O error occurs
  146.      */
  147.     private int fill(final ByteBuffer buf) {
  148.         int read = GeometryIOUtils.applyAsIntUnchecked(in::read, buf.array());
  149.         buf.rewind();

  150.         return read;
  151.     }

  152.     /** Read a vector from the given byte buffer.
  153.      * @param buf buffer to read from
  154.      * @return vector containing the next 3 double values from the
  155.      *      given buffer
  156.      */
  157.     private Vector3D readVector(final ByteBuffer buf) {
  158.         final double x = buf.getFloat();
  159.         final double y = buf.getFloat();
  160.         final double z = buf.getFloat();

  161.         return Vector3D.of(x, y, z);
  162.     }

  163.     /** Return an exception stating that data is not available for the file
  164.      * component with the given name.
  165.      * @param name name of the file component missing data
  166.      * @return exception instance
  167.      */
  168.     private static IllegalStateException dataNotAvailable(final String name) {
  169.         return GeometryIOUtils.parseError("Failed to read STL " + name + ": data not available");
  170.     }
  171. }