diff --git a/src/RWGltf/RWGltf_GltfJsonParser.cxx b/src/RWGltf/RWGltf_GltfJsonParser.cxx index b43a3ebdc9..89feb12a9f 100644 --- a/src/RWGltf/RWGltf_GltfJsonParser.cxx +++ b/src/RWGltf/RWGltf_GltfJsonParser.cxx @@ -37,6 +37,20 @@ namespace //! Material extension. const char THE_KHR_materials_common[] = "KHR_materials_common"; const char THE_KHR_binary_glTF[] = "KHR_binary_glTF"; + + //! Data buffer referring to a portion of another buffer. + class RWGltf_SubBuffer : public NCollection_Buffer + { + public: + RWGltf_SubBuffer (const Handle(NCollection_Buffer)& theBase, + Standard_Size theOffset, + Standard_Size theLength) + : NCollection_Buffer (Handle(NCollection_BaseAllocator)(), theLength, theBase->ChangeData() + theOffset), + myBaseBuffer (theBase) {} + + private: + Handle(NCollection_Buffer) myBaseBuffer; + }; } //! Find member of the object in a safe way. @@ -736,15 +750,11 @@ bool RWGltf_GltfJsonParser::gltfParseTexture (Handle(Image_Texture)& theTexture, if (aBinVal != NULL) { - //const RWGltf_JsonValue* aMimeTypeVal = findObjectMember (*aBinVal, "mimeType"); - //const RWGltf_JsonValue* aWidthVal = findObjectMember (*aBinVal, "width"); - //const RWGltf_JsonValue* aHeightVal = findObjectMember (*aBinVal, "height"); if (aBufferViewName == NULL) { reportGltfWarning ("Invalid texture node '" + aTextureId + "' points to invalid data source."); return false; } - const RWGltf_JsonValue* aBufferView = myGltfRoots[RWGltf_GltfRootElement_BufferViews].FindChild (*aBufferViewName); if (aBufferView == NULL || !aBufferView->IsObject()) @@ -752,47 +762,33 @@ bool RWGltf_GltfJsonParser::gltfParseTexture (Handle(Image_Texture)& theTexture, reportGltfWarning ("Invalid texture node '" + aTextureId + "' points to invalid buffer view '" + getKeyString (*aBufferViewName) + "'."); return false; } - - const RWGltf_JsonValue* aBufferName = findObjectMember (*aBufferView, "buffer"); - const RWGltf_JsonValue* aByteLength = findObjectMember (*aBufferView, "byteLength"); - const RWGltf_JsonValue* aByteOffset = findObjectMember (*aBufferView, "byteOffset"); - if (aBufferName != NULL - && aBufferName->IsString() - && !IsEqual (aBufferName->GetString(), "binary_glTF")) - { - reportGltfError ("BufferView '" + getKeyString (*aBufferViewName) + "' does not define binary_glTF buffer."); - return false; - } - - RWGltf_GltfBufferView aBuffView; - aBuffView.ByteOffset = aByteOffset != NULL && aByteOffset->IsNumber() - ? (int64_t )aByteOffset->GetDouble() - : 0; - aBuffView.ByteLength = aByteLength != NULL && aByteLength->IsNumber() - ? (int64_t )aByteLength->GetDouble() - : 0; - if (aBuffView.ByteLength < 0) - { - reportGltfError ("BufferView '" + getKeyString (*aBufferViewName) + "' defines invalid byteLength."); - return false; - } - else if (aBuffView.ByteOffset < 0) - { - reportGltfError ("BufferView '" + getKeyString (*aBufferViewName) + "' defines invalid byteOffset."); - return false; - } - - - const int64_t anOffset = myBinBodyOffset + aBuffView.ByteOffset; - theTexture = new Image_Texture (myFilePath, anOffset, aBuffView.ByteLength); - return true; + return gltfParseTexturInGlbBuffer (theTexture, *aBinVal, getKeyString (*aBufferViewName), *aBufferView); } } const RWGltf_JsonValue* anUriVal = findObjectMember (*anImgNode, "uri"); - if (anUriVal == NULL - || !anUriVal->IsString()) + if (anUriVal == NULL) { + const RWGltf_JsonValue* aBufferViewName = findObjectMember (*anImgNode, "bufferView"); + if (aBufferViewName == NULL) + { + reportGltfWarning ("Invalid texture node '" + aTextureId + "' points to invalid data source."); + return false; + } + + const RWGltf_JsonValue* aBufferView = myGltfRoots[RWGltf_GltfRootElement_BufferViews].FindChild (*aBufferViewName); + if (aBufferView == NULL + || !aBufferView->IsObject()) + { + reportGltfWarning ("Invalid texture node '" + aTextureId + "' points to invalid buffer view '" + getKeyString (*aBufferViewName) + "'."); + return false; + } + return gltfParseTextureInBufferView (theTexture, getKeyString (*aSrcVal), getKeyString (*aBufferViewName), *aBufferView); + } + + if (!anUriVal->IsString()) + { + reportGltfWarning ("Invalid texture node '" + aTextureId + "' points to invalid data source."); return false; } @@ -827,6 +823,150 @@ bool RWGltf_GltfJsonParser::gltfParseTexture (Handle(Image_Texture)& theTexture, return true; } +// ======================================================================= +// function : gltfParseTexturInGlbBuffer +// purpose : +// ======================================================================= +bool RWGltf_GltfJsonParser::gltfParseTexturInGlbBuffer (Handle(Image_Texture)& theTexture, + const RWGltf_JsonValue& theBinVal, + const TCollection_AsciiString& theBufferViewId, + const RWGltf_JsonValue& theBufferView) +{ + const RWGltf_JsonValue* aMimeTypeVal = findObjectMember (theBinVal, "mimeType"); + //const RWGltf_JsonValue* aWidthVal = findObjectMember (theBinVal, "width"); + //const RWGltf_JsonValue* aHeightVal = findObjectMember (theBinVal, "height"); + (void )aMimeTypeVal; + + const RWGltf_JsonValue* aBufferName = findObjectMember (theBufferView, "buffer"); + const RWGltf_JsonValue* aByteLength = findObjectMember (theBufferView, "byteLength"); + const RWGltf_JsonValue* aByteOffset = findObjectMember (theBufferView, "byteOffset"); + if (aBufferName != NULL + && aBufferName->IsString() + && !IsEqual (aBufferName->GetString(), "binary_glTF")) + { + reportGltfError ("BufferView '" + theBufferViewId + "' does not define binary_glTF buffer."); + return false; + } + + RWGltf_GltfBufferView aBuffView; + aBuffView.ByteOffset = aByteOffset != NULL && aByteOffset->IsNumber() + ? (int64_t )aByteOffset->GetDouble() + : 0; + aBuffView.ByteLength = aByteLength != NULL && aByteLength->IsNumber() + ? (int64_t )aByteLength->GetDouble() + : 0; + if (aBuffView.ByteLength <= 0) + { + reportGltfError ("BufferView '" + theBufferViewId + "' defines invalid byteLength."); + return false; + } + else if (aBuffView.ByteOffset < 0) + { + reportGltfError ("BufferView '" + theBufferViewId + "' defines invalid byteOffset."); + return false; + } + + const int64_t anOffset = myBinBodyOffset + aBuffView.ByteOffset; + theTexture = new Image_Texture (myFilePath, anOffset, aBuffView.ByteLength); + return true; +} + +// ======================================================================= +// function : gltfParseTextureInBufferView +// purpose : +// ======================================================================= +bool RWGltf_GltfJsonParser::gltfParseTextureInBufferView (Handle(Image_Texture)& theTexture, + const TCollection_AsciiString& theSourceId, + const TCollection_AsciiString& theBufferViewId, + const RWGltf_JsonValue& theBufferView) +{ + const RWGltf_JsonValue* aBufferName = findObjectMember (theBufferView, "buffer"); + const RWGltf_JsonValue* aByteLength = findObjectMember (theBufferView, "byteLength"); + const RWGltf_JsonValue* aByteOffset = findObjectMember (theBufferView, "byteOffset"); + if (aBufferName == NULL) + { + reportGltfError ("BufferView '" + theBufferViewId + "' does not define buffer."); + return false; + } + + const TCollection_AsciiString aBufferId = getKeyString (*aBufferName); + const RWGltf_JsonValue* aBuffer = myGltfRoots[RWGltf_GltfRootElement_Buffers].FindChild (*aBufferName); + if (aBuffer == NULL + || !aBuffer->IsObject()) + { + reportGltfError ("BufferView '" + theBufferViewId + "' refers to non-existing buffer."); + return false; + } + + RWGltf_GltfBufferView aBuffView; + aBuffView.ByteOffset = aByteOffset != NULL && aByteOffset->IsNumber() + ? (int64_t )aByteOffset->GetDouble() + : 0; + aBuffView.ByteLength = aByteLength != NULL && aByteLength->IsNumber() + ? (int64_t )aByteLength->GetDouble() + : 0; + if (aBuffView.ByteLength <= 0) + { + reportGltfError ("BufferView '" + theBufferViewId + "' defines invalid byteLength."); + return false; + } + else if (aBuffView.ByteOffset < 0) + { + reportGltfError ("BufferView '" + theBufferViewId + "' defines invalid byteOffset."); + return false; + } + + const RWGltf_JsonValue* anUriVal = findObjectMember (*aBuffer, "uri"); + if (anUriVal == NULL + || !anUriVal->IsString()) + { + reportGltfError ("Buffer '" + aBufferId + "' does not define uri."); + return false; + } + + const char* anUriData = anUriVal->GetString(); + if (::strncmp (anUriData, "data:application/octet-stream;base64,", 37) == 0) + { + Handle(NCollection_Buffer) aBaseBuffer; + if (!myDecodedBuffers.Find (aBufferId, aBaseBuffer)) + { + aBaseBuffer = FSD_Base64Decoder::Decode ((const Standard_Byte* )anUriData + 37, anUriVal->GetStringLength() - 37); + myDecodedBuffers.Bind (aBufferId, aBaseBuffer); + } + + Handle(RWGltf_SubBuffer) aSubBuffer = new RWGltf_SubBuffer (aBaseBuffer, (Standard_Size )aBuffView.ByteOffset, (Standard_Size )aBuffView.ByteLength); + theTexture = new Image_Texture (aSubBuffer, myFilePath + "@" + theSourceId); + return true; + } + + const TCollection_AsciiString anUri (anUriData); + if (anUri.IsEmpty()) + { + reportGltfError ("Buffer '" + aBufferId + "' does not define uri."); + return false; + } + + const TCollection_AsciiString aPath = myFolder + anUri; + bool isFileExist = false; + if (!myProbedFiles.Find (aPath, isFileExist)) + { + isFileExist = OSD_File (aPath).Exists(); + myProbedFiles.Bind (aPath, isFileExist); + } + if (!isFileExist) + { + reportGltfError ("Buffer '" + aBufferId + "' refers to non-existing file '" + anUri + "'."); + return false; + } + + theTexture = new Image_Texture (aPath, aBuffView.ByteOffset, aBuffView.ByteLength); + if (myExternalFiles != NULL) + { + myExternalFiles->Add (aPath); + } + return true; +} + // ======================================================================= // function : gltfParseScene // purpose : @@ -1548,7 +1688,7 @@ bool RWGltf_GltfJsonParser::gltfParseBufferView (const Handle(RWGltf_GltfLatePri } } - if (aBuffView.ByteLength < 0) + if (aBuffView.ByteLength <= 0) { reportGltfError ("BufferView '" + theName + "' defines invalid byteLength."); return false; diff --git a/src/RWGltf/RWGltf_GltfJsonParser.pxx b/src/RWGltf/RWGltf_GltfJsonParser.pxx index 1e4e2419eb..9ea399b98a 100644 --- a/src/RWGltf/RWGltf_GltfJsonParser.pxx +++ b/src/RWGltf/RWGltf_GltfJsonParser.pxx @@ -150,6 +150,18 @@ protected: Standard_EXPORT bool gltfParseTexture (Handle(Image_Texture)& theTexture, const RWGltf_JsonValue* theTextureId); + //! Parse texture definition in binary buffer of GLB file. + Standard_EXPORT bool gltfParseTexturInGlbBuffer (Handle(Image_Texture)& theTexture, + const RWGltf_JsonValue& theBinVal, + const TCollection_AsciiString& theBufferViewId, + const RWGltf_JsonValue& theBufferViewName); + + //! Parse texture definition in binary buffer of glTF file. + Standard_EXPORT bool gltfParseTextureInBufferView (Handle(Image_Texture)& theTexture, + const TCollection_AsciiString& theSourceId, + const TCollection_AsciiString& theBufferViewhId, + const RWGltf_JsonValue& theBufferView); + //! Bind material definition to the map. Standard_EXPORT void gltfBindMaterial (const Handle(RWGltf_MaterialMetallicRoughness)& theMatPbr, const Handle(RWGltf_MaterialCommon)& theMatCommon); diff --git a/tests/de_mesh/gltf_read/cubeemb b/tests/de_mesh/gltf_read/cubeemb new file mode 100644 index 0000000000..67961d11cf --- /dev/null +++ b/tests/de_mesh/gltf_read/cubeemb @@ -0,0 +1,44 @@ +puts "========" +puts "0031312: Data Exchange - RWGltf_CafReader fails reading texture embedded into base64 bufferView" +puts "========" + +# glTF file content +set cubeGltf { +{ +"asset":{"generator":"","version":"2.0"}, +"scene":0, +"scenes":[{"name":"Scene","nodes":[0]}], +"nodes":[{"mesh":0,"name":"Cube"}], +"materials":[{"name":"Material","pbrMetallicRoughness":{"baseColorTexture":{"index":0,"texCoord":0},"metallicFactor":0,"roughnessFactor":0.4}}], +"meshes":[{"name":"Cube","primitives":[{"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]}], +"textures":[{"source":0}], +"images":[{"bufferView":4,"mimeType":"image/jpeg","name":"UVGrid"}], +"accessors":[ +{"bufferView":0,"componentType":5126,"count":24,"type":"VEC3"}, +{"bufferView":1,"componentType":5126,"count":24,"type":"VEC3"}, +{"bufferView":2,"componentType":5126,"count":24,"type":"VEC2"}, +{"bufferView":3,"componentType":5123,"count":36,"type":"SCALAR"} +], +"bufferViews":[ +{"buffer":0,"byteLength":288,"byteOffset":0}, +{"buffer":0,"byteLength":288,"byteOffset":288}, +{"buffer":0,"byteLength":192,"byteOffset":576}, +{"buffer":0,"byteLength":72,"byteOffset":768}, +{"buffer":0,"byteLength":2870,"byteOffset":840} +], +"buffers":[{ +"byteLength":3712, +"uri":"data:application/octet-stream;base64,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" +}] +} +} + +set fd [open ${imagedir}/${casename}.gltf w] +fconfigure $fd -translation lf +puts $fd $cubeGltf +close $fd + +ReadGltf D ${imagedir}/${casename}.gltf +XGetOneShape s D +checknbshapes s -face 1 -compound 0 +checktrinfo s -tri 12 -nod 24