1use std::path::Path;
2
3use crate::json::Value;
4
5use crate::extensions::{meshopt_extension, meshopt_extension_mut};
6#[cfg(feature = "draco-decode")]
7use crate::PrimitiveRef;
8use crate::{Document, Error, ExtensionRegistry, ResourceStore, Result, ValidationProfile};
9#[cfg(feature = "resources")]
10use crate::{ExternalAssetIndex, FileIndex};
11use draco_io::{
12 meshopt, parse_gltf_container, resolve_gltf_buffers, GltfBufferReference, GltfContainerFormat,
13 MeshoptFilter, MeshoptMode, ResourceLimits, ResourceResolver,
14};
15#[cfg(not(target_arch = "wasm32"))]
16use draco_io::{ExternalFilePolicy, FileResourceResolver};
17
18#[derive(Clone)]
20pub struct Import {
21 pub document: Document,
23 pub resources: ResourceStore,
25 pub input_format: GltfContainerFormat,
27 profile: ValidationProfile,
28 #[cfg(any(feature = "draco-decode", feature = "draco-encode"))]
29 pub(crate) extensions: ExtensionRegistry,
30 #[cfg(feature = "resources")]
31 provenance: Vec<String>,
32}
33
34#[derive(Clone, Debug)]
40pub struct GltfOutput {
41 pub json: Vec<u8>,
43 pub resources: Vec<GltfResource>,
45}
46
47#[derive(Clone, Debug)]
49pub struct GltfResource {
50 pub uri: String,
52 pub bytes: Vec<u8>,
54}
55
56pub const DEFAULT_EXTERNAL_ASSET_DEPTH: usize = 32;
58
59#[cfg(feature = "resources")]
62struct PackagedResolver<'a> {
63 import: &'a Import,
64 fallback: &'a dyn ResourceResolver,
65}
66
67#[cfg(feature = "resources")]
68impl ResourceResolver for PackagedResolver<'_> {
69 fn resolve(&self, uri: &str) -> std::result::Result<Vec<u8>, draco_io::GltfError> {
70 let Some(file) = self
71 .import
72 .document
73 .files()
74 .into_iter()
75 .find(|file| file.name() == Some(uri))
76 else {
77 return self.fallback.resolve(uri);
78 };
79 if file.value().get("bufferView").is_some() {
80 return self
81 .import
82 .embedded_file_bytes(file.value())
83 .map_err(|error| draco_io::GltfError::InvalidGltf(error.to_string()));
84 }
85 if let Some(source) = file.value().get("uri").and_then(Value::as_str) {
86 return draco_io::resolve_resource_uri(source, Some(self.fallback), None);
87 }
88 Err(draco_io::GltfError::InvalidGltf(format!(
89 "packaged file {uri:?} has no source"
90 )))
91 }
92}
93
94impl Import {
95 #[cfg(feature = "write")]
96 pub(crate) const fn validation_profile(&self) -> ValidationProfile {
97 self.profile
98 }
99
100 #[cfg(feature = "write")]
101 pub(crate) fn validate_after_write(&self) -> Result<()> {
102 self.document.validate(self.profile)?;
103 #[cfg(feature = "draco-decode")]
104 self.extensions.validate(&self.document)?;
105 Ok(())
106 }
107
108 pub fn validate(&self, extensions: &ExtensionRegistry) -> Result<()> {
112 self.document.validate(self.profile)?;
113 extensions.validate(&self.document)?;
114 Ok(())
115 }
116
117 #[cfg(all(feature = "write", feature = "draco-decode"))]
118 pub(crate) fn ensure_transform_safe(&self, primitive: PrimitiveRef<'_>) -> Result<()> {
119 let Some(extensions) = primitive
120 .value()
121 .get("extensions")
122 .and_then(Value::as_object)
123 else {
124 return Ok(());
125 };
126 for (name, _) in extensions {
127 if !self.extensions.allows_binary_transform(name) {
128 return Err(Error::Extension(format!(
129 "cannot transform primitive with extension {name:?}: its binary-reference semantics are not registered as transform-safe"
130 )));
131 }
132 }
133 Ok(())
134 }
135
136 #[cfg(feature = "draco-encode")]
137 pub(crate) fn ensure_document_binary_transform_safe(&self) -> Result<()> {
138 fn visit(value: &Value, registry: &ExtensionRegistry) -> Result<()> {
139 match value {
140 Value::Array(values) => {
141 for value in values {
142 visit(value, registry)?;
143 }
144 }
145 Value::Object(values) => {
146 for (name, value) in values {
147 if name == "extensions" {
148 let extensions = value.as_object().ok_or_else(|| {
149 Error::Extension("extensions is not an object".into())
150 })?;
151 for (extension, _) in extensions {
152 if !registry.allows_binary_transform(extension) {
153 return Err(Error::Extension(format!(
154 "cannot produce Draco-only output with extension {extension:?}: its binary-reference semantics are not registered as transform-safe"
155 )));
156 }
157 }
158 }
159 visit(value, registry)?;
160 }
161 }
162 _ => {}
163 }
164 Ok(())
165 }
166 visit(self.document.as_value(), &self.extensions)
167 }
168
169 #[cfg(feature = "draco-decode")]
171 pub fn draco_primitives(&self) -> impl Iterator<Item = PrimitiveRef<'_>> + '_ {
172 self.document
173 .meshes()
174 .into_iter()
175 .flat_map(move |mesh| {
176 let count = mesh
177 .value()
178 .get("primitives")
179 .and_then(Value::as_array)
180 .map_or(0, |values| values.len());
181 (0..count)
182 .filter_map(move |primitive| self.document.primitive(mesh.index(), primitive))
183 })
184 .filter(|primitive| {
185 primitive
186 .extension(crate::KHR_DRACO_MESH_COMPRESSION)
187 .is_some()
188 })
189 }
190
191 #[cfg(feature = "draco-decode")]
193 pub fn decode_draco_primitive(&self, primitive: PrimitiveRef<'_>) -> Result<draco_core::Mesh> {
194 self.validate(&self.extensions)?;
195 let mesh = self
196 .extensions
197 .decode_primitive(&self.document, &self.resources, primitive)?;
198 self.validate_decoded_draco_counts(primitive, &mesh)?;
199 Ok(mesh)
200 }
201
202 #[cfg(feature = "draco-decode")]
203 fn validate_decoded_draco_counts(
204 &self,
205 primitive: PrimitiveRef<'_>,
206 mesh: &draco_core::Mesh,
207 ) -> Result<()> {
208 let decoded_points = u64::try_from(mesh.num_points())
209 .map_err(|_| Error::ResourceLimit("decoded Draco point count exceeds u64".into()))?;
210 for (semantic, index) in primitive.attribute_indices() {
211 let declared = self
212 .document
213 .accessor(index)
214 .and_then(|accessor| accessor.count())
215 .ok_or_else(|| {
216 Error::Validation(vec![format!(
217 "Draco attribute {semantic:?} accessor count is missing"
218 )])
219 })?;
220 if declared > decoded_points {
235 return Err(crate::GeometryError::DracoAccessorCount {
236 semantic: semantic.into(),
237 decoded: decoded_points,
238 declared,
239 }
240 .into());
241 }
242 }
243
244 if primitive.mode() == crate::PrimitiveMode::Triangles.to_gltf() {
245 if let Some(index) = primitive.indices() {
246 let declared = self
247 .document
248 .accessor(index)
249 .and_then(|accessor| accessor.count())
250 .ok_or_else(|| {
251 Error::Validation(vec!["Draco index accessor count is missing".into()])
252 })?;
253 let decoded = mesh
254 .num_faces()
255 .checked_mul(3)
256 .and_then(|count| u64::try_from(count).ok())
257 .ok_or_else(|| {
258 Error::ResourceLimit("decoded Draco index count exceeds u64".into())
259 })?;
260 if declared != decoded {
261 return Err(crate::GeometryError::DracoAccessorCount {
262 semantic: "indices".into(),
263 decoded,
264 declared,
265 }
266 .into());
267 }
268 }
269 }
270 Ok(())
271 }
272
273 #[cfg(feature = "geometry")]
279 pub fn read_primitive(
280 &self,
281 primitive: crate::PrimitiveIndex,
282 ) -> Result<crate::PackedGeometry> {
283 let reference = self
284 .document
285 .primitive(primitive.mesh, primitive.primitive)
286 .ok_or_else(|| Error::Extension("primitive out of range".into()))?;
287 let mode = crate::PrimitiveMode::from_gltf(reference.mode()).ok_or_else(|| {
288 Error::Geometry(crate::GeometryError::InvalidPrimitiveMode(reference.mode()))
289 })?;
290 if reference
291 .extension(crate::KHR_DRACO_MESH_COMPRESSION)
292 .is_some()
293 {
294 #[cfg(feature = "draco-decode")]
295 {
296 let contract = crate::extensions::parse_draco_extension(
297 reference.extension(crate::KHR_DRACO_MESH_COMPRESSION),
298 )?
299 .ok_or_else(|| Error::Extension("missing Draco extension".into()))?;
300 let decoded = self.decode_draco_primitive(reference)?;
301 let normalized: std::collections::BTreeMap<String, bool> = reference
307 .attribute_indices()
308 .map(|(semantic, index)| {
309 let normalized = self
310 .document
311 .accessor(index)
312 .is_some_and(crate::Accessor::normalized);
313 (semantic.to_owned(), normalized)
314 })
315 .collect();
316 let geometry = crate::PackedGeometry::from_draco_mesh(
317 mode,
318 &decoded,
319 &contract.attributes,
320 &normalized,
321 )?;
322 geometry.validate(self.profile)?;
323 return Ok(geometry);
324 }
325 #[cfg(not(feature = "draco-decode"))]
326 return Err(Error::Extension(
327 "Draco primitive reading requires feature draco-decode".into(),
328 ));
329 }
330
331 let source = crate::DocumentAccessorSource::new(&self.document, &self.resources);
332 let attributes = reference
333 .attribute_indices()
334 .map(|(semantic, index)| {
335 let data = source.read_geometry_accessor(index.0)?;
336 let component_type = crate::ComponentType::from_gltf(data.component_type as u64)
337 .ok_or_else(|| {
338 Error::Extension(format!(
339 "unsupported accessor componentType {}",
340 data.component_type
341 ))
342 })?;
343 crate::PackedAttribute::new(
344 semantic,
345 data.count,
346 data.components,
347 component_type,
348 data.normalized,
349 data.bytes,
350 )
351 .map(|attribute| attribute.with_source_accessor(index.0))
356 .map_err(Error::Geometry)
357 })
358 .collect::<Result<Vec<_>>>()?;
359 let indices = reference
360 .indices()
361 .map(|index| {
362 let data = source.read_geometry_accessor(index.0)?;
363 let component_type = crate::ComponentType::from_gltf(data.component_type as u64)
364 .ok_or_else(|| {
365 Error::Extension(format!(
366 "unsupported index componentType {}",
367 data.component_type
368 ))
369 })?;
370 crate::PackedIndices::new(data.count, component_type, data.bytes)
371 .map(|indices| indices.with_source_accessor(index.0))
372 .map_err(Error::Geometry)
373 })
374 .transpose()?;
375 let geometry = crate::PackedGeometry::new(mode, attributes, indices)?;
376 geometry.validate(self.profile)?;
377 Ok(geometry)
378 }
379
380 #[cfg(feature = "draco-encode")]
383 pub(crate) fn decode_geometry_primitive(
384 &self,
385 primitive: PrimitiveRef<'_>,
386 ) -> Result<(draco_core::Mesh, Vec<(String, u32)>)> {
387 if primitive
388 .extension(crate::KHR_DRACO_MESH_COMPRESSION)
389 .is_some()
390 {
391 return Err(Error::Extension("primitive uses Draco compression".into()));
392 }
393 let value = primitive.value();
394 let attributes = value
395 .get("attributes")
396 .and_then(Value::as_object)
397 .ok_or_else(|| Error::Extension("primitive attributes are invalid".into()))?
398 .iter()
399 .map(|(semantic, value)| {
400 value
401 .as_u64()
402 .and_then(|value| usize::try_from(value).ok())
403 .map(|index| (semantic.clone(), index))
404 .ok_or_else(|| Error::Extension(format!("attribute {semantic} is invalid")))
405 })
406 .collect::<Result<Vec<_>>>()?;
407 let indices = value
408 .get("indices")
409 .and_then(Value::as_u64)
410 .and_then(|value| usize::try_from(value).ok());
411 let mode = value.get("mode").and_then(Value::as_u64).unwrap_or(4) as u32;
412 let source = crate::DocumentAccessorSource::new(&self.document, &self.resources);
413 Ok(draco_io::decode_geometry(
414 &source,
415 mode,
416 &attributes,
417 indices,
418 )?)
419 }
420
421 pub fn to_bytes(&self, output: crate::OutputFormat) -> Result<Vec<u8>> {
438 let format = match output {
439 crate::OutputFormat::GltfJson => {
440 if self.document.buffers().into_iter().any(|buffer| {
441 buffer.value().get("uri").and_then(Value::as_str).is_none()
442 && self
443 .resources
444 .buffers
445 .get(buffer.index().0)
446 .is_some_and(|bytes| !bytes.is_empty())
447 }) {
448 return Err(Error::Extension(
449 "GltfJson cannot carry materialized companion buffers; use to_gltf_output()"
450 .into(),
451 ));
452 }
453 return self.document.to_json_bytes();
454 }
455 crate::OutputFormat::SameAsInput => self.input_format,
456 crate::OutputFormat::GlbV2 => draco_io::GltfContainerFormat::GlbV2,
457 crate::OutputFormat::GlbV3 => draco_io::GltfContainerFormat::GlbV3,
458 };
459 if format.is_glb() {
460 let (json, bin) = self.consolidated_glb_payload()?;
461 return Ok(draco_io::gltf_container::build_glb_from_json(
462 &json, &bin, format,
463 )?);
464 }
465 self.document.to_json_bytes()
466 }
467
468 pub fn to_gltf_output(&self) -> Result<GltfOutput> {
485 let declared = self.document.buffers().len();
486 if declared != self.resources.buffers.len() {
487 return Err(Error::ResourceLimit(format!(
488 "document declares {declared} buffers but resource store has {}",
489 self.resources.buffers.len()
490 )));
491 }
492 let mut document = self.document.clone();
493 let mut resources = Vec::new();
494 let buffers = document
495 .as_value_mut()
496 .get_mut("buffers")
497 .and_then(Value::as_array_mut)
498 .ok_or_else(|| Error::Validation(vec!["buffers is not an array".into()]))?;
499 for (index, (buffer, bytes)) in buffers.iter_mut().zip(&self.resources.buffers).enumerate()
500 {
501 let uri = buffer.get("uri").and_then(Value::as_str).map(str::to_owned);
502 let uri = match uri {
503 Some(uri) if uri.starts_with("data:") => continue,
504 Some(uri) => uri,
505 None => {
506 let uri = format!("buffer-{index}.bin");
507 buffer["uri"] = Value::from(uri.as_str());
508 uri
509 }
510 };
511 resources.push(GltfResource {
512 uri,
513 bytes: bytes.clone(),
514 });
515 }
516 Ok(GltfOutput {
517 json: document.to_json_bytes()?,
518 resources,
519 })
520 }
521
522 fn consolidated_glb_payload(&self) -> Result<(Vec<u8>, Vec<u8>)> {
525 let declared = self.document.buffers().len();
526 if declared != self.resources.buffers.len() {
527 return Err(Error::ResourceLimit(format!(
528 "document declares {declared} buffers but resource store has {}",
529 self.resources.buffers.len()
530 )));
531 }
532 let mut offsets = Vec::with_capacity(declared);
533 let mut bin = Vec::new();
534 for resource in &self.resources.buffers {
535 while !bin.len().is_multiple_of(4) {
536 bin.push(0);
537 }
538 offsets.push(bin.len());
539 bin.try_reserve(resource.len())
540 .map_err(|_| Error::ResourceLimit("GLB consolidation allocation failed".into()))?;
541 bin.extend_from_slice(resource);
542 }
543 let mut document = self.document.clone();
544 let root = document.as_value_mut();
545 if let Some(views) = root.get_mut("bufferViews").and_then(Value::as_array_mut) {
546 for (index, view) in views.iter_mut().enumerate() {
547 if let Some((name, extension)) = meshopt_extension_mut(view.get_mut("extensions")) {
550 rebase_buffer_reference(
551 extension,
552 &offsets,
553 &format!("bufferViews[{index}].extensions.{name}"),
554 )?;
555 }
556 rebase_buffer_reference(view, &offsets, &format!("bufferViews[{index}]"))?;
557 }
558 }
559 root["buffers"] = Value::Array(vec![Value::object([(
560 "byteLength",
561 Value::from(bin.len()),
562 )])]);
563 Ok((document.to_json_bytes()?, bin))
564 }
565
566 #[cfg(all(feature = "write", feature = "draco-decode"))]
568 pub fn decompress_in_place(&mut self) -> Result<()> {
569 let mut candidate = self.clone();
570 candidate.decompress_in_place_inner()?;
571 *self = candidate;
572 Ok(())
573 }
574
575 #[cfg(all(feature = "write", feature = "draco-decode"))]
576 fn decompress_in_place_inner(&mut self) -> Result<()> {
577 let mut primitives = Vec::new();
578 for mesh in self.document.meshes() {
579 let count = mesh
580 .value()
581 .get("primitives")
582 .and_then(Value::as_array)
583 .map_or(0, |values| values.len());
584 for primitive_index in 0..count {
585 let primitive = self
586 .document
587 .primitive(mesh.index(), primitive_index)
588 .unwrap();
589 if primitive
590 .extension(crate::KHR_DRACO_MESH_COMPRESSION)
591 .is_none()
592 {
593 continue;
594 }
595 self.ensure_transform_safe(primitive)?;
596 primitives.push(crate::PrimitiveIndex::new(mesh.index(), primitive_index));
597 }
598 }
599 for primitive in primitives {
600 let geometry = self.read_primitive(primitive)?;
601 self.write_raw_primitive_inner(primitive, &geometry)?;
602 }
603 self.document.validate(self.profile)?;
604 self.extensions.validate(&self.document)?;
605 Ok(())
606 }
607
608 #[cfg(feature = "resources")]
610 pub fn external_files(&self) -> impl Iterator<Item = FileIndex> + '_ {
611 self.document.files().into_iter().map(|file| file.index())
612 }
613
614 #[cfg(feature = "resources")]
616 pub fn load_external_asset(
617 &self,
618 asset: ExternalAssetIndex,
619 resolver: &dyn ResourceResolver,
620 limits: &ResourceLimits,
621 profile: ValidationProfile,
622 extensions: &ExtensionRegistry,
623 ) -> Result<Self> {
624 let file = self
625 .document
626 .external_asset(asset)
627 .and_then(|asset| asset.file())
628 .ok_or_else(|| {
629 Error::Extension(format!("external asset {} is out of range", asset.0))
630 })?;
631 self.load_asset(file, resolver, limits, profile, extensions)
632 }
633
634 #[cfg(feature = "resources")]
637 pub fn provenance(&self) -> &[String] {
638 &self.provenance
639 }
640
641 #[cfg(feature = "resources")]
643 pub fn load_asset(
644 &self,
645 file: FileIndex,
646 resolver: &dyn ResourceResolver,
647 limits: &ResourceLimits,
648 profile: ValidationProfile,
649 extensions: &ExtensionRegistry,
650 ) -> Result<Self> {
651 self.load_asset_with_depth(
652 file,
653 resolver,
654 limits,
655 profile,
656 extensions,
657 DEFAULT_EXTERNAL_ASSET_DEPTH,
658 )
659 }
660
661 #[cfg(feature = "resources")]
663 pub fn load_asset_with_depth(
664 &self,
665 file: FileIndex,
666 resolver: &dyn ResourceResolver,
667 limits: &ResourceLimits,
668 profile: ValidationProfile,
669 extensions: &ExtensionRegistry,
670 max_depth: usize,
671 ) -> Result<Self> {
672 let max_depth = limits
673 .max_external_asset_depth
674 .map_or(max_depth, |limit| limit.min(max_depth));
675 if self.provenance.len() >= max_depth {
676 return Err(Error::ResourceLimit(format!(
677 "nested glTF asset depth exceeds {max_depth}"
678 )));
679 }
680 let entry = self
681 .document
682 .file(file)
683 .ok_or_else(|| Error::Extension(format!("file {} is out of range", file.0)))?;
684 let packaged = entry.buffer_view().is_some()
685 || entry.uri().is_some_and(|uri| uri.starts_with("data:"));
686 let source = entry.uri().map(str::to_owned).unwrap_or_else(|| {
687 format!(
688 "bufferView:{}",
689 entry.value()["bufferView"].as_u64().unwrap_or(u64::MAX)
690 )
691 });
692 if self.provenance.iter().any(|ancestor| ancestor == &source) {
693 return Err(Error::Extension(format!(
694 "cyclic external glTF asset reference: {source}"
695 )));
696 }
697 let bytes = if let Some(uri) = entry.uri() {
698 draco_io::resolve_resource_uri(uri, Some(resolver), limits.max_resource_bytes)?
699 } else {
700 self.embedded_file_bytes(entry.value())?
701 };
702 let mut loaded = if packaged {
703 let packaged_resolver = PackagedResolver {
704 import: self,
705 fallback: resolver,
706 };
707 parse_with_options(
708 &bytes,
709 None,
710 Some(&packaged_resolver),
711 limits,
712 profile,
713 extensions,
714 )?
715 } else {
716 parse_with_options(&bytes, None, Some(resolver), limits, profile, extensions)?
717 };
718 loaded.provenance = self.provenance.clone();
719 loaded.provenance.push(source);
720 Ok(loaded)
721 }
722
723 #[cfg(feature = "resources")]
724 fn embedded_file_bytes(&self, file: &Value) -> Result<Vec<u8>> {
725 let view = file
726 .get("bufferView")
727 .and_then(Value::as_u64)
728 .and_then(|index| usize::try_from(index).ok())
729 .ok_or_else(|| Error::Extension("file has neither uri nor bufferView".into()))?;
730 let view = self
731 .document
732 .buffer_view(crate::BufferViewIndex(view))
733 .ok_or_else(|| Error::Extension("file bufferView is out of range".into()))?;
734 let buffer = view
735 .buffer()
736 .ok_or_else(|| Error::Extension("file bufferView has no buffer".into()))?;
737 let bytes = self
738 .resources
739 .buffers
740 .get(buffer.0)
741 .ok_or_else(|| Error::ResourceLimit("file buffer is not materialized".into()))?;
742 let start = usize::try_from(view.byte_offset())
743 .map_err(|_| Error::ResourceLimit("file byteOffset exceeds this platform".into()))?;
744 let length = view
745 .byte_length()
746 .and_then(|length| usize::try_from(length).ok())
747 .ok_or_else(|| Error::Extension("file bufferView has no byteLength".into()))?;
748 let end = start
749 .checked_add(length)
750 .filter(|end| *end <= bytes.len())
751 .ok_or_else(|| Error::Extension("file bufferView is outside its buffer".into()))?;
752 Ok(bytes[start..end].to_vec())
753 }
754}
755
756fn rebase_buffer_reference(value: &mut Value, offsets: &[usize], label: &str) -> Result<()> {
761 let buffer = value
762 .get("buffer")
763 .and_then(Value::as_u64)
764 .and_then(|value| usize::try_from(value).ok())
765 .ok_or_else(|| Error::Validation(vec![format!("{label}.buffer is not a valid index")]))?;
766 let prefix = *offsets.get(buffer).ok_or_else(|| {
767 Error::Validation(vec![format!(
768 "{label}.buffer references missing buffer {buffer}"
769 )])
770 })?;
771 let offset = value.get("byteOffset").and_then(Value::as_u64).unwrap_or(0);
772 let offset = usize::try_from(offset)
773 .ok()
774 .and_then(|offset| prefix.checked_add(offset))
775 .ok_or_else(|| Error::ResourceLimit(format!("{label} byteOffset overflow")))?;
776 value["buffer"] = Value::from(0usize);
777 value["byteOffset"] = Value::from(offset);
778 Ok(())
779}
780
781fn decode_meshopt_buffer_views(document: &Document, buffers: &mut [Vec<u8>]) -> Result<()> {
787 let Some(views) = document
788 .as_value()
789 .get("bufferViews")
790 .and_then(Value::as_array)
791 else {
792 return Ok(());
793 };
794 for (index, view) in views.iter().enumerate() {
795 let Some((_, extension)) = meshopt_extension(view.get("extensions")) else {
796 continue;
797 };
798 let fail = |message: &str| Error::Extension(format!("bufferViews[{index}]: {message}"));
799 let number = |value: Option<&Value>| {
800 value
801 .and_then(Value::as_u64)
802 .and_then(|value| usize::try_from(value).ok())
803 };
804
805 let source_buffer = number(extension.get("buffer"))
806 .filter(|buffer| *buffer < buffers.len())
807 .ok_or_else(|| fail("meshopt buffer is invalid"))?;
808 let source_offset = number(extension.get("byteOffset")).unwrap_or(0);
809 let source_length = number(extension.get("byteLength"))
810 .ok_or_else(|| fail("meshopt byteLength is invalid"))?;
811 let source = buffers[source_buffer]
812 .get(source_offset..)
813 .and_then(|bytes| bytes.get(..source_length))
814 .ok_or_else(|| fail("meshopt range is outside its buffer"))?
815 .to_vec();
816
817 let count =
818 number(extension.get("count")).ok_or_else(|| fail("meshopt count is invalid"))?;
819 let stride = number(extension.get("byteStride"))
820 .ok_or_else(|| fail("meshopt byteStride is invalid"))?;
821 let mode = MeshoptMode::from_name(
822 extension
823 .get("mode")
824 .and_then(Value::as_str)
825 .ok_or_else(|| fail("meshopt mode is missing"))?,
826 )?;
827 let filter = match extension.get("filter").and_then(Value::as_str) {
828 Some(name) => MeshoptFilter::from_name(name)?,
829 None => MeshoptFilter::None,
830 };
831
832 let target_buffer = number(view.get("buffer"))
833 .filter(|buffer| *buffer < buffers.len())
834 .ok_or_else(|| fail("buffer is invalid"))?;
835 let target_offset = number(view.get("byteOffset")).unwrap_or(0);
836 let target_length =
837 number(view.get("byteLength")).ok_or_else(|| fail("byteLength is invalid"))?;
838 let target = buffers[target_buffer]
839 .get_mut(target_offset..)
840 .and_then(|bytes| bytes.get_mut(..target_length))
841 .ok_or_else(|| fail("buffer view is outside its buffer"))?;
842
843 meshopt::decode_buffer_view(target, &source, mode, filter, count, stride)?;
844 }
845 Ok(())
846}
847
848pub fn parse(bytes: &[u8], profile: ValidationProfile) -> Result<Import> {
852 parse_with_options(
853 bytes,
854 None,
855 None,
856 &ResourceLimits::default(),
857 profile,
858 &ExtensionRegistry::default(),
859 )
860}
861
862#[cfg(not(target_arch = "wasm32"))]
863pub fn open(path: impl AsRef<Path>, profile: ValidationProfile) -> Result<Import> {
865 let path = path.as_ref();
866 let bytes = std::fs::read(path)?;
867 let resolver = FileResourceResolver::new(
868 path.parent().unwrap_or_else(|| Path::new(".")),
869 ExternalFilePolicy::ConfineToBase,
870 );
871 parse_with_options(
872 &bytes,
873 path.parent(),
874 Some(&resolver),
875 &ResourceLimits::default(),
876 profile,
877 &ExtensionRegistry::default(),
878 )
879}
880
881pub fn parse_with_options(
883 bytes: &[u8],
884 _base: Option<&Path>,
885 resolver: Option<&dyn ResourceResolver>,
886 limits: &ResourceLimits,
887 profile: ValidationProfile,
888 extensions: &ExtensionRegistry,
889) -> Result<Import> {
890 let container = parse_gltf_container(bytes)?;
891 let document = Document::from_json_bytes(container.json)?;
892 document.validate(profile)?;
893 extensions.validate(&document)?;
894 let mut references = Vec::new();
895 for buffer in document.buffers() {
896 let uri = buffer.value().get("uri").and_then(Value::as_str);
897 let byte_length = buffer
898 .value()
899 .get("byteLength")
900 .and_then(Value::as_u64)
901 .and_then(|value| usize::try_from(value).ok())
902 .ok_or_else(|| {
903 Error::Validation(vec![format!(
904 "buffer {} byteLength is invalid",
905 buffer.index().0
906 )])
907 })?;
908 let meshopt_fallback = meshopt_extension(buffer.value().get("extensions"))
909 .and_then(|(_, value)| value.get("fallback"))
910 .is_some_and(|value| matches!(value, Value::Bool(true)));
911 references.push(GltfBufferReference {
912 uri,
913 byte_length,
914 meshopt_fallback,
915 });
916 }
917 let mut buffers = resolve_gltf_buffers(
918 &references,
919 container.format,
920 container.bin,
921 resolver,
922 limits,
923 )?;
924 decode_meshopt_buffer_views(&document, &mut buffers)?;
925 Ok(Import {
926 document,
927 resources: ResourceStore { buffers },
928 input_format: container.format,
929 profile,
930 #[cfg(any(feature = "draco-decode", feature = "draco-encode"))]
931 extensions: extensions.clone(),
932 #[cfg(feature = "resources")]
933 provenance: Vec::new(),
934 })
935}
936
937#[cfg(test)]
938mod tests {
939 use super::*;
940 use draco_io::gltf_container::build_glb_from_json;
941
942 fn meshopt_vertex_stream() -> Vec<u8> {
945 let mut stream = vec![0xa0u8];
946 for _ in 0..4 {
947 stream.push(0x03);
948 stream.resize(stream.len() + 16, 0);
949 }
950 stream.resize(stream.len() + 28, 0);
951 stream.extend_from_slice(&[1, 2, 3, 4]);
952 stream
953 }
954
955 #[test]
956 fn meshopt_buffer_views_decode_into_the_fallback_buffer() {
957 let bin = meshopt_vertex_stream();
958 let json = format!(
959 r#"{{"asset":{{"version":"2.0"}},
960 "extensionsUsed":["EXT_meshopt_compression"],
961 "extensionsRequired":["EXT_meshopt_compression"],
962 "buffers":[{{"byteLength":{}}},
963 {{"byteLength":4,"extensions":{{"EXT_meshopt_compression":{{"fallback":true}}}}}}],
964 "bufferViews":[{{"buffer":1,"byteOffset":0,"byteLength":4,"byteStride":4,
965 "extensions":{{"EXT_meshopt_compression":{{"buffer":0,"byteOffset":0,"byteLength":{},
966 "byteStride":4,"mode":"ATTRIBUTES","count":1}}}}}}]}}"#,
967 bin.len(),
968 bin.len()
969 );
970 let glb = build_glb_from_json(json.as_bytes(), &bin, GltfContainerFormat::GlbV2).unwrap();
971
972 let import = parse(&glb, ValidationProfile::Gltf20).unwrap();
973
974 assert_eq!(import.resources.buffers[1], vec![1, 2, 3, 4]);
975 }
976
977 #[test]
984 fn the_pre_ratification_extension_name_decodes_the_same_way() {
985 let bin = meshopt_vertex_stream();
986 let json = format!(
987 r#"{{"asset":{{"version":"2.0"}},
988 "extensionsUsed":["KHR_meshopt_compression"],
989 "extensionsRequired":["KHR_meshopt_compression"],
990 "buffers":[{{"byteLength":{}}},
991 {{"byteLength":4,"extensions":{{"KHR_meshopt_compression":{{"fallback":true}}}}}}],
992 "bufferViews":[{{"buffer":1,"byteOffset":0,"byteLength":4,"byteStride":4,
993 "extensions":{{"KHR_meshopt_compression":{{"buffer":0,"byteOffset":0,"byteLength":{},
994 "byteStride":4,"mode":"ATTRIBUTES","count":1}}}}}}]}}"#,
995 bin.len(),
996 bin.len()
997 );
998 let glb = build_glb_from_json(json.as_bytes(), &bin, GltfContainerFormat::GlbV2).unwrap();
999
1000 let import = parse(&glb, ValidationProfile::Gltf20).unwrap();
1001
1002 assert_eq!(import.resources.buffers[1], vec![1, 2, 3, 4]);
1003 }
1004
1005 #[test]
1009 fn glb_output_rebases_a_meshopt_source_buffer_that_is_not_first() {
1010 let stream = meshopt_vertex_stream();
1011 let uri: String = stream
1012 .iter()
1013 .map(|byte| format!("%{byte:02X}"))
1014 .collect::<Vec<_>>()
1015 .concat();
1016 let json = format!(
1017 r#"{{"asset":{{"version":"2.0"}},
1018 "extensionsUsed":["EXT_meshopt_compression"],
1019 "extensionsRequired":["EXT_meshopt_compression"],
1020 "buffers":[{{"byteLength":4,"extensions":{{"EXT_meshopt_compression":{{"fallback":true}}}}}},
1021 {{"byteLength":{},"uri":"data:application/octet-stream,{uri}"}}],
1022 "bufferViews":[{{"buffer":0,"byteOffset":0,"byteLength":4,"byteStride":4,
1023 "extensions":{{"EXT_meshopt_compression":{{"buffer":1,"byteOffset":0,"byteLength":{},
1024 "byteStride":4,"mode":"ATTRIBUTES","count":1}}}}}}]}}"#,
1025 stream.len(),
1026 stream.len()
1027 );
1028
1029 let import = parse(json.as_bytes(), ValidationProfile::Gltf20).unwrap();
1030 assert_eq!(import.resources.buffers[0], vec![1, 2, 3, 4]);
1031
1032 let glb = import.to_bytes(crate::OutputFormat::GlbV2).unwrap();
1033 let reimported = parse(&glb, ValidationProfile::Gltf20).unwrap();
1034
1035 assert_eq!(
1036 &reimported.resources.buffers[0][..4],
1037 &[1, 2, 3, 4],
1038 "the consolidated buffer must still decode to the same vertex"
1039 );
1040 }
1041
1042 #[test]
1043 fn an_optional_extension_keeps_the_stored_fallback_data() {
1044 let json = r#"{"asset":{"version":"2.0"},
1047 "extensionsUsed":["EXT_meshopt_compression"],
1048 "buffers":[{"byteLength":4,"uri":"data:application/octet-stream;base64,AQIDBA==",
1049 "extensions":{"EXT_meshopt_compression":{"fallback":true}}}]}"#;
1050
1051 let import = parse(json.as_bytes(), ValidationProfile::Gltf20).unwrap();
1052
1053 assert_eq!(import.resources.buffers[0], vec![1, 2, 3, 4]);
1054 }
1055}