mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 00:14:41 +00:00
Lottie export: embed only files that are PNG, JPEG, GIF or WebP images (#3008)
* fix(lottie): export embeds only files that are PNG, JPEG, GIF or WebP images * address review: match full PNG and GIF signatures
This commit is contained in:
+18
-10
@@ -145,16 +145,18 @@ fn hex(c: [f32; 3]) -> String {
|
||||
format!("#{:02x}{:02x}{:02x}", b(c[0]), b(c[1]), b(c[2]))
|
||||
}
|
||||
|
||||
fn mime_of(path: &str) -> &'static str {
|
||||
let lower = path.to_ascii_lowercase();
|
||||
if lower.ends_with(".jpg") || lower.ends_with(".jpeg") {
|
||||
"image/jpeg"
|
||||
} else if lower.ends_with(".webp") {
|
||||
"image/webp"
|
||||
} else if lower.ends_with(".gif") {
|
||||
"image/gif"
|
||||
/// The image type `bytes` hold, by their signature; `None` for anything else.
|
||||
pub(crate) fn image_mime(bytes: &[u8]) -> Option<&'static str> {
|
||||
if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
|
||||
Some("image/png")
|
||||
} else if bytes.starts_with(&[0xff, 0xd8, 0xff]) {
|
||||
Some("image/jpeg")
|
||||
} else if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
|
||||
Some("image/gif")
|
||||
} else if bytes.starts_with(b"RIFF") && bytes.get(8..12) == Some(b"WEBP") {
|
||||
Some("image/webp")
|
||||
} else {
|
||||
"image/png"
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +288,13 @@ impl Doc<'_> {
|
||||
self.ex.warn(format!("{}: cannot read image `{}` (layer exported as a null)", l.name, f.path));
|
||||
return None;
|
||||
};
|
||||
let uri = format!("data:{};base64,{}", mime_of(&f.path), crate::base64::encode(&bytes));
|
||||
// What a project calls a still is embedded only when it is one: the path can name any
|
||||
// file (an imported animation's asset), and the export would carry its bytes verbatim.
|
||||
let Some(mime) = image_mime(&bytes) else {
|
||||
self.ex.warn(format!("{}: `{}` is not a PNG, JPEG, GIF or WebP image (layer exported as a null)", l.name, f.path));
|
||||
return None;
|
||||
};
|
||||
let uri = format!("data:{mime};base64,{}", crate::base64::encode(&bytes));
|
||||
self.assets.push(json!({"id": id, "nm": it.name, "w": f.width, "h": f.height, "u": "", "p": uri, "e": 1}));
|
||||
self.done.insert(id.clone());
|
||||
Some(id)
|
||||
|
||||
@@ -725,7 +725,7 @@ fn effects_expressions_and_warnings() {
|
||||
#[test]
|
||||
fn images_embed_as_base64_and_come_back() {
|
||||
let (mut p, cid) = setup();
|
||||
let png: Vec<u8> = vec![0x89, b'P', b'N', b'G', 1, 2, 3, 4, 5];
|
||||
let png: Vec<u8> = vec![0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3, 4, 5];
|
||||
let fid = p.add_item(
|
||||
"pic.png",
|
||||
Label::Lavender,
|
||||
@@ -775,6 +775,17 @@ fn images_embed_as_base64_and_come_back() {
|
||||
assert!(f.path.starts_with("/tmp/image_") && f.path.ends_with(".png"), "{}", f.path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_signatures_are_matched_in_full() {
|
||||
use crate::export::image_mime;
|
||||
assert_eq!(image_mime(b"\x89PNG\r\n\x1a\n\0"), Some("image/png"));
|
||||
assert_eq!(image_mime(b"GIF89a\0"), Some("image/gif"));
|
||||
assert_eq!(image_mime(b"GIF87a\0"), Some("image/gif"));
|
||||
// Near matches (a prefix of the signature, or text that starts with it) are not images.
|
||||
assert_eq!(image_mime(b"\x89PNG\x01\x02\x03\x04"), None);
|
||||
assert_eq!(image_mime(b"GIF8 is a text file"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dotlottie_archive_roundtrip() {
|
||||
let (mut p, cid) = setup();
|
||||
@@ -985,3 +996,22 @@ fn per_corner_roundness_exports_equal_radii_and_warns_when_mixed() {
|
||||
assert_eq!(rc(&json)["r"]["k"], json!(5.0));
|
||||
assert!(!warnings.iter().any(|w| w.contains("per-corner")), "{warnings:?}");
|
||||
}
|
||||
|
||||
/// A footage item called a still whose file is not an image is not embedded.
|
||||
#[test]
|
||||
fn only_image_bytes_are_embedded() {
|
||||
let (mut p, cid) = setup();
|
||||
let fid = p.add_item(
|
||||
"secret.png",
|
||||
Label::Lavender,
|
||||
None,
|
||||
ItemKind::Footage(Footage { path: "/virtual/secret.png".into(), kind: FootageKind::Still, width: 8, height: 8, has_video: true, ..Default::default() }),
|
||||
);
|
||||
let c = comp(&p, cid);
|
||||
let l = build::layer(&mut p, &c, "secret.png", LayerSource::Footage { item: fid }, (8, 8), None);
|
||||
push(&mut p, cid, l);
|
||||
let read = |path: &str| (path == "/virtual/secret.png").then(|| b"root:x:0:0:root:/root:/bin/sh\n".to_vec());
|
||||
let res = export_comp(&p, cid, &ExportOptions::default(), &read).unwrap();
|
||||
assert!(res.json["assets"].as_array().is_none_or(|a| a.is_empty()), "{}", res.json["assets"]);
|
||||
assert!(res.warnings.iter().any(|w| w.contains("not a PNG")), "{:?}", res.warnings);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user