【问题标题】:Verifying that an STL file is ASCII or binary验证 STL 文件是 ASCII 还是二进制
【发布时间】:2018-03-15 05:34:19
【问题描述】:

在阅读了关于 STL 文件格式的specs 之后,我想写一些测试来确保一个文件实际上是一个有效的二进制或 ASCII 文件。

可以通过在字节 0 处找到文本“solid”,然后是空格(十六进制值 \x20),然后是可选的文本字符串来确定基于 ASCII 的 STL 文件,然后换行符。

二进制 STL 文件有一个保留的 80 字节头,后跟一个 4 字节的无符号整数 (NumberOfTriangles),然后50 个字节的数据,用于指定的每个 NumberOfTriangles 个方面。

每个三角形面的长度为 50 字节:12 个单精度(4 字节)浮点数,后跟一个无符号短(2 字节)无符号整数。

如果一个二进制文件的长度正好是 84 + NumberOfTriangles*50 个字节,那么它通常可以被认为是一个有效的二进制文件。

不幸的是,二进制文件可以包含文本“solid”,该文本从 80 字节标题的内容中的字节 0 开始。因此,仅针对该关键字的测试不能肯定地判断文件是 ASCII 还是二进制文件。

这是我目前所拥有的:

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;
    }
}

到目前为止,这对我有用,但我担心纯 ASCII 文件的第 80 个字节可能包含一些 ASCII 字符,当转换为 uint32_t 时,这些字符实际上可能等于文件的长度(不太可能,但并非不可能)。

在验证我是否可以“绝对确定”文件是 ASCII 还是二进制文件时,是否有其他步骤有用?

更新:

按照@Powerswitch 和@RemyLebeau 的建议,我正在对关键字进行进一步的测试。这就是我现在得到的:

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;
}

它似乎可以处理更多的边缘情况,我尝试以一种可以优雅地处理超大(几千兆字节)STL 文件的方式编写它,而不需要将整个文件立即加载到内存中进行扫描对于“endsolid”文本。

随时提供任何反馈和建议(尤其是对于未来寻求解决方案的人)。

【问题讨论】:

  • 维基百科文章说 二进制 STL 文件有一个 80 个字符的标题(通常会被忽略,但绝不应以“solid”开头,因为这会导致大多数软件认为这是一个 ASCII STL 文件)
  • 是的。但是在测试从像Thingiverse 这样的地方下载的随机 STL 文件时,那里的许多二进制 STL 文件实际上确实以“solid”开头,即使它们“不应该”。因此,检查前 5-6 个字节并不总是有保证的原因之一。

标签: c++ qt


【解决方案1】:

如果文件不是以"solid " 开头,并且文件大小正好是84 + (numTriangles * 50) 字节,其中numTriangles 是从偏移量80 读取的,则文件是二进制文件。

如果文件大小至少为 15 字节(对于没有三角形的 ASCII 文件来说绝对最小值)并且以 "solid " 开头,则读取其后面的 name 直到达到换行符。检查下一行是以"facet " 开头还是"endsolid [name]"(不允许其他值)。如果是"facet ",则查找文件末尾并确保它以"endsolid [name]" 的行结尾。如果所有这些都为真,则文件为 ASCII。

将任何其他组合视为无效。

所以,是这样的:

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;
}

【讨论】:

    【解决方案2】:

    在验证我是否可以“绝对确定”文件是 ASCII 还是二进制文件时,是否有其他步骤有用?

    由于 stl 规范中没有格式标签,因此您不能绝对确定文件格式。

    在大多数情况下,检查文件开头的“solid”就足够了。此外,您可以检查更多关键字,如“facet”或“vertex”,以确保它是 ASCII。这些字应该只以 ASCII 格式出现(或在无用的二进制标头中),但二进制浮点数巧合形成这些字的可能性很小。因此,您还可以检查关键字的顺序是否正确。

    当然还要检查二进制标头中的长度是否与文件长度匹配。

    但是:如果您读取线性文件并希望没有人将“solid”字样放在二进制标头中,您的代码会运行得更快。如果文件以“solid”开头,也许您应该更喜欢 ASCII 解析,如果 ASCII 解析失败,则使用二进制解析器作为后备。

    【讨论】:

    • 如果您下载随机 STL 文件(一个好的 repo 是 Thingiverse),您可能会注意到许多二进制文件实际上在 80 字节的标头中以“solid”开头。不过,搜索“facet”和“vertex”等其他关键字的建议实际上可能非常可行。我怀疑任何二进制文件都会在标题之后包含这些单词,这是我可以开始搜索的地方。
    猜你喜欢
    • 2012-04-30
    • 1970-01-01
    • 2013-01-15
    • 2010-09-22
    • 2012-01-03
    • 2022-01-20
    • 2012-05-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多