Question: Free shapes missing when reading XCAF XML in PythonOCC

Hi everyone,

I’m new to the OCCT community, so this is probably a dumb question, but I’m running into a puzzling issue.

We have a C++ script that generates an XML/XCAF document from a STEP file. When we read back this XML in C++, everything works fine — the document has 1 free shape as expected.

However, when I try to read the same XML file in PythonOCC, I get 0 free shapes. Here’s a minimal example of what I’m doing in Python:

from OCC.Core.TDocStd import TDocStd_Document,TDocStd_Application
from OCC.Core.XCAFApp import XCAFApp_Application
from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool
from OCC.Core.XmlXCAFDrivers import xmlxcafdrivers
from OCC.Core.PCDM import PCDM_ReaderStatus
from OCC.Core.TDF import TDF_LabelSequence, TDF_Label

# ----------- Input data (XML file w/metadata) --------------
INP_FILE = "C:/Users/vnoailles/Downloads/test.xml"
# ------------------------------------------------------------

# Create XDE application and register drivers
app = XCAFApp_Application.GetApplication()
xmlxcafdrivers.DefineFormat(app)

# Initialize document and open file
doc = TDocStd_Document("XmlXCAF") 
status = app.Open(INP_FILE,doc)

# Report status
print("Open status:", status, "OK is", PCDM_ReaderStatus.PCDM_RS_OK)

if status != PCDM_ReaderStatus.PCDM_RS_OK:
    raise RuntimeError(f"Failed to open XML document, status = {status}")

# Access XDE tools
shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main())

# Get root labels 
labels = TDF_LabelSequence()     #Initialize TDF labels
shape_tool.GetFreeShapes(labels) #Populate labels with FreeShapes
print(f"OCAF Document - Free shapes found: {labels.Length()}")
if labels.Length() == 0:
    raise RuntimeError("No free shapes found in STEP file")

And here is the code I have in C++:

// corrected_xcaf_io.cpp
#include <STEPCAFControl_Reader.hxx>
#include <TDocStd_Application.hxx>
#include <TDocStd_Document.hxx>
#include <XCAFApp_Application.hxx>
#include <XmlXCAFDrivers.hxx>
#include <XCAFDoc_DocumentTool.hxx>
#include <XCAFDoc_ShapeTool.hxx>
#include <XCAFDoc_ColorTool.hxx>
#include <PCDM.hxx>
#include <TDF_LabelSequence.hxx>
#include <TDataStd_Name.hxx>
#include <TCollection_ExtendedString.hxx>
#include <Quantity_Color.hxx>
#include <IFSelect_ReturnStatus.hxx>
#include <TCollection_AsciiString.hxx>

#include <iostream>
#include <stdexcept>

#define STEP_PATH "C:/Users/vnoailles/Downloads/testStep/base_wall_mount_vc.stp"
#define XML_PATH  "C:/Users/vnoailles/Downloads/test.xml"

