From 3237edc480f0f0491da2aad176b7646dbbe4779f Mon Sep 17 00:00:00 2001
From: Mathieu Westphal <mathieu.westphal@kitware.com>
Date: Sun, 16 Feb 2025 20:12:21 +0100
Subject: [PATCH] OBJImporter: Read mtllib keyword when parsing OBJ

---
 IO/Import/vtkOBJImporter.cxx | 136 +++++++++++++++++++++++++----------
 1 file changed, 98 insertions(+), 38 deletions(-)

diff --git a/IO/Import/vtkOBJImporter.cxx b/IO/Import/vtkOBJImporter.cxx
index 14d1e6fa81a..5f76c1955d3 100644
--- a/IO/Import/vtkOBJImporter.cxx
+++ b/IO/Import/vtkOBJImporter.cxx
@@ -366,24 +366,113 @@ int vtkOBJPolyDataProcessor::RequestData(vtkInformation* vtkNotUsed(request),
     delete this->parsedMTLs[k];
   }
 
-  // If the MTLFileName is not set explicitly, we assume *.obj.mtl as the MTL
-  // filename
+  // If the MTLFileName is not set explicitly, we look for a mtllib in the obj
+  // if not available we look for .mtl or *.obj.mtl as the MTL filename
   std::string mtlname = this->MTLFileName;
+  char *pLine, *pEnd, *cmd;
+  auto _extractLine = [&](char* rawLine)
+  {
+    pLine = rawLine;
+    pEnd = rawLine + strlen(rawLine);
+
+    // watch for BOM
+    if (pEnd - pLine > 3 && pLine[0] == -17 && pLine[1] == -69 && pLine[2] == -65)
+    {
+      pLine += 3;
+    }
+
+    // find the first non-whitespace character
+    while (isspace(*pLine) && pLine < pEnd)
+    {
+      pLine++;
+    }
+
+    // this first non-whitespace is the command
+    cmd = pLine;
+
+    // skip over non-whitespace
+    while (!isspace(*pLine) && pLine < pEnd)
+    {
+      pLine++;
+    }
+
+    // terminate command
+    if (pLine < pEnd)
+    {
+      *pLine = '\0';
+      pLine++;
+    }
+  };
+
   if (this->DefaultMTLFileName)
   {
-    mtlname = this->FileName + ".mtl";
-    if (vtksys::SystemTools::FileExists(mtlname))
+    bool mtllibDefined = false;
+    { // (make a local scope section to emphasise that the variables below are only used here)
+
+      const int MAX_LINE = 100000;
+      char rawLine[MAX_LINE];
+
+      while (fgets(rawLine, MAX_LINE, in) != nullptr)
+      {
+        _extractLine(rawLine);
+
+        // in the OBJ format the first characters determine how to interpret the line:
+        // Skip comments
+        if (strcmp(cmd, "#") == 0)
+        {
+          continue;
+        }
+        // mtllib is the first non-commmented line
+        else if (strcmp(cmd, "mtllib") == 0)
+        {
+          while (isspace(*pLine) && pLine < pEnd)
+          {
+            pLine++;
+          }
+          // Recover the mtllib without the \n at the end
+          mtlname = vtksys::SystemTools::GetFilenamePath(this->FileName) + "/" +
+            std::string(pLine, strlen(pLine) - 1);
+          mtllibDefined = true;
+          break;
+        }
+        // no mtllib in this file, just break;
+        else
+        {
+          break;
+        }
+      }
+      // Reset file position
+      rewind(in);
+    }
+
+    if (mtllibDefined)
     {
-      this->MTLFileName = mtlname;
+      if (vtksys::SystemTools::FileExists(mtlname))
+      {
+        this->MTLFileName = mtlname;
+      }
+      else
+      {
+        vtkErrorMacro(<< "The MTL file set by the mtllib command " << mtlname
+                      << " could not be found");
+      }
     }
     else
     {
-      mtlname = vtksys::SystemTools::GetFilenamePath(this->FileName) + "/" +
-        vtksys::SystemTools::GetFilenameWithoutLastExtension(this->FileName) + ".mtl";
+      mtlname = this->FileName + ".mtl";
       if (vtksys::SystemTools::FileExists(mtlname))
       {
         this->MTLFileName = mtlname;
       }
+      else
+      {
+        mtlname = vtksys::SystemTools::GetFilenamePath(this->FileName) + "/" +
+          vtksys::SystemTools::GetFilenameWithoutLastExtension(this->FileName) + ".mtl";
+        if (vtksys::SystemTools::FileExists(mtlname))
+        {
+          this->MTLFileName = mtlname;
+        }
+      }
     }
   }
   else
@@ -395,7 +484,7 @@ int vtkOBJPolyDataProcessor::RequestData(vtkInformation* vtkNotUsed(request),
   }
 
   int mtlParseResult;
-  this->parsedMTLs = ParseOBJandMTL(MTLFileName, mtlParseResult);
+  this->parsedMTLs = ParseOBJandMTL(this->MTLFileName, mtlParseResult);
   if (this->parsedMTLs.empty())
   { // construct a default material to define the single polydata's actor.
     this->parsedMTLs.push_back(new vtkOBJImportedMaterial);
@@ -463,36 +552,7 @@ int vtkOBJPolyDataProcessor::RequestData(vtkInformation* vtkNotUsed(request),
     while (everything_ok && fgets(rawLine, MAX_LINE, in) != nullptr)
     { /** While OK and there is another line in the file */
       lineNr++;
-      char* pLine = rawLine;
-      char* pEnd = rawLine + strlen(rawLine);
-
-      // watch for BOM
-      if (pEnd - pLine > 3 && pLine[0] == -17 && pLine[1] == -69 && pLine[2] == -65)
-      {
-        pLine += 3;
-      }
-
-      // find the first non-whitespace character
-      while (isspace(*pLine) && pLine < pEnd)
-      {
-        pLine++;
-      }
-
-      // this first non-whitespace is the command
-      const char* cmd = pLine;
-
-      // skip over non-whitespace
-      while (!isspace(*pLine) && pLine < pEnd)
-      {
-        pLine++;
-      }
-
-      // terminate command
-      if (pLine < pEnd)
-      {
-        *pLine = '\0';
-        pLine++;
-      }
+      _extractLine(rawLine);
 
       // in the OBJ format the first characters determine how to interpret the line:
       static long lastVertexIndex = 0;
-- 
GitLab