use std::collections::{BTreeSet, HashMap};
#[cfg(feature = "gltf-reader")]
use std::path::Path;
use serde_json::{Map, Value};
use crate::gltf_geometry::GltfError;
#[cfg(feature = "gltf-reader")]
use crate::gltf_reader::GltfReader;
use crate::gltf_writer::{encode_draco_mesh_with_info, QuantizationBits};
type Result<T> = std::result::Result<T, GltfError>;
const KHR_DRACO: &str = "KHR_draco_mesh_compression";
const MODE_TRIANGLES: u64 = 4;
#[cfg(feature = "gltf-reader")]
const GLB_MAGIC: u32 = 0x4654_6C67;
#[cfg(feature = "gltf-reader")]
const GLB_VERSION: u32 = 2;
#[cfg(feature = "gltf-reader")]
const GLB_CHUNK_JSON: u32 = 0x4E4F_534A;
#[cfg(feature = "gltf-reader")]
const GLB_CHUNK_BIN: u32 = 0x004E_4942;
#[cfg(feature = "gltf-reader")]
#[derive(Clone, Copy, PartialEq, Eq)]
enum Container {
Glb,
Gltf,
}
#[cfg(feature = "gltf-reader")]
pub fn compress_gltf_bytes(
input: &[u8],
quantization: Option<QuantizationBits>,
) -> Result<Vec<u8>> {
compress_gltf_bytes_with_base_path(input, None, quantization)
}
#[cfg(feature = "gltf-reader")]
pub fn compress_gltf_bytes_with_base_path(
input: &[u8],
base_path: Option<&Path>,
quantization: Option<QuantizationBits>,
) -> Result<Vec<u8>> {
let is_glb = input.len() >= 4 && read_u32_le(&input[0..4]) == GLB_MAGIC;
let (doc, container) = if is_glb {
let (json_bytes, _) = split_glb(input)?;
(serde_json::from_slice::<Value>(json_bytes)?, Container::Glb)
} else {
(serde_json::from_slice::<Value>(input)?, Container::Gltf)
};
let reader = GltfReader::from_bytes_lenient_with_base_path(input, base_path)?;
let source_buffers: Vec<Vec<u8>> = reader.buffers().to_vec();
let (doc, bin) = compress_gltf_value(doc, &source_buffers, quantization, |mesh, prim| {
reader.decode_primitive_with_semantics(mesh, prim)
})?;
serialize(&doc, &bin, container)
}
pub fn compress_gltf_value<F>(
mut doc: Value,
buffers: &[Vec<u8>],
quantization: Option<QuantizationBits>,
decode: F,
) -> Result<(Value, Vec<u8>)>
where
F: Fn(usize, usize) -> Result<(draco_core::Mesh, Vec<(String, u32)>)>,
{
if !doc.is_object() {
return Err(GltfError::InvalidGltf("glTF root is not an object".into()));
}
let quant = quantization.unwrap_or_default();
let accessor_users = count_accessor_users(&doc);
let plans = build_plans(&doc, &decode, &accessor_users, &quant)?;
apply_accessor_mutations(&mut doc, &plans)?;
add_generated_indices(&mut doc, &plans)?;
let repack = repack_buffers(&mut doc, buffers, &plans)?;
for (i, plan) in plans.iter().enumerate() {
let draco_bv = repack.draco_buffer_views[i];
set_primitive_draco_extension(&mut doc, plan, draco_bv)?;
}
if !plans.is_empty() {
ensure_extension_listed(&mut doc, "extensionsUsed");
ensure_extension_listed(&mut doc, "extensionsRequired");
}
set_single_buffer(&mut doc, repack.bin.len());
Ok((doc, repack.bin))
}
struct CompressPlan {
mesh_idx: usize,
prim_idx: usize,
draco_bytes: Vec<u8>,
semantic_to_id: Vec<(String, usize)>,
attribute_accessors: Vec<usize>,
indices_accessor: Option<usize>,
num_points: usize,
num_indices: usize,
}
fn build_plans<F>(
doc: &Value,
decode: &F,
accessor_users: &HashMap<usize, usize>,
quant: &QuantizationBits,
) -> Result<Vec<CompressPlan>>
where
F: Fn(usize, usize) -> Result<(draco_core::Mesh, Vec<(String, u32)>)>,
{
let mut plans = Vec::new();
let Some(meshes) = doc.get("meshes").and_then(Value::as_array) else {
return Ok(plans);
};
for (mesh_idx, mesh) in meshes.iter().enumerate() {
let Some(primitives) = mesh.get("primitives").and_then(Value::as_array) else {
continue;
};
for (prim_idx, prim) in primitives.iter().enumerate() {
if let Some(plan) =
plan_for_primitive(prim, mesh_idx, prim_idx, decode, accessor_users, quant)?
{
plans.push(plan);
}
}
}
Ok(plans)
}
fn plan_for_primitive<F>(
prim: &Value,
mesh_idx: usize,
prim_idx: usize,
decode: &F,
accessor_users: &HashMap<usize, usize>,
quant: &QuantizationBits,
) -> Result<Option<CompressPlan>>
where
F: Fn(usize, usize) -> Result<(draco_core::Mesh, Vec<(String, u32)>)>,
{
let mode = prim
.get("mode")
.and_then(Value::as_u64)
.unwrap_or(MODE_TRIANGLES);
if mode != MODE_TRIANGLES {
return Ok(None);
}
if prim
.get("extensions")
.and_then(|e| e.get(KHR_DRACO))
.is_some()
{
return Ok(None);
}
let indices_accessor = prim
.get("indices")
.and_then(Value::as_u64)
.map(|i| i as usize);
let Some(attributes) = prim.get("attributes").and_then(Value::as_object) else {
return Ok(None);
};
if attributes.is_empty() || !attributes.contains_key("POSITION") {
return Ok(None);
}
let mut attribute_accessors = Vec::new();
for accessor in attributes.values() {
let Some(acc) = accessor.as_u64() else {
return Ok(None);
};
attribute_accessors.push(acc as usize);
}
let exclusive = |acc: usize| accessor_users.get(&acc).copied().unwrap_or(0) == 1;
let indices_exclusive = indices_accessor.map(exclusive).unwrap_or(true);
if !indices_exclusive || !attribute_accessors.iter().copied().all(exclusive) {
return Ok(None);
}
let (mesh, semantic_to_uid) = match decode(mesh_idx, prim_idx) {
Ok(out) => out,
Err(_) => return Ok(None),
};
let (draco_bytes, info) = match encode_draco_mesh_with_info(&mesh, quant) {
Ok(out) => out,
Err(_) => return Ok(None),
};
let produced: BTreeSet<&str> = semantic_to_uid.iter().map(|(s, _)| s.as_str()).collect();
let original: BTreeSet<&str> = attributes.keys().map(String::as_str).collect();
if produced != original {
return Ok(None);
}
let semantic_to_id: Vec<(String, usize)> = semantic_to_uid
.into_iter()
.map(|(s, u)| (s, u as usize))
.collect();
Ok(Some(CompressPlan {
mesh_idx,
prim_idx,
draco_bytes,
semantic_to_id,
attribute_accessors,
indices_accessor,
num_points: info.num_encoded_points,
num_indices: info.num_encoded_faces * 3,
}))
}
fn count_accessor_users(doc: &Value) -> HashMap<usize, usize> {
let mut users: HashMap<usize, usize> = HashMap::new();
let Some(meshes) = doc.get("meshes").and_then(Value::as_array) else {
return users;
};
for mesh in meshes {
let Some(primitives) = mesh.get("primitives").and_then(Value::as_array) else {
continue;
};
for prim in primitives {
if let Some(attrs) = prim.get("attributes").and_then(Value::as_object) {
for accessor in attrs.values() {
if let Some(a) = accessor.as_u64() {
*users.entry(a as usize).or_default() += 1;
}
}
}
if let Some(a) = prim.get("indices").and_then(Value::as_u64) {
*users.entry(a as usize).or_default() += 1;
}
}
}
users
}
fn apply_accessor_mutations(doc: &mut Value, plans: &[CompressPlan]) -> Result<()> {
let accessors = doc
.get_mut("accessors")
.and_then(Value::as_array_mut)
.ok_or_else(|| GltfError::InvalidGltf("missing accessors array".into()))?;
for plan in plans {
for &acc in &plan.attribute_accessors {
strip_geometry_accessor(accessors, acc, plan.num_points)?;
}
if let Some(indices) = plan.indices_accessor {
strip_geometry_accessor(accessors, indices, plan.num_indices)?;
}
}
Ok(())
}
fn add_generated_indices(doc: &mut Value, plans: &[CompressPlan]) -> Result<()> {
for plan in plans {
if plan.indices_accessor.is_some() {
continue;
}
let accessors = doc
.get_mut("accessors")
.and_then(Value::as_array_mut)
.ok_or_else(|| GltfError::InvalidGltf("missing accessors array".into()))?;
let new_idx = accessors.len();
accessors.push(serde_json::json!({
"componentType": 5125u64, "count": plan.num_indices as u64,
"type": "SCALAR",
}));
let prim = primitive_mut(doc, plan)?;
prim.insert("indices".into(), Value::from(new_idx as u64));
}
Ok(())
}
fn primitive_mut<'a>(
doc: &'a mut Value,
plan: &CompressPlan,
) -> Result<&'a mut Map<String, Value>> {
doc.get_mut("meshes")
.and_then(Value::as_array_mut)
.and_then(|m| m.get_mut(plan.mesh_idx))
.and_then(|m| m.get_mut("primitives"))
.and_then(Value::as_array_mut)
.and_then(|p| p.get_mut(plan.prim_idx))
.and_then(Value::as_object_mut)
.ok_or_else(|| GltfError::InvalidGltf("primitive vanished during rewrite".into()))
}
fn strip_geometry_accessor(accessors: &mut [Value], idx: usize, count: usize) -> Result<()> {
let accessor = accessors
.get_mut(idx)
.and_then(Value::as_object_mut)
.ok_or_else(|| GltfError::InvalidGltf(format!("accessor {} out of range", idx)))?;
accessor.remove("bufferView");
accessor.remove("byteOffset");
accessor.insert("count".into(), Value::from(count));
Ok(())
}
struct Repack {
bin: Vec<u8>,
draco_buffer_views: Vec<usize>,
}
fn repack_buffers(
doc: &mut Value,
source_buffers: &[Vec<u8>],
plans: &[CompressPlan],
) -> Result<Repack> {
let old_views: Vec<Value> = doc
.get("bufferViews")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let mut referenced = BTreeSet::new();
collect_buffer_view_refs(doc, &mut referenced);
let mut bin: Vec<u8> = Vec::new();
let mut new_views: Vec<Value> = Vec::new();
let mut remap: HashMap<usize, usize> = HashMap::new();
for &old_idx in &referenced {
let view = old_views
.get(old_idx)
.and_then(Value::as_object)
.ok_or_else(|| GltfError::InvalidGltf(format!("buffer view {} invalid", old_idx)))?;
let bytes = buffer_view_bytes(view, source_buffers)?;
align_to_4(&mut bin);
let offset = bin.len();
bin.extend_from_slice(bytes);
let mut new_view = view.clone();
new_view.insert("buffer".into(), Value::from(0u64));
new_view.insert("byteOffset".into(), Value::from(offset as u64));
new_view.insert("byteLength".into(), Value::from(bytes.len() as u64));
let new_idx = new_views.len();
new_views.push(Value::Object(new_view));
remap.insert(old_idx, new_idx);
}
remap_buffer_view_refs(doc, &remap);
let mut draco_buffer_views = Vec::with_capacity(plans.len());
for plan in plans {
align_to_4(&mut bin);
let offset = bin.len();
bin.extend_from_slice(&plan.draco_bytes);
let new_idx = new_views.len();
new_views.push(serde_json::json!({
"buffer": 0,
"byteOffset": offset as u64,
"byteLength": plan.draco_bytes.len() as u64,
}));
draco_buffer_views.push(new_idx);
}
if new_views.is_empty() {
doc.as_object_mut().unwrap().remove("bufferViews");
} else {
doc.as_object_mut()
.unwrap()
.insert("bufferViews".into(), Value::Array(new_views));
}
Ok(Repack {
bin,
draco_buffer_views,
})
}
fn buffer_view_bytes<'a>(view: &Map<String, Value>, buffers: &'a [Vec<u8>]) -> Result<&'a [u8]> {
let buffer_idx = view
.get("buffer")
.and_then(Value::as_u64)
.ok_or_else(|| GltfError::InvalidGltf("buffer view missing buffer index".into()))?
as usize;
let offset = view.get("byteOffset").and_then(Value::as_u64).unwrap_or(0) as usize;
let length = view
.get("byteLength")
.and_then(Value::as_u64)
.ok_or_else(|| GltfError::InvalidGltf("buffer view missing byteLength".into()))?
as usize;
let buffer = buffers
.get(buffer_idx)
.ok_or_else(|| GltfError::InvalidGltf(format!("buffer {} not resolved", buffer_idx)))?;
let end = offset
.checked_add(length)
.filter(|&e| e <= buffer.len())
.ok_or_else(|| GltfError::InvalidGltf("buffer view out of range".into()))?;
Ok(&buffer[offset..end])
}
fn collect_buffer_view_refs(value: &Value, out: &mut BTreeSet<usize>) {
match value {
Value::Object(map) => {
for (key, child) in map {
if let Some(idx) = child.as_u64().filter(|_| key == "bufferView") {
out.insert(idx as usize);
}
collect_buffer_view_refs(child, out);
}
}
Value::Array(items) => {
for item in items {
collect_buffer_view_refs(item, out);
}
}
_ => {}
}
}
fn remap_buffer_view_refs(value: &mut Value, remap: &HashMap<usize, usize>) {
match value {
Value::Object(map) => {
for (key, child) in map.iter_mut() {
if let Some(old) = child.as_u64().filter(|_| key == "bufferView") {
if let Some(&new) = remap.get(&(old as usize)) {
*child = Value::from(new as u64);
}
}
remap_buffer_view_refs(child, remap);
}
}
Value::Array(items) => {
for item in items {
remap_buffer_view_refs(item, remap);
}
}
_ => {}
}
}
fn set_primitive_draco_extension(
doc: &mut Value,
plan: &CompressPlan,
draco_buffer_view: usize,
) -> Result<()> {
let prim = primitive_mut(doc, plan)?;
let mut attributes = Map::new();
for (semantic, id) in &plan.semantic_to_id {
attributes.insert(semantic.clone(), Value::from(*id as u64));
}
let draco = serde_json::json!({
"bufferView": draco_buffer_view as u64,
"attributes": Value::Object(attributes),
});
let extensions = prim
.entry("extensions")
.or_insert_with(|| Value::Object(Map::new()));
if !extensions.is_object() {
return Err(GltfError::InvalidGltf(
"primitive.extensions is not an object".into(),
));
}
extensions
.as_object_mut()
.unwrap()
.insert(KHR_DRACO.into(), draco);
Ok(())
}
fn ensure_extension_listed(doc: &mut Value, key: &str) {
let root = doc.as_object_mut().unwrap();
let list = root.entry(key).or_insert_with(|| Value::Array(Vec::new()));
let Some(arr) = list.as_array_mut() else {
return;
};
if !arr.iter().any(|v| v.as_str() == Some(KHR_DRACO)) {
arr.push(Value::from(KHR_DRACO));
}
}
fn set_single_buffer(doc: &mut Value, bin_len: usize) {
let root = doc.as_object_mut().unwrap();
if bin_len == 0 {
root.remove("buffers");
return;
}
let mut buffer = Map::new();
buffer.insert("byteLength".into(), Value::from(bin_len as u64));
root.insert("buffers".into(), Value::Array(vec![Value::Object(buffer)]));
}
#[cfg(feature = "gltf-reader")]
fn serialize(doc: &Value, bin: &[u8], container: Container) -> Result<Vec<u8>> {
match container {
Container::Gltf => {
let mut doc = doc.clone();
if !bin.is_empty() {
let uri = format!(
"data:application/octet-stream;base64,{}",
base64_encode(bin)
);
if let Some(buffers) = doc.get_mut("buffers").and_then(Value::as_array_mut) {
if let Some(buffer) = buffers.get_mut(0).and_then(Value::as_object_mut) {
buffer.insert("uri".into(), Value::from(uri));
}
}
}
Ok(serde_json::to_vec(&doc)?)
}
Container::Glb => build_glb(doc, bin),
}
}
#[cfg(feature = "gltf-reader")]
fn build_glb(doc: &Value, bin: &[u8]) -> Result<Vec<u8>> {
let mut json = serde_json::to_vec(doc)?;
while !json.len().is_multiple_of(4) {
json.push(b' ');
}
let mut bin_padded = bin.to_vec();
while !bin_padded.len().is_multiple_of(4) {
bin_padded.push(0);
}
let has_bin = !bin_padded.is_empty();
let mut total = 12 + 8 + json.len();
if has_bin {
total += 8 + bin_padded.len();
}
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&GLB_MAGIC.to_le_bytes());
out.extend_from_slice(&GLB_VERSION.to_le_bytes());
out.extend_from_slice(&(total as u32).to_le_bytes());
out.extend_from_slice(&(json.len() as u32).to_le_bytes());
out.extend_from_slice(&GLB_CHUNK_JSON.to_le_bytes());
out.extend_from_slice(&json);
if has_bin {
out.extend_from_slice(&(bin_padded.len() as u32).to_le_bytes());
out.extend_from_slice(&GLB_CHUNK_BIN.to_le_bytes());
out.extend_from_slice(&bin_padded);
}
Ok(out)
}
#[cfg(feature = "gltf-reader")]
fn split_glb(data: &[u8]) -> Result<(&[u8], Option<&[u8]>)> {
if data.len() < 12 {
return Err(GltfError::InvalidGlb(
"file too small for GLB header".into(),
));
}
if read_u32_le(&data[0..4]) != GLB_MAGIC {
return Err(GltfError::InvalidGlb("bad GLB magic".into()));
}
let total = read_u32_le(&data[8..12]) as usize;
if total > data.len() {
return Err(GltfError::InvalidGlb("GLB length exceeds data".into()));
}
let mut json: Option<&[u8]> = None;
let mut bin: Option<&[u8]> = None;
let mut pos = 12;
while pos + 8 <= total {
let len = read_u32_le(&data[pos..pos + 4]) as usize;
let kind = read_u32_le(&data[pos + 4..pos + 8]);
let start = pos + 8;
let end = start
.checked_add(len)
.filter(|&e| e <= total)
.ok_or_else(|| GltfError::InvalidGlb("GLB chunk out of range".into()))?;
match kind {
GLB_CHUNK_JSON => json = Some(&data[start..end]),
GLB_CHUNK_BIN => bin = Some(&data[start..end]),
_ => {}
}
pos = end;
}
let json = json.ok_or_else(|| GltfError::InvalidGlb("GLB has no JSON chunk".into()))?;
Ok((json, bin))
}
#[cfg(feature = "gltf-reader")]
fn read_u32_le(bytes: &[u8]) -> u32 {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
fn align_to_4(buf: &mut Vec<u8>) {
while !buf.len().is_multiple_of(4) {
buf.push(0);
}
}
#[cfg(feature = "gltf-reader")]
fn base64_encode(data: &[u8]) -> String {
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(TABLE[(n >> 18) as usize & 0x3F] as char);
out.push(TABLE[(n >> 12) as usize & 0x3F] as char);
out.push(if chunk.len() > 1 {
TABLE[(n >> 6) as usize & 0x3F] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
TABLE[n as usize & 0x3F] as char
} else {
'='
});
}
out
}