Skip to main content

draco_io/
gltf_compress.rs

1//! Document-preserving glTF Draco compression.
2//!
3//! [`compress_gltf_bytes`] takes a self-contained glTF or GLB document and
4//! returns a copy whose triangle-mesh geometry is compressed with
5//! `KHR_draco_mesh_compression`, while **everything else in the document is
6//! carried through untouched**: materials, textures, images, samplers, cameras,
7//! nodes, animations, skins, `extras`, and unknown extension JSON that has no
8//! opaque binary references. Unknown buffer/view/offset-like extension fields
9//! are rejected because they cannot be remapped safely.
10//!
11//! This is the key difference from the `read meshes -> write fresh glTF` path,
12//! which only models geometry and therefore drops materials and other content.
13//! Here we mutate the original JSON document in place and only touch the parts
14//! that change: the compressed primitives, their geometry accessors, the
15//! buffer, and the buffer views.
16//!
17//! # What gets compressed
18//!
19//! A primitive is compressed only when its structure can be reproduced. The
20//! default quantization is intentionally lossy; set an attribute class to
21//! `None` in [`QuantizationOptions`] to disable its quantization:
22//!
23//! - triangle list (`mode` 4 or absent), indexed or non-indexed (a fresh
24//!   indices accessor is generated for the non-indexed case),
25//! - not already Draco-compressed,
26//! - its geometry accessors are not shared with any other primitive,
27//! - decoding and re-encoding succeed and reproduce the exact attribute set.
28//!
29//! All standard attribute semantics are compressed, including `TANGENT`,
30//! `JOINTS_n`, `WEIGHTS_n`, multiple `TEXCOORD_n`/`COLOR_n`, and custom `_*`
31//! attributes: non-standard ones ride along inside the Draco stream as generic
32//! attributes and are named by the extension's attribute map (the glTF semantic
33//! lives in the map, not in the Draco attribute). Skinned and tangent-bearing
34//! meshes are therefore compressed, not just preserved.
35//!
36//! Primitives that fall outside this scope — already Draco, sharing geometry
37//! accessors, sparse accessors, or an attribute layout the encoder rejects —
38//! are left uncompressed but fully preserved, along with the rest of the
39//! document.
40//!
41//! Non-triangle primitives are likewise left uncompressed, and this is required
42//! by the spec, not a limitation: `KHR_draco_mesh_compression` restricts the
43//! primitive `mode` to `TRIANGLES` or `TRIANGLE_STRIP` ("Restrictions on
44//! geometry type"), so point clouds (`POINTS`) and line modes cannot be
45//! Draco-compressed in glTF at all. (Only `TRIANGLES` is compressed here;
46//! `TRIANGLE_STRIP` is allowed by the spec but uncommon and left as-is.)
47
48use std::collections::{BTreeSet, HashMap};
49#[cfg(feature = "gltf-reader")]
50use std::path::Path;
51
52use draco_core::decoder_buffer::DecoderBuffer;
53use draco_core::mesh::Mesh;
54use draco_core::mesh_decoder::MeshDecoder;
55use serde_json::{Map, Value};
56
57pub use crate::gltf_container::OutputFormat;
58#[cfg(feature = "gltf-reader")]
59use crate::gltf_container::{
60    parse_gltf_container, serialize_gltf_document, FileResourceResolver, ResourceLimits,
61    ResourceResolver,
62};
63use crate::gltf_geometry::{
64    component_type_for_data_type, gltf_type_for_num_components, validate_semantic_accessor,
65    GltfError,
66};
67// The byte API parses + resolves buffers through the reader; the in-memory
68// `compress_gltf_value` does not need it.
69use crate::gltf_khr_draco::{parse_khr_draco_mesh_compression, validate_khr_draco_document};
70#[cfg(feature = "gltf-reader")]
71use crate::gltf_reader::GltfReader;
72use crate::gltf_writer::encode_draco_mesh_with_info;
73
74type Result<T> = std::result::Result<T, GltfError>;
75
76const KHR_DRACO: &str = "KHR_draco_mesh_compression";
77const MODE_TRIANGLES: u64 = 4;
78const MODE_TRIANGLE_STRIP: u64 = 5;
79
80/// Draco mesh encoding method selection.
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
82pub enum EncodingMethod {
83    /// Let the encoder choose based on the geometry and speed settings.
84    #[default]
85    Auto,
86    /// Force the sequential mesh encoder.
87    Sequential,
88    /// Force the EdgeBreaker mesh encoder.
89    Edgebreaker,
90}
91
92/// Per-attribute quantization. `None` disables quantization for that class.
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub struct QuantizationOptions {
95    pub position: Option<u8>,
96    pub normal: Option<u8>,
97    pub color: Option<u8>,
98    pub texcoord: Option<u8>,
99    pub generic: Option<u8>,
100}
101
102impl Default for QuantizationOptions {
103    fn default() -> Self {
104        Self {
105            position: Some(14),
106            normal: Some(10),
107            color: Some(8),
108            texcoord: Some(12),
109            generic: Some(8),
110        }
111    }
112}
113
114/// Options shared by the document compressor and geometry writer.
115#[derive(Clone, Copy, Debug, PartialEq, Eq)]
116pub struct GltfCompressionOptions {
117    pub quantization: QuantizationOptions,
118    pub encoding_speed: u8,
119    pub decoding_speed: u8,
120    pub encoding_method: EncodingMethod,
121    pub output_format: OutputFormat,
122}
123
124impl GltfCompressionOptions {
125    /// Validate all numeric ranges without clamping.
126    pub fn validate(&self) -> Result<()> {
127        if self.encoding_speed > 10 {
128            return Err(GltfError::InvalidOptions(format!(
129                "encoding_speed {} is outside 0..=10",
130                self.encoding_speed
131            )));
132        }
133        if self.decoding_speed > 10 {
134            return Err(GltfError::InvalidOptions(format!(
135                "decoding_speed {} is outside 0..=10",
136                self.decoding_speed
137            )));
138        }
139        validate_quantization("position", self.quantization.position, 1, 31)?;
140        validate_quantization("normal", self.quantization.normal, 2, 30)?;
141        validate_quantization("color", self.quantization.color, 1, 31)?;
142        validate_quantization("texcoord", self.quantization.texcoord, 1, 31)?;
143        validate_quantization("generic", self.quantization.generic, 1, 31)?;
144        Ok(())
145    }
146}
147
148impl Default for GltfCompressionOptions {
149    fn default() -> Self {
150        Self {
151            quantization: QuantizationOptions::default(),
152            encoding_speed: 5,
153            decoding_speed: 5,
154            encoding_method: EncodingMethod::Auto,
155            output_format: OutputFormat::SameAsInput,
156        }
157    }
158}
159
160fn validate_quantization(name: &str, bits: Option<u8>, min: u8, max: u8) -> Result<()> {
161    if bits.is_some_and(|bits| !(min..=max).contains(&bits)) {
162        return Err(GltfError::InvalidOptions(format!(
163            "{name} quantization must be None or {min}..={max}"
164        )));
165    }
166    Ok(())
167}
168
169/// Stable location of a primitive in a glTF document.
170#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
171pub struct PrimitiveLocation {
172    pub mesh: usize,
173    pub primitive: usize,
174}
175
176/// Why a valid primitive was preserved instead of compressed.
177#[derive(Clone, Debug, PartialEq, Eq)]
178pub enum PreserveReason {
179    AlreadyDraco,
180    UnsupportedMode { mode: u32 },
181    UnsupportedLayout { detail: String },
182    SparseAccessor { accessor: usize },
183    MorphTargets,
184    SharedAccessor { accessor: usize },
185}
186
187/// One preserved primitive and its typed reason.
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct PreservedPrimitive {
190    pub primitive: PrimitiveLocation,
191    pub reason: PreserveReason,
192}
193
194/// Primitive-by-primitive result of compression.
195#[derive(Clone, Debug, Default, PartialEq, Eq)]
196pub struct CompressionReport {
197    pub compressed_primitives: Vec<PrimitiveLocation>,
198    pub preserved_primitives: Vec<PreservedPrimitive>,
199}
200
201/// Data plus its compression report.
202#[derive(Clone, Debug, PartialEq, Eq)]
203pub struct CompressionOutput<T> {
204    pub data: T,
205    pub report: CompressionReport,
206}
207
208fn json_index(value: &Value, label: &str) -> Result<usize> {
209    value
210        .as_u64()
211        .and_then(|value| usize::try_from(value).ok())
212        .ok_or_else(|| GltfError::InvalidGltf(format!("{label} is not a valid index")))
213}
214
215/// Compress the geometry of a self-contained glTF/GLB document with Draco,
216/// preserving all other document content.
217///
218/// `input` may be GLB bytes or glTF JSON whose buffers/images are embedded as
219/// data URIs (use [`compress_gltf_bytes_with_base_path`] for external files).
220/// The output container matches the input (GLB in -> GLB out, glTF in -> glTF
221/// out with an embedded buffer).
222#[cfg(feature = "gltf-reader")]
223pub fn compress_gltf_bytes(input: &[u8]) -> Result<CompressionOutput<Vec<u8>>> {
224    compress_gltf_bytes_with_options(input, &GltfCompressionOptions::default())
225}
226
227/// Compress with explicit options and no external file resolver.
228#[cfg(feature = "gltf-reader")]
229pub fn compress_gltf_bytes_with_options(
230    input: &[u8],
231    options: &GltfCompressionOptions,
232) -> Result<CompressionOutput<Vec<u8>>> {
233    compress_gltf_bytes_impl(input, None, &ResourceLimits::default(), options)
234}
235
236/// Like [`compress_gltf_bytes`], but resolves external buffers/`.bin` files
237/// relative to `base_path`.
238#[cfg(feature = "gltf-reader")]
239pub fn compress_gltf_bytes_with_base_path(
240    input: &[u8],
241    base_path: Option<&Path>,
242    options: &GltfCompressionOptions,
243) -> Result<CompressionOutput<Vec<u8>>> {
244    let resolver =
245        base_path.map(|base| FileResourceResolver::new(base, crate::ExternalFilePolicy::Allow));
246    compress_gltf_bytes_impl(
247        input,
248        resolver
249            .as_ref()
250            .map(|resolver| resolver as &dyn ResourceResolver),
251        &ResourceLimits::default(),
252        options,
253    )
254}
255
256/// Compress using a caller-provided resource resolver and quotas.
257#[cfg(feature = "gltf-reader")]
258pub fn compress_gltf_bytes_with_resolver(
259    input: &[u8],
260    resolver: &dyn ResourceResolver,
261    limits: &ResourceLimits,
262    options: &GltfCompressionOptions,
263) -> Result<CompressionOutput<Vec<u8>>> {
264    compress_gltf_bytes_impl(input, Some(resolver), limits, options)
265}
266
267#[cfg(feature = "gltf-reader")]
268fn compress_gltf_bytes_impl(
269    input: &[u8],
270    resolver: Option<&dyn ResourceResolver>,
271    limits: &ResourceLimits,
272    options: &GltfCompressionOptions,
273) -> Result<CompressionOutput<Vec<u8>>> {
274    options.validate()?;
275    let container = parse_gltf_container(input)?;
276    let doc = serde_json::from_slice::<Value>(container.json)?;
277
278    // Reuse the reader (lenient: do not reject skins/animations/morph targets,
279    // we only preserve them) for geometry decoding and resolved buffer bytes.
280    let reader = if let Some(resolver) = resolver {
281        GltfReader::from_bytes_lenient_with_resolver(input, resolver, limits)?
282    } else {
283        GltfReader::from_bytes_lenient(input)?
284    };
285    let compressed = compress_gltf_value(doc, reader.buffers(), options, |mesh, prim| {
286        reader.decode_primitive_with_semantics(mesh, prim)
287    })?;
288    let (doc, bin) = compressed.data;
289    let data = serialize_gltf_document(&doc, &bin, container.format, options.output_format)?;
290    Ok(CompressionOutput {
291        data,
292        report: compressed.report,
293    })
294}
295
296/// The document-preserving compression core: transforms a parsed glTF document
297/// (`doc`) in place, returning the mutated document plus the new single binary
298/// blob. Geometry decoding is supplied by `decode`, so callers that already have
299/// a parsed scene (e.g. a `gltf-rs` document) can compress without re-parsing
300/// through the byte API.
301///
302/// `decode(mesh_index, primitive_index)` returns the primitive's geometry as a
303/// [`draco_core::Mesh`] plus the `(glTF semantic, Draco attribute id)` mapping;
304/// an `Err` marks the primitive as not compressible (it is preserved). `buffers`
305/// holds the resolved bytes for each glTF buffer (used for repacking the
306/// non-compressed buffer views).
307///
308/// The returned document's single buffer carries `byteLength` but no URI; the
309/// caller embeds the returned `bin` (as a GLB BIN chunk or a data URI).
310pub fn compress_gltf_value<F>(
311    mut doc: Value,
312    buffers: &[Vec<u8>],
313    options: &GltfCompressionOptions,
314    decode: F,
315) -> Result<CompressionOutput<(Value, Vec<u8>)>>
316where
317    F: Fn(usize, usize) -> Result<(draco_core::Mesh, Vec<(String, u32)>)>,
318{
319    if !doc.is_object() {
320        return Err(GltfError::InvalidGltf("glTF root is not an object".into()));
321    }
322    options.validate()?;
323    validate_gltf_document_for_repacking(&doc, buffers)?;
324
325    // Reference-count accessor usage across every primitive so we only mutate
326    // accessors that belong exclusively to a single primitive we compress.
327    let accessor_users = count_accessor_users(&doc)?;
328
329    let (plans, mut report) = build_plans(&doc, buffers, &decode, &accessor_users, options)?;
330
331    // Mutate accessors of compressed primitives: drop their buffer view and set
332    // the count to the Draco-encoded value. Done before scanning for orphans so
333    // the now-unreferenced geometry buffer views fall out naturally.
334    apply_accessor_mutations(&mut doc, &plans)?;
335
336    // Non-indexed primitives need a generated indices accessor (Draco glTF
337    // primitives are indexed).
338    add_generated_indices(&mut doc, &plans)?;
339
340    // Repack the binary: keep only buffer views still referenced by the JSON,
341    // append one Draco buffer view per compressed primitive, and reindex every
342    // buffer-view reference in the document.
343    let repack = repack_buffers(&mut doc, buffers, &plans)?;
344
345    // Write the Draco extension onto each compressed primitive (after reindex,
346    // so the freshly appended buffer-view indices are not remapped).
347    for (i, plan) in plans.iter().enumerate() {
348        let draco_bv = repack.draco_buffer_views[i];
349        set_primitive_draco_extension(&mut doc, plan, draco_bv)?;
350    }
351
352    if !plans.is_empty() {
353        ensure_extension_listed(&mut doc, "extensionsUsed")?;
354        ensure_extension_listed(&mut doc, "extensionsRequired")?;
355    }
356
357    set_single_buffer(&mut doc, repack.bin.len())?;
358
359    report.compressed_primitives = plans
360        .iter()
361        .map(|plan| PrimitiveLocation {
362            mesh: plan.mesh_idx,
363            primitive: plan.prim_idx,
364        })
365        .collect();
366    Ok(CompressionOutput {
367        data: (doc, repack.bin),
368        report,
369    })
370}
371
372/// Consolidate resolved glTF buffers using the same known-reference and opaque
373/// extension policy as the compressor.
374pub fn consolidate_gltf_buffers(
375    mut document: Value,
376    buffers: &[Vec<u8>],
377) -> Result<(Value, Vec<u8>)> {
378    if !document.is_object() {
379        return Err(GltfError::InvalidGltf("glTF root is not an object".into()));
380    }
381    validate_gltf_document_for_repacking(&document, buffers)?;
382    let repack = repack_buffers(&mut document, buffers, &[])?;
383    set_single_buffer(&mut document, repack.bin.len())?;
384    Ok((document, repack.bin))
385}
386
387#[derive(Clone, Copy)]
388struct ValidatedBufferView {
389    buffer: usize,
390    byte_offset: usize,
391    byte_length: usize,
392    byte_stride: Option<usize>,
393}
394
395/// Validate binary declarations and every accessor reference before deciding
396/// whether a primitive is compressible. Preserve reasons are only for valid
397/// but unsupported data; malformed sparse/morph/shared geometry must never be
398/// converted into a successful report entry.
399/// Validate a parsed document before any binary consolidation or compression.
400///
401/// This combines strict KHR Draco validation, conservative opaque-reference
402/// rejection, accessor/reference validation, and checked buffer-view bounds.
403pub fn validate_gltf_document_for_repacking(document: &Value, buffers: &[Vec<u8>]) -> Result<()> {
404    validate_khr_draco_document(document)?;
405    reject_opaque_binary_references(document)?;
406    validate_gltf_document_binary_layout(document, buffers)
407}
408
409/// Validate all glTF binary declarations and accessor references.
410pub fn validate_gltf_document_binary_layout(document: &Value, buffers: &[Vec<u8>]) -> Result<()> {
411    let declared_buffers = optional_array(document, "buffers")?;
412    if declared_buffers.len() != buffers.len() {
413        return Err(GltfError::InvalidGltf(format!(
414            "document declares {} buffers but {} were resolved",
415            declared_buffers.len(),
416            buffers.len()
417        )));
418    }
419    for (index, declaration) in declared_buffers.iter().enumerate() {
420        let declaration = declaration
421            .as_object()
422            .ok_or_else(|| GltfError::InvalidGltf(format!("buffer {index} is not an object")))?;
423        let byte_length = required_usize(declaration, "byteLength", "buffer")?;
424        let actual = buffers[index].len();
425        if actual < byte_length {
426            return Err(GltfError::InvalidGltf(format!(
427                "buffer {index} byteLength {byte_length} exceeds resolved length {actual}"
428            )));
429        }
430    }
431
432    let view_values = optional_array(document, "bufferViews")?;
433    let mut views = Vec::new();
434    views.try_reserve_exact(view_values.len()).map_err(|_| {
435        GltfError::ResourceLimitExceeded("bufferView validation allocation failed".into())
436    })?;
437    for (index, value) in view_values.iter().enumerate() {
438        let view = value.as_object().ok_or_else(|| {
439            GltfError::InvalidGltf(format!("bufferView {index} is not an object"))
440        })?;
441        let buffer = required_usize(view, "buffer", "bufferView")?;
442        let byte_offset = optional_usize(view, "byteOffset", "bufferView")?.unwrap_or(0);
443        let byte_length = required_usize(view, "byteLength", "bufferView")?;
444        let byte_stride = optional_usize(view, "byteStride", "bufferView")?;
445        if let Some(stride) = byte_stride {
446            if !(4..=252).contains(&stride) || !stride.is_multiple_of(4) {
447                return Err(GltfError::InvalidGltf(format!(
448                    "bufferView {index} byteStride must be a multiple of 4 in 4..=252"
449                )));
450            }
451        }
452        let buffer_data = buffers.get(buffer).ok_or_else(|| {
453            GltfError::InvalidGltf(format!(
454                "bufferView {index} references invalid buffer {buffer}"
455            ))
456        })?;
457        let end = byte_offset
458            .checked_add(byte_length)
459            .filter(|end| *end <= buffer_data.len())
460            .ok_or_else(|| GltfError::InvalidGltf(format!("bufferView {index} is out of range")))?;
461        let _ = end;
462        views.push(ValidatedBufferView {
463            buffer,
464            byte_offset,
465            byte_length,
466            byte_stride,
467        });
468    }
469
470    let accessor_values = optional_array(document, "accessors")?;
471    for (index, value) in accessor_values.iter().enumerate() {
472        validate_accessor(index, value, &views, buffers)?;
473    }
474    validate_primitive_accessor_contracts(document, accessor_values, &views, buffers)?;
475    validate_accessor_references(document, accessor_values.len())
476}
477
478fn optional_array<'a>(document: &'a Value, key: &str) -> Result<&'a [Value]> {
479    match document.get(key) {
480        Some(value) => value
481            .as_array()
482            .map(Vec::as_slice)
483            .ok_or_else(|| GltfError::InvalidGltf(format!("{key} is not an array"))),
484        None => Ok(&[]),
485    }
486}
487
488fn required_usize(object: &Map<String, Value>, key: &str, label: &str) -> Result<usize> {
489    let value = object
490        .get(key)
491        .ok_or_else(|| GltfError::InvalidGltf(format!("{label} is missing {key}")))?;
492    json_index(value, &format!("{label}.{key}"))
493}
494
495fn optional_usize(object: &Map<String, Value>, key: &str, label: &str) -> Result<Option<usize>> {
496    object
497        .get(key)
498        .map(|value| json_index(value, &format!("{label}.{key}")))
499        .transpose()
500}
501
502fn component_size(component_type: u64, label: &str) -> Result<usize> {
503    match component_type {
504        5120 | 5121 => Ok(1),
505        5122 | 5123 | 5131 => Ok(2),
506        5124..=5126 => Ok(4),
507        5130 | 5132 | 5133 => Ok(8),
508        _ => Err(GltfError::InvalidGltf(format!(
509            "{label} has invalid componentType {component_type}"
510        ))),
511    }
512}
513
514fn accessor_element_size(accessor_type: &str, component_size: usize) -> Result<usize> {
515    let (columns, rows) = match accessor_type {
516        "SCALAR" => (1usize, 1usize),
517        "VEC2" => (1, 2),
518        "VEC3" => (1, 3),
519        "VEC4" => (1, 4),
520        "MAT2" => (2, 2),
521        "MAT3" => (3, 3),
522        "MAT4" => (4, 4),
523        _ => {
524            return Err(GltfError::InvalidGltf(format!(
525                "accessor has invalid type {accessor_type}"
526            )));
527        }
528    };
529    let column_size = rows
530        .checked_mul(component_size)
531        .ok_or_else(|| GltfError::InvalidGltf("accessor element size overflow".into()))?;
532    let column_stride = if columns > 1 && component_size < 4 {
533        column_size
534            .checked_add(3)
535            .map(|size| size / 4 * 4)
536            .ok_or_else(|| GltfError::InvalidGltf("matrix element size overflow".into()))?
537    } else {
538        column_size
539    };
540    columns
541        .checked_mul(column_stride)
542        .ok_or_else(|| GltfError::InvalidGltf("accessor element size overflow".into()))
543}
544
545fn validate_range_in_view(
546    view: ValidatedBufferView,
547    byte_offset: usize,
548    count: usize,
549    element_size: usize,
550    stride: usize,
551    label: &str,
552) -> Result<()> {
553    if stride < element_size {
554        return Err(GltfError::InvalidGltf(format!(
555            "{label} stride {stride} is smaller than element size {element_size}"
556        )));
557    }
558    let byte_length = count
559        .checked_sub(1)
560        .and_then(|prefix| prefix.checked_mul(stride))
561        .and_then(|prefix| prefix.checked_add(element_size))
562        .ok_or_else(|| GltfError::InvalidGltf(format!("{label} byte range overflow")))?;
563    let end = byte_offset
564        .checked_add(byte_length)
565        .ok_or_else(|| GltfError::InvalidGltf(format!("{label} byte range overflow")))?;
566    if end > view.byte_length {
567        return Err(GltfError::InvalidGltf(format!(
568            "{label} does not fit its bufferView"
569        )));
570    }
571    Ok(())
572}
573
574fn validate_accessor(
575    index: usize,
576    value: &Value,
577    views: &[ValidatedBufferView],
578    buffers: &[Vec<u8>],
579) -> Result<()> {
580    let accessor = value
581        .as_object()
582        .ok_or_else(|| GltfError::InvalidGltf(format!("accessor {index} is not an object")))?;
583    let component_type = accessor
584        .get("componentType")
585        .and_then(Value::as_u64)
586        .ok_or_else(|| {
587            GltfError::InvalidGltf(format!("accessor {index} has invalid componentType"))
588        })?;
589    let component_size = component_size(component_type, &format!("accessor {index}"))?;
590    let accessor_type = accessor
591        .get("type")
592        .and_then(Value::as_str)
593        .ok_or_else(|| GltfError::InvalidGltf(format!("accessor {index} has invalid type")))?;
594    let element_size = accessor_element_size(accessor_type, component_size)?;
595    let count = required_usize(accessor, "count", &format!("accessor {index}"))?;
596    if count == 0 {
597        return Err(GltfError::InvalidGltf(format!(
598            "accessor {index} count must be greater than zero"
599        )));
600    }
601    if accessor
602        .get("normalized")
603        .is_some_and(|normalized| !normalized.is_boolean())
604    {
605        return Err(GltfError::InvalidGltf(format!(
606            "accessor {index}.normalized is not a boolean"
607        )));
608    }
609    let byte_offset = optional_usize(accessor, "byteOffset", "accessor")?.unwrap_or(0);
610    if !byte_offset.is_multiple_of(component_size) {
611        return Err(GltfError::InvalidGltf(format!(
612            "accessor {index} byteOffset is not component-aligned"
613        )));
614    }
615    if let Some(view_index) = optional_usize(accessor, "bufferView", "accessor")? {
616        let view = *views.get(view_index).ok_or_else(|| {
617            GltfError::InvalidGltf(format!(
618                "accessor {index} references invalid bufferView {view_index}"
619            ))
620        })?;
621        let stride = view.byte_stride.unwrap_or(element_size);
622        validate_range_in_view(
623            view,
624            byte_offset,
625            count,
626            element_size,
627            stride,
628            &format!("accessor {index}"),
629        )?;
630    } else if byte_offset != 0 {
631        return Err(GltfError::InvalidGltf(format!(
632            "accessor {index} has byteOffset without bufferView"
633        )));
634    }
635
636    if let Some(sparse) = accessor.get("sparse") {
637        validate_sparse_accessor(index, sparse, count, element_size, views, buffers)?;
638    }
639    Ok(())
640}
641
642fn validate_sparse_accessor(
643    accessor_index: usize,
644    sparse: &Value,
645    accessor_count: usize,
646    element_size: usize,
647    views: &[ValidatedBufferView],
648    buffers: &[Vec<u8>],
649) -> Result<()> {
650    let sparse = sparse.as_object().ok_or_else(|| {
651        GltfError::InvalidGltf(format!("accessor {accessor_index}.sparse is not an object"))
652    })?;
653    let count = required_usize(sparse, "count", "sparse accessor")?;
654    if count == 0 || count > accessor_count {
655        return Err(GltfError::InvalidGltf(format!(
656            "accessor {accessor_index} has invalid sparse count {count}"
657        )));
658    }
659
660    let indices = sparse
661        .get("indices")
662        .and_then(Value::as_object)
663        .ok_or_else(|| {
664            GltfError::InvalidGltf(format!(
665                "accessor {accessor_index}.sparse.indices is not an object"
666            ))
667        })?;
668    let indices_view_index = required_usize(indices, "bufferView", "sparse indices")?;
669    let indices_view = *views.get(indices_view_index).ok_or_else(|| {
670        GltfError::InvalidGltf(format!(
671            "sparse indices references invalid bufferView {indices_view_index}"
672        ))
673    })?;
674    if indices_view.byte_stride.is_some() {
675        return Err(GltfError::InvalidGltf(
676            "sparse indices bufferView must not define byteStride".into(),
677        ));
678    }
679    let index_component = indices
680        .get("componentType")
681        .and_then(Value::as_u64)
682        .ok_or_else(|| GltfError::InvalidGltf("sparse indices componentType is invalid".into()))?;
683    let index_size = match index_component {
684        5121 => 1,
685        5123 => 2,
686        5125 => 4,
687        _ => {
688            return Err(GltfError::InvalidGltf(format!(
689                "sparse indices has invalid componentType {index_component}"
690            )));
691        }
692    };
693    let indices_offset = optional_usize(indices, "byteOffset", "sparse indices")?.unwrap_or(0);
694    if !indices_offset.is_multiple_of(index_size) {
695        return Err(GltfError::InvalidGltf(
696            "sparse indices byteOffset is not component-aligned".into(),
697        ));
698    }
699    validate_range_in_view(
700        indices_view,
701        indices_offset,
702        count,
703        index_size,
704        index_size,
705        "sparse indices",
706    )?;
707    let indices_buffer = buffers
708        .get(indices_view.buffer)
709        .ok_or_else(|| GltfError::InvalidGltf("sparse indices buffer is out of range".into()))?;
710    let indices_start = indices_view
711        .byte_offset
712        .checked_add(indices_offset)
713        .ok_or_else(|| GltfError::InvalidGltf("sparse indices offset overflow".into()))?;
714    let mut previous = None;
715    for sparse_index in 0..count {
716        let offset = sparse_index
717            .checked_mul(index_size)
718            .and_then(|offset| indices_start.checked_add(offset))
719            .ok_or_else(|| GltfError::InvalidGltf("sparse indices offset overflow".into()))?;
720        let end = offset
721            .checked_add(index_size)
722            .ok_or_else(|| GltfError::InvalidGltf("sparse indices offset overflow".into()))?;
723        let bytes = indices_buffer
724            .get(offset..end)
725            .ok_or_else(|| GltfError::InvalidGltf("sparse indices are out of range".into()))?;
726        let value = match index_component {
727            5121 => bytes[0] as u32,
728            5123 => u16::from_le_bytes([bytes[0], bytes[1]]) as u32,
729            5125 => u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
730            _ => {
731                return Err(GltfError::InvalidGltf(
732                    "sparse indices component type changed after validation".into(),
733                ));
734            }
735        } as usize;
736        if value >= accessor_count {
737            return Err(GltfError::InvalidGltf(format!(
738                "accessor {accessor_index} sparse index {value} is out of range"
739            )));
740        }
741        if previous.is_some_and(|previous| value <= previous) {
742            return Err(GltfError::InvalidGltf(format!(
743                "accessor {accessor_index} sparse indices are not strictly increasing"
744            )));
745        }
746        previous = Some(value);
747    }
748
749    let values = sparse
750        .get("values")
751        .and_then(Value::as_object)
752        .ok_or_else(|| {
753            GltfError::InvalidGltf(format!(
754                "accessor {accessor_index}.sparse.values is not an object"
755            ))
756        })?;
757    let values_view_index = required_usize(values, "bufferView", "sparse values")?;
758    let values_view = *views.get(values_view_index).ok_or_else(|| {
759        GltfError::InvalidGltf(format!(
760            "sparse values references invalid bufferView {values_view_index}"
761        ))
762    })?;
763    if values_view.byte_stride.is_some() {
764        return Err(GltfError::InvalidGltf(
765            "sparse values bufferView must not define byteStride".into(),
766        ));
767    }
768    let values_offset = optional_usize(values, "byteOffset", "sparse values")?.unwrap_or(0);
769    validate_range_in_view(
770        values_view,
771        values_offset,
772        count,
773        element_size,
774        element_size,
775        "sparse values",
776    )
777}
778
779fn validate_primitive_accessor_contracts(
780    document: &Value,
781    accessors: &[Value],
782    views: &[ValidatedBufferView],
783    buffers: &[Vec<u8>],
784) -> Result<()> {
785    for (mesh_index, mesh) in optional_array(document, "meshes")?.iter().enumerate() {
786        let primitives = mesh
787            .get("primitives")
788            .and_then(Value::as_array)
789            .ok_or_else(|| {
790                GltfError::InvalidGltf(format!("mesh {mesh_index}.primitives is not an array"))
791            })?;
792        for (primitive_index, primitive) in primitives.iter().enumerate() {
793            let primitive = primitive.as_object().ok_or_else(|| {
794                GltfError::InvalidGltf(format!(
795                    "primitive {mesh_index}:{primitive_index} is not an object"
796                ))
797            })?;
798            let attributes = primitive
799                .get("attributes")
800                .and_then(Value::as_object)
801                .ok_or_else(|| {
802                    GltfError::InvalidGltf(format!(
803                        "primitive {mesh_index}:{primitive_index}.attributes is not an object"
804                    ))
805                })?;
806            let mut vertex_count = None;
807            for (semantic, accessor_index) in attributes {
808                let accessor_index = json_index(accessor_index, "primitive attribute accessor")?;
809                let accessor = accessors
810                    .get(accessor_index)
811                    .and_then(Value::as_object)
812                    .ok_or_else(|| {
813                        GltfError::InvalidGltf(format!(
814                            "primitive {mesh_index}:{primitive_index} accessor {accessor_index} is out of range"
815                        ))
816                    })?;
817                let count = required_usize(accessor, "count", "primitive attribute accessor")?;
818                if let Some(expected) = vertex_count {
819                    if expected != count {
820                        return Err(GltfError::InvalidGltf(format!(
821                            "primitive {mesh_index}:{primitive_index} attribute counts do not match"
822                        )));
823                    }
824                } else {
825                    vertex_count = Some(count);
826                }
827                let component_type = accessor
828                    .get("componentType")
829                    .and_then(Value::as_u64)
830                    .and_then(|value| u32::try_from(value).ok())
831                    .ok_or_else(|| {
832                        GltfError::InvalidGltf(format!(
833                            "accessor {accessor_index} has invalid componentType"
834                        ))
835                    })?;
836                // glTF 2.1 component types are outside the current codec scope
837                // and are preserved later. Core glTF 2.0 layouts must satisfy
838                // the semantic contract even when another preserve reason wins.
839                if matches!(component_type, 5120 | 5121 | 5122 | 5123 | 5126) {
840                    let accessor_type =
841                        accessor
842                            .get("type")
843                            .and_then(Value::as_str)
844                            .ok_or_else(|| {
845                                GltfError::InvalidGltf(format!(
846                                    "accessor {accessor_index} has invalid type"
847                                ))
848                            })?;
849                    let normalized = match accessor.get("normalized") {
850                        None => false,
851                        Some(Value::Bool(normalized)) => *normalized,
852                        Some(_) => {
853                            return Err(GltfError::InvalidGltf(format!(
854                                "accessor {accessor_index}.normalized is not a boolean"
855                            )));
856                        }
857                    };
858                    validate_semantic_accessor(
859                        semantic,
860                        accessor_type,
861                        component_type,
862                        normalized,
863                    )?;
864                }
865            }
866            let vertex_count = vertex_count.unwrap_or(0);
867            let mode = primitive
868                .get("mode")
869                .and_then(Value::as_u64)
870                .unwrap_or(MODE_TRIANGLES);
871            let element_count = if let Some(indices) = primitive.get("indices") {
872                let accessor_index = json_index(indices, "primitive indices accessor")?;
873                let values =
874                    read_unsigned_scalar_accessor(accessor_index, accessors, views, buffers)?;
875                if values.iter().any(|&index| index as usize >= vertex_count) {
876                    return Err(GltfError::InvalidGltf(format!(
877                        "primitive {mesh_index}:{primitive_index} index is out of range for {vertex_count} vertices"
878                    )));
879                }
880                values.len()
881            } else {
882                vertex_count
883            };
884            validate_primitive_element_count(mode, element_count, mesh_index, primitive_index)?;
885
886            if let Some(targets) = primitive.get("targets") {
887                let targets = targets.as_array().ok_or_else(|| {
888                    GltfError::InvalidGltf("primitive.targets is not an array".into())
889                })?;
890                for (target_index, target) in targets.iter().enumerate() {
891                    let target = target.as_object().ok_or_else(|| {
892                        GltfError::InvalidGltf("morph target is not an object".into())
893                    })?;
894                    if target.is_empty() {
895                        return Err(GltfError::InvalidGltf("morph target is empty".into()));
896                    }
897                    for (semantic, accessor_index) in target {
898                        let accessor_index = json_index(accessor_index, "morph target accessor")?;
899                        let accessor = accessors
900                            .get(accessor_index)
901                            .and_then(Value::as_object)
902                            .ok_or_else(|| {
903                                GltfError::InvalidGltf(format!(
904                                    "morph target accessor {accessor_index} is out of range"
905                                ))
906                            })?;
907                        let count = required_usize(accessor, "count", "morph target accessor")?;
908                        if count != vertex_count
909                            || !matches!(semantic.as_str(), "POSITION" | "NORMAL" | "TANGENT")
910                            || accessor.get("type").and_then(Value::as_str) != Some("VEC3")
911                            || accessor.get("componentType").and_then(Value::as_u64) != Some(5126)
912                            || accessor
913                                .get("normalized")
914                                .is_some_and(|normalized| normalized != &Value::Bool(false))
915                        {
916                            return Err(GltfError::InvalidGltf(format!(
917                                "primitive {mesh_index}:{primitive_index} morph target {target_index} {semantic} accessor has an invalid contract"
918                            )));
919                        }
920                    }
921                }
922            }
923        }
924    }
925    Ok(())
926}
927
928fn validate_primitive_element_count(
929    mode: u64,
930    count: usize,
931    mesh: usize,
932    primitive: usize,
933) -> Result<()> {
934    let valid = match mode {
935        0 => count >= 1,
936        1 => count >= 2 && count.is_multiple_of(2),
937        2 | 3 => count >= 2,
938        4 => count >= 3 && count.is_multiple_of(3),
939        5 | 6 => count >= 3,
940        _ => false,
941    };
942    if !valid {
943        return Err(GltfError::InvalidGltf(format!(
944            "primitive {mesh}:{primitive} has invalid element count {count} for mode {mode}"
945        )));
946    }
947    Ok(())
948}
949
950fn read_unsigned_scalar_accessor(
951    accessor_index: usize,
952    accessors: &[Value],
953    views: &[ValidatedBufferView],
954    buffers: &[Vec<u8>],
955) -> Result<Vec<u32>> {
956    let accessor = accessors
957        .get(accessor_index)
958        .and_then(Value::as_object)
959        .ok_or_else(|| {
960            GltfError::InvalidGltf(format!("accessor {accessor_index} is out of range"))
961        })?;
962    if accessor.get("type").and_then(Value::as_str) != Some("SCALAR")
963        || accessor
964            .get("normalized")
965            .is_some_and(|normalized| normalized != &Value::Bool(false))
966    {
967        return Err(GltfError::InvalidGltf(format!(
968            "indices accessor {accessor_index} has an invalid contract"
969        )));
970    }
971    let component_type = accessor
972        .get("componentType")
973        .and_then(Value::as_u64)
974        .ok_or_else(|| GltfError::InvalidGltf("indices componentType is invalid".into()))?;
975    let value_size = match component_type {
976        5121 => 1,
977        5123 => 2,
978        5125 => 4,
979        _ => {
980            return Err(GltfError::InvalidGltf(format!(
981                "indices accessor {accessor_index} has invalid componentType {component_type}"
982            )));
983        }
984    };
985    let count = required_usize(accessor, "count", "indices accessor")?;
986    let mut values = Vec::new();
987    values.try_reserve_exact(count).map_err(|_| {
988        GltfError::ResourceLimitExceeded("indices validation allocation failed".into())
989    })?;
990    values.resize(count, 0);
991    if let Some(view_index) = optional_usize(accessor, "bufferView", "indices accessor")? {
992        let view = *views.get(view_index).ok_or_else(|| {
993            GltfError::InvalidGltf(format!("indices bufferView {view_index} is out of range"))
994        })?;
995        if view.byte_stride.is_some() {
996            return Err(GltfError::InvalidGltf(
997                "indices bufferView must not define byteStride".into(),
998            ));
999        }
1000        let byte_offset = optional_usize(accessor, "byteOffset", "indices accessor")?.unwrap_or(0);
1001        for (index, value) in values.iter_mut().enumerate() {
1002            *value = read_unsigned_from_view(
1003                view,
1004                byte_offset,
1005                index,
1006                value_size,
1007                component_type,
1008                buffers,
1009                "indices accessor",
1010            )?;
1011        }
1012    }
1013    if let Some(sparse) = accessor.get("sparse").and_then(Value::as_object) {
1014        let sparse_count = required_usize(sparse, "count", "sparse accessor")?;
1015        let sparse_indices = sparse
1016            .get("indices")
1017            .and_then(Value::as_object)
1018            .ok_or_else(|| GltfError::InvalidGltf("sparse indices are invalid".into()))?;
1019        let sparse_values = sparse
1020            .get("values")
1021            .and_then(Value::as_object)
1022            .ok_or_else(|| GltfError::InvalidGltf("sparse values are invalid".into()))?;
1023        let sparse_index_component = sparse_indices
1024            .get("componentType")
1025            .and_then(Value::as_u64)
1026            .ok_or_else(|| GltfError::InvalidGltf("sparse index type is invalid".into()))?;
1027        let sparse_index_size = component_size(sparse_index_component, "sparse indices")?;
1028        let index_view = *views
1029            .get(required_usize(
1030                sparse_indices,
1031                "bufferView",
1032                "sparse indices",
1033            )?)
1034            .ok_or_else(|| GltfError::InvalidGltf("sparse indices view is invalid".into()))?;
1035        let value_view = *views
1036            .get(required_usize(
1037                sparse_values,
1038                "bufferView",
1039                "sparse values",
1040            )?)
1041            .ok_or_else(|| GltfError::InvalidGltf("sparse values view is invalid".into()))?;
1042        let index_offset =
1043            optional_usize(sparse_indices, "byteOffset", "sparse indices")?.unwrap_or(0);
1044        let value_offset =
1045            optional_usize(sparse_values, "byteOffset", "sparse values")?.unwrap_or(0);
1046        for sparse_index in 0..sparse_count {
1047            let destination = read_unsigned_from_view(
1048                index_view,
1049                index_offset,
1050                sparse_index,
1051                sparse_index_size,
1052                sparse_index_component,
1053                buffers,
1054                "sparse indices",
1055            )? as usize;
1056            let value = read_unsigned_from_view(
1057                value_view,
1058                value_offset,
1059                sparse_index,
1060                value_size,
1061                component_type,
1062                buffers,
1063                "sparse values",
1064            )?;
1065            *values
1066                .get_mut(destination)
1067                .ok_or_else(|| GltfError::InvalidGltf("sparse index is out of range".into()))? =
1068                value;
1069        }
1070    }
1071    Ok(values)
1072}
1073
1074fn read_unsigned_from_view(
1075    view: ValidatedBufferView,
1076    byte_offset: usize,
1077    index: usize,
1078    component_size: usize,
1079    component_type: u64,
1080    buffers: &[Vec<u8>],
1081    label: &str,
1082) -> Result<u32> {
1083    let start = index
1084        .checked_mul(component_size)
1085        .and_then(|offset| byte_offset.checked_add(offset))
1086        .and_then(|offset| view.byte_offset.checked_add(offset))
1087        .ok_or_else(|| GltfError::InvalidGltf(format!("{label} offset overflow")))?;
1088    let end = start
1089        .checked_add(component_size)
1090        .ok_or_else(|| GltfError::InvalidGltf(format!("{label} offset overflow")))?;
1091    let bytes = buffers
1092        .get(view.buffer)
1093        .and_then(|buffer| buffer.get(start..end))
1094        .ok_or_else(|| GltfError::InvalidGltf(format!("{label} is out of range")))?;
1095    match component_type {
1096        5121 => Ok(bytes[0] as u32),
1097        5123 => Ok(u16::from_le_bytes([bytes[0], bytes[1]]) as u32),
1098        5125 => Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])),
1099        _ => Err(GltfError::InvalidGltf(format!(
1100            "{label} has invalid componentType {component_type}"
1101        ))),
1102    }
1103}
1104
1105fn validate_accessor_reference(value: &Value, count: usize, label: &str) -> Result<()> {
1106    let index = json_index(value, label)?;
1107    if index >= count {
1108        return Err(GltfError::InvalidGltf(format!(
1109            "{label} {index} is out of range"
1110        )));
1111    }
1112    Ok(())
1113}
1114
1115fn validate_accessor_references(document: &Value, accessor_count: usize) -> Result<()> {
1116    for (mesh_index, mesh) in optional_array(document, "meshes")?.iter().enumerate() {
1117        let mesh = mesh
1118            .as_object()
1119            .ok_or_else(|| GltfError::InvalidGltf(format!("mesh {mesh_index} is not an object")))?;
1120        let primitives = mesh
1121            .get("primitives")
1122            .and_then(Value::as_array)
1123            .ok_or_else(|| {
1124                GltfError::InvalidGltf(format!("mesh {mesh_index}.primitives is not an array"))
1125            })?;
1126        if primitives.is_empty() {
1127            return Err(GltfError::InvalidGltf(format!(
1128                "mesh {mesh_index} has no primitives"
1129            )));
1130        }
1131        for (primitive_index, primitive) in primitives.iter().enumerate() {
1132            let primitive = primitive.as_object().ok_or_else(|| {
1133                GltfError::InvalidGltf(format!(
1134                    "primitive {mesh_index}:{primitive_index} is not an object"
1135                ))
1136            })?;
1137            let attributes = primitive
1138                .get("attributes")
1139                .and_then(Value::as_object)
1140                .ok_or_else(|| {
1141                    GltfError::InvalidGltf(format!(
1142                        "primitive {mesh_index}:{primitive_index}.attributes is not an object"
1143                    ))
1144                })?;
1145            if attributes.is_empty() {
1146                return Err(GltfError::InvalidGltf(format!(
1147                    "primitive {mesh_index}:{primitive_index}.attributes is empty"
1148                )));
1149            }
1150            for accessor in attributes.values() {
1151                validate_accessor_reference(accessor, accessor_count, "primitive attribute")?;
1152            }
1153            if let Some(indices) = primitive.get("indices") {
1154                validate_accessor_reference(indices, accessor_count, "primitive indices")?;
1155            }
1156            if let Some(mode) = primitive.get("mode") {
1157                let mode = mode.as_u64().ok_or_else(|| {
1158                    GltfError::InvalidGltf("primitive.mode is not an integer".into())
1159                })?;
1160                if mode > 6 {
1161                    return Err(GltfError::InvalidGltf(format!(
1162                        "primitive mode {mode} is outside the glTF enum"
1163                    )));
1164                }
1165            }
1166            if let Some(targets) = primitive.get("targets") {
1167                let targets = targets.as_array().ok_or_else(|| {
1168                    GltfError::InvalidGltf("primitive.targets is not an array".into())
1169                })?;
1170                for target in targets {
1171                    let target = target.as_object().ok_or_else(|| {
1172                        GltfError::InvalidGltf("morph target is not an object".into())
1173                    })?;
1174                    if target.is_empty() {
1175                        return Err(GltfError::InvalidGltf("morph target is empty".into()));
1176                    }
1177                    for accessor in target.values() {
1178                        validate_accessor_reference(
1179                            accessor,
1180                            accessor_count,
1181                            "morph target accessor",
1182                        )?;
1183                    }
1184                }
1185            }
1186        }
1187    }
1188
1189    for animation in optional_array(document, "animations")? {
1190        if let Some(samplers) = animation.get("samplers") {
1191            let samplers = samplers.as_array().ok_or_else(|| {
1192                GltfError::InvalidGltf("animation.samplers is not an array".into())
1193            })?;
1194            for sampler in samplers {
1195                let sampler = sampler.as_object().ok_or_else(|| {
1196                    GltfError::InvalidGltf("animation sampler is not an object".into())
1197                })?;
1198                for key in ["input", "output"] {
1199                    let accessor = sampler.get(key).ok_or_else(|| {
1200                        GltfError::InvalidGltf(format!("animation sampler is missing {key}"))
1201                    })?;
1202                    validate_accessor_reference(accessor, accessor_count, "animation accessor")?;
1203                }
1204            }
1205        }
1206    }
1207    for skin in optional_array(document, "skins")? {
1208        if let Some(accessor) = skin.get("inverseBindMatrices") {
1209            validate_accessor_reference(accessor, accessor_count, "inverseBindMatrices")?;
1210        }
1211    }
1212    for node in optional_array(document, "nodes")? {
1213        if let Some(attributes) = node
1214            .get("extensions")
1215            .and_then(|extensions| extensions.get("EXT_mesh_gpu_instancing"))
1216            .and_then(|extension| extension.get("attributes"))
1217        {
1218            let attributes = attributes.as_object().ok_or_else(|| {
1219                GltfError::InvalidGltf("EXT_mesh_gpu_instancing.attributes is not an object".into())
1220            })?;
1221            for accessor in attributes.values() {
1222                validate_accessor_reference(accessor, accessor_count, "instancing accessor")?;
1223            }
1224        }
1225    }
1226    Ok(())
1227}
1228
1229/// A primitive that will be compressed, with everything needed to rewrite it.
1230struct CompressPlan {
1231    mesh_idx: usize,
1232    prim_idx: usize,
1233    draco_bytes: Vec<u8>,
1234    /// `(glTF semantic, Draco attribute id)` for the extension's attribute map.
1235    semantic_to_id: Vec<(String, u32)>,
1236    /// Accessor index for each attribute.
1237    attribute_accessors: Vec<usize>,
1238    /// The source indices accessor, or `None` for a non-indexed primitive (a
1239    /// fresh indices accessor is generated, since Draco glTF primitives are
1240    /// always indexed).
1241    indices_accessor: Option<usize>,
1242    num_points: usize,
1243    num_indices: usize,
1244}
1245
1246fn build_plans<F>(
1247    doc: &Value,
1248    buffers: &[Vec<u8>],
1249    decode: &F,
1250    accessor_users: &HashMap<usize, usize>,
1251    options: &GltfCompressionOptions,
1252) -> Result<(Vec<CompressPlan>, CompressionReport)>
1253where
1254    F: Fn(usize, usize) -> Result<(draco_core::Mesh, Vec<(String, u32)>)>,
1255{
1256    let mut plans = Vec::new();
1257    let mut report = CompressionReport::default();
1258    let Some(meshes) = doc.get("meshes").and_then(Value::as_array) else {
1259        return Ok((plans, report));
1260    };
1261
1262    for (mesh_idx, mesh) in meshes.iter().enumerate() {
1263        let Some(primitives) = mesh.get("primitives").and_then(Value::as_array) else {
1264            continue;
1265        };
1266        for (prim_idx, prim) in primitives.iter().enumerate() {
1267            let location = PrimitiveLocation {
1268                mesh: mesh_idx,
1269                primitive: prim_idx,
1270            };
1271            match plan_for_primitive(
1272                doc,
1273                prim,
1274                buffers,
1275                location,
1276                decode,
1277                accessor_users,
1278                options,
1279            )? {
1280                PlanDecision::Compress(plan) => plans.push(plan),
1281                PlanDecision::Preserve(reason) => {
1282                    report.preserved_primitives.push(PreservedPrimitive {
1283                        primitive: location,
1284                        reason,
1285                    })
1286                }
1287            }
1288        }
1289    }
1290    Ok((plans, report))
1291}
1292
1293enum PlanDecision {
1294    Compress(CompressPlan),
1295    Preserve(PreserveReason),
1296}
1297
1298fn validate_existing_draco_primitive(
1299    document: &Value,
1300    primitive: &Value,
1301    buffers: &[Vec<u8>],
1302    mode: u32,
1303) -> Result<()> {
1304    let extension = parse_khr_draco_mesh_compression(document, primitive)?.ok_or_else(|| {
1305        GltfError::InvalidGltf("primitive has no KHR_draco_mesh_compression payload".into())
1306    })?;
1307    let views = document
1308        .get("bufferViews")
1309        .and_then(Value::as_array)
1310        .ok_or_else(|| GltfError::InvalidGltf("missing bufferViews array".into()))?;
1311    let view = views
1312        .get(extension.buffer_view)
1313        .and_then(Value::as_object)
1314        .ok_or_else(|| {
1315            GltfError::InvalidGltf(format!(
1316                "Draco bufferView {} is out of range",
1317                extension.buffer_view
1318            ))
1319        })?;
1320    let data = buffer_view_bytes(view, buffers)?;
1321    let mut decoder_buffer = DecoderBuffer::new(data);
1322    let mut mesh = Mesh::new();
1323    MeshDecoder::new()
1324        .decode(&mut decoder_buffer, &mut mesh)
1325        .map_err(GltfError::DracoDecode)?;
1326
1327    let primitive_attributes = primitive
1328        .get("attributes")
1329        .and_then(Value::as_object)
1330        .ok_or_else(|| GltfError::InvalidGltf("primitive.attributes is not an object".into()))?;
1331    let accessors = document
1332        .get("accessors")
1333        .and_then(Value::as_array)
1334        .ok_or_else(|| GltfError::InvalidGltf("missing accessors array".into()))?;
1335
1336    for (semantic, &unique_id) in &extension.attributes {
1337        let accessor_index = primitive_attributes
1338            .get(semantic)
1339            .ok_or_else(|| {
1340                GltfError::InvalidGltf(format!(
1341                    "Draco semantic {semantic} is absent from primitive.attributes"
1342                ))
1343            })
1344            .and_then(|value| json_index(value, "Draco attribute accessor"))?;
1345        let accessor = accessors
1346            .get(accessor_index)
1347            .and_then(Value::as_object)
1348            .ok_or_else(|| {
1349                GltfError::InvalidGltf(format!("accessor {accessor_index} is out of range"))
1350            })?;
1351        let accessor_type = accessor
1352            .get("type")
1353            .and_then(Value::as_str)
1354            .ok_or_else(|| {
1355                GltfError::InvalidGltf(format!("accessor {accessor_index} has invalid type"))
1356            })?;
1357        let component_type = accessor
1358            .get("componentType")
1359            .and_then(Value::as_u64)
1360            .and_then(|value| u32::try_from(value).ok())
1361            .ok_or_else(|| {
1362                GltfError::InvalidGltf(format!(
1363                    "accessor {accessor_index} has invalid componentType"
1364                ))
1365            })?;
1366        let normalized = match accessor.get("normalized") {
1367            None => false,
1368            Some(Value::Bool(normalized)) => *normalized,
1369            Some(_) => {
1370                return Err(GltfError::InvalidGltf("normalized is not a boolean".into()));
1371            }
1372        };
1373        let semantic_spec =
1374            validate_semantic_accessor(semantic, accessor_type, component_type, normalized)?;
1375        let attribute = mesh.attribute_by_unique_id(unique_id).ok_or_else(|| {
1376            GltfError::InvalidGltf(format!(
1377                "Draco unique attribute id {unique_id} for {semantic} is absent"
1378            ))
1379        })?;
1380        if attribute.attribute_type() != semantic_spec.attribute_type
1381            || gltf_type_for_num_components(attribute.num_components())? != accessor_type
1382            || component_type_for_data_type(attribute.data_type())? != component_type
1383            || attribute.normalized() != normalized
1384        {
1385            return Err(GltfError::InvalidGltf(format!(
1386                "decoded Draco attribute {semantic} does not match accessor {accessor_index}"
1387            )));
1388        }
1389        let count = required_usize(accessor, "count", "Draco attribute accessor")?;
1390        if attribute.size() != count || count != mesh.num_points() {
1391            return Err(GltfError::InvalidGltf(format!(
1392                "decoded Draco attribute {semantic} count {} does not match accessor count {count}",
1393                attribute.size()
1394            )));
1395        }
1396    }
1397
1398    if let Some(indices) = primitive.get("indices") {
1399        let accessor_index = json_index(indices, "Draco indices accessor")?;
1400        let accessor = accessors
1401            .get(accessor_index)
1402            .and_then(Value::as_object)
1403            .ok_or_else(|| {
1404                GltfError::InvalidGltf(format!("accessor {accessor_index} is out of range"))
1405            })?;
1406        if accessor.get("type").and_then(Value::as_str) != Some("SCALAR")
1407            || accessor
1408                .get("componentType")
1409                .and_then(Value::as_u64)
1410                .is_none_or(|component| !matches!(component, 5121 | 5123 | 5125))
1411            || accessor
1412                .get("normalized")
1413                .is_some_and(|normalized| normalized != &Value::Bool(false))
1414        {
1415            return Err(GltfError::InvalidGltf(
1416                "Draco indices accessor has an invalid contract".into(),
1417            ));
1418        }
1419        let expected_count = if mode == MODE_TRIANGLES as u32 {
1420            mesh.num_faces().checked_mul(3)
1421        } else {
1422            mesh.num_faces().checked_add(2)
1423        }
1424        .ok_or_else(|| GltfError::InvalidGltf("decoded Draco index count overflow".into()))?;
1425        let count = required_usize(accessor, "count", "Draco indices accessor")?;
1426        if count != expected_count {
1427            return Err(GltfError::InvalidGltf(format!(
1428                "Draco indices accessor count {count} does not match decoded count {expected_count}"
1429            )));
1430        }
1431    }
1432    Ok(())
1433}
1434
1435fn plan_for_primitive<F>(
1436    doc: &Value,
1437    prim: &Value,
1438    buffers: &[Vec<u8>],
1439    location: PrimitiveLocation,
1440    decode: &F,
1441    accessor_users: &HashMap<usize, usize>,
1442    options: &GltfCompressionOptions,
1443) -> Result<PlanDecision>
1444where
1445    F: Fn(usize, usize) -> Result<(draco_core::Mesh, Vec<(String, u32)>)>,
1446{
1447    let mesh_idx = location.mesh;
1448    let prim_idx = location.primitive;
1449    // KHR_draco_mesh_compression restricts compressed primitives to TRIANGLES
1450    // or TRIANGLE_STRIP ("Restrictions on geometry type"), so point clouds
1451    // (POINTS) and line modes can never be Draco-compressed in glTF. We compress
1452    // the triangle list (mode 4 / default); everything else is preserved as-is.
1453    let mode = prim
1454        .get("mode")
1455        .map(|value| {
1456            value
1457                .as_u64()
1458                .ok_or_else(|| GltfError::InvalidGltf("primitive.mode is not an integer".into()))
1459        })
1460        .transpose()?
1461        .unwrap_or(MODE_TRIANGLES);
1462    let mode_u32 = u32::try_from(mode)
1463        .map_err(|_| GltfError::InvalidGltf("primitive.mode exceeds u32".into()))?;
1464    // A repeated compression request must prove that an existing Draco payload
1465    // and all of its accessor contracts are valid before reporting it as
1466    // preserved. Schema-only validation would turn corrupt input into a
1467    // successful AlreadyDraco result.
1468    if prim
1469        .get("extensions")
1470        .and_then(|extensions| extensions.get(KHR_DRACO))
1471        .is_some()
1472    {
1473        validate_existing_draco_primitive(doc, prim, buffers, mode_u32)?;
1474        return Ok(PlanDecision::Preserve(PreserveReason::AlreadyDraco));
1475    }
1476    if mode != MODE_TRIANGLES && mode != MODE_TRIANGLE_STRIP {
1477        return Ok(PlanDecision::Preserve(PreserveReason::UnsupportedMode {
1478            mode: mode_u32,
1479        }));
1480    }
1481    if mode == MODE_TRIANGLE_STRIP {
1482        return Ok(PlanDecision::Preserve(PreserveReason::UnsupportedLayout {
1483            detail: "TRIANGLE_STRIP compression is not implemented".into(),
1484        }));
1485    }
1486    if let Some(targets) = prim.get("targets") {
1487        let targets = targets
1488            .as_array()
1489            .ok_or_else(|| GltfError::InvalidGltf("primitive.targets is not an array".into()))?;
1490        if !targets.is_empty() {
1491            return Ok(PlanDecision::Preserve(PreserveReason::MorphTargets));
1492        }
1493    }
1494    // Indexed and non-indexed triangle lists are both supported. Non-indexed
1495    // primitives get a freshly generated indices accessor below, since Draco
1496    // glTF primitives are always indexed.
1497    let indices_accessor = prim
1498        .get("indices")
1499        .map(|value| json_index(value, "indices accessor"))
1500        .transpose()?;
1501
1502    // Collect attribute semantics + accessors; require a round-trippable set.
1503    let Some(attributes) = prim.get("attributes").and_then(Value::as_object) else {
1504        return Err(GltfError::InvalidGltf(
1505            "primitive.attributes must be a non-empty object".into(),
1506        ));
1507    };
1508    if attributes.is_empty() || !attributes.contains_key("POSITION") {
1509        return Ok(PlanDecision::Preserve(PreserveReason::UnsupportedLayout {
1510            detail: "primitive has no POSITION attribute".into(),
1511        }));
1512    }
1513    let mut attribute_accessors = Vec::new();
1514    for accessor in attributes.values() {
1515        attribute_accessors.push(json_index(accessor, "attribute accessor")?);
1516    }
1517
1518    let accessors = doc
1519        .get("accessors")
1520        .and_then(Value::as_array)
1521        .ok_or_else(|| GltfError::InvalidGltf("missing accessors array".into()))?;
1522    for &accessor in attribute_accessors.iter().chain(indices_accessor.iter()) {
1523        let value = accessors
1524            .get(accessor)
1525            .and_then(Value::as_object)
1526            .ok_or_else(|| {
1527                GltfError::InvalidGltf(format!("accessor {accessor} is out of range"))
1528            })?;
1529        if value.contains_key("sparse") {
1530            return Ok(PlanDecision::Preserve(PreserveReason::SparseAccessor {
1531                accessor,
1532            }));
1533        }
1534    }
1535
1536    // All geometry accessors must be used by exactly this primitive, so that
1537    // dropping their buffer view / changing their count cannot corrupt another
1538    // primitive that shares them.
1539    let exclusive = |acc: usize| accessor_users.get(&acc).copied().unwrap_or(0) == 1;
1540    if let Some(accessor) = indices_accessor.filter(|accessor| !exclusive(*accessor)) {
1541        return Ok(PlanDecision::Preserve(PreserveReason::SharedAccessor {
1542            accessor,
1543        }));
1544    }
1545    if let Some(accessor) = attribute_accessors
1546        .iter()
1547        .copied()
1548        .find(|accessor| !exclusive(*accessor))
1549    {
1550        return Ok(PlanDecision::Preserve(PreserveReason::SharedAccessor {
1551            accessor,
1552        }));
1553    }
1554
1555    // Decode geometry with the original glTF semantic names. An unsupported
1556    // attribute/layout means "leave this primitive uncompressed".
1557    let (mesh, semantic_to_uid) = match decode(mesh_idx, prim_idx) {
1558        Ok(out) => out,
1559        Err(GltfError::Unsupported(detail)) => {
1560            return Ok(PlanDecision::Preserve(PreserveReason::UnsupportedLayout {
1561                detail,
1562            }))
1563        }
1564        Err(error) => return Err(error),
1565    };
1566    let (draco_bytes, info) = match encode_draco_mesh_with_info(&mesh, options) {
1567        Ok(out) => out,
1568        Err(crate::gltf_writer::GltfWriteError::Unsupported(detail)) => {
1569            return Ok(PlanDecision::Preserve(PreserveReason::UnsupportedLayout {
1570                detail,
1571            }))
1572        }
1573        Err(crate::gltf_writer::GltfWriteError::InvalidMesh(detail)) => {
1574            return Err(GltfError::InvalidGltf(detail))
1575        }
1576        Err(crate::gltf_writer::GltfWriteError::InvalidOptions(detail)) => {
1577            return Err(GltfError::InvalidOptions(detail))
1578        }
1579        Err(crate::gltf_writer::GltfWriteError::ResourceLimit(detail)) => {
1580            return Err(GltfError::ResourceLimitExceeded(detail))
1581        }
1582        Err(crate::gltf_writer::GltfWriteError::DracoEncode(source)) => {
1583            return Err(GltfError::DracoEncode(source))
1584        }
1585        Err(error) => {
1586            return Err(GltfError::InvalidGltf(format!(
1587                "Draco writer failed: {error}"
1588            )))
1589        }
1590    };
1591
1592    // The decoded attribute set must match the source primitive exactly, so the
1593    // extension's attribute map is faithful (no dropped or renamed attribute).
1594    let produced: BTreeSet<&str> = semantic_to_uid.iter().map(|(s, _)| s.as_str()).collect();
1595    let original: BTreeSet<&str> = attributes.keys().map(String::as_str).collect();
1596    if produced != original {
1597        return Err(GltfError::InvalidGltf(
1598            "decoded attribute set does not match primitive.attributes".into(),
1599        ));
1600    }
1601    let semantic_to_id = semantic_to_uid;
1602
1603    let num_indices = info
1604        .num_encoded_faces
1605        .checked_mul(3)
1606        .ok_or_else(|| GltfError::ResourceLimitExceeded("index count overflow".into()))?;
1607
1608    Ok(PlanDecision::Compress(CompressPlan {
1609        mesh_idx,
1610        prim_idx,
1611        draco_bytes,
1612        semantic_to_id,
1613        attribute_accessors,
1614        indices_accessor,
1615        num_points: info.num_encoded_points,
1616        num_indices,
1617    }))
1618}
1619
1620/// Counts, for each accessor index, how many primitives reference it (via
1621/// attributes or indices) across the whole document.
1622fn count_accessor_users(doc: &Value) -> Result<HashMap<usize, usize>> {
1623    let mut users: HashMap<usize, usize> = HashMap::new();
1624    let mut add = |value: &Value, label: &str| -> Result<()> {
1625        let accessor = json_index(value, label)?;
1626        let count = users.entry(accessor).or_default();
1627        *count = count
1628            .checked_add(1)
1629            .ok_or_else(|| GltfError::InvalidGltf("accessor use count overflow".into()))?;
1630        Ok(())
1631    };
1632
1633    if let Some(meshes) = doc.get("meshes") {
1634        let meshes = meshes
1635            .as_array()
1636            .ok_or_else(|| GltfError::InvalidGltf("meshes is not an array".into()))?;
1637        for mesh in meshes {
1638            let primitives = mesh
1639                .get("primitives")
1640                .and_then(Value::as_array)
1641                .ok_or_else(|| GltfError::InvalidGltf("mesh.primitives is not an array".into()))?;
1642            for prim in primitives {
1643                let attrs = prim
1644                    .get("attributes")
1645                    .and_then(Value::as_object)
1646                    .ok_or_else(|| {
1647                        GltfError::InvalidGltf("primitive.attributes is not an object".into())
1648                    })?;
1649                for accessor in attrs.values() {
1650                    add(accessor, "primitive attribute accessor")?;
1651                }
1652                if let Some(accessor) = prim.get("indices") {
1653                    add(accessor, "primitive indices accessor")?;
1654                }
1655                if let Some(targets) = prim.get("targets") {
1656                    let targets = targets.as_array().ok_or_else(|| {
1657                        GltfError::InvalidGltf("primitive.targets is not an array".into())
1658                    })?;
1659                    for target in targets {
1660                        let target = target.as_object().ok_or_else(|| {
1661                            GltfError::InvalidGltf("morph target is not an object".into())
1662                        })?;
1663                        for accessor in target.values() {
1664                            add(accessor, "morph target accessor")?;
1665                        }
1666                    }
1667                }
1668            }
1669        }
1670    }
1671
1672    if let Some(animations) = doc.get("animations") {
1673        for animation in animations
1674            .as_array()
1675            .ok_or_else(|| GltfError::InvalidGltf("animations is not an array".into()))?
1676        {
1677            if let Some(samplers) = animation.get("samplers") {
1678                for sampler in samplers.as_array().ok_or_else(|| {
1679                    GltfError::InvalidGltf("animation.samplers is not an array".into())
1680                })? {
1681                    let sampler = sampler.as_object().ok_or_else(|| {
1682                        GltfError::InvalidGltf("animation sampler is not an object".into())
1683                    })?;
1684                    for key in ["input", "output"] {
1685                        let accessor = sampler.get(key).ok_or_else(|| {
1686                            GltfError::InvalidGltf(format!("animation sampler is missing {key}"))
1687                        })?;
1688                        add(accessor, "animation sampler accessor")?;
1689                    }
1690                }
1691            }
1692        }
1693    }
1694
1695    if let Some(skins) = doc.get("skins") {
1696        for skin in skins
1697            .as_array()
1698            .ok_or_else(|| GltfError::InvalidGltf("skins is not an array".into()))?
1699        {
1700            if let Some(accessor) = skin.get("inverseBindMatrices") {
1701                add(accessor, "skin inverseBindMatrices accessor")?;
1702            }
1703        }
1704    }
1705
1706    if let Some(nodes) = doc.get("nodes") {
1707        for node in nodes
1708            .as_array()
1709            .ok_or_else(|| GltfError::InvalidGltf("nodes is not an array".into()))?
1710        {
1711            if let Some(attributes) = node
1712                .get("extensions")
1713                .and_then(|extensions| extensions.get("EXT_mesh_gpu_instancing"))
1714                .and_then(|extension| extension.get("attributes"))
1715            {
1716                let attributes = attributes.as_object().ok_or_else(|| {
1717                    GltfError::InvalidGltf(
1718                        "EXT_mesh_gpu_instancing.attributes is not an object".into(),
1719                    )
1720                })?;
1721                for accessor in attributes.values() {
1722                    add(accessor, "EXT_mesh_gpu_instancing accessor")?;
1723                }
1724            }
1725        }
1726    }
1727
1728    Ok(users)
1729}
1730
1731fn apply_accessor_mutations(doc: &mut Value, plans: &[CompressPlan]) -> Result<()> {
1732    let accessors = doc
1733        .get_mut("accessors")
1734        .and_then(Value::as_array_mut)
1735        .ok_or_else(|| GltfError::InvalidGltf("missing accessors array".into()))?;
1736
1737    for plan in plans {
1738        for &acc in &plan.attribute_accessors {
1739            strip_geometry_accessor(accessors, acc, plan.num_points)?;
1740        }
1741        if let Some(indices) = plan.indices_accessor {
1742            strip_geometry_accessor(accessors, indices, plan.num_indices)?;
1743        }
1744    }
1745    Ok(())
1746}
1747
1748/// For each non-indexed plan, append a fresh `SCALAR`/`UNSIGNED_INT` indices
1749/// accessor (no buffer view — the indices live in the Draco stream) and point
1750/// the primitive at it. Appending keeps existing accessor indices stable, and
1751/// the new accessor has no buffer view so it does not affect buffer repacking.
1752fn add_generated_indices(doc: &mut Value, plans: &[CompressPlan]) -> Result<()> {
1753    for plan in plans {
1754        if plan.indices_accessor.is_some() {
1755            continue;
1756        }
1757        let accessors = doc
1758            .get_mut("accessors")
1759            .and_then(Value::as_array_mut)
1760            .ok_or_else(|| GltfError::InvalidGltf("missing accessors array".into()))?;
1761        let new_idx = accessors.len();
1762        accessors.push(serde_json::json!({
1763            "componentType": 5125u64, // UNSIGNED_INT
1764            "count": plan.num_indices as u64,
1765            "type": "SCALAR",
1766        }));
1767        let prim = primitive_mut(doc, plan)?;
1768        prim.insert("indices".into(), Value::from(new_idx as u64));
1769    }
1770    Ok(())
1771}
1772
1773/// Mutable access to a plan's primitive JSON object.
1774fn primitive_mut<'a>(
1775    doc: &'a mut Value,
1776    plan: &CompressPlan,
1777) -> Result<&'a mut Map<String, Value>> {
1778    doc.get_mut("meshes")
1779        .and_then(Value::as_array_mut)
1780        .and_then(|m| m.get_mut(plan.mesh_idx))
1781        .and_then(|m| m.get_mut("primitives"))
1782        .and_then(Value::as_array_mut)
1783        .and_then(|p| p.get_mut(plan.prim_idx))
1784        .and_then(Value::as_object_mut)
1785        .ok_or_else(|| GltfError::InvalidGltf("primitive vanished during rewrite".into()))
1786}
1787
1788/// Removes an accessor's buffer view (its data now lives in Draco) and sets the
1789/// count to the Draco-encoded element count. Other fields (type, componentType,
1790/// min/max, normalized) are preserved.
1791fn strip_geometry_accessor(accessors: &mut [Value], idx: usize, count: usize) -> Result<()> {
1792    let accessor = accessors
1793        .get_mut(idx)
1794        .and_then(Value::as_object_mut)
1795        .ok_or_else(|| GltfError::InvalidGltf(format!("accessor {} out of range", idx)))?;
1796    accessor.remove("bufferView");
1797    accessor.remove("byteOffset");
1798    accessor.insert("count".into(), Value::from(count));
1799    Ok(())
1800}
1801
1802struct Repack {
1803    bin: Vec<u8>,
1804    /// New buffer-view index for each plan's Draco stream, in plan order.
1805    draco_buffer_views: Vec<usize>,
1806}
1807
1808fn repack_buffers(
1809    doc: &mut Value,
1810    source_buffers: &[Vec<u8>],
1811    plans: &[CompressPlan],
1812) -> Result<Repack> {
1813    // Which buffer views are still referenced anywhere in the JSON (accessors,
1814    // images, surviving Draco extensions, and any unknown extension)? Scanning
1815    // by key name covers known and unknown referrers uniformly.
1816    let mut referenced = BTreeSet::new();
1817    collect_buffer_view_refs(doc, &mut referenced)?;
1818
1819    // Move the old table out of the document. Kept entries are then moved into
1820    // the new table rather than deep-cloning arbitrary extension JSON.
1821    let old_views = doc
1822        .as_object_mut()
1823        .ok_or_else(|| GltfError::InvalidGltf("glTF root is not an object".into()))?
1824        .remove("bufferViews");
1825    let mut old_views = match old_views {
1826        Some(Value::Array(views)) => views,
1827        Some(_) => {
1828            return Err(GltfError::InvalidGltf("bufferViews is not an array".into()));
1829        }
1830        None => Vec::new(),
1831    };
1832
1833    // Build the new binary: kept views (remapped), then one view per Draco blob.
1834    let mut bin: Vec<u8> = Vec::new();
1835    let mut new_views: Vec<Value> = Vec::new();
1836    let new_view_count = referenced
1837        .len()
1838        .checked_add(plans.len())
1839        .ok_or_else(|| GltfError::ResourceLimitExceeded("bufferView table size overflow".into()))?;
1840    new_views.try_reserve_exact(new_view_count).map_err(|_| {
1841        GltfError::ResourceLimitExceeded("bufferView table allocation failed".into())
1842    })?;
1843    let mut remap: HashMap<usize, usize> = HashMap::new();
1844    remap.try_reserve(referenced.len()).map_err(|_| {
1845        GltfError::ResourceLimitExceeded("bufferView remap allocation failed".into())
1846    })?;
1847
1848    for &old_idx in &referenced {
1849        let view = old_views
1850            .get_mut(old_idx)
1851            .ok_or_else(|| GltfError::InvalidGltf(format!("buffer view {old_idx} invalid")))?;
1852        let mut new_view = match std::mem::take(view) {
1853            Value::Object(view) => view,
1854            _ => {
1855                return Err(GltfError::InvalidGltf(format!(
1856                    "buffer view {old_idx} is not an object"
1857                )));
1858            }
1859        };
1860        let bytes = buffer_view_bytes(&new_view, source_buffers)?;
1861        let byte_length = bytes.len();
1862        align_to_4(&mut bin)?;
1863        let offset = bin.len();
1864        append_bytes(&mut bin, bytes, "buffer view")?;
1865
1866        new_view.insert("buffer".into(), Value::from(0u64));
1867        new_view.insert("byteOffset".into(), Value::from(offset as u64));
1868        new_view.insert("byteLength".into(), Value::from(byte_length as u64));
1869        let new_idx = new_views.len();
1870        new_views.push(Value::Object(new_view));
1871        remap.insert(old_idx, new_idx);
1872    }
1873
1874    // Reindex every buffer-view reference in the document to the kept set.
1875    remap_buffer_view_refs(doc, &remap)?;
1876
1877    // Append the Draco buffer views (not present in the JSON yet, so they are
1878    // intentionally added after the remap pass).
1879    let mut draco_buffer_views = Vec::new();
1880    draco_buffer_views
1881        .try_reserve_exact(plans.len())
1882        .map_err(|_| {
1883            GltfError::ResourceLimitExceeded("Draco bufferView table allocation failed".into())
1884        })?;
1885    for plan in plans {
1886        align_to_4(&mut bin)?;
1887        let offset = bin.len();
1888        append_bytes(&mut bin, &plan.draco_bytes, "Draco bitstream")?;
1889        let new_idx = new_views.len();
1890        new_views.push(serde_json::json!({
1891            "buffer": 0,
1892            "byteOffset": offset as u64,
1893            "byteLength": plan.draco_bytes.len() as u64,
1894        }));
1895        draco_buffer_views.push(new_idx);
1896    }
1897
1898    let root = doc
1899        .as_object_mut()
1900        .ok_or_else(|| GltfError::InvalidGltf("glTF root is not an object".into()))?;
1901    if new_views.is_empty() {
1902        root.remove("bufferViews");
1903    } else {
1904        root.insert("bufferViews".into(), Value::Array(new_views));
1905    }
1906
1907    Ok(Repack {
1908        bin,
1909        draco_buffer_views,
1910    })
1911}
1912
1913fn buffer_view_bytes<'a>(view: &Map<String, Value>, buffers: &'a [Vec<u8>]) -> Result<&'a [u8]> {
1914    let buffer_idx: usize = view
1915        .get("buffer")
1916        .and_then(Value::as_u64)
1917        .ok_or_else(|| GltfError::InvalidGltf("buffer view missing buffer index".into()))?
1918        .try_into()
1919        .map_err(|_| GltfError::InvalidGltf("buffer index cannot fit usize".into()))?;
1920    let offset_u64 = view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0);
1921    let offset = usize::try_from(offset_u64)
1922        .map_err(|_| GltfError::InvalidGltf("buffer view offset cannot fit usize".into()))?;
1923    let length: usize = view
1924        .get("byteLength")
1925        .and_then(Value::as_u64)
1926        .ok_or_else(|| GltfError::InvalidGltf("buffer view missing byteLength".into()))?
1927        .try_into()
1928        .map_err(|_| GltfError::InvalidGltf("buffer view length cannot fit usize".into()))?;
1929    let buffer = buffers
1930        .get(buffer_idx)
1931        .ok_or_else(|| GltfError::InvalidGltf(format!("buffer {} not resolved", buffer_idx)))?;
1932    let end = offset
1933        .checked_add(length)
1934        .filter(|&e| e <= buffer.len())
1935        .ok_or_else(|| GltfError::InvalidGltf("buffer view out of range".into()))?;
1936    Ok(&buffer[offset..end])
1937}
1938
1939/// Recursively collect every integer found under a `"bufferView"` key.
1940fn collect_buffer_view_refs(value: &Value, out: &mut BTreeSet<usize>) -> Result<()> {
1941    match value {
1942        Value::Object(map) => {
1943            for (key, child) in map {
1944                if key == "extras" {
1945                    continue;
1946                }
1947                if key == "extensions" {
1948                    if let Some(extensions) = child.as_object() {
1949                        if let Some(draco) = extensions.get(KHR_DRACO) {
1950                            collect_buffer_view_refs(draco, out)?;
1951                        }
1952                        if let Some(metadata) = extensions.get("EXT_structural_metadata") {
1953                            collect_structural_metadata_refs(metadata, out)?;
1954                        }
1955                    }
1956                    continue;
1957                }
1958                if key == "bufferView" {
1959                    out.insert(json_index(child, "bufferView")?);
1960                }
1961                collect_buffer_view_refs(child, out)?;
1962            }
1963        }
1964        Value::Array(items) => {
1965            for item in items {
1966                collect_buffer_view_refs(item, out)?;
1967            }
1968        }
1969        _ => {}
1970    }
1971    Ok(())
1972}
1973
1974/// Recursively remap every integer under a `"bufferView"` key using `remap`.
1975fn remap_buffer_view_refs(value: &mut Value, remap: &HashMap<usize, usize>) -> Result<()> {
1976    match value {
1977        Value::Object(map) => {
1978            for (key, child) in map.iter_mut() {
1979                if key == "extras" {
1980                    continue;
1981                }
1982                if key == "extensions" {
1983                    if let Some(extensions) = child.as_object_mut() {
1984                        if let Some(draco) = extensions.get_mut(KHR_DRACO) {
1985                            remap_buffer_view_refs(draco, remap)?;
1986                        }
1987                        if let Some(metadata) = extensions.get_mut("EXT_structural_metadata") {
1988                            remap_structural_metadata_refs(metadata, remap)?;
1989                        }
1990                    }
1991                    continue;
1992                }
1993                if key == "bufferView" {
1994                    let old = json_index(child, "bufferView")?;
1995                    let new = remap.get(&old).ok_or_else(|| {
1996                        GltfError::InvalidGltf(format!("bufferView {old} has no repack mapping"))
1997                    })?;
1998                    *child = Value::from(*new as u64);
1999                }
2000                remap_buffer_view_refs(child, remap)?;
2001            }
2002        }
2003        Value::Array(items) => {
2004            for item in items {
2005                remap_buffer_view_refs(item, remap)?;
2006            }
2007        }
2008        _ => {}
2009    }
2010    Ok(())
2011}
2012
2013const STRUCTURAL_METADATA_BUFFER_VIEW_KEYS: &[&str] = &["values", "arrayOffsets", "stringOffsets"];
2014
2015fn structural_metadata_properties(value: &Value) -> Result<Vec<&Map<String, Value>>> {
2016    let Some(tables) = value.get("propertyTables") else {
2017        return Ok(Vec::new());
2018    };
2019    let tables = tables.as_array().ok_or_else(|| {
2020        GltfError::InvalidGltf("EXT_structural_metadata.propertyTables is not an array".into())
2021    })?;
2022    let mut properties = Vec::new();
2023    for table in tables {
2024        let Some(table_properties) = table.get("properties") else {
2025            continue;
2026        };
2027        let table_properties = table_properties.as_object().ok_or_else(|| {
2028            GltfError::InvalidGltf(
2029                "EXT_structural_metadata property table properties is not an object".into(),
2030            )
2031        })?;
2032        for property in table_properties.values() {
2033            properties.push(property.as_object().ok_or_else(|| {
2034                GltfError::InvalidGltf("EXT_structural_metadata property is not an object".into())
2035            })?);
2036        }
2037    }
2038    Ok(properties)
2039}
2040
2041fn collect_structural_metadata_refs(metadata: &Value, out: &mut BTreeSet<usize>) -> Result<()> {
2042    for property in structural_metadata_properties(metadata)? {
2043        for key in STRUCTURAL_METADATA_BUFFER_VIEW_KEYS {
2044            if let Some(value) = property.get(*key) {
2045                out.insert(json_index(value, "EXT_structural_metadata bufferView")?);
2046            }
2047        }
2048    }
2049    Ok(())
2050}
2051
2052fn remap_structural_metadata_refs(
2053    metadata: &mut Value,
2054    remap: &HashMap<usize, usize>,
2055) -> Result<()> {
2056    let Some(tables) = metadata.get_mut("propertyTables") else {
2057        return Ok(());
2058    };
2059    let tables = tables.as_array_mut().ok_or_else(|| {
2060        GltfError::InvalidGltf("EXT_structural_metadata.propertyTables is not an array".into())
2061    })?;
2062    for table in tables {
2063        let Some(properties) = table.get_mut("properties") else {
2064            continue;
2065        };
2066        let properties = properties.as_object_mut().ok_or_else(|| {
2067            GltfError::InvalidGltf(
2068                "EXT_structural_metadata property table properties is not an object".into(),
2069            )
2070        })?;
2071        for property in properties.values_mut() {
2072            let property = property.as_object_mut().ok_or_else(|| {
2073                GltfError::InvalidGltf("EXT_structural_metadata property is not an object".into())
2074            })?;
2075            for key in STRUCTURAL_METADATA_BUFFER_VIEW_KEYS {
2076                if let Some(value) = property.get_mut(*key) {
2077                    let old = json_index(value, "EXT_structural_metadata bufferView")?;
2078                    let new = remap.get(&old).ok_or_else(|| {
2079                        GltfError::InvalidGltf(format!(
2080                            "EXT_structural_metadata bufferView {old} has no repack mapping"
2081                        ))
2082                    })?;
2083                    *value = Value::from(*new as u64);
2084                }
2085            }
2086        }
2087    }
2088    Ok(())
2089}
2090
2091fn known_non_binary_extension(name: &str) -> bool {
2092    name == "KHR_texture_transform"
2093        || name == "EXT_mesh_features"
2094        || name == "EXT_mesh_gpu_instancing"
2095        || name == "KHR_lights_punctual"
2096        || name == "KHR_animation_pointer"
2097        || matches!(
2098            name,
2099            "KHR_materials_anisotropy"
2100                | "KHR_materials_clearcoat"
2101                | "KHR_materials_diffuse_transmission"
2102                | "KHR_materials_dispersion"
2103                | "KHR_materials_emissive_strength"
2104                | "KHR_materials_ior"
2105                | "KHR_materials_iridescence"
2106                | "KHR_materials_pbrSpecularGlossiness"
2107                | "KHR_materials_sheen"
2108                | "KHR_materials_specular"
2109                | "KHR_materials_transmission"
2110                | "KHR_materials_unlit"
2111                | "KHR_materials_variants"
2112                | "KHR_materials_volume"
2113                | "KHR_texture_basisu"
2114                | "EXT_texture_webp"
2115                | "EXT_texture_avif"
2116                | "MSFT_lod"
2117        )
2118}
2119
2120fn reject_opaque_binary_references(document: &Value) -> Result<()> {
2121    fn scan(value: &Value, path: &str) -> Result<()> {
2122        match value {
2123            Value::Object(object) => {
2124                for (key, child) in object {
2125                    let normalized: String = key
2126                        .chars()
2127                        .filter(|character| character.is_ascii_alphanumeric())
2128                        .flat_map(char::to_lowercase)
2129                        .collect();
2130                    let looks_binary = normalized.contains("buffer")
2131                        || normalized.contains("offset")
2132                        || normalized.contains("stride")
2133                        || (normalized.contains("byte") && normalized.contains("length"));
2134                    if looks_binary {
2135                        return Err(GltfError::OpaqueBinaryReference(format!("{path}.{key}")));
2136                    }
2137                    scan(child, &format!("{path}.{key}"))?;
2138                }
2139            }
2140            Value::Array(values) => {
2141                for (index, child) in values.iter().enumerate() {
2142                    scan(child, &format!("{path}[{index}]"))?;
2143                }
2144            }
2145            _ => {}
2146        }
2147        Ok(())
2148    }
2149
2150    fn walk(value: &Value, path: &str) -> Result<()> {
2151        match value {
2152            Value::Object(object) => {
2153                for (key, child) in object {
2154                    if key == "extras" {
2155                        continue;
2156                    }
2157                    if key == "extensions" {
2158                        let extensions = child.as_object().ok_or_else(|| {
2159                            GltfError::InvalidGltf(format!("{path}.extensions is not an object"))
2160                        })?;
2161                        for (name, extension) in extensions {
2162                            let extension_path = format!("{path}.extensions.{name}");
2163                            if name == KHR_DRACO
2164                                || name == "EXT_structural_metadata"
2165                                || known_non_binary_extension(name)
2166                            {
2167                                // The extension's own binary layout is known (or
2168                                // known not to contain binary references), but
2169                                // nested extension objects are independent and
2170                                // must still be classified recursively.
2171                                walk(extension, &extension_path)?;
2172                            } else {
2173                                scan(extension, &extension_path)?;
2174                            }
2175                        }
2176                        continue;
2177                    }
2178                    walk(child, &format!("{path}.{key}"))?;
2179                }
2180            }
2181            Value::Array(values) => {
2182                for (index, child) in values.iter().enumerate() {
2183                    walk(child, &format!("{path}[{index}]"))?;
2184                }
2185            }
2186            _ => {}
2187        }
2188        Ok(())
2189    }
2190
2191    walk(document, "$")
2192}
2193
2194fn set_primitive_draco_extension(
2195    doc: &mut Value,
2196    plan: &CompressPlan,
2197    draco_buffer_view: usize,
2198) -> Result<()> {
2199    let prim = primitive_mut(doc, plan)?;
2200
2201    let mut attributes = Map::new();
2202    for (semantic, id) in &plan.semantic_to_id {
2203        attributes.insert(semantic.clone(), Value::from(*id as u64));
2204    }
2205    let draco = serde_json::json!({
2206        "bufferView": draco_buffer_view as u64,
2207        "attributes": Value::Object(attributes),
2208    });
2209
2210    let extensions = prim
2211        .entry("extensions")
2212        .or_insert_with(|| Value::Object(Map::new()));
2213    if !extensions.is_object() {
2214        return Err(GltfError::InvalidGltf(
2215            "primitive.extensions is not an object".into(),
2216        ));
2217    }
2218    let extensions = extensions
2219        .as_object_mut()
2220        .ok_or_else(|| GltfError::InvalidGltf("primitive.extensions is not an object".into()))?;
2221    extensions.insert(KHR_DRACO.into(), draco);
2222    Ok(())
2223}
2224
2225/// Adds `KHR_draco_mesh_compression` to a root string array (creating it if
2226/// absent) without duplicating it.
2227fn ensure_extension_listed(doc: &mut Value, key: &str) -> Result<()> {
2228    let root = doc
2229        .as_object_mut()
2230        .ok_or_else(|| GltfError::InvalidGltf("glTF root is not an object".into()))?;
2231    let list = root.entry(key).or_insert_with(|| Value::Array(Vec::new()));
2232    let arr = list
2233        .as_array_mut()
2234        .ok_or_else(|| GltfError::InvalidGltf(format!("{key} is not an array")))?;
2235    if !arr.iter().any(|v| v.as_str() == Some(KHR_DRACO)) {
2236        arr.push(Value::from(KHR_DRACO));
2237    }
2238    Ok(())
2239}
2240
2241/// Collapses the document to a single buffer of `bin_len` bytes (carrying
2242/// `byteLength` but no URI). The caller embeds the bytes: `serialize` fills a
2243/// data URI for glTF output, or writes a GLB BIN chunk.
2244fn set_single_buffer(doc: &mut Value, bin_len: usize) -> Result<()> {
2245    let root = doc
2246        .as_object_mut()
2247        .ok_or_else(|| GltfError::InvalidGltf("glTF root is not an object".into()))?;
2248    if bin_len == 0 {
2249        root.remove("buffers");
2250        return Ok(());
2251    }
2252    let mut buffer = Map::new();
2253    let bin_len = u64::try_from(bin_len)
2254        .map_err(|_| GltfError::ResourceLimitExceeded("buffer exceeds u64".into()))?;
2255    buffer.insert("byteLength".into(), Value::from(bin_len));
2256    root.insert("buffers".into(), Value::Array(vec![Value::Object(buffer)]));
2257    Ok(())
2258}
2259
2260fn align_to_4(buf: &mut Vec<u8>) -> Result<()> {
2261    let padding = (4 - buf.len() % 4) % 4;
2262    let new_len = buf
2263        .len()
2264        .checked_add(padding)
2265        .ok_or_else(|| GltfError::ResourceLimitExceeded("alignment size overflow".into()))?;
2266    buf.try_reserve(padding)
2267        .map_err(|_| GltfError::ResourceLimitExceeded("alignment allocation failed".into()))?;
2268    buf.resize(new_len, 0);
2269    Ok(())
2270}
2271
2272fn append_bytes(buf: &mut Vec<u8>, bytes: &[u8], label: &str) -> Result<()> {
2273    buf.len()
2274        .checked_add(bytes.len())
2275        .ok_or_else(|| GltfError::ResourceLimitExceeded(format!("{label} size overflow")))?;
2276    buf.try_reserve(bytes.len())
2277        .map_err(|_| GltfError::ResourceLimitExceeded(format!("{label} allocation failed")))?;
2278    buf.extend_from_slice(bytes);
2279    Ok(())
2280}