Skip to main content

draco_gltf/
import.rs

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/// Lossless glTF document plus its resolved resources.
19#[derive(Clone)]
20pub struct Import {
21    /// Lossless parsed glTF document.
22    pub document: Document,
23    /// Resolved buffer resources indexed by document buffer index.
24    pub resources: ResourceStore,
25    /// Container format from which this import was read.
26    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/// A portable JSON glTF document and the companion resources it references.
35///
36/// Write `json` to the `.gltf` file and each [`GltfResource`] relative to it.
37/// Data URIs remain embedded in `json`; every materialized buffer with a
38/// non-data URI is returned exactly once in `resources`.
39#[derive(Clone, Debug)]
40pub struct GltfOutput {
41    /// Serialized JSON document bytes.
42    pub json: Vec<u8>,
43    /// Companion resources to write relative to the JSON document.
44    pub resources: Vec<GltfResource>,
45}
46
47/// One companion resource produced by [`Import::to_gltf_output`].
48#[derive(Clone, Debug)]
49pub struct GltfResource {
50    /// Relative URI assigned to the companion resource.
51    pub uri: String,
52    /// Resource bytes.
53    pub bytes: Vec<u8>,
54}
55
56/// Default maximum explicit nested-asset depth for [`Import::load_asset`].
57pub const DEFAULT_EXTERNAL_ASSET_DEPTH: usize = 32;
58
59/// Resolves URIs in an embedded child against the parent's virtual `files`
60/// directory before falling back to the caller's resolver.
61#[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    /// Validates the document and all registered extension handlers.
109    ///
110    /// With `strict-validation`, this also checks the complete scene graph.
111    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    /// Iterates primitives carrying the built-in Draco extension.
170    #[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    /// Decodes a primitive through the supplied extension registry.
192    #[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            // Only an accessor that promises more vertices than the stream can
221            // supply is fatal: the missing ones have nowhere to come from.
222            //
223            // The other direction is what real encoders emit. Draco stores
224            // connectivity per position vertex and re-splits it at attribute
225            // seams while decoding, so a mesh whose normals or texture
226            // coordinates break along an edge decodes to more points than the
227            // accessor written before compression declares. glTF-Pipeline,
228            // Blender and the Draco encoder itself all produce such files —
229            // Three.js's ferrari.glb among them — and every browser viewer
230            // reads them, because the decoded geometry is self-consistent:
231            // indices, positions and attributes all come out of the same
232            // stream. Refusing them would reject working files over metadata
233            // the extension has already superseded.
234            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    /// Reads one ordinary or Draco-compressed primitive into packed buffers.
274    ///
275    /// Sparse overlays and byte strides are materialized without changing
276    /// component types or normalization flags. Draco is decoded only when the
277    /// `draco-decode` feature is enabled.
278    #[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                // The accessor, not the Draco attribute, defines how the
302                // decoded integers are read. KHR_draco_mesh_compression makes
303                // the accessor authoritative, and encoders leave the Draco
304                // flag unset, so a normalized COLOR_0 would otherwise reach the
305                // consumer as raw 0..65535 values.
306                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                // Only the uncompressed path can name a source accessor. A
352                // Draco primitive's bytes come from its own codec stream, so
353                // two primitives naming one accessor say nothing about whether
354                // their vertex data is the same.
355                .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    /// Decodes an ordinary (non-Draco) triangle or point primitive through the
381    /// same packed geometry contract used by readers and writers.
382    #[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    /// Serializes this import into the requested container format.
422    ///
423    /// [`crate::OutputFormat::GltfJson`] is valid only when every materialized
424    /// buffer already has an embedded or external URI. For transformed scenes
425    /// that need newly generated companion buffers, use
426    /// [`Import::to_gltf_output`] instead. GLB output embeds all resolved
427    /// buffers in one binary chunk.
428    ///
429    /// ```
430    /// # use draco_gltf::{import_slice, OutputFormat};
431    /// # let input = br#"{"asset":{"version":"2.0"},"buffers":[],"meshes":[]}"#;
432    /// let scene = import_slice(input, None)?;
433    /// let glb = scene.to_bytes(OutputFormat::GlbV2)?;
434    /// assert_eq!(&glb[0..4], b"glTF");
435    /// # Ok::<(), draco_gltf::Error>(())
436    /// ```
437    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    /// Serializes a self-contained `.gltf` output bundle.
469    ///
470    /// Unlike [`Import::to_bytes`] with [`crate::OutputFormat::GltfJson`],
471    /// this method returns companion buffer payloads as well. Buffers without
472    /// a URI (for example a Draco payload appended during compression) receive
473    /// a deterministic `buffer-{index}.bin` URI in the returned JSON.
474    ///
475    /// ```
476    /// # use draco_gltf::import_slice;
477    /// # let input = br#"{"asset":{"version":"2.0"},"buffers":[],"meshes":[]}"#;
478    /// let scene = import_slice(input, None)?;
479    /// let output = scene.to_gltf_output()?;
480    /// assert!(!output.json.is_empty());
481    /// assert!(output.resources.is_empty());
482    /// # Ok::<(), draco_gltf::Error>(())
483    /// ```
484    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    /// Creates a GLB payload by consolidating resolved buffers while retaining
523    /// every bufferView index and all non-resource JSON verbatim.
524    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                // The compressed range of a meshopt view names its own buffer,
548                // so it has to follow the view onto the consolidated buffer.
549                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    /// Materializes all Draco primitives as ordinary indexed triangle geometry.
567    #[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    /// Lists declared glTF 2.1 `files` entries without resolving them.
609    #[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    /// Explicitly resolves and parses an external-asset model declaration.
615    #[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    /// URI chain leading to this import. It is intended for diagnostics and
635    /// explicit cycle detection; it never triggers recursive loading itself.
636    #[cfg(feature = "resources")]
637    pub fn provenance(&self) -> &[String] {
638        &self.provenance
639    }
640
641    /// Explicitly resolves and parses one nested glTF file.
642    #[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    /// Explicitly loads one nested asset with a caller-selected graph depth limit.
662    #[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
756/// Rebases one `{buffer, byteOffset}` pair onto the consolidated GLB buffer.
757///
758/// `offsets` holds where each declared buffer starts in the merged binary
759/// chunk, indexed the way the document declared them.
760fn 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
781/// Expands every `EXT_meshopt_compression` buffer view into its target buffer.
782///
783/// The extension stores compressed views in a real buffer and points the plain
784/// glTF view at a zero-filled fallback buffer, so decoding here keeps every
785/// downstream accessor read unaware of the compression.
786fn 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
848/// Parses glTF or GLB bytes and applies the selected profile's basic checks.
849///
850/// Enable `strict-validation` to validate all cross-references before loading.
851pub 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"))]
863/// Opens a glTF or GLB file and applies the selected profile's basic checks.
864pub 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
881/// Parses a container with explicit resource, quota, profile and extension options.
882pub 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    /// One vertex of four zero deltas, so the decoded value is the tail
943    /// baseline `[1, 2, 3, 4]`. Every byte group uses the literal encoding.
944    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    /// gltfpack wrote `KHR_meshopt_compression` before the extension was
978    /// ratified under the `EXT_` prefix, and assets carrying that spelling are
979    /// still in circulation. The extension object, the bitstream and the
980    /// fallback convention are identical, so refusing them refuses a file over
981    /// its name: the fallback buffer has no URI, and a reader that does not
982    /// recognise the extension sees a buffer it has no reason to accept.
983    #[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    /// GLB output merges every declared buffer into one binary chunk, so a
1006    /// compressed range that does not start the chunk only survives when the
1007    /// extension's own offsets are rebased along with the buffer view's.
1008    #[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        // Without `extensionsRequired` the fallback buffer holds real data, so
1045        // it must still be resolved from its URI rather than zeroed.
1046        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}