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 }