Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ anicraft-text = { path = "crates/text" }
anicraft-plugins = { path = "crates/plugins" }
anicraft-audio = { path = "crates/audio" }
anicraft-gpu = { path = "crates/gpu" }
craft-xfl = { git = "https://github.com/storytold/craft-libs", rev = "b423529943cc8b389e04cde6f08a08b92d0c4dea" }
craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "de7640a00688cff31470e44c3e43285ed7beb244", version = "=0.1.0" }
anicraft-guide = { path = "crates/guide" }
craft-anicraft-format = { git = "https://github.com/storytold/craft-libs", rev = "b77af389f30bb10de10d8436ca9b39fe94e3bb67" }
# The GPU stage (ADR 0019) uses the wgpu that eframe's egui-wgpu brings, with the same
Expand Down
4 changes: 2 additions & 2 deletions crates/xfl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ anicraft-doc.workspace = true
anicraft-xml.workspace = true
# Lays imported text out with the fonts on this computer (ADR 0012, ADR 0014).
anicraft-text.workspace = true
# Inflate for .fla (zip) archives; already in the tree through png (MIT OR Zlib OR Apache-2.0).
craft-xfl.workspace = true
miniz_oxide = "0.8"
thiserror.workspace = true

[dev-dependencies]
png.workspace = true

[lints]
workspace = true
workspace = true
95 changes: 11 additions & 84 deletions crates/xfl/src/edges.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,87 +36,15 @@ pub struct Edge {
pub segs: Vec<Seg>,
}

const TWIPS: f32 = 20.0;
const MAX_SEGS: usize = 2_000_000;

fn number(s: &[u8], i: &mut usize) -> Option<f32> {
while s.get(*i).is_some_and(|c| c.is_ascii_whitespace()) {
*i += 1;
}
let start = *i;
if s.get(*i) == Some(&b'#') {
*i += 1;
let h0 = *i;
while s.get(*i).is_some_and(u8::is_ascii_hexdigit) {
*i += 1;
}
let int = std::str::from_utf8(s.get(h0..*i)?).ok()?;
let mut frac = 0u32;
if s.get(*i) == Some(&b'.') {
*i += 1;
let f0 = *i;
while s.get(*i).is_some_and(u8::is_ascii_hexdigit) && *i - f0 < 2 {
*i += 1;
}
let f = std::str::from_utf8(s.get(f0..*i)?).ok()?;
// One digit means sixteenths of 256ths: "#A.8" = 10.5.
frac = u32::from_str_radix(f, 16).ok()? << (if f.len() == 1 { 4 } else { 0 });
}
let v = u32::from_str_radix(if int.is_empty() { "0" } else { int }, 16).ok()?;
let fixed = (v.wrapping_shl(8) | frac) as i32;
return Some(fixed as f32 / 256.0);
}
if matches!(s.get(*i), Some(b'-' | b'+')) {
*i += 1;
}
while s.get(*i).is_some_and(|c| c.is_ascii_digit() || *c == b'.' || *c == b'e' || *c == b'E') {
*i += 1;
}
std::str::from_utf8(s.get(start..*i)?).ok()?.parse::<f32>().ok().filter(|v| v.is_finite())
}

/// Decodes an `edges` attribute into segments (pixels). Stops quietly at anything it can't
/// read, keeping what came before.
/// Decodes an `edges` attribute through craft-libs and maps its neutral geometry to AniCraft.
pub fn parse(s: &str) -> Vec<Seg> {
let b = s.as_bytes();
let mut i = 0;
let mut cur = Vec2::ZERO;
let mut out = Vec::new();
let pt = |b: &[u8], i: &mut usize| -> Option<Vec2> { Some(Vec2::new(number(b, i)? / TWIPS, number(b, i)? / TWIPS)) };
while let Some(&c) = b.get(i) {
i += 1;
match c {
b'!' => match pt(b, &mut i) {
Some(p) => cur = p,
None => break,
},
b'|' | b'/' => match pt(b, &mut i) {
Some(p) => {
out.push(Seg { from: cur, ctrl: None, to: p });
cur = p;
}
None => break,
},
b'[' | b']' => match (pt(b, &mut i), pt(b, &mut i)) {
(Some(q), Some(p)) => {
out.push(Seg { from: cur, ctrl: Some(q), to: p });
cur = p;
}
_ => break,
},
b'S' => {
while b.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
}
c if c.is_ascii_whitespace() => {}
_ => break,
}
if out.len() >= MAX_SEGS {
break;
}
}
out
craft_xfl::edges::parse_edge_segments(s)
.into_iter()
.map(|segment| {
let point = |p: craft_xfl::edges::Point| Vec2::new(p.x, p.y);
Seg { from: point(segment.from), ctrl: segment.control.map(point), to: point(segment.to) }
})
.collect()
}