void writeXML()
{
    // init app and XML driver
    Handle(XCAFApp_Application) app = XCAFApp_Application::GetApplication();
    XmlXCAFDrivers::DefineFormat(app);

    // create new document (XmlXCAF)
    Handle(TDocStd_Document) doc;
    app->NewDocument("XmlXCAF", doc);

    // read STEP
    STEPCAFControl_Reader reader;
    IFSelect_ReturnStatus rf = reader.ReadFile(STEP_PATH);
    if (rf != IFSelect_RetDone) {
        throw std::runtime_error("STEP read failed (IFSelect_ReturnStatus != IFSelect_RetDone)");
    }

    // transfer STEP into OCAF/XCAF document
    if (!reader.Transfer(doc)) {
        throw std::runtime_error("STEPCAFControl_Reader::Transfer() failed");
    }
    std::cout << "Imported STEP into XCAF document.\n";

    // tools
    TDF_Label mainLabel = doc->Main();
    Handle(XCAFDoc_ShapeTool) shapeTool = XCAFDoc_DocumentTool::ShapeTool(mainLabel);
    Handle(XCAFDoc_ColorTool) colorTool = XCAFDoc_DocumentTool::ColorTool(mainLabel);

    // get free shapes
    TDF_LabelSequence freeShapes;
    shapeTool->GetFreeShapes(freeShapes);
    std::cout << "Free shapes found: " << freeShapes.Length() << "\n";

    if (freeShapes.Length() > 0) {
        TDF_Label shapeLabel = freeShapes.Value(1);

        // set a color (example)
        Quantity_Color red(1.0, 0.0, 0.0, Quantity_TOC_RGB);
        colorTool->SetColor(shapeLabel, red, XCAFDoc_ColorGen);

        // set a name using the TDataStd_Name attribute (correct approach)
        TCollection_ExtendedString nameExt("PrismShape");
        TDataStd_Name::Set(shapeLabel, nameExt);
        std::cout << "Set name 'PrismShape' and color on first free shape.\n";
    }

    // save document as XML
    PCDM_StoreStatus s = app->SaveAs(doc, TCollection_ExtendedString(XML_PATH));
    if (s != PCDM_StoreStatus::PCDM_SS_OK) {
        throw std::runtime_error("Failed to save XCAF document to XML (SaveAs returned non-OK).");
    }
    std::cout << "Saved XCAF document to: " << XML_PATH << "\n";

    app->Close(doc);
}

void readXML()
{
    Handle(XCAFApp_Application) app = XCAFApp_Application::GetApplication();
    XmlXCAFDrivers::DefineFormat(app);

    // create and open doc
    Handle(TDocStd_Document) doc = new TDocStd_Document("XmlXCAF");
    PCDM_ReaderStatus rstatus = app->Open(XML_PATH, doc);
    if (rstatus != PCDM_ReaderStatus::PCDM_RS_OK) {
        throw std::runtime_error(std::string("Error: cannot open document. Status = ") + std::to_string((int)rstatus));
    }
    std::cout << "OCAF document loaded successfully.\n";

    TDF_Label mainLabel = doc->Main();
    Handle(XCAFDoc_ShapeTool) shapeTool = XCAFDoc_DocumentTool::ShapeTool(mainLabel);
    Handle(XCAFDoc_ColorTool) colorTool = XCAFDoc_DocumentTool::ColorTool(mainLabel);

    TDF_LabelSequence freeShapes;
    shapeTool->GetFreeShapes(freeShapes);
    std::cout << "OCAF Document - Free shapes found: " << freeShapes.Length() << "\n";

    if (freeShapes.Length() > 0) {
        TDF_Label shapeLabel = freeShapes.Value(1);

        // read the name attribute (if present)
        Handle(TDataStd_Name) nameAttr;
        if (shapeLabel.FindAttribute(TDataStd_Name::GetID(), nameAttr)) {
            TCollection_ExtendedString ext = nameAttr->Get();
            // convert to ASCII for printing (simple approach)
            TCollection_AsciiString ascii(ext);
            std::cout << "Shape name: " << ascii.ToCString() << "\n";
        }
        else {
            std::cout << "No TDataStd_Name attribute attached to the label.\n";
        }
    }

    app->Close(doc);

}

int main()
{
    try {
        writeXML();
        readXML();
    }
    catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << "\n";
        return 1;
    }
    return 0;
}

Also here is the C++ output :

Imported STEP into XCAF document.
Free shapes found: 1
Set name 'PrismShape' and color on first free shape.
Saved XCAF document to: C:/Users/vnoailles/Downloads/prism_wBCs.xml
OCAF document loaded successfully.
OCAF Document - Free shapes found: 1
Shape name: PrismShape

And here is the output on python side

