Coverage Report

Created: 2026-10-28 07:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/assimp/code/AssetLib/AMF/AMFImporter.cpp
Line
Count
Source
1
/*
2
---------------------------------------------------------------------------
3
Open Asset Import Library (assimp)
4
---------------------------------------------------------------------------
5
6
Copyright (c) 2006-2026, assimp team
7
8
All rights reserved.
9
10
Redistribution and use of this software in source and binary forms,
11
with or without modification, are permitted provided that the following
12
conditions are met:
13
14
* Redistributions of source code must retain the above
15
copyright notice, this list of conditions and the
16
following disclaimer.
17
18
* Redistributions in binary form must reproduce the above
19
copyright notice, this list of conditions and the
20
following disclaimer in the documentation and/or other
21
materials provided with the distribution.
22
23
* Neither the name of the assimp team, nor the names of its
24
contributors may be used to endorse or promote products
25
derived from this software without specific prior
26
written permission of the assimp team.
27
28
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
31
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
32
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
33
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
34
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
35
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
36
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
37
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
38
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
---------------------------------------------------------------------------
40
*/
41
42
#ifndef ASSIMP_BUILD_NO_AMF_IMPORTER
43
44
// Header files, Assimp.
45
#include "AMFImporter.hpp"
46
47
#include <assimp/DefaultIOSystem.h>
48
#include <assimp/fast_atof.h>
49
#include <assimp/StringUtils.h>
50
51
// Header files, stdlib.
52
#include <memory>
53
54
namespace Assimp {
55
56
static constexpr aiImporterDesc Description = {
57
    "Additive manufacturing file format(AMF) Importer",
58
    "smalcom",
59
    "",
60
    "See documentation in source code. Chapter: Limitations.",
61
    aiImporterFlags_SupportTextFlavour | aiImporterFlags_LimitedSupport | aiImporterFlags_Experimental,
62
    0,
63
    0,
64
    0,
65
    0,
66
    "amf"
67
};
68
69
134k
void AMFImporter::Clear() {
70
134k
    mNodeElement_Cur = nullptr;
71
134k
    mUnit.clear();
72
134k
    mMaterial_Converted.clear();
73
134k
    mTexture_Converted.clear();
74
    // Delete all elements
75
134k
    if (!mNodeElement_List.empty()) {
76
1.61M
        for (AMFNodeElementBase *ne : mNodeElement_List) {
77
1.61M
            delete ne;
78
1.61M
        }
79
80
2.75k
        mNodeElement_List.clear();
81
2.75k
    }
82
134k
}
83
84
AMFImporter::AMFImporter() AI_NO_EXCEPT :
85
130k
        mNodeElement_Cur(nullptr),
86
130k
        mXmlParser(nullptr) {
87
    // empty
88
130k
}
89
90
130k
AMFImporter::~AMFImporter() {
91
130k
    delete mXmlParser;
92
    // Clear() is accounting if data already is deleted. So, just check again if all data is deleted.
93
130k
    Clear();
94
130k
}
95
96
/*********************************************************************************************************************************************/
97
/************************************************************ Functions: find set ************************************************************/
98
/*********************************************************************************************************************************************/
99
100
0
bool AMFImporter::Find_NodeElement(const std::string &pID, const AMFNodeElementBase::EType pType, AMFNodeElementBase **pNodeElement) const {
101
0
    for (AMFNodeElementBase *ne : mNodeElement_List) {
102
0
        if ((ne->ID == pID) && (ne->Type == pType)) {
103
0
            if (pNodeElement != nullptr) {
104
0
                *pNodeElement = ne;
105
0
            }
106
107
0
            return true;
108
0
        }
109
0
    } // for(CAMFImporter_NodeElement* ne: mNodeElement_List)
110
111
0
    return false;
112
0
}
113
114
25.8k
bool AMFImporter::Find_ConvertedNode(const std::string &pID, NodeArray &nodeArray, aiNode **pNode) const {
115
25.8k
    aiString node_name(pID.c_str());
116
37.2k
    for (std::unique_ptr<aiNode> &node : nodeArray) {
117
37.2k
        if (node->mName == node_name) {
118
25.7k
            if (pNode != nullptr) {
119
25.7k
                *pNode = node.get();
120
25.7k
            }
121
122
25.7k
            return true;
123
25.7k
        }
124
37.2k
    } // for(aiNode* node: pNodeList)
125
126
130
    return false;
127
25.8k
}
128
129
3.29k
bool AMFImporter::Find_ConvertedMaterial(const std::string &pID, const SPP_Material **pConvertedMaterial) const {
130
13.2k
    for (const SPP_Material &mat : mMaterial_Converted) {
131
13.2k
        if (mat.ID == pID) {
132
3.24k
            if (pConvertedMaterial != nullptr) {
133
3.24k
                *pConvertedMaterial = &mat;
134
3.24k
            }
135
136
3.24k
            return true;
137
3.24k
        }
138
13.2k
    } // for(const SPP_Material& mat: mMaterial_Converted)
139
140
53
    return false;
141
3.29k
}
142
143
/*********************************************************************************************************************************************/
144
/************************************************************ Functions: throw set ***********************************************************/
145
/*********************************************************************************************************************************************/
146
147
0
void AMFImporter::Throw_CloseNotFound(const std::string &nodeName) {
148
0
    throw DeadlyImportError("Close tag for node <" + nodeName + "> not found. Seems file is corrupt.");
149
0
}
150
151
0
void AMFImporter::Throw_IncorrectAttr(const std::string &nodeName, const std::string &attrName) {
152
0
    throw DeadlyImportError("Node <" + nodeName + "> has incorrect attribute \"" + attrName + "\".");
153
0
}
154
155
108
void AMFImporter::Throw_IncorrectAttrValue(const std::string &nodeName, const std::string &attrName) {
156
108
    throw DeadlyImportError("Attribute \"" + attrName + "\" in node <" + nodeName + "> has incorrect value.");
157
108
}
158
159
0
void AMFImporter::Throw_MoreThanOnceDefined(const std::string &nodeName, const std::string &pNodeType, const std::string &pDescription) {
160
0
    throw DeadlyImportError("\"" + pNodeType + "\" node can be used only once in " + nodeName + ". Description: " + pDescription);
161
0
}
162
163
183
void AMFImporter::Throw_ID_NotFound(const std::string &pID) const {
164
183
    throw DeadlyImportError("Not found node with name \"", pID, "\".");
165
183
}
166
167
/*********************************************************************************************************************************************/
168
/************************************************************* Functions: XML set ************************************************************/
169
/*********************************************************************************************************************************************/
170
171
0
void AMFImporter::XML_CheckNode_MustHaveChildren(pugi::xml_node &node) {
172
0
    if (node.children().begin() == node.children().end()) {
173
0
        throw DeadlyImportError(std::string("Node <") + node.name() + "> must have children.");
174
0
    }
175
0
}
176
177
0
bool AMFImporter::XML_SearchNode(const std::string &nodeName) {
178
0
    return nullptr != mXmlParser->findNode(nodeName);
179
0
}
180
181
0
static bool ParseHelper_Decode_Base64_IsBase64(const char pChar) {
182
0
    return (isalnum((unsigned char)pChar) || (pChar == '+') || (pChar == '/'));
183
0
}
184
185
0
void AMFImporter::ParseHelper_Decode_Base64(const std::string &pInputBase64, std::vector<uint8_t> &pOutputData) const {
186
    // With help from
187
    // René Nyffenegger http://www.adp-gmbh.ch/cpp/common/base64.html
188
0
    const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
189
190
0
    uint8_t tidx = 0;
191
0
    uint8_t arr4[4], arr3[3];
192
193
    // check input data
194
0
    if (pInputBase64.size() % 4) {
195
0
        throw DeadlyImportError("Base64-encoded data must have size multiply of four.");
196
0
    }
197
198
    // prepare output place
199
0
    pOutputData.clear();
200
0
    pOutputData.reserve(pInputBase64.size() / 4 * 3);
201
202
0
    for (size_t in_len = pInputBase64.size(), in_idx = 0; (in_len > 0) && (pInputBase64[in_idx] != '='); in_len--) {
203
0
        if (ParseHelper_Decode_Base64_IsBase64(pInputBase64[in_idx])) {
204
0
            arr4[tidx++] = pInputBase64[in_idx++];
205
0
            if (tidx == 4) {
206
0
                for (tidx = 0; tidx < 4; tidx++)
207
0
                    arr4[tidx] = (uint8_t)base64_chars.find(arr4[tidx]);
208
209
0
                arr3[0] = (arr4[0] << 2) + ((arr4[1] & 0x30) >> 4);
210
0
                arr3[1] = ((arr4[1] & 0x0F) << 4) + ((arr4[2] & 0x3C) >> 2);
211
0
                arr3[2] = ((arr4[2] & 0x03) << 6) + arr4[3];
212
0
                for (tidx = 0; tidx < 3; tidx++)
213
0
                    pOutputData.push_back(arr3[tidx]);
214
215
0
                tidx = 0;
216
0
            } // if(tidx == 4)
217
0
        } // if(ParseHelper_Decode_Base64_IsBase64(pInputBase64[in_idx]))
218
0
        else {
219
0
            in_idx++;
220
0
        } // if(ParseHelper_Decode_Base64_IsBase64(pInputBase64[in_idx])) else
221
0
    }
222
223
0
    if (tidx) {
224
0
        for (uint8_t i = tidx; i < 4; i++)
225
0
            arr4[i] = 0;
226
0
        for (uint8_t i = 0; i < 4; i++)
227
0
            arr4[i] = (uint8_t)(base64_chars.find(arr4[i]));
228
229
0
        arr3[0] = (arr4[0] << 2) + ((arr4[1] & 0x30) >> 4);
230
0
        arr3[1] = ((arr4[1] & 0x0F) << 4) + ((arr4[2] & 0x3C) >> 2);
231
0
        arr3[2] = ((arr4[2] & 0x03) << 6) + arr4[3];
232
0
        for (uint8_t i = 0; i < (tidx - 1); i++)
233
0
            pOutputData.push_back(arr3[i]);
234
0
    }
235
0
}
236
237
3.34k
void AMFImporter::ParseFile(const std::string &pFile, IOSystem *pIOHandler) {
238
3.34k
    std::unique_ptr<IOStream> file(pIOHandler->Open(pFile, "rb"));
239
240
    // Check whether we can read from the file
241
3.34k
    if (file == nullptr) {
242
0
        throw DeadlyImportError("Failed to open AMF file ", pFile, ".");
243
0
    }
244
245
3.34k
    mXmlParser = new XmlParser();
246
3.34k
    if (!mXmlParser->parse(file.get())) {
247
338
        delete mXmlParser;
248
338
        mXmlParser = nullptr;
249
338
        throw DeadlyImportError("Failed to create XML reader for file ", pFile, ".");
250
338
    }
251
252
    // Start reading, search for root tag <amf>
253
3.01k
    if (!mXmlParser->hasNode("amf")) {
254
22
        throw DeadlyImportError("Root node \"amf\" not found.");
255
22
    }
256
2.98k
    ParseNode_Root();
257
2.98k
} // namespace Assimp
258
259
1.35M
void AMFImporter::ParseHelper_Node_Enter(AMFNodeElementBase *node) {
260
1.35M
    mNodeElement_Cur->Child.push_back(node); // add new element to current element child list.
261
1.35M
    mNodeElement_Cur = node;
262
1.35M
}
263
264
1.35M
void AMFImporter::ParseHelper_Node_Exit() {
265
1.35M
    if (mNodeElement_Cur != nullptr) mNodeElement_Cur = mNodeElement_Cur->Parent;
266
1.35M
}
267
268
// <amf
269
// unit="" - The units to be used. May be "inch", "millimeter", "meter", "feet", or "micron".
270
// version="" - Version of file format.
271
// >
272
// </amf>
273
// Root XML element.
274
// Multi elements - No.
275
2.98k
void AMFImporter::ParseNode_Root() {
276
2.98k
    AMFNodeElementBase *ne = nullptr;
277
2.98k
    XmlNode *root = mXmlParser->findNode("amf");
278
2.98k
    if (nullptr == root) {
279
0
        throw DeadlyImportError("Root node \"amf\" not found.");
280
0
    }
281
2.98k
    XmlNode node = *root;
282
2.98k
    mUnit = ai_tolower(std::string(node.attribute("unit").as_string()));
283
284
2.98k
    mVersion = node.attribute("version").as_string();
285
286
    // Read attributes for node <amf>.
287
    // Check attributes
288
2.98k
    if (!mUnit.empty()) {
289
216
        if ((mUnit != "inch") && (mUnit != "millimeters") && (mUnit != "millimeter") && (mUnit != "meter") && (mUnit != "feet") && (mUnit != "micron")) {
290
108
            Throw_IncorrectAttrValue("unit", mUnit);
291
108
        }
292
216
    }
293
294
    // create root node element.
295
2.98k
    ne = new AMFRoot(nullptr);
296
297
2.98k
    mNodeElement_Cur = ne; // set first "current" element
298
    // and assign attribute's values
299
2.98k
    ((AMFRoot *)ne)->Unit = mUnit;
300
2.98k
    ((AMFRoot *)ne)->Version = mVersion;
301
302
    // Check for child nodes
303
503k
    for (XmlNode &currentNode : node.children() ) {
304
503k
        const std::string currentName = currentNode.name();
305
503k
        if (currentName == "object") {
306
193k
            ParseNode_Object(currentNode);
307
309k
        } else if (currentName == "material") {
308
7.64k
            ParseNode_Material(currentNode);
309
301k
        } else if (currentName == "texture") {
310
54
            ParseNode_Texture(currentNode);
311
301k
        } else if (currentName == "constellation") {
312
5.27k
            ParseNode_Constellation(currentNode);
313
296k
        } else if (currentName == "metadata") {
314
190k
            ParseNode_Metadata(currentNode);
315
190k
        }
316
503k
        mNodeElement_Cur = ne;
317
503k
    }
318
2.98k
    mNodeElement_Cur = ne; // force restore "current" element
319
2.98k
    mNodeElement_List.push_back(ne); // add to node element list because its a new object in graph.
320
2.98k
}
321
322
// <constellation
323
// id="" - The Object ID of the new constellation being defined.
324
// >
325
// </constellation>
326
// A collection of objects or constellations with specific relative locations.
327
// Multi elements - Yes.
328
// Parent element - <amf>.
329
5.27k
void AMFImporter::ParseNode_Constellation(XmlNode &node) {
330
5.27k
    std::string id = node.attribute("id").as_string();
331
332
    // create and if needed - define new grouping object.
333
5.27k
    AMFNodeElementBase *ne = new AMFConstellation(mNodeElement_Cur);
334
335
5.27k
    AMFConstellation &als = *((AMFConstellation *)ne); // alias for convenience
336
337
5.27k
    if (!id.empty()) {
338
4.07k
        als.ID = id;
339
4.07k
    }
340
341
    // Check for child nodes
342
5.27k
    if (!node.empty()) {
343
5.27k
        ParseHelper_Node_Enter(ne);
344
84.8k
        for (XmlNode currentNode = node.first_child(); currentNode; currentNode = currentNode.next_sibling()) {
345
79.5k
            std::string name = currentNode.name();
346
79.5k
            if (name == "instance") {
347
30.8k
                ParseNode_Instance(currentNode);
348
48.7k
            } else if (name == "metadata") {
349
33.8k
                ParseNode_Metadata(currentNode);
350
33.8k
            }
351
79.5k
        }
352
5.27k
        ParseHelper_Node_Exit();
353
5.27k
    } else {
354
0
        mNodeElement_Cur->Child.push_back(ne);
355
0
    }
356
5.27k
    mNodeElement_List.push_back(ne); // and to node element list because its a new object in graph.
357
5.27k
}
358
359
// <instance
360
// objectid="" - The Object ID of the new constellation being defined.
361
// >
362
// </instance>
363
// A collection of objects or constellations with specific relative locations.
364
// Multi elements - Yes.
365
// Parent element - <amf>.
366
30.8k
void AMFImporter::ParseNode_Instance(XmlNode &node) {
367
30.8k
    AMFNodeElementBase *ne(nullptr);
368
369
    // Read attributes for node <constellation>.
370
30.8k
    std::string objectid = node.attribute("objectid").as_string();
371
372
    // used object id must be defined, check that.
373
30.8k
    if (objectid.empty()) {
374
0
        throw DeadlyImportError("\"objectid\" in <instance> must be defined.");
375
0
    }
376
    // create and define new grouping object.
377
30.8k
    ne = new AMFInstance(mNodeElement_Cur);
378
30.8k
    AMFInstance &als = *((AMFInstance *)ne);
379
30.8k
    als.ObjectID = objectid;
380
381
30.8k
    if (!node.empty()) {
382
30.8k
        ParseHelper_Node_Enter(ne);
383
198k
        for (auto &currentNode : node.children()) {
384
198k
            const std::string &currentName = currentNode.name();
385
198k
            if (currentName == "deltax") {
386
25.8k
                XmlParser::getValueAsReal(currentNode, als.Delta.x);
387
172k
            } else if (currentName == "deltay") {
388
24.6k
                XmlParser::getValueAsReal(currentNode, als.Delta.y);
389
147k
            } else if (currentName == "deltaz") {
390
24.9k
                XmlParser::getValueAsReal(currentNode, als.Delta.z);
391
122k
            } else if (currentName == "rx") {
392
25.6k
                XmlParser::getValueAsReal(currentNode, als.Delta.x);
393
97.3k
            } else if (currentName == "ry") {
394
25.5k
                XmlParser::getValueAsReal(currentNode, als.Delta.y);
395
71.7k
            } else if (currentName == "rz") {
396
26.6k
                XmlParser::getValueAsReal(currentNode, als.Delta.z);
397
26.6k
            }
398
198k
        }
399
30.8k
        ParseHelper_Node_Exit();
400
30.8k
    } else {
401
0
        mNodeElement_Cur->Child.push_back(ne);
402
0
    }
403
404
30.8k
    mNodeElement_List.push_back(ne); // and to node element list because its a new object in graph.
405
30.8k
}
406
407
// <object
408
// id="" - A unique ObjectID for the new object being defined.
409
// >
410
// </object>
411
// An object definition.
412
// Multi elements - Yes.
413
// Parent element - <amf>.
414
193k
void AMFImporter::ParseNode_Object(XmlNode &node) {
415
193k
    AMFNodeElementBase *ne = nullptr;
416
417
    // Read attributes for node <object>.
418
193k
    std::string id = node.attribute("id").as_string();
419
420
    // create and if needed - define new geometry object.
421
193k
    ne = new AMFObject(mNodeElement_Cur);
422
423
193k
    AMFObject &als = *((AMFObject *)ne); // alias for convenience
424
425
193k
    if (!id.empty()) {
426
24.4k
        als.ID = id;
427
24.4k
    }
428
429
    // Check for child nodes
430
193k
    if (!node.empty()) {
431
193k
        ParseHelper_Node_Enter(ne);
432
193k
        for (auto &currentNode : node.children()) {
433
73.6k
            const std::string &currentName = currentNode.name();
434
73.6k
            if (currentName == "color") {
435
11.1k
                ParseNode_Color(currentNode);
436
62.5k
            } else if (currentName == "mesh") {
437
39.0k
                ParseNode_Mesh(currentNode);
438
39.0k
            } else if (currentName == "metadata") {
439
2.71k
                ParseNode_Metadata(currentNode);
440
2.71k
            }
441
73.6k
        }
442
193k
        ParseHelper_Node_Exit();
443
193k
    } else {
444
0
        mNodeElement_Cur->Child.push_back(ne); // Add element to child list of current element
445
0
    }
446
447
193k
    mNodeElement_List.push_back(ne); // and to node element list because its a new object in graph.
448
193k
}
449
450
// <metadata
451
// type="" - The type of the attribute.
452
// >
453
// </metadata>
454
// Specify additional information about an entity.
455
// Multi elements - Yes.
456
// Parent element - <amf>, <object>, <volume>, <material>, <vertex>.
457
//
458
// Reserved types are:
459
// "Name" - The alphanumeric label of the entity, to be used by the interpreter if interacting with the user.
460
// "Description" - A description of the content of the entity
461
// "URL" - A link to an external resource relating to the entity
462
// "Author" - Specifies the name(s) of the author(s) of the entity
463
// "Company" - Specifying the company generating the entity
464
// "CAD" - specifies the name of the originating CAD software and version
465
// "Revision" - specifies the revision of the entity
466
// "Tolerance" - specifies the desired manufacturing tolerance of the entity in entity's unit system
467
// "Volume" - specifies the total volume of the entity, in the entity's unit system, to be used for verification (object and volume only)
468
252k
void AMFImporter::ParseNode_Metadata(XmlNode &node) {
469
252k
    AMFNodeElementBase *ne = nullptr;
470
471
252k
    std::string type = node.attribute("type").as_string(), value;
472
252k
    XmlParser::getValueAsString(node, value);
473
474
    // read attribute
475
252k
    ne = new AMFMetadata(mNodeElement_Cur);
476
252k
    ((AMFMetadata *)ne)->MetaType = type;
477
252k
    ((AMFMetadata *)ne)->Value = value;
478
252k
    mNodeElement_Cur->Child.push_back(ne); // Add element to child list of current element
479
252k
    mNodeElement_List.push_back(ne); // and to node element list because its a new object in graph.
480
252k
}
481
482
12.4k
bool AMFImporter::CanRead(const std::string &pFile, IOSystem *pIOHandler, bool /*pCheckSig*/) const {
483
12.4k
    static const char *tokens[] = { "<amf" };
484
12.4k
    return SearchFileHeaderForToken(pIOHandler, pFile, tokens, AI_COUNT_OF(tokens));
485
12.4k
}
486
487
136k
const aiImporterDesc *AMFImporter::GetInfo() const {
488
136k
    return &Description;
489
136k
}
490
491
3.34k
void AMFImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSystem *pIOHandler) {
492
3.34k
    Clear(); // delete old graph.
493
3.34k
    ParseFile(pFile, pIOHandler);
494
3.34k
    Postprocess_BuildScene(pScene);
495
    // scene graph is ready, exit.
496
3.34k
}
497
498
} // namespace Assimp
499
500
#endif // !ASSIMP_BUILD_NO_AMF_IMPORTER