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 }