Open status: 0 OK is PCDM_ReaderStatus.PCDM_RS_OK
OCAF Document - Free shapes found: 0
Traceback (most recent call last):
  File "C:\Users\vnoailles\Desktop\CAD-Project-Test-Archi\FastAPI\main.py", line 34, in <module>
    raise RuntimeError("No free shapes found in STEP file")
RuntimeError: No free shapes found in STEP file

You can also see the stp file used and the xml generated in attached file

gkv311 n's picture
void readXML()
{
    Handle(XCAFApp_Application) app = XCAFApp_Application::GetApplication();
    XmlXCAFDrivers::DefineFormat(app);

    // create and open doc
    Handle(TDocStd_Document) doc = new TDocStd_Document("XmlXCAF");
    PCDM_ReaderStatus rstatus = app->Open(XML_PATH, doc);
    if (rstatus != PCDM_ReaderStatus::PCDM_RS_OK) {
        throw std::runtime_error(std::string("Error: cannot open document. Status = ") + std::to_string((int)rstatus));
    }

The used method XCAFApp_Application::Open() has peculiar definition to pay attention:

  //! Retrieves the document from specified file.
  //! In order not to override a version of the document which is already in memory,
  //! this method can be made to depend on the value returned by IsInSession.
  //! @param[in]  thePath  file path to open
  //! @param[out] theDoc   result document
  //! @param[in]  theRange optional progress indicator
  //! @return reading status
  PCDM_ReaderStatus Open (const TCollection_ExtendedString& thePath,
                          Handle(TDocStd_Document)& theDoc,
                          const Message_ProgressRange& theRange = Message_ProgressRange())
  {
    return Open (thePath, theDoc, Handle(PCDM_ReaderFilter)(), theRange);
  }

Although might think that the method will fill in the document passed in parameter theDoc, it is not (see also @param[out], it is not @param[in,out]). Instead, the function will create a new document and return it to the passed variable reference. So that doc = new TDocStd_Document("XmlXCAF") line before has no effect and you may pass an empty Handle(TDocStd_Document).

This syntax has dramatic consequences when automatically wrapped (via SWIG in this case) into another languages like Python, which follows different paradigm of working with pointers and references compared to C++. Moreover, XCAFApp_Application::Open() has several overloads, which only adds complexity to automatic wrapping.

I have tried your code in pythonOCC 7.9.0 and have struggled that even first assertion doesn't pass:

# ----------- Input data (XML file w/metadata) --------------
INP_FILE = "C:/Users/vnoailles/Downloads/test.xml"
# ------------------------------------------------------------

# Create XDE application and register drivers
app = XCAFApp_Application.GetApplication()
xmlxcafdrivers.DefineFormat(app)

# Initialize document and open file
doc = TDocStd_Document("XmlXCAF") 
status = app.Open(INP_FILE,doc)

# Report status
print("Open status:", status, "OK is", PCDM_ReaderStatus.PCDM_RS_OK)

if status != PCDM_ReaderStatus.PCDM_RS_OK:
    raise RuntimeError(f"Failed to open XML document, status = {status}")

In fact, status wasn't of type PCDM_ReaderStatus at all, and it looked like function actually returned TDocStd_Document instance.

tmpDoc = TDocStd_Document("XmlXCAF") 
doc = app.Open(INP_FILE,tmpDoc)

Probing this document confirmed my assumption that this was indeed expected opened document having 1 free root shape (while dummy document passed via arguments remained empty).

I have looked at the C++ code generated by SWIG for this method, and it is become excessively confusing considering multiple overloads it tries to combine, and indeed in this overload scenario it returns newly created TDocStd_Document instead as return value, which contradicts to automatically generated documentation for the method by pythonOCC building routines.

I cannot say that the behavior is exactly the same on older version of pythonOCC / SWIG, but I suppose that anyway SWIG struggles with wrapping this method and will need some human help here.

I suggest asking this question on pythonOCC site, so maybe they already has some alternative Pythonic methods for opening XCAF documents, and/or will consider improving documentation / wrapping of this API.