/// Hash key for an end point (to 1/100 px, which absorbs the format's rounding).
Expand Down Expand Up @@ -205,10 +133,9 @@ mod tests {
assert_eq!(s[1].ctrl, Some(Vec2::new(20.0, 0.0)));
assert_eq!(s[2], Seg { from: Vec2::new(20.0, 10.0), ctrl: None, to: Vec2::new(0.0, 10.0) });
// Hex fixed point: #A.8 = 10.5 twips; a negative two's-complement value.
let mut i = 0;
assert_eq!(number(b"#A.8", &mut i), Some(10.5));
let mut i = 0;
assert_eq!(number(b"#FFFFFF.80", &mut i), Some(-0.5));
let hex = parse("!#A.8 -20|#FFFFFF.80 0");
assert_eq!(hex[0].from, Vec2::new(0.525, -1.0));
assert_eq!(hex[0].to, Vec2::new(-0.025, 0.0));
assert!(parse("!1 2|x").is_empty(), "stops at garbage, never panics");
}

Expand Down
15 changes: 14 additions & 1 deletion crates/xfl/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,27 @@ struct RawHint {
pub enum XflError {
#[error("{0}")]
Xml(#[from] xml::XmlError),
#[error("{0}")]
#[error("{}", ani_zip_error(.0))]
Zip(#[from] crate::zip::ZipError),
#[error("I/O: {0}")]
Io(#[from] std::io::Error),
#[error("this isn't an Animate document: {0}")]
NotXfl(String),
}

/// Keep AniCraft's established import diagnostics while the shared XFL reader stays neutral.
fn ani_zip_error(error: &crate::zip::ZipError) -> String {
match error {
crate::zip::ZipError::UnsupportedArchive(feature) => {
format!("the archive uses a zip feature AniCraft can't read ({feature})")
}
crate::zip::ZipError::Unsupported(name, feature) => {
format!("'{name}' uses a zip feature AniCraft can't read ({feature})")
}
other => other.to_string(),
}
}

/// What the import couldn't bring across, by kind ("3 bitmaps"), the scripts it brought that
/// run and those it kept without running, and the fonts it couldn't find.
#[derive(Debug, Default, Clone, PartialEq)]
Expand Down
11 changes: 10 additions & 1 deletion crates/xfl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@ pub mod text;
pub mod video;
/// The shared XML reader (`anicraft-xml`), under its old path.
pub use anicraft_xml as xml;
pub mod zip;
/// XFL/FLA ZIP package reader, shared with other consumers through `craft-xfl`.
pub mod zip {
pub use craft_xfl::{Archive, MAX_DEPTH, MAX_ENTRIES, MAX_ENTRY, MAX_RATIO, MAX_TOTAL, ZipError, crc32};

#[cfg(test)]
pub(crate) use crate::pack;
}

pub use import::{MAX_FLA_BYTES, MAX_NAMED, Report, XflError, import_fla, import_fla_with_fonts, import_path, import_with, import_with_fonts};

#[cfg(test)]
#[path = "../tests/support/fla.rs"]
pub(crate) mod pack;
#[cfg(test)]
mod tests;
#[cfg(test)]
Expand Down
8 changes: 8 additions & 0 deletions crates/xfl/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ use crate::import::{XflError, import_with};
use anicraft_doc::{ElementKind, LayerKind, LoopMode, Paint, Rgba, SymbolKind, evaluate};
use std::collections::HashMap;

#[test]
fn shared_zip_errors_keep_anicraft_import_wording() {
let entry = XflError::from(crate::zip::ZipError::Unsupported("a.xml".into(), "encryption"));
assert_eq!(entry.to_string(), "'a.xml' uses a zip feature AniCraft can't read (encryption)");
let archive = XflError::from(crate::zip::ZipError::UnsupportedArchive("split across several files"));
assert_eq!(archive.to_string(), "the archive uses a zip feature AniCraft can't read (split across several files)");
}

const DOC: &str = r##"<DOMDocument xmlns="http://ns.adobe.com/xfl/2008/" width="640" height="360" frameRate="30" backgroundColor="#336699">
<symbols><Include href="Characters/Ball.xml"/><Include href="Missing.xml"/></symbols>
<timelines>
Expand Down
Loading
Loading