mod alembic;
mod amf;
mod blend;
mod cad;
mod citygml;
mod containers;
pub(crate) mod dicom;
mod directx;
mod draco;
mod dwg;
mod dxf;
mod fbx;
mod gltf;
mod hdf5_owned;
mod ifc;
mod legacy_scene;
mod lidar;
mod lightburn;
mod mesh;
mod quakemdl;
mod scientific;
mod slpk;
mod spz;
mod step;
mod step_budget;
mod step_faceted;
mod step_parameters;
mod step_tessellation;
mod survey;
mod threeds;
mod threemf;
mod transform;
mod usd;
mod vdb;
mod volume;
mod vrml;
mod xbf;
use crate::{Result, scene::Scene};
use std::path::Path;
pub const MODEL_EXTENSIONS: &[&str] = &[
"stl", "obj", "ply", "off", "pts", "ptx", "pcd", "splat", "gltf", "glb", "3mf", "vtk", "vtp",
"vtu", "vtr", "vts", "vtm", "dae", "las", "laz", "step", "stp", "stpnc", "p21", "210", "mdl",
"3ds", "wrl", "vrml", "gml", "usd", "usda", "usdc", "usdz", "spz", "vti", "nrrd", "nhdr",
"mhd", "mha", "dxf", "bpf", "bin", "qi", "sbet", "csd", "x", "drc", "fbx", "vtkhdf", "e",
"ex2", "exo", "g", "nc", "cdf", "ncdf", "slpk", "abc", "ifc", "vdb", "iges", "igs", "brep",
"xbf", "amf",
];
pub const IMAGE_EXTENSIONS: &[&str] = &[
"png", "jpg", "jpeg", "bmp", "tga", "hdr", "webp", "exr", "tif", "tiff", "dcm",
];
pub const NATIVE_GEOMETRY_EXTENSIONS: &[&str] = &["blend", "lbrn", "lbrn2", "dwg"];
pub fn extension(path: &Path) -> Result<String> {
path.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
.ok_or_else(|| "input needs a recognized file extension".into())
}
pub fn load(path: &Path) -> Result<Scene> {
let scene = match extension(path)?.as_str() {
"amf" => amf::load(path)?,
"blend" => blend::load(path)?,
"lbrn" | "lbrn2" => lightburn::load(path)?,
"dwg" => dwg::load(path)?,
"stl" | "obj" | "ply" | "off" | "pts" | "ptx" | "pcd" | "splat" => mesh::load(path)?,
"gltf" | "glb" => gltf::load(path)?,
"dae" => legacy_scene::load(path)?,
"gml" => citygml::load(path)?,
"dxf" => dxf::load(path)?,
"x" => directx::load(path)?,
"drc" => draco::load(path)?,
"fbx" => fbx::load(path)?,
"abc" => alembic::load(path)?,
"ifc" => ifc::load(path)?,
"vdb" => vdb::load(path)?,
"iges" | "igs" | "brep" => cad::load(path)?,
"xbf" => xbf::load(path)?,
"slpk" => slpk::load(path)?,
"vtkhdf" | "e" | "ex2" | "exo" | "g" | "nc" | "cdf" | "ncdf" => containers::load(path)?,
"usd" | "usda" | "usdc" | "usdz" => usd::load(path)?,
"las" | "laz" => lidar::load(path)?,
"step" | "stp" | "stpnc" | "p21" | "210" => step::load(path)?,
"mdl" => quakemdl::load(path)?,
"spz" => spz::load(path)?,
"bpf" | "bin" | "qi" | "sbet" | "csd" => survey::load(path)?,
"vti" | "nrrd" | "nhdr" | "mhd" | "mha" => volume::load(path)?,
"3ds" => threeds::load(path)?,
"wrl" | "vrml" => vrml::load(path)?,
"3mf" => threemf::load(path)?,
"vtk" | "vtp" | "vtu" | "vtr" | "vts" | "vtm" => scientific::load(path)?,
ext => {
return Err(format!(
"unsupported model format .{ext}; see --list-formats for implemented readers"
)
.into());
}
};
scene.validate()?;
Ok(scene)
}
pub(crate) const MAX_INPUT_BYTES: u64 = 512 * 1024 * 1024;
pub(crate) fn read_file(path: &Path) -> Result<Vec<u8>> {
use std::io::Read;
let file = std::fs::File::open(path)?;
let mut data = Vec::new();
file.take(MAX_INPUT_BYTES + 1).read_to_end(&mut data)?;
if data.len() as u64 > MAX_INPUT_BYTES {
return Err("input exceeds the 512 MiB limit".into());
}
Ok(data)
}
pub(crate) fn relative_resource(parent: &Path, uri: &str) -> Result<std::path::PathBuf> {
let mut decoded = Vec::new();
let bytes = uri.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 2 >= bytes.len() {
return Err("truncated percent escape in resource URI".into());
}
let hex = std::str::from_utf8(&bytes[i + 1..i + 3])?;
decoded.push(u8::from_str_radix(hex, 16)?);
i += 3;
} else {
decoded.push(bytes[i]);
i += 1;
}
}
let text = String::from_utf8(decoded)?;
if text.contains([':', '\0', '\\', '?', '#']) {
return Err("resource URI must be a relative local path".into());
}
let resource = Path::new(&text);
if resource.is_absolute()
|| resource
.components()
.any(|p| matches!(p, std::path::Component::ParentDir))
{
return Err("resource URI must stay within the model directory".into());
}
let root = parent.canonicalize()?;
let path = root.join(resource).canonicalize()?;
if !path.starts_with(&root) {
return Err("resource resolves outside the model directory".into());
}
Ok(path)
}