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.ByteArrayInputStream;
20  import java.io.ByteArrayOutputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.UncheckedIOException;
24  import java.nio.charset.Charset;
25  import java.nio.charset.StandardCharsets;
26  import java.util.Arrays;
27  
28  import org.apache.commons.geometry.core.GeometryTestUtils;
29  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
30  import org.apache.commons.geometry.euclidean.threed.Vector3D;
31  import org.junit.jupiter.api.Assertions;
32  import org.junit.jupiter.api.Test;
33  
34  class BinaryStlFacetDefinitionReaderTest {
35  
36      private static final double TEST_EPS = 1e-10;
37  
38      private static final String LONG_STRING =
39              "A long string that will most definitely exceed the 80 byte length of the binary STL file format header.";
40  
41      private final ByteArrayOutputStream out = new ByteArrayOutputStream();
42  
43      @Test
44      void testHeader_zeros() throws IOException {
45          // arrange
46          final byte[] bytes = new byte[StlConstants.BINARY_HEADER_BYTES + 4];
47          out.write(bytes);
48  
49          final byte[] expectedHeader = new byte[StlConstants.BINARY_HEADER_BYTES];
50          System.arraycopy(bytes, 0, expectedHeader, 0, expectedHeader.length);
51  
52          try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
53              // act/assert
54              Assertions.assertArrayEquals(expectedHeader, reader.getHeader().array());
55              Assertions.assertEquals(0L, reader.getNumTriangles());
56  
57              Assertions.assertNull(reader.readFacet());
58          }
59      }
60  
61      @Test
62      void testHeader_ones() throws IOException {
63          // arrange
64          final byte[] bytes = new byte[StlConstants.BINARY_HEADER_BYTES + 4];
65          Arrays.fill(bytes, (byte) -1);
66          out.write(bytes);
67  
68          final byte[] expectedHeader = new byte[StlConstants.BINARY_HEADER_BYTES];
69          System.arraycopy(bytes, 0, expectedHeader, 0, expectedHeader.length);
70  
71          try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
72              // act/assert
73              Assertions.assertArrayEquals(expectedHeader, reader.getHeader().array());
74              Assertions.assertEquals(0xffffffffL, reader.getNumTriangles());
75          }
76      }
77  
78      @Test
79      void testHeader_shortString() throws IOException {
80          // arrange
81          out.write(createHeader("Hello!", StandardCharsets.UTF_8, 1));
82  
83          try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
84              // act/assert
85              Assertions.assertEquals("Hello!", reader.getHeaderAsString());
86              Assertions.assertEquals(1L, reader.getNumTriangles());
87          }
88      }
89  
90      @Test
91      void testHeader_longString() throws IOException {
92          // arrange
93          out.write(createHeader(LONG_STRING, StandardCharsets.UTF_8, 8736720));
94  
95          try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
96              // act/assert
97              Assertions.assertEquals(LONG_STRING.substring(0, StlConstants.BINARY_HEADER_BYTES),
98                      reader.getHeaderAsString());
99              Assertions.assertEquals(8736720L, reader.getNumTriangles());
100         }
101     }
102 
103     @Test
104     void testHeader_longString_givenCharset() throws IOException {
105         // arrange
106         out.write(createHeader(LONG_STRING, StandardCharsets.UTF_16, 256));
107 
108         try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
109             // act/assert
110             Assertions.assertEquals("A long string that will most definitely",
111                     reader.getHeaderAsString(StandardCharsets.UTF_16));
112             Assertions.assertEquals(256L, reader.getNumTriangles());
113         }
114     }
115 
116     @Test
117     void testGetHeader_noData() throws IOException {
118         // arrange
119         out.write(new byte[32]);
120 
121         try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
122             // act/assert
123             GeometryTestUtils.assertThrowsWithMessage(
124                     () -> reader.getHeader(),
125                     IllegalStateException.class, "Failed to read STL header: data not available");
126         }
127     }
128 
129     @Test
130     void testGetHeader_noTriangleCount() throws IOException {
131         // arrange
132         out.write(new byte[StlConstants.BINARY_HEADER_BYTES]);
133 
134         try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
135             // act/assert
136             GeometryTestUtils.assertThrowsWithMessage(
137                     () -> reader.getHeader(),
138                     IllegalStateException.class, "Failed to read STL triangle count: data not available");
139         }
140     }
141 
142     @Test
143     void testGetHeader_ioException() throws IOException {
144         // arrange
145         final InputStream failIn = new InputStream() {
146             @Override
147             public int read() throws IOException {
148                 throw new IOException("read");
149             }
150         };
151 
152         try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(failIn)) {
153             // act/assert
154             GeometryTestUtils.assertThrowsWithMessage(
155                     () -> reader.getHeader(),
156                     UncheckedIOException.class, "IOException: read");
157         }
158     }
159 
160     @Test
161     void testReadFacet_noData() throws IOException {
162         // arrange
163         out.write(createHeader(1));
164 
165         // act/assert
166         try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
167             // act/assert
168             GeometryTestUtils.assertThrowsWithMessage(
169                     () -> reader.readFacet(),
170                     IllegalStateException.class, "Failed to read STL triangle at index 0: data not available");
171         }
172     }
173 
174     @Test
175     void testReadFacet() throws IOException {
176         // arrange
177         out.write(createHeader(2));
178 
179         out.write(getBytes(Vector3D.of(1, 2, 3)));
180         out.write(getBytes(Vector3D.of(4, 5, 6)));
181         out.write(getBytes(Vector3D.of(7, 8, 9)));
182         out.write(getBytes(Vector3D.of(10, 11, 12)));
183         out.write(getBytes((short) 1));
184 
185         out.write(getBytes(Vector3D.of(-1, -2, -3)));
186         out.write(getBytes(Vector3D.of(-4, -5, -6)));
187         out.write(getBytes(Vector3D.of(-7, -8, -9)));
188         out.write(getBytes(Vector3D.of(-10, -11, -12)));
189         out.write(getBytes((short) 65535));
190 
191         try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
192             Assertions.assertEquals(2, reader.getNumTriangles());
193 
194             final BinaryStlFacetDefinition facet1 = reader.readFacet();
195 
196             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), facet1.getNormal(), TEST_EPS);
197             Assertions.assertEquals(3, facet1.getVertices().size());
198             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4, 5, 6), facet1.getVertices().get(0), TEST_EPS);
199             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(7, 8, 9), facet1.getVertices().get(1), TEST_EPS);
200             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(10, 11, 12), facet1.getVertices().get(2), TEST_EPS);
201 
202             Assertions.assertEquals(1, facet1.getAttributeValue());
203 
204             final BinaryStlFacetDefinition facet2 = reader.readFacet();
205 
206             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-1, -2, -3), facet2.getNormal(), TEST_EPS);
207             Assertions.assertEquals(3, facet2.getVertices().size());
208             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-4, -5, -6), facet2.getVertices().get(0), TEST_EPS);
209             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-7, -8, -9), facet2.getVertices().get(1), TEST_EPS);
210             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(-10, -11, -12), facet2.getVertices().get(2), TEST_EPS);
211 
212             Assertions.assertEquals(65535, facet2.getAttributeValue());
213 
214             Assertions.assertNull(reader.readFacet());
215         }
216     }
217 
218     @Test
219     void testReadFacet_stopsWhenTriangleCountReached() throws IOException {
220         // arrange
221         out.write(createHeader(1));
222 
223         out.write(getBytes(Vector3D.of(1, 2, 3)));
224         out.write(getBytes(Vector3D.of(4, 5, 6)));
225         out.write(getBytes(Vector3D.of(7, 8, 9)));
226         out.write(getBytes(Vector3D.of(10, 11, 12)));
227         out.write(getBytes((short) 1));
228 
229         out.write(getBytes(Vector3D.of(-1, -2, -3)));
230         out.write(getBytes(Vector3D.of(-4, -5, -6)));
231         out.write(getBytes(Vector3D.of(-7, -8, -9)));
232         out.write(getBytes(Vector3D.of(-10, -11, -12)));
233         out.write(getBytes((short) 65535));
234 
235         try (BinaryStlFacetDefinitionReader reader = new BinaryStlFacetDefinitionReader(getInput())) {
236             Assertions.assertEquals(1, reader.getNumTriangles());
237 
238             final BinaryStlFacetDefinition facet = reader.readFacet();
239 
240             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(1, 2, 3), facet.getNormal(), TEST_EPS);
241             Assertions.assertEquals(3, facet.getVertices().size());
242             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(4, 5, 6), facet.getVertices().get(0), TEST_EPS);
243             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(7, 8, 9), facet.getVertices().get(1), TEST_EPS);
244             EuclideanTestUtils.assertCoordinatesEqual(Vector3D.of(10, 11, 12), facet.getVertices().get(2), TEST_EPS);
245 
246             Assertions.assertEquals(1, facet.getAttributeValue());
247 
248             Assertions.assertNull(reader.readFacet());
249         }
250     }
251 
252     private ByteArrayInputStream getInput() {
253         return new ByteArrayInputStream(out.toByteArray());
254     }
255 
256     private static byte[] createHeader(final int count) {
257         return createHeader("", StandardCharsets.UTF_8, count);
258     }
259 
260     private static byte[] createHeader(final String str, final Charset charset, final int count) {
261         final byte[] result = new byte[StlConstants.BINARY_HEADER_BYTES + 4];
262 
263         final byte[] strBytes = str.getBytes(charset);
264         System.arraycopy(strBytes, 0, result, 0, Math.min(StlConstants.BINARY_HEADER_BYTES, strBytes.length));
265 
266         final byte[] countBytes = getBytes(count);
267         System.arraycopy(countBytes, 0, result, StlConstants.BINARY_HEADER_BYTES, countBytes.length);
268 
269         return result;
270     }
271 
272     private static byte[] getBytes(final Vector3D vec) {
273         final byte[] result = new byte[Float.BYTES * 3];
274         int offset = 0;
275 
276         System.arraycopy(getBytes((float) vec.getX()), 0, result, offset, Float.BYTES);
277         offset += Float.BYTES;
278 
279         System.arraycopy(getBytes((float) vec.getY()), 0, result, offset, Float.BYTES);
280         offset += Float.BYTES;
281 
282         System.arraycopy(getBytes((float) vec.getZ()), 0, result, offset, Float.BYTES);
283 
284         return result;
285     }
286 
287     private static byte[] getBytes(final float value) {
288         return getBytes(Float.floatToIntBits(value));
289     }
290 
291     private static byte[] getBytes(final int value) {
292         final byte[] bytes = new byte[4];
293         bytes[0] = (byte) (value & 0x000000ff);
294         bytes[1] = (byte) ((value & 0x0000ff00) >> 8);
295         bytes[2] = (byte) ((value & 0x00ff0000) >> 16);
296         bytes[3] = (byte) ((value & 0xff000000) >> 24);
297 
298         return bytes;
299     }
300 
301     private static byte[] getBytes(final short value) {
302         final byte[] bytes = new byte[2];
303         bytes[0] = (byte) (value & 0x00ff);
304         bytes[1] = (byte) ((value & 0xff00) >> 8);
305 
306         return bytes;
307     }
308 }