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}