use crate::Model;
use crate::error::{Error, Result};
use crate::model::*;
use crate::opc::Package;
use quick_xml::Reader;
use std::io::Read;
use super::{parse_attributes, validate_attributes};
pub(super) fn parse_color(color_str: &str) -> Option<(u8, u8, u8, u8)> {
let color_str = color_str.trim_start_matches('#');
if color_str.len() == 6 {
let r = u8::from_str_radix(&color_str[0..2], 16).ok()?;
let g = u8::from_str_radix(&color_str[2..4], 16).ok()?;
let b = u8::from_str_radix(&color_str[4..6], 16).ok()?;
Some((r, g, b, 255))
} else if color_str.len() == 8 {
let r = u8::from_str_radix(&color_str[0..2], 16).ok()?;
let g = u8::from_str_radix(&color_str[2..4], 16).ok()?;
let b = u8::from_str_radix(&color_str[4..6], 16).ok()?;
let a = u8::from_str_radix(&color_str[6..8], 16).ok()?;
Some((r, g, b, a))
} else {
None
}
}
pub(super) fn parse_base_material<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
index: usize,
) -> Result<Material> {
let attrs = parse_attributes(reader, e)?;
validate_attributes(&attrs, &["name", "displaycolor"], "base")?;
let mut material = Material::new(index);
material.name = attrs.get("name").cloned();
if let Some(color_str) = attrs.get("displaycolor")
&& let Some(color) = parse_color(color_str)
{
material.color = Some(color);
}
Ok(material)
}
pub(super) fn parse_texture2d<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
resource_parse_order: usize,
) -> Result<Texture2D> {
let attrs = parse_attributes(reader, e)?;
let id = attrs
.get("id")
.ok_or_else(|| Error::missing_attribute("texture2d", "id"))?
.parse::<usize>()?;
let path = attrs
.get("path")
.ok_or_else(|| Error::missing_attribute("texture2d", "path"))?
.to_string();
let contenttype = attrs
.get("contenttype")
.ok_or_else(|| Error::InvalidXml("texture2d missing contenttype attribute".to_string()))?
.to_string();
let mut texture = Texture2D::new(id, path, contenttype);
texture.parse_order = resource_parse_order;
if let Some(tileu_str) = attrs.get("tilestyleu") {
texture.tilestyleu = match tileu_str.to_lowercase().as_str() {
"wrap" => TileStyle::Wrap,
"mirror" => TileStyle::Mirror,
"clamp" => TileStyle::Clamp,
"none" => TileStyle::None,
_ => TileStyle::Wrap,
};
}
if let Some(tilev_str) = attrs.get("tilestylev") {
texture.tilestylev = match tilev_str.to_lowercase().as_str() {
"wrap" => TileStyle::Wrap,
"mirror" => TileStyle::Mirror,
"clamp" => TileStyle::Clamp,
"none" => TileStyle::None,
_ => TileStyle::Wrap,
};
}
if let Some(filter_str) = attrs.get("filter") {
texture.filter = match filter_str.to_lowercase().as_str() {
"auto" => FilterMode::Auto,
"linear" => FilterMode::Linear,
"nearest" => FilterMode::Nearest,
_ => FilterMode::Auto,
};
}
Ok(texture)
}
pub(super) fn validate_texture_file_paths<R: Read + std::io::Seek>(
package: &mut Package<R>,
model: &Model,
) -> Result<()> {
let encrypted_files: Vec<String> = model
.secure_content
.as_ref()
.map(|sc| sc.encrypted_files.clone())
.unwrap_or_default();
for texture in &model.resources.texture2d_resources {
if encrypted_files.contains(&texture.path) {
continue;
}
let normalized_path = texture.path.trim_start_matches('/');
let file_exists = package.has_file(normalized_path) || package.has_file(&texture.path);
if !file_exists {
return Err(Error::InvalidModel(format!(
"Texture2D resource {}: Path '{}' references a file that does not exist in the 3MF package.\n\
Per 3MF Material Extension spec, texture paths must reference valid files in the package.\n\
Check that:\n\
- The texture file is included in the 3MF package\n\
- The path is correct (case-sensitive)\n\
- The path format follows 3MF conventions\n\
Available files can be checked using ZIP archive tools.",
texture.id, texture.path
)));
}
}
Ok(())
}
pub(super) fn parse_basematerials_start<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
resource_parse_order: usize,
) -> Result<BaseMaterialGroup> {
let attrs = parse_attributes(reader, e)?;
let id = attrs
.get("id")
.ok_or_else(|| Error::missing_attribute("basematerials", "id"))?
.parse::<usize>()?;
let mut group = BaseMaterialGroup::new(id);
group.parse_order = resource_parse_order;
Ok(group)
}
pub(super) fn parse_base_element<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<BaseMaterial> {
let attrs = parse_attributes(reader, e)?;
validate_attributes(&attrs, &["name", "displaycolor"], "base")?;
let name = attrs.get("name").cloned().unwrap_or_default();
let displaycolor = if let Some(color_str) = attrs.get("displaycolor") {
parse_color(color_str).unwrap_or((255, 255, 255, 255))
} else {
(255, 255, 255, 255)
};
Ok(BaseMaterial::new(name, displaycolor))
}
pub(super) fn parse_colorgroup_start<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
resource_parse_order: usize,
) -> Result<ColorGroup> {
let attrs = parse_attributes(reader, e)?;
let id = attrs
.get("id")
.ok_or_else(|| Error::missing_attribute("colorgroup", "id"))?
.parse::<usize>()?;
let mut group = ColorGroup::new(id);
group.parse_order = resource_parse_order;
Ok(group)
}
pub(super) fn parse_color_element<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
colorgroup_id: usize,
) -> Result<(u8, u8, u8, u8)> {
let attrs = parse_attributes(reader, e)?;
let color_str = attrs
.get("color")
.ok_or_else(|| Error::missing_attribute("color", "color"))?;
parse_color(color_str).ok_or_else(|| {
Error::InvalidXml(format!(
"Invalid color format '{}' in colorgroup {}.\n\
Colors must be in format #RRGGBB or #RRGGBBAA where each component is a hexadecimal value (0-9, A-F).\n\
Examples: #FF0000 (red), #00FF0080 (semi-transparent green)",
color_str, colorgroup_id
))
})
}
pub(super) fn parse_texture2dgroup_start<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
resource_parse_order: usize,
) -> Result<Texture2DGroup> {
let attrs = parse_attributes(reader, e)?;
let id = attrs
.get("id")
.ok_or_else(|| Error::missing_attribute("texture2dgroup", "id"))?
.parse::<usize>()?;
let texid = attrs
.get("texid")
.ok_or_else(|| Error::missing_attribute("texture2dgroup", "texid"))?
.parse::<usize>()?;
let mut group = Texture2DGroup::new(id, texid);
group.parse_order = resource_parse_order;
Ok(group)
}
pub(super) fn parse_tex2coord<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<Tex2Coord> {
let attrs = parse_attributes(reader, e)?;
let u = attrs
.get("u")
.ok_or_else(|| Error::missing_attribute("tex2coord", "u"))?
.parse::<f32>()?;
let v = attrs
.get("v")
.ok_or_else(|| Error::missing_attribute("tex2coord", "v"))?
.parse::<f32>()?;
Ok(Tex2Coord::new(u, v))
}
pub(super) fn parse_compositematerials_start<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
resource_parse_order: usize,
) -> Result<CompositeMaterials> {
let attrs = parse_attributes(reader, e)?;
let id = attrs
.get("id")
.ok_or_else(|| Error::InvalidXml("compositematerials missing id attribute".to_string()))?
.parse::<usize>()?;
let matid = attrs
.get("matid")
.ok_or_else(|| Error::InvalidXml("compositematerials missing matid attribute".to_string()))?
.parse::<usize>()?;
let matindices_str = attrs.get("matindices").ok_or_else(|| {
Error::InvalidXml("compositematerials missing matindices attribute".to_string())
})?;
let matindices: Vec<usize> = matindices_str
.split_whitespace()
.map(|s| {
s.parse::<usize>().map_err(|_| {
Error::InvalidXml(format!(
"compositematerials matindices contains invalid value '{}'",
s
))
})
})
.collect::<Result<Vec<usize>>>()?;
if matindices.is_empty() {
return Err(Error::InvalidXml(
"compositematerials matindices must contain at least one valid index".to_string(),
));
}
let mut group = CompositeMaterials::new(id, matid, matindices);
group.parse_order = resource_parse_order;
Ok(group)
}
pub(super) fn parse_composite<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<Composite> {
let attrs = parse_attributes(reader, e)?;
let values_str = attrs
.get("values")
.ok_or_else(|| Error::InvalidXml("composite missing values attribute".to_string()))?;
let values: Vec<f32> = values_str
.split_whitespace()
.map(|s| {
s.parse::<f32>().map_err(|_| {
Error::InvalidXml(format!("composite values contains invalid number '{}'", s))
})
})
.collect::<Result<Vec<f32>>>()?;
if values.is_empty() {
return Err(Error::InvalidXml(
"composite values must contain at least one valid number".to_string(),
));
}
Ok(Composite::new(values))
}
pub(super) fn parse_multiproperties_start<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
resource_parse_order: usize,
) -> Result<MultiProperties> {
let attrs = parse_attributes(reader, e)?;
let id = attrs
.get("id")
.ok_or_else(|| Error::InvalidXml("multiproperties missing id attribute".to_string()))?
.parse::<usize>()?;
let pids_str = attrs
.get("pids")
.ok_or_else(|| Error::InvalidXml("multiproperties missing pids attribute".to_string()))?;
let pids: Vec<usize> = pids_str
.split_whitespace()
.map(|s| {
s.parse::<usize>().map_err(|_| {
Error::InvalidXml(format!(
"multiproperties pids contains invalid value '{}'",
s
))
})
})
.collect::<Result<Vec<usize>>>()?;
if pids.is_empty() {
return Err(Error::InvalidXml(
"multiproperties pids must contain at least one valid ID".to_string(),
));
}
let mut multi = MultiProperties::new(id, pids);
multi.parse_order = resource_parse_order;
if let Some(blend_str) = attrs.get("blendmethods") {
multi.blendmethods = blend_str
.split_whitespace()
.filter_map(|s| match s.to_lowercase().as_str() {
"mix" => Some(BlendMethod::Mix),
"multiply" => Some(BlendMethod::Multiply),
_ => None,
})
.collect();
}
Ok(multi)
}
pub(super) fn parse_multi<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<Multi> {
let attrs = parse_attributes(reader, e)?;
let pindices_str = attrs
.get("pindices")
.ok_or_else(|| Error::InvalidXml("multi missing pindices attribute".to_string()))?;
let pindices: Vec<usize> = if pindices_str.trim().is_empty() {
Vec::new()
} else {
pindices_str
.split_whitespace()
.map(|s| {
s.parse::<usize>().map_err(|_| {
Error::InvalidXml(format!("multi pindices contains invalid value '{}'", s))
})
})
.collect::<Result<Vec<usize>>>()?
};
Ok(Multi::new(pindices))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse_model_xml;
#[test]
fn test_parse_color() {
assert_eq!(parse_color("#FF0000"), Some((255, 0, 0, 255)));
assert_eq!(parse_color("#00FF00"), Some((0, 255, 0, 255)));
assert_eq!(parse_color("#0000FF"), Some((0, 0, 255, 255)));
assert_eq!(parse_color("#FF000080"), Some((255, 0, 0, 128)));
assert_eq!(parse_color("#00FF00FF"), Some((0, 255, 0, 255)));
assert_eq!(parse_color("#FF"), None);
assert_eq!(parse_color("FF0000"), Some((255, 0, 0, 255)));
}
#[test]
fn test_parse_color_black_white() {
assert_eq!(parse_color("#000000"), Some((0, 0, 0, 255)));
assert_eq!(parse_color("#FFFFFF"), Some((255, 255, 255, 255)));
}
#[test]
fn test_parse_color_zero_alpha() {
assert_eq!(parse_color("#FF000000"), Some((255, 0, 0, 0)));
}
#[test]
fn test_parse_base_materials_via_xml() {
let xml = r##"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<basematerials id="1">
<base name="Red" displaycolor="#FF0000"/>
<base name="Green" displaycolor="#00FF00"/>
<base name="Transparent" displaycolor="#FFFFFF00"/>
</basematerials>
<object id="2" pid="1" pindex="0">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>"##;
let model = parse_model_xml(xml).unwrap();
let group = &model.resources.base_material_groups[0];
assert_eq!(group.materials.len(), 3);
assert_eq!(group.materials[0].displaycolor, (255, 0, 0, 255));
assert_eq!(group.materials[1].displaycolor, (0, 255, 0, 255));
assert_eq!(group.materials[2].displaycolor.3, 0);
}
#[test]
fn test_parse_texture2d_via_xml() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02">
<resources>
<texture2d id="1" path="/3D/Textures/tex.png" contenttype="image/png"
tilestyleu="wrap" tilestylev="mirror" filter="linear"/>
<object id="2">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert_eq!(model.resources.texture2d_resources.len(), 1);
let tex = &model.resources.texture2d_resources[0];
assert_eq!(tex.id, 1);
assert_eq!(tex.path, "/3D/Textures/tex.png");
assert_eq!(tex.contenttype, "image/png");
}
#[test]
fn test_parse_texture2d_missing_id_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<texture2d path="/3D/Textures/tex.png" contenttype="image/png"/>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
}
#[test]
fn test_parse_texture2dgroup_with_tex2coords() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02">
<resources>
<texture2d id="1" path="/3D/Textures/tex.png" contenttype="image/png"/>
<texture2dgroup id="2" texid="1">
<tex2coord u="0.0" v="0.0"/>
<tex2coord u="1.0" v="0.0"/>
<tex2coord u="0.5" v="1.0"/>
</texture2dgroup>
<object id="3" pid="2" pindex="0">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2" p1="0" p2="1" p3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="3"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert_eq!(model.resources.texture2d_groups.len(), 1);
let group = &model.resources.texture2d_groups[0];
assert_eq!(group.tex2coords.len(), 3);
assert_eq!(group.tex2coords[0].u, 0.0);
assert_eq!(group.tex2coords[1].u, 1.0);
}
#[test]
fn test_parse_compositematerials_via_xml() {
let xml = r##"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02">
<resources>
<basematerials id="1">
<base name="Red" displaycolor="#FF0000"/>
<base name="Blue" displaycolor="#0000FF"/>
</basematerials>
<compositematerials id="2" matid="1" matindices="0 1">
<composite values="0.5 0.5"/>
<composite values="0.8 0.2"/>
</compositematerials>
<object id="3" pid="2" pindex="0">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="3"/>
</build>
</model>"##;
let model = parse_model_xml(xml).unwrap();
assert_eq!(model.resources.composite_materials.len(), 1);
assert_eq!(model.resources.composite_materials[0].composites.len(), 2);
}
#[test]
fn test_parse_multiproperties_via_xml() {
let xml = r##"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02">
<resources>
<basematerials id="1">
<base name="Red" displaycolor="#FF0000"/>
<base name="Blue" displaycolor="#0000FF"/>
</basematerials>
<colorgroup id="2">
<color color="#FF0000"/>
<color color="#00FF00"/>
</colorgroup>
<multiproperties id="3" pids="1 2" blendmethods="mix">
<multi pindices="0 0"/>
<multi pindices="1 1"/>
</multiproperties>
<object id="4" pid="3" pindex="0">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="4"/>
</build>
</model>"##;
let model = parse_model_xml(xml).unwrap();
assert_eq!(model.resources.multi_properties.len(), 1);
assert_eq!(model.resources.multi_properties[0].multis.len(), 2);
}
#[test]
fn test_parse_tilestyles() {
for (u_style, v_style) in &[
("wrap", "wrap"),
("mirror", "clamp"),
("clamp", "none"),
("none", "mirror"),
] {
let xml = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<texture2d id="1" path="/3D/t.png" contenttype="image/png"
tilestyleu="{u}" tilestylev="{v}"/>
<object id="2">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build><item objectid="2"/></build>
</model>"#,
u = u_style,
v = v_style
);
assert!(
parse_model_xml(&xml).is_ok(),
"Failed for tilestyleu={}, tilestylev={}",
u_style,
v_style
);
}
}
}