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
This commit is contained in:
Kyle Bayes
2023-07-19 06:50:35 -07:00
committed by Copybara-Service
parent 5f327b0c31
commit 5fcdae771b
12 changed files with 556 additions and 44 deletions
+10 -8
View File
@@ -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);
+43 -10
View File
@@ -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<unsigned char>& 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) {
+7
View File
@@ -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
+81
View File
@@ -18,12 +18,14 @@
#include <cstddef>
#include <cstdio>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <mujoco/mjmacro.h>
#include <mujoco/mjtnum.h>
#include <mujoco/mujoco.h>
#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<std::string_view> 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<std::string_view> 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 "";
}
}
+10 -1
View File
@@ -15,6 +15,7 @@
#ifndef MUJOCO_SRC_USER_USER_UTIL_H_
#define MUJOCO_SRC_USER_USER_UTIL_H_
#include <optional>
#include <string>
#include <string_view>
@@ -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<std::string_view> mjuu_parseContentTypeAttrType(std::string_view text);
// return subtype from content_type format {type}/{subtype}[;{parameter}={value}]
std::optional<std::string_view> 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_
+6 -3
View File
@@ -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);
+5 -2
View File
@@ -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);