From 5fcdae771b4e79d5e82e5831de2a0b6c5b445b81 Mon Sep 17 00:00:00 2001 From: Kyle Bayes Date: Wed, 19 Jul 2023 06:50:35 -0700 Subject: [PATCH] Add content_type attribute that takes a MIME type to specify the file contents for meshes, hfields, and textures. PiperOrigin-RevId: 549302344 Change-Id: I30a8adfef5b5d87bb20479329f3396892f5137ff --- doc/XMLreference.rst | 34 ++++- doc/XMLschema.rst | 28 +++-- doc/changelog.rst | 5 +- src/user/user_mesh.cc | 18 +-- src/user/user_objects.cc | 53 ++++++-- src/user/user_objects.h | 7 ++ src/user/user_util.cc | 81 ++++++++++++ src/user/user_util.h | 11 +- src/xml/xml_native_reader.cc | 9 +- src/xml/xml_native_writer.cc | 7 +- test/user/user_mesh_test.cc | 129 ++++++++++++++++++- test/user/user_objects_test.cc | 218 +++++++++++++++++++++++++++++++++ 12 files changed, 556 insertions(+), 44 deletions(-) diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index cd637571..6c4ef5d5 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -2081,7 +2081,10 @@ This adds an element to the tuple. This is a grouping element for defining assets. It does not have attributes. Assets are created in the model so that they can be referenced from other model elements; recall the discussion of :ref:`Assets ` in the Overview -chapter. +chapter. Assets opened from a file can be identified in two different ways: filename extensions or the ``content_type`` +attribute. MuJoCo will attempt to open a file specified by the content type provided, and only defaults to the filename +extension if no ``content_type`` attribute is specified. The content type is ignored if the asset isn't loaded from a +file. .. _asset-texture: @@ -2097,8 +2100,8 @@ chapter. textures. Alternatively the data can be generated by the compiler as a procedural texture. Because different texture types require different parameters, only a subset of the attributes below are used for any given texture. | MuJoCo 2.0 introduced a second file format for loading textures, in addition to PNG. If the file name extension is - different from .png or .PNG, MuJoCo assumes that the texture is in the new format. This is a custom binary file - format, containing the following data: + different from .png or .PNG, or if the ``content_type`` attribute is set to ``image/vnd.mujoco.texture``, then MuJoCo + assumes that the texture is in the new format. This is a custom binary file format, containing the following data: .. code:: Text @@ -2153,6 +2156,13 @@ chapter. texrepeat attribute of :ref:`material `. The data can be loaded from a singlefile or created procedurally. +.. _asset-texture-content_type: + +:at:`content_type`: :at-val:`string, optional` + If the file attribute is specified, then this sets the `MIME `_ type of the file + to be loaded. Any filename extensions will be ignored. Currently ``image/png`` and ``image/vnd.mujoco.texture`` are + supported. + .. _asset-texture-file: :at:`file`: :at-val:`string, optional` @@ -2335,6 +2345,13 @@ also known as terrain map, is a 2D matrix of elevation data. The data can be spe Name of the height field, used for referencing. If the name is omitted and a file name is specified, the height field name equals the file name without the path and extension. +.. _asset-hfield-content_type: + +:at:`content_type`: :at-val: `string, optional` + If the file attribute is specified, then this sets the `MIME `_ type of the file + to be loaded. Any filename extensions will be overloaded. Currently ``image/png`` and ``image/vnd.mujoco.hfield`` + are supported. + .. _asset-hfield-file: :at:`file`: :at-val:`string, optional` @@ -2500,12 +2517,19 @@ The full list of processing steps applied by the compiler to each mesh is as fol :at:`class`: :at-val:`string, optional` Defaults class for setting unspecified attributes (only scale in this case). +.. _asset-mesh-content_type: + +:at:`content_type`: :at-val:`string, optional` + If the file attribute is specified, then this sets the `MIME `_ type of the file + to be loaded. Any filename extensions will be overloaded. Currently ``model/vnd.mujoco.msh``, ``model/obj``, and + ``model/stl`` are supported. + .. _asset-mesh-file: :at:`file`: :at-val:`string, optional` The file from which the mesh will be loaded. The path is determined as described in the meshdir attribute of - :ref:`compiler `. The file extension must be "stl" or "msh" (not case sensitive) specifying the file type. - If the file name is omitted, the vertex attribute becomes required. + :ref:`compiler `. The file extension must be "stl", "msh", or "obj" (not case sensitive) specifying the + file type. If the file name is omitted, the vertex attribute becomes required. .. _asset-mesh-scale: diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 11f2eb14..87ac744b 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -509,41 +509,43 @@ | :ref:`texture | \* | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`name` | :ref:`type` | :ref:`file` | | +| | | | :ref:`name` | :ref:`type` | :ref:`content_type` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`gridsize` | :ref:`gridlayout` | :ref:`fileright` | | +| | | | :ref:`file` | :ref:`gridsize` | :ref:`gridlayout` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`fileleft` | :ref:`fileup` | :ref:`filedown` | | +| | | | :ref:`fileright` | :ref:`fileleft` | :ref:`fileup` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`filefront` | :ref:`fileback` | :ref:`builtin` | | +| | | | :ref:`filedown` | :ref:`filefront` | :ref:`fileback` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`rgb1` | :ref:`rgb2` | :ref:`mark` | | +| | | | :ref:`builtin` | :ref:`rgb1` | :ref:`rgb2` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`markrgb` | :ref:`random` | :ref:`width` | | +| | | | :ref:`mark` | :ref:`markrgb` | :ref:`random` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`height` | :ref:`hflip` | :ref:`vflip` | | +| | | | :ref:`width` | :ref:`height` | :ref:`hflip` | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`vflip` | | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| asset |br| |_| |L| | | .. table:: | | :ref:`hfield | \* | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`name` | :ref:`file` | :ref:`nrow` | | +| | | | :ref:`name` | :ref:`content_type` | :ref:`file` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`ncol` | :ref:`size` | | | +| | | | :ref:`nrow` | :ref:`ncol` | :ref:`size` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| asset |br| |_| |L| | | .. table:: | | :ref:`mesh | \* | :class: mjcf-attributes | | ` | | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`name` | :ref:`class` | :ref:`file` | | +| | | | :ref:`name` | :ref:`class` | :ref:`content_type` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`vertex` | :ref:`normal` | :ref:`texcoord` | | +| | | | :ref:`file` | :ref:`vertex` | :ref:`normal` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`face` | :ref:`refpos` | :ref:`refquat` | | +| | | | :ref:`texcoord` | :ref:`face` | :ref:`refpos` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | -| | | | :ref:`scale` | :ref:`smoothnormal` | | | +| | | | :ref:`refquat` | :ref:`scale` | :ref:`smoothnormal` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_| asset |br| |_| |L| | | .. table:: | diff --git a/doc/changelog.rst b/doc/changelog.rst index d3ce31f3..0586d9ac 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -9,11 +9,14 @@ General ^^^^^^^ - Added primitive collider for sphere-cylinder contacts, previously this pair used the generic convex-convex collider. +- Added an optional ``content_type`` attribute to hfield, texture, and mesh assets. This attribute supports a formatted + `MIME `_ string used to determine the type of the asset file without resorting to + pulling the type from the file extension. - Added analytic derivatives for quaternion :ref:`subtraction` and :ref:`integration` (rotation with an angular velocity). Derivatives are in the 3D tangent space. - Added :ref:`mjv_connector` which has identical functionality to :ref:`mjv_makeConnector`, but with more convenient "from-to" argument parametrization. :ref:`mjv_makeConnector` is now deprecated. -- Bump oldest supported MacOS from version 10.12 to 11. MacOS 11 is the oldest version still maintained by Apple. +- Bumped oldest supported MacOS from version 10.12 to 11. MacOS 11 is the oldest version still maintained by Apple. Python bindings ^^^^^^^^^^^^^^^ diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 29b01be3..17b6d612 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -188,21 +188,23 @@ void mjCMesh::Compile(int vfs_provider) { file = mjuu_strippath(file); } - // load STL, OBJ or MSH - string ext = mjuu_getext(file); - if (strcasecmp(ext.c_str(), ".stl") && - strcasecmp(ext.c_str(), ".obj") && - strcasecmp(ext.c_str(), ".msh")) { - throw mjCError(this, "Unknown mesh file type: %s", file.c_str()); + std::string asset_type = GetAssetContentType(file, content_type); + if (asset_type.empty()) { + throw mjCError(this, "unknown mesh content type for file: '%s'", file.c_str()); + } + + if (asset_type != "model/stl" && asset_type != "model/obj" + && asset_type != "model/vnd.mujoco.msh") { + throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); } string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); mjResource* resource = LoadResource(filename, vfs_provider); try { - if (!strcasecmp(ext.c_str(), ".stl")) { + if (asset_type == "model/stl") { LoadSTL(resource); - } else if (!strcasecmp(ext.c_str(), ".obj")) { + } else if (asset_type == "model/obj") { LoadOBJ(resource); } else { LoadMSH(resource); diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 255774e7..66e3d934 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -507,6 +507,23 @@ mjResource* mjCBase::LoadResource(string filename, int provider) { } +// Get and sanitize content type from raw_text if not empty, otherwise parse +// content type from resource_name; throw error on failure +std::string mjCBase::GetAssetContentType(std::string_view resource_name, + std::string_view raw_text) { + if (!raw_text.empty()) { + auto type = mjuu_parseContentTypeAttrType(raw_text); + auto subtype = mjuu_parseContentTypeAttrSubtype(raw_text); + if (!type.has_value() || !subtype.has_value()) { + throw mjCError(this, "invalid format for content_type"); + } + return std::string(*type) + "/" + std::string(*subtype); + } else { + return mjuu_extToContentType(resource_name); + } +} + + //------------------ class mjCBody implementation -------------------------------------------------- // constructor @@ -2149,15 +2166,22 @@ void mjCHField::Compile(int vfs_provider) { "hfield '%s' (id = %d) specified from file and manually", name.c_str(), id); } - // make filename + std::string asset_type = GetAssetContentType(file, content_type); + + // fallback to custom + if (asset_type.empty()) { + asset_type = "image/vnd.mujoco.hfield"; + } + + if (asset_type != "image/png" && asset_type != "image/vnd.mujoco.hfield") { + throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); + } + string filename = mjuu_makefullname(model->modelfiledir, model->meshdir, file); mjResource* resource = LoadResource(filename, vfs_provider); - // load depending on format - string ext = mjuu_getext(filename); - try { - if (!strcasecmp(ext.c_str(), ".png")) { + if (asset_type == "image/png") { LoadPNG(resource); } else { LoadCustom(resource); @@ -2548,15 +2572,24 @@ void mjCTexture::LoadCustom(mjResource* resource, void mjCTexture::LoadFlip(string filename, int vfs_provider, std::vector& image, unsigned int& w, unsigned int& h) { - // dispatch to PNG or Custom loaded - string ext = mjuu_getext(filename); + std::string asset_type = GetAssetContentType(filename, content_type); + + // fallback to custom + if (asset_type.empty()) { + asset_type = "image/vnd.mujoco.texture"; + } + + if (asset_type != "image/png" && asset_type != "image/vnd.mujoco.texture") { + throw mjCError(this, "unsupported content type: '%s'", asset_type.c_str()); + } + mjResource* resource = LoadResource(filename, vfs_provider); try { - if (!strcasecmp(ext.c_str(), ".png")) { - LoadPNG(resource, image, w, h); + if (asset_type == "image/png") { + LoadPNG(resource, image, w, h); } else { - LoadCustom(resource, image, w, h); + LoadCustom(resource, image, w, h); } mju_closeResource(resource); } catch(mjCError err) { diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 556548cd..2172d7a3 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -173,6 +173,10 @@ class mjCBase { // load resource if found (fallback to OS filesystem) mjResource* LoadResource(std::string filename, int provider); + // Get and sanitize content type from raw_text if not empty, otherwise parse + // content type from resource_name; throw on failure + std::string GetAssetContentType(std::string_view resource_name, std::string_view raw_text); + std::string name; // object name std::string classname; // defaults class name int id; // object id @@ -529,6 +533,7 @@ class mjCMesh: public mjCBase { // returns a bounding volume given a face mjCBoundingVolume GetBoundingVolume(int faceid); + std::string content_type; // content type of file std::string file; // mesh file double refpos[3]; // reference position (translate) double refquat[4]; // reference orientation (rotate) @@ -652,6 +657,7 @@ class mjCHField : public mjCBase { friend class mjXWriter; public: + std::string content_type; // content type of file std::string file; // file: (nrow, ncol, [elevation data]) double size[4]; // hfield size (ignore referencing geom size) int nrow; // number of rows @@ -691,6 +697,7 @@ class mjCTexture : public mjCBase { int width; // width in pixels // method 2: single file + std::string content_type; // content type of file std::string file; // png file to load; use for all sides of cube int gridsize[2]; // size of grid for composite file; (1,1)-repeat char gridlayout[13]; // row-major: L,R,F,B,U,D for faces; . for unused diff --git a/src/user/user_util.cc b/src/user/user_util.cc index a49a496c..4882d4a5 100644 --- a/src/user/user_util.cc +++ b/src/user/user_util.cc @@ -18,12 +18,14 @@ #include #include #include +#include #include #include #include #include #include +#include "engine/engine_crossplatform.h" #include "engine/engine_util_spatial.h" using std::isnan; @@ -596,3 +598,82 @@ string mjuu_makefullname(string filedir, string meshdir, string filename) { // default return filedir + meshdir + filename; } + + +// return true if the text is in a valid content type format: +// {type}/{subtype}[;{parameter}={value}] +static bool mjuu_isValidContentType(std::string_view text) { + // find a forward slash that's not the last character + size_t n = text.find('/'); + if (n == std::string::npos || n == text.size() - 1) { + return false; + } + + size_t m = text.find(';'); + if (m == std::string::npos) { + return true; + } + + if (m + 1 <= n) { + return false; + } + + // just check if there's an equal sign; this isn't robust enough for general + // validation, but works for our scope, hence this is a private helper + // function + size_t s = text.find('='); + if (s == std::string::npos || s + 1 <= m) { + return false; + } + + return true; +} + + + +// return type from content_type format {type}/{subtype}[;{parameter}={value}] +// return empty string on invalid format +std::optional mjuu_parseContentTypeAttrType(std::string_view text) { + if (!mjuu_isValidContentType(text)) { + return std::nullopt; + } + + return { text.substr(0, text.find('/')) }; +} + + + +// return subtype from content_type format {type}/{subtype}[;{parameter}={value}] +// return empty string on invalid format +std::optional mjuu_parseContentTypeAttrSubtype(std::string_view text) { + if (!mjuu_isValidContentType(text)) { + return std::nullopt; + } + + size_t n = text.find('/'); + size_t m = text.find(';', n + 1); + if (m == std::string::npos) { + return { text.substr(n+1) }; + } + + return { text.substr(n + 1, m - n - 1) }; +} + + + +// convert filename extension to content type; return empty string if not found +std::string mjuu_extToContentType(std::string_view filename) { + std::string ext = mjuu_getext(filename); + + if (!strcasecmp(ext.c_str(), ".stl")) { + return "model/stl"; + } else if (!strcasecmp(ext.c_str(), ".obj")) { + return "model/obj"; + } else if (!strcasecmp(ext.c_str(), ".msh")) { + return "model/vnd.mujoco.msh"; + } else if (!strcasecmp(ext.c_str(), ".png")) { + return "image/png"; + } else { + return ""; + } +} diff --git a/src/user/user_util.h b/src/user/user_util.h index 2e14a015..fa230137 100644 --- a/src/user/user_util.h +++ b/src/user/user_util.h @@ -15,6 +15,7 @@ #ifndef MUJOCO_SRC_USER_USER_UTIL_H_ #define MUJOCO_SRC_USER_USER_UTIL_H_ +#include #include #include @@ -23,7 +24,6 @@ extern const double mjNAN; // used to mark undefined fields const double mjEPS = 1E-14; // minimum value in various calculations const double mjMINMASS = 1E-6; // minimum mass allowed - // check if numeric variable is defined: !_isnan(num) bool mjuu_defined(const double num); @@ -152,4 +152,13 @@ bool mjuu_isabspath(std::string path); // assemble full filename std::string mjuu_makefullname(std::string filedir, std::string meshdir, std::string filename); +// return type from content_type format {type}/{subtype}[;{parameter}={value}] +std::optional mjuu_parseContentTypeAttrType(std::string_view text); + +// return subtype from content_type format {type}/{subtype}[;{parameter}={value}] +std::optional mjuu_parseContentTypeAttrSubtype(std::string_view text); + +// convert filename extension to content type; return empty string if not found +std::string mjuu_extToContentType(std::string_view filename); + #endif // MUJOCO_SRC_USER_USER_UTIL_H_ diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index 97157dda..e21e7e60 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -212,12 +212,12 @@ static const char* MJCF[nMJCF][mjXATTRNUM] = { {"asset", "*", "0"}, {"<"}, - {"texture", "*", "21", "name", "type", "file", "gridsize", "gridlayout", + {"texture", "*", "22", "name", "type", "content_type", "file", "gridsize", "gridlayout", "fileright", "fileleft", "fileup", "filedown", "filefront", "fileback", "builtin", "rgb1", "rgb2", "mark", "markrgb", "random", "width", "height", "hflip", "vflip"}, - {"hfield", "*", "5", "name", "file", "nrow", "ncol", "size"}, - {"mesh", "*", "11", "name", "class", "file", "vertex", "normal", + {"hfield", "*", "6", "name", "content_type", "file", "nrow", "ncol", "size"}, + {"mesh", "*", "12", "name", "class", "content_type", "file", "vertex", "normal", "texcoord", "face", "refpos", "refquat", "scale", "smoothnormal"}, {"skin", "*", "9", "name", "file", "material", "rgba", "inflate", "vertex", "texcoord", "face", "group"}, @@ -1186,6 +1186,7 @@ void mjXReader::OneMesh(XMLElement* elem, mjCMesh* pmesh) { // read attributes ReadAttrTxt(elem, "name", pmesh->name); ReadAttrTxt(elem, "class", pmesh->classname); + ReadAttrTxt(elem, "content_type", pmesh->content_type); ReadAttrTxt(elem, "file", pmesh->file); ReadAttr(elem, "refpos", 3, pmesh->refpos, text); ReadAttr(elem, "refquat", 4, pmesh->refquat, text); @@ -2511,6 +2512,7 @@ void mjXReader::Asset(XMLElement* section) { ptex->type = (mjtTexture)n; } ReadAttrTxt(elem, "name", ptex->name); + ReadAttrTxt(elem, "content_type", ptex->content_type); ReadAttrTxt(elem, "file", ptex->file); ReadAttrInt(elem, "width", &ptex->width); ReadAttrInt(elem, "height", &ptex->height); @@ -2583,6 +2585,7 @@ void mjXReader::Asset(XMLElement* section) { // read attributes ReadAttrTxt(elem, "name", phf->name); + ReadAttrTxt(elem, "content_type", phf->content_type); ReadAttrTxt(elem, "file", phf->file); ReadAttrInt(elem, "nrow", &phf->nrow); ReadAttrInt(elem, "ncol", &phf->ncol); diff --git a/src/xml/xml_native_writer.cc b/src/xml/xml_native_writer.cc index 00abbe16..b3d9dc72 100644 --- a/src/xml/xml_native_writer.cc +++ b/src/xml/xml_native_writer.cc @@ -90,6 +90,7 @@ void mjXWriter::OneMesh(XMLElement* elem, mjCMesh* pmesh, mjCDef* def) { if (!writingdefaults) { WriteAttrTxt(elem, "name", pmesh->name); WriteAttrTxt(elem, "class", pmesh->classname); + WriteAttrTxt(elem, "content_type", pmesh->content_type); WriteAttrTxt(elem, "file", pmesh->file); // write vertex data @@ -1255,9 +1256,10 @@ void mjXWriter::Asset(XMLElement* root) { WriteAttrInt(elem, "height", ptex->height); } - // write texures loaded from files + // write textures loaded from files else { - // write singe file + // write single file + WriteAttrTxt(elem, "content_type", ptex->content_type); WriteAttrTxt(elem, "file", ptex->file); // write separate files @@ -1317,6 +1319,7 @@ void mjXWriter::Asset(XMLElement* root) { WriteAttrTxt(elem, "name", phf->name); WriteAttr(elem, "size", 4, phf->size); if (!phf->file.empty()) { + WriteAttrTxt(elem, "content_type", phf->content_type); WriteAttrTxt(elem, "file", phf->file); } else { WriteAttrInt(elem, "nrow", phf->nrow); diff --git a/test/user/user_mesh_test.cc b/test/user/user_mesh_test.cc index dd42e715..d3c65a4f 100644 --- a/test/user/user_mesh_test.cc +++ b/test/user/user_mesh_test.cc @@ -88,7 +88,7 @@ TEST_F(MjCMeshTest, UnknownMeshFormat) { LoadModelFromString(xml.c_str(), error.data(), error.size()); ASSERT_THAT(model, testing::IsNull()) << "Should fail to load a mesh named: " << name; - EXPECT_THAT(error.data(), HasSubstr("Unknown mesh file type")); + EXPECT_THAT(error.data(), HasSubstr("unknown mesh content type for file")); EXPECT_THAT(error.data(), HasSubstr(name)); } } @@ -170,6 +170,133 @@ TEST_F(MjCMeshTest, LoadSTLWithVFS) { EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } +// ------------- test content_type attributes ---------------------------------- + +TEST_F(MjCMeshTest, LoadMSHWithContentType) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try opening the file (not found obviously) + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); +} + +TEST_F(MjCMeshTest, LoadOBJWithContentType) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try opening the file (not found obviously) + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); +} + +TEST_F(MjCMeshTest, LoadSTLWithContentType) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try opening the file (not found obviously) + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); +} + +TEST_F(MjCMeshTest, LoadMSHWithContentTypeError) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should error with unknown file type + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("unsupported content type: 'model/unknown'")); +} + +TEST_F(MjCMeshTest, LoadMSHWithContentTypeParam) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try opening the file (not found obviously) + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); +} + // ------------- test vertex de-duplication (STL) ------------------------------ TEST_F(MjCMeshTest, DeDuplicateSTLVertices) { diff --git a/test/user/user_objects_test.cc b/test/user/user_objects_test.cc index 2139638a..7f4ce069 100644 --- a/test/user/user_objects_test.cc +++ b/test/user/user_objects_test.cc @@ -150,6 +150,224 @@ TEST_F(VfsTest, TextureCustomWithVFS) { EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); } +// ------------------------ test content_type attribute ------------------------ + +using ContentTypeTest = MujocoTest; + +TEST_F(ContentTypeTest, HFieldPngWithContentType) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try loading the file + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); +} + +TEST_F(ContentTypeTest, HFieldCustomWithContentType) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try loading the file + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); +} + +TEST_F(ContentTypeTest, HFieldWithContentTypeError) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try loading the file + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("unsupported content type: 'image/jpeg'")); +} + +TEST_F(ContentTypeTest, TexturePngWithContentType) { + static constexpr char xml[] = R"( + + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try loading the file + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); + } + +TEST_F(ContentTypeTest, TextureCustomWithContentType) { + static constexpr char xml[] = R"( + + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try loading the file + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("resource not found via provider or OS filesystem")); + } + +TEST_F(ContentTypeTest, TextureWithContentTypeError) { + static constexpr char xml[] = R"( + + + + + + + + + + + )"; + + char error[1024]; + size_t error_sz = 1024; + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + + // should try loading the file + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, IsNull()); + EXPECT_THAT(error, HasSubstr("unsupported content type: 'image/jpeg'")); + } + +TEST_F(ContentTypeTest, TextureLoadPng) { + static constexpr char filename[] = "tiny"; + static constexpr char xml[] = R"( + + + + + + + + + + + )"; + + // credit: https://www.mjt.me.uk/posts/smallest-png/ + static constexpr unsigned char tiny[] = + { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, + 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x03, 0x00, + 0x00, 0x00, 0x66, 0xBC, 0x3A, 0x25, 0x00, 0x00, 0x00, + 0x03, 0x50, 0x4C, 0x54, 0x45, 0xB5, 0xD0, 0xD0, 0x63, + 0x04, 0x16, 0xEA, 0x00, 0x00, 0x00, 0x1F, 0x49, 0x44, + 0x41, 0x54, 0x68, 0x81, 0xED, 0xC1, 0x01, 0x0D, 0x00, + 0x00, 0x00, 0xC2, 0xA0, 0xF7, 0x4F, 0x6D, 0x0E, 0x37, + 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xBE, 0x0D, 0x21, 0x00, 0x00, 0x01, 0x9A, 0x60, 0xE1, + 0xD5, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, + 0xAE, 0x42, 0x60, 0x82 }; + size_t tiny_sz = sizeof(tiny); + + char error[1024]; + size_t error_sz = 1024; + + + // load VFS on the heap + auto vfs = std::make_unique(); + mj_defaultVFS(vfs.get()); + mj_makeEmptyFileVFS(vfs.get(), filename, 105); + int i = mj_findFileVFS(vfs.get(), filename); + memcpy(vfs->filedata[i], tiny, tiny_sz); + + // loading the file should be successful + mjModel* model = LoadModelFromString(xml, error, error_sz, vfs.get()); + EXPECT_THAT(model, NotNull()); + + mj_deleteModel(model); + mj_deleteFileVFS(vfs.get(), filename); + } + // ------------------------ test keyframes ------------------------------------- using KeyframeTest = MujocoTest;