Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Verifying that an STL file is ASCII or binary

Tags:

c++

qt

After reading the specs on the STL file format, I want to write a few tests to ensure that a file is, in fact, a valid binary or ASCII file.

An ASCII-based STL file can be determined by finding the text "solid" at byte 0, followed by a space (hex value \x20), and then an optional text string, followed by a newline.

A binary STL file has a reserved 80-byte header, followed by a 4-byte unsigned integer (NumberOfTriangles), and then 50 bytes of data for each of the NumberOfTriangles facets specified.

Each triangle facet is 50 bytes in length: 12 single-precision (4-byte) floats followed by an unsigned short (2-byte) unsigned integer.

If a binary file is exactly 84 + NumberOfTriangles*50 bytes long, it can be typically be considered to be a valid binary file.

Unfortunately, binary files can contain the text "solid" starting at byte 0 in the contents of the 80-byte header. Therefore, a test for only that keyword cannot positively rule that a file is ASCII or binary.

This is what I have so far:

STL_STATUS getStlFileFormat(const QString &path)
{
    // Each facet contains:
    //  - Normals: 3 floats (4 bytes)
    //  - Vertices: 3x floats (4 bytes each, 12 bytes total)
    //  - AttributeCount: 1 short (2 bytes)
    // Total: 50 bytes per facet
    const size_t facetSize = 3*sizeof(float_t) + 3*3*sizeof(float_t) + sizeof(uint16_t);

    QFile file(path);
    if (!file.open(QIODevice::ReadOnly))
    {
        qDebug("\n\tUnable to open \"%s\"", qPrintable(path));
        return STL_INVALID;
    }

    QFileInfo fileInfo(path);
    size_t fileSize = fileInfo.size();

    if (fileSize < 84)
    {
        // 80-byte header + 4-byte "number of triangles" marker
        qDebug("\n\tThe STL file is not long enough (%u bytes).", uint(fileSize));
        return STL_INVALID;
    }

    // Look for text "solid" in first 5 bytes, indicating the possibility that this is an ASCII STL format.
    QByteArray fiveBytes = file.read(5);

    // Header is from bytes 0-79; numTriangleBytes starts at byte offset 80.
    if (!file.seek(80))
    {
        qDebug("\n\tCannot seek to the 80th byte (after the header)");
        return STL_INVALID;
    }

    // Read the number of triangles, uint32_t (4 bytes), little-endian
    QByteArray nTrianglesBytes = file.read(4);
    file.close();

    uint32_t nTriangles = *((uint32_t*)nTrianglesBytes.data());

    // Verify that file size equals the sum of header + nTriangles value + all triangles
    size_t targetSize = 84 + nTriangles * facetSize;
    if (fileSize == targetSize)
    {
        return STL_BINARY;
    }
    else if (fiveBytes.contains("solid"))
    {
        return STL_ASCII;
    }
    else
    {
        return STL_INVALID;
    }
}

So far, this has worked for me, but I'm worried that a plain ASCII file's 80th byte could contain some ASCII characters that, when translated to a uint32_t, could actually equal the length of the file (very unlikely, but not impossible).

Are there additional steps that would prove useful in validating whether I can be "absolutely sure" that a file is either ASCII or binary?

UPDATE:

Following the advice of @Powerswitch and @RemyLebeau, I'm doing further tests for keywords. This is what I've got now:

STL_STATUS getStlFileFormat(const QString &path)
{
    // Each facet contains:
    //  - Normals: 3 floats (4 bytes)
    //  - Vertices: 3x floats (4 byte each, 12 bytes total)
    //  - AttributeCount: 1 short (2 bytes)
    // Total: 50 bytes per facet
    const size_t facetSize = 3*sizeof(float_t) + 3*3*sizeof(float_t) + sizeof(uint16_t);

    QFile file(path);
    bool canFileBeOpened = file.open(QIODevice::ReadOnly);
    if (!canFileBeOpened)
    {
        qDebug("\n\tUnable to open \"%s\"", qPrintable(path));
        return STL_INVALID;
    }

    QFileInfo fileInfo(path);
    size_t fileSize = fileInfo.size();

    // The minimum size of an empty ASCII file is 15 bytes.
    if (fileSize < 15)
    {
        // "solid " and "endsolid " markers for an ASCII file
        qDebug("\n\tThe STL file is not long enough (%u bytes).", uint(fileSize));
        file.close();
        return STL_INVALID;
    }

    // Binary files should never start with "solid ", but just in case, check for ASCII, and if not valid
    // then check for binary...

    // Look for text "solid " in first 6 bytes, indicating the possibility that this is an ASCII STL format.
    QByteArray sixBytes = file.read(6);
    if (sixBytes.startsWith("solid "))
    {
        QString line;
        QTextStream in(&file);
        while (!in.atEnd())
        {
            line = in.readLine();
            if (line.contains("endsolid"))
            {
                file.close();
                return STL_ASCII;
            }
        }
    }

    // Wasn't an ASCII file. Reset and check for binary.
    if (!file.reset())
    {
        qDebug("\n\tCannot seek to the 0th byte (before the header)");
        file.close();
        return STL_INVALID;
    }

    // 80-byte header + 4-byte "number of triangles" for a binary file
    if (fileSize < 84)
    {
        qDebug("\n\tThe STL file is not long enough (%u bytes).", uint(fileSize));
        file.close();
        return STL_INVALID;
    }

    // Header is from bytes 0-79; numTriangleBytes starts at byte offset 80.
    if (!file.seek(80))
    {
        qDebug("\n\tCannot seek to the 80th byte (after the header)");
        file.close();
        return STL_INVALID;
    }

    // Read the number of triangles, uint32_t (4 bytes), little-endian
    QByteArray nTrianglesBytes = file.read(4);
    if (nTrianglesBytes.size() != 4)
    {
        qDebug("\n\tCannot read the number of triangles (after the header)");
        file.close();
        return STL_INVALID;
    }

    uint32_t nTriangles = *((uint32_t*)nTrianglesBytes.data());

    // Verify that file size equals the sum of header + nTriangles value + all triangles
    if (fileSize == (84 + (nTriangles * facetSize)))
    {
        file.close();
        return STL_BINARY;
    }

    return STL_INVALID;
}

