Files
effectcraft/crates/lottie/src/zip.rs
T
Brandon ThomasandClaude Opus 5.5 c4100bcf80 M11.4: Lottie export/import
New crate effectcraft-lottie (L3): export a comp to Lottie JSON / dotLottie
(precomp and base64 image assets; shape, solid, null, precomp, image and text
layers or glyph shapes; transforms with parenting, separated position,
anchor; keyframes with speed/influence converted to normalised i/o tangents,
hold keys, spatial ti/to; all shape items incl. gradients, dashes, trim,
repeater and path operators; masks, track mattes, blend modes, time remap,
timing, markers, optional expressions, a few Lottie effects) with a warnings
list for what Lottie cannot express, and the inverse import. Commands
file.exportLottie / file.importLottie with File > Export / Import entries.
Round-trip tests (values and rendered frames) and hand-written fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:56:03 -04:00

105 lines
3.9 KiB
Rust

//! A minimal ZIP writer (stored entries, PKWARE APPNOTE 6.3 local headers + central directory)
//! for `.lottie` (dotLottie) archives.
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for &b in data {
crc ^= b as u32;
for _ in 0..8 {
crc = if crc & 1 != 0 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 };
}
}
!crc
}
/// Build a ZIP archive with the given (name, bytes) entries, uncompressed.
pub fn store(entries: &[(String, Vec<u8>)]) -> Vec<u8> {
let mut out = Vec::new();
let mut central = Vec::new();
let put16 = |v: &mut Vec<u8>, x: u16| v.extend_from_slice(&x.to_le_bytes());
let put32 = |v: &mut Vec<u8>, x: u32| v.extend_from_slice(&x.to_le_bytes());
for (name, data) in entries {
let offset = out.len() as u32;
let crc = crc32(data);
let n = name.as_bytes();
// Local file header.
put32(&mut out, 0x0403_4b50);
put16(&mut out, 20); // version needed
put16(&mut out, 0x0800); // flags: UTF-8 names
put16(&mut out, 0); // stored
put16(&mut out, 0); // time
put16(&mut out, 0x21); // date (1980-01-01)
put32(&mut out, crc);
put32(&mut out, data.len() as u32);
put32(&mut out, data.len() as u32);
put16(&mut out, n.len() as u16);
put16(&mut out, 0);
out.extend_from_slice(n);
out.extend_from_slice(data);
// Central directory record.
put32(&mut central, 0x0201_4b50);
put16(&mut central, 20); // made by
put16(&mut central, 20);
put16(&mut central, 0x0800);
put16(&mut central, 0);
put16(&mut central, 0);
put16(&mut central, 0x21);
put32(&mut central, crc);
put32(&mut central, data.len() as u32);
put32(&mut central, data.len() as u32);
put16(&mut central, n.len() as u16);
put16(&mut central, 0); // extra
put16(&mut central, 0); // comment
put16(&mut central, 0); // disk
put16(&mut central, 0); // internal attrs
put32(&mut central, 0); // external attrs
put32(&mut central, offset);
central.extend_from_slice(n);
}
let cd_offset = out.len() as u32;
let cd_len = central.len() as u32;
out.extend_from_slice(&central);
put32(&mut out, 0x0605_4b50);
put16(&mut out, 0);
put16(&mut out, 0);
put16(&mut out, entries.len() as u16);
put16(&mut out, entries.len() as u16);
put32(&mut out, cd_len);
put32(&mut out, cd_offset);
put16(&mut out, 0);
out
}
/// Read the stored (uncompressed) entries of a ZIP archive written by [`store`] or any other
/// writer that did not compress. Compressed entries are skipped.
pub fn read_stored(data: &[u8]) -> Vec<(String, Vec<u8>)> {
let mut out = vec![];
let mut i = 0usize;
let u16_at = |p: usize| data.get(p..p + 2).map(|b| u16::from_le_bytes([b[0], b[1]]) as usize);
let u32_at = |p: usize| data.get(p..p + 4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize);
while u32_at(i) == Some(0x0403_4b50) {
let (Some(method), Some(size), Some(nlen), Some(xlen)) = (u16_at(i + 8), u32_at(i + 18), u16_at(i + 26), u16_at(i + 28)) else { break };
let name_start = i + 30;
let body = name_start + nlen + xlen;
let (Some(name), Some(bytes)) = (data.get(name_start..name_start + nlen), data.get(body..body + size)) else { break };
if method == 0 {
out.push((String::from_utf8_lossy(name).to_string(), bytes.to_vec()));
}
i = body + size;
}
out
}
#[cfg(test)]
mod tests {
#[test]
fn crc_and_roundtrip() {
assert_eq!(super::crc32(b"123456789"), 0xCBF4_3926);
let z = super::store(&[("a.json".into(), b"{}".to_vec()), ("b/c.txt".into(), b"hello".to_vec())]);
let back = super::read_stored(&z);
assert_eq!(back.len(), 2);
assert_eq!(back[1].0, "b/c.txt");
assert_eq!(back[1].1, b"hello");
}
}