It appears to handle more edge cases, and I've attempted to write it in a way that handles extremely large (a few gigabyte) STL files gracefully without requiring the ENTIRE file to be loaded into memory at once for it to scan for the "endsolid" text.

Feel free to provide any feedback and suggestions (especially for people in the future looking for solutions).

like image 363
OnlineCop Avatar asked Oct 03 '14 00:10

OnlineCop


People also ask

Are STL files binary?

The binary representation of the STL (STereoLithography) file format is a simple, openly documented format for describing the surface of an object as a triangular mesh. Since its introduction in the late 1980s, STL has become a de facto standard for rapid prototyping and 3D printing.

How are STL files encoded?

The STL file format provides two different ways of storing information about the triangular facets that tile the object surface. These are called ASCII encoding and binary encoding. In both formats, the information of each triangle is stored as: The coordinates of the vertices.


1 Answers

If the file does not begin with "solid ", and if the file size is exactly 84 + (numTriangles * 50) bytes, where numTriangles is read from offset 80, then the file is binary.

If the file size is at least 15 bytes (absolute minimum for an ASCII file with no triangles) and begins with "solid ", read the name that follows it until a line break is reached. Check if the next line either begins with "facet " or is "endsolid [name]" (no other value is allowed). If "facet ", seek to the end of the file and make sure it ends with a line that says "endsolid [name]". If all of these are true, the file is ASCII.

Treat any other combination as invalid.

So, something like this:

STL_STATUS getStlFileFormat(const QString &path)
{
    QFile file(path);
    if (!file.open(QIODevice::ReadOnly))
    {
        qDebug("\n\tUnable to open \"%s\"", qPrintable(path));
        return STL_INVALID;
    }

    QFileInfo fileInfo(path);
    size_t fileSize = fileInfo.size();

    // Look for text "solid " in first 6 bytes, indicating the possibility that this is an ASCII STL format.

    if (fileSize < 15)
    {
        // "solid " and "endsolid " markers for an ASCII file
        qDebug("\n\tThe STL file is not long enough (%u bytes).", uint(fileSize));
        return STL_INVALID;
    }

    // binary files should never start with "solid ", but
    // just in case, check for ASCII, and if not valid then
    // check for binary...

    QByteArray sixBytes = file.read(6);
    if (sixBytes.startsWith("solid "))
    {
        QByteArray name = file.readLine();
        QByteArray endLine = name.prepend("endsolid ");

        QByteArray nextLine = file.readLine();
        if (line.startsWith("facet "))
        {
            // TODO: seek to the end of the file, read the last line,
            // and make sure it is "endsolid [name]"...
            /*
            line = ...;
            if (!line.startsWith(endLine))
                return STL_INVALID;
            */
            return STL_ASCII;
        }
        if (line.startsWith(endLine))
            return STL_ASCII;

        // reset and check for binary...
        if (!file.reset())
        {
            qDebug("\n\tCannot seek to the 0th byte (before the header)");
            return STL_INVALID;
        }
    }

    if (fileSize < 84)
    {
        // 80-byte header + 4-byte "number of triangles" for a binary file
        qDebug("\n\tThe STL file is not long enough (%u bytes).", uint(fileSize));
        return STL_INVALID;
    }

    // Header is from bytes 0-79; numTriangleBytes starts at byte offset 80.
    if (!file.seek(80))
    {
        qDebug("\n\tCannot seek to the 80th byte (after the header)");
        return STL_INVALID;
    }

    // Read the number of triangles, uint32_t (4 bytes), little-endian
    QByteArray nTrianglesBytes = file.read(4);
    if (nTrianglesBytes.size() != 4)
    {
        qDebug("\n\tCannot read the number of triangles (after the header)");
        return STL_INVALID;
    }            

    uint32_t nTriangles = *((uint32_t*)nTrianglesBytes.data());

    // Verify that file size equals the sum of header + nTriangles value + all triangles
    if (fileSize == (84 + (nTriangles * 50)))
        return STL_BINARY;

    return STL_INVALID;
}
like image 101
Remy Lebeau Avatar answered Oct 07 '22 07:10

Remy Lebeau