mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
Merge remote-tracking branch 'origin/main' into batch/r2-2
# Conflicts: # crates/text/src/fontdb.rs
This commit is contained in:
Generated
+1
-2
@@ -2613,8 +2613,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
[[package]]
|
||||
name = "krilla"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27da593198b20eeba65caeb73c2bbeec3e53ab08fa549898312ce81c4fce5e33"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef#9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bumpalo",
|
||||
|
||||
@@ -128,3 +128,8 @@ codegen-units = 1
|
||||
debug = false
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
||||
# craft-libs' vendored krilla 0.8.2: decoded PNGs are capped at 1 GiB and reserved fallibly, and a
|
||||
# panic while drawing unwinds without aborting.
|
||||
[patch.crates-io]
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "9f75d3dc19b4b514c4f3d4847ed1ec1b0e86b4ef" }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The file subcommands: info, export, convert, render and run.
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
use std::process::ExitCode;
|
||||
|
||||
@@ -19,7 +20,7 @@ fn load_noted(path: &str) -> CliResult<(Document, Vec<String>)> {
|
||||
}
|
||||
|
||||
fn ext(path: &str) -> String {
|
||||
Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase()
|
||||
Path::new(path).extension().and_then(OsStr::to_str).unwrap_or("").to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn image_format(e: &str) -> Option<ExportFormat> {
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::store::Store;
|
||||
use crate::test_util::{TempDir, config};
|
||||
|
||||
fn args(s: &[&str]) -> Vec<String> {
|
||||
s.iter().map(|a| a.to_string()).collect()
|
||||
s.iter().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
fn run(dir: &std::path::Path, command: Command) -> Result<String, String> {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
//! store replays such a log anyway and reports any change the snapshot lacked. A log ends at its
|
||||
//! first damaged record (a write cut short).
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -49,7 +50,7 @@ pub fn free_bytes(path: &Path) -> Option<u64> {
|
||||
|
||||
/// `path` with `.tmp` added: where its next version is written first.
|
||||
pub fn temp_of(path: &Path) -> PathBuf {
|
||||
let mut name = path.file_name().map(|n| n.to_os_string()).unwrap_or_default();
|
||||
let mut name = path.file_name().map(OsStr::to_os_string).unwrap_or_default();
|
||||
name.push(".tmp");
|
||||
path.with_file_name(name)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::args::{Args, file_url_path, is_invite, parse};
|
||||
|
||||
fn args(a: &[&str]) -> Vec<String> {
|
||||
a.iter().map(|s| s.to_string()).collect()
|
||||
a.iter().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -103,7 +103,7 @@ impl BlendMode {
|
||||
/// The mode named by its label or CSS keyword, ignoring case, spaces and dashes. Labels come
|
||||
/// first: "normal" is Normal, not pass-through (whose CSS keyword is also `normal`).
|
||||
pub fn from_name(s: &str) -> Option<Self> {
|
||||
let key: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_ascii_lowercase();
|
||||
let key: String = s.chars().filter(char::is_ascii_alphanumeric).collect::<String>().to_ascii_lowercase();
|
||||
Self::ALL
|
||||
.into_iter()
|
||||
.find(|m| m.label().replace(' ', "").to_ascii_lowercase() == key)
|
||||
|
||||
@@ -475,7 +475,7 @@ impl WidthPreset {
|
||||
}
|
||||
|
||||
pub fn from_id(s: &str) -> Option<WidthPreset> {
|
||||
let s: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
|
||||
let s: String = s.chars().filter(char::is_ascii_alphanumeric).collect();
|
||||
WidthPreset::ALL
|
||||
.into_iter()
|
||||
.find(|p| p.id().eq_ignore_ascii_case(&s) || p.label().replace(' ', "").eq_ignore_ascii_case(&s))
|
||||
|
||||
@@ -786,6 +786,38 @@ fn push_changes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"layers": pushed}))
|
||||
}
|
||||
|
||||
fn restore_prop_defs(doc: &Document, props: &BTreeMap<String, Value>) -> BTreeMap<String, PropDef> {
|
||||
let mut defs = BTreeMap::new();
|
||||
for (k, v) in props {
|
||||
let def = match v {
|
||||
Value::Bool(b) => PropDef::Boolean { default: *b },
|
||||
Value::String(t) => PropDef::Text { default: t.clone() },
|
||||
other => match id_of(other) {
|
||||
Some(c) if doc.contains(c) => PropDef::InstanceSwap { default: c, preferred: Vec::new() },
|
||||
_ => continue,
|
||||
},
|
||||
};
|
||||
defs.insert(k.clone(), def);
|
||||
}
|
||||
defs
|
||||
}
|
||||
|
||||
fn restore_id_map(doc: &mut Document, i: NodeId, component: NodeId, sub: &[NodeId]) -> BTreeMap<NodeId, NodeId> {
|
||||
let mut map: BTreeMap<NodeId, NodeId> = BTreeMap::new();
|
||||
let mut used: BTreeSet<NodeId> = BTreeSet::new();
|
||||
map.insert(i, component);
|
||||
used.insert(component);
|
||||
for d in sub.iter().filter(|d| **d != i) {
|
||||
let src = doc.node(*d).and_then(|n| n.source);
|
||||
let nid = match src {
|
||||
Some(s) if !doc.contains(s) && used.insert(s) => s,
|
||||
_ => doc.alloc_id(),
|
||||
};
|
||||
map.insert(*d, nid);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
fn restore_main(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "component.restoreMain";
|
||||
let i = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select an instance"))?;
|
||||
@@ -804,35 +836,13 @@ fn restore_main(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// The main comes back with its old id (other instances point to it) and its layers with the
|
||||
// ids their copies mirror, so every instance re-links by source.
|
||||
let sub = doc.subtree(i);
|
||||
let mut map: BTreeMap<NodeId, NodeId> = BTreeMap::new();
|
||||
let mut used: BTreeSet<NodeId> = BTreeSet::new();
|
||||
map.insert(i, component);
|
||||
used.insert(component);
|
||||
for d in sub.iter().filter(|d| **d != i) {
|
||||
let src = doc.node(*d).and_then(|n| n.source);
|
||||
let nid = match src {
|
||||
Some(s) if !doc.contains(s) && used.insert(s) => s,
|
||||
_ => doc.alloc_id(),
|
||||
};
|
||||
map.insert(*d, nid);
|
||||
}
|
||||
let map = restore_id_map(doc, i, component, &sub);
|
||||
let mut nodes = Vec::new();
|
||||
let mut nested = Vec::new();
|
||||
for d in &sub {
|
||||
let Some(mut n) = doc.node(*d).cloned() else { continue };
|
||||
if *d == i {
|
||||
let mut defs = BTreeMap::new();
|
||||
for (k, v) in &props {
|
||||
let def = match v {
|
||||
Value::Bool(b) => PropDef::Boolean { default: *b },
|
||||
Value::String(t) => PropDef::Text { default: t.clone() },
|
||||
other => match id_of(other) {
|
||||
Some(c) if doc.contains(c) => PropDef::InstanceSwap { default: c, preferred: Vec::new() },
|
||||
_ => continue,
|
||||
},
|
||||
};
|
||||
defs.insert(k.clone(), def);
|
||||
}
|
||||
let defs = restore_prop_defs(doc, &props);
|
||||
n.kind = NodeKind::Component { description: String::new(), props: defs };
|
||||
n.source = None;
|
||||
n.overrides.clear();
|
||||
|
||||
@@ -495,11 +495,11 @@ pub(crate) fn write(path: &str, _bytes: &[u8]) -> Result<()> {
|
||||
}
|
||||
|
||||
fn ext(path: &str) -> String {
|
||||
std::path::Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase()
|
||||
std::path::Path::new(path).extension().and_then(std::ffi::OsStr::to_str).unwrap_or("").to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn file_stem(path: &str) -> String {
|
||||
std::path::Path::new(path).file_stem().and_then(|e| e.to_str()).unwrap_or("Untitled").to_string()
|
||||
std::path::Path::new(path).file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Untitled").to_string()
|
||||
}
|
||||
|
||||
/// The counts the import report shows for a freshly loaded document.
|
||||
|
||||
@@ -9,7 +9,7 @@ pub fn run<T>(cmd: &str, f: impl FnOnce() -> Result<T>) -> Result<T> {
|
||||
Err(payload) => {
|
||||
let msg = payload
|
||||
.downcast_ref::<&str>()
|
||||
.map(|s| s.to_string())
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| payload.downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "panic".to_string());
|
||||
log::error!("command `{cmd}` panicked: {msg}");
|
||||
|
||||
@@ -339,7 +339,7 @@ pub fn publish(doc: &Document, prev: Option<&PublishedLibrary>, sel: &Selection,
|
||||
.map(|v| v.id.as_str())
|
||||
.collect();
|
||||
let (bound, mode_colls) = used_variables(doc, &units);
|
||||
let var_ids = alias_closure(&doc.variables, offered_vars.iter().map(|v| v.to_string()).chain(bound));
|
||||
let var_ids = alias_closure(&doc.variables, offered_vars.iter().map(ToString::to_string).chain(bound));
|
||||
snap.variables.variables = doc.variables.variables.iter().filter(|v| var_ids.contains(&v.id)).cloned().collect();
|
||||
let coll_ids: BTreeSet<&str> = snap
|
||||
.variables
|
||||
|
||||
@@ -28,6 +28,53 @@ fn fill(doc: &mut Document, id: NodeId, c: Color) {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_screen_header(doc: &mut Document, screen: NodeId, s: usize) -> DocResult<()> {
|
||||
let header = doc.create(screen, NodeKind::Frame, "Header", 0.0, 0.0, SCREEN_W, 40.0)?;
|
||||
if let Some(n) = doc.node_mut(header) {
|
||||
n.layout = Some(AutoLayout { gap: 8.0, padding: [8.0; 4], ..AutoLayout::default() });
|
||||
n.fills = vec![Paint::solid(Color::from_rgb8_const(0xF5, 0xF5, 0xF5))];
|
||||
}
|
||||
doc.create(header, text(&format!("Screen {}", s + 1)), "Title", 0.0, 0.0, 120.0, 16.0)?;
|
||||
for k in 0..2 {
|
||||
let icon = doc.create(header, ellipse(), &format!("Icon {}", k + 1), 0.0, 0.0, 20.0, 20.0)?;
|
||||
fill(doc, icon, Color::from_rgb8_const(0x99, 0x99, 0x99));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_card_group(doc: &mut Document, screen: NodeId) -> DocResult<()> {
|
||||
let card = doc.create(screen, NodeKind::Group, "Card", 12.0, 52.0, 160.0, 64.0)?;
|
||||
// Children are in the group's space; normalising fits the group around them.
|
||||
let cbg = doc.create(card, NodeKind::Rectangle, "Card background", 0.0, 0.0, 160.0, 64.0)?;
|
||||
fill(doc, cbg, Color::from_rgb8_const(0xEE, 0xF4, 0xFF));
|
||||
doc.create(card, ellipse(), "Avatar", 8.0, 8.0, 32.0, 32.0)?;
|
||||
doc.create(card, text("Ada Lovelace"), "Name", 48.0, 8.0, 100.0, 16.0)?;
|
||||
doc.create(card, text("Analyst"), "Role", 48.0, 28.0, 100.0, 16.0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_screen_grid(doc: &mut Document, screen: NodeId, s: usize, rest: usize, made: &mut usize) -> DocResult<()> {
|
||||
for k in 0..rest {
|
||||
let (gx, gy) = (184.0 + (k % 8) as f64 * 21.0, 52.0 + (k / 8) as f64 * 34.0);
|
||||
let hue = ((s * PER_SCREEN + k) % 360) as f32 / 360.0;
|
||||
// The grid starts with a rectangle so even the smallest workload has one.
|
||||
let kind = (k + 2) % 5;
|
||||
let id = match kind {
|
||||
0 => doc.create(screen, text(&format!("Item {k}")), "Text", gx, gy, 20.0, 14.0)?,
|
||||
1 | 3 => doc.create(screen, ellipse(), "Ellipse", gx, gy, 18.0, 18.0)?,
|
||||
_ => doc.create(screen, NodeKind::Rectangle, "Rectangle", gx, gy, 18.0, 18.0)?,
|
||||
};
|
||||
if kind != 0 {
|
||||
fill(doc, id, Color::new(hue, 0.5, 1.0 - hue, 1.0));
|
||||
if let Some(n) = doc.node_mut(id) {
|
||||
n.corners.radii = [3.0; 4];
|
||||
}
|
||||
}
|
||||
*made += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A page of about `layers` layers (pages not counted), normalised like an opened file. Returns
|
||||
/// the document and its page.
|
||||
pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
|
||||
@@ -53,28 +100,13 @@ pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
|
||||
let budget = |made: usize| layers.saturating_sub(made);
|
||||
// Auto layout header: title + two icons (4 layers).
|
||||
if budget(made) >= 4 {
|
||||
let header = doc.create(screen, NodeKind::Frame, "Header", 0.0, 0.0, SCREEN_W, 40.0)?;
|
||||
if let Some(n) = doc.node_mut(header) {
|
||||
n.layout = Some(AutoLayout { gap: 8.0, padding: [8.0; 4], ..AutoLayout::default() });
|
||||
n.fills = vec![Paint::solid(Color::from_rgb8_const(0xF5, 0xF5, 0xF5))];
|
||||
}
|
||||
doc.create(header, text(&format!("Screen {}", s + 1)), "Title", 0.0, 0.0, 120.0, 16.0)?;
|
||||
for k in 0..2 {
|
||||
let icon = doc.create(header, ellipse(), &format!("Icon {}", k + 1), 0.0, 0.0, 20.0, 20.0)?;
|
||||
fill(&mut doc, icon, Color::from_rgb8_const(0x99, 0x99, 0x99));
|
||||
}
|
||||
add_screen_header(&mut doc, screen, s)?;
|
||||
made += 4;
|
||||
}
|
||||
// A card group (5 layers).
|
||||
// The card and the instances leave one layer for the grid, which starts with a rectangle.
|
||||
if budget(made) > 5 {
|
||||
let card = doc.create(screen, NodeKind::Group, "Card", 12.0, 52.0, 160.0, 64.0)?;
|
||||
// Children are in the group's space; normalising fits the group around them.
|
||||
let cbg = doc.create(card, NodeKind::Rectangle, "Card background", 0.0, 0.0, 160.0, 64.0)?;
|
||||
fill(&mut doc, cbg, Color::from_rgb8_const(0xEE, 0xF4, 0xFF));
|
||||
doc.create(card, ellipse(), "Avatar", 8.0, 8.0, 32.0, 32.0)?;
|
||||
doc.create(card, text("Ada Lovelace"), "Name", 48.0, 8.0, 100.0, 16.0)?;
|
||||
doc.create(card, text("Analyst"), "Role", 48.0, 28.0, 100.0, 16.0)?;
|
||||
add_card_group(&mut doc, screen)?;
|
||||
made += 5;
|
||||
}
|
||||
// Three button instances (3 layers each).
|
||||
@@ -87,24 +119,7 @@ pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
|
||||
}
|
||||
// Fill the rest of the screen with a grid of shapes and text.
|
||||
let rest = PER_SCREEN.saturating_sub(4 + 5 + 9 + 1).min(budget(made));
|
||||
for k in 0..rest {
|
||||
let (gx, gy) = (184.0 + (k % 8) as f64 * 21.0, 52.0 + (k / 8) as f64 * 34.0);
|
||||
let hue = ((s * PER_SCREEN + k) % 360) as f32 / 360.0;
|
||||
// The grid starts with a rectangle so even the smallest workload has one.
|
||||
let kind = (k + 2) % 5;
|
||||
let id = match kind {
|
||||
0 => doc.create(screen, text(&format!("Item {k}")), "Text", gx, gy, 20.0, 14.0)?,
|
||||
1 | 3 => doc.create(screen, ellipse(), "Ellipse", gx, gy, 18.0, 18.0)?,
|
||||
_ => doc.create(screen, NodeKind::Rectangle, "Rectangle", gx, gy, 18.0, 18.0)?,
|
||||
};
|
||||
if kind != 0 {
|
||||
fill(&mut doc, id, Color::new(hue, 0.5, 1.0 - hue, 1.0));
|
||||
if let Some(n) = doc.node_mut(id) {
|
||||
n.corners.radii = [3.0; 4];
|
||||
}
|
||||
}
|
||||
made += 1;
|
||||
}
|
||||
add_screen_grid(&mut doc, screen, s, rest, &mut made)?;
|
||||
if budget(made) == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ impl Kit {
|
||||
pub fn variables(&mut self, collection: &str, modes: &[&str], vars: Vec<(&str, &str, Vec<Value>)>) -> Result<()> {
|
||||
self.run("variable.createCollection", json!({"name": collection, "modes": modes}))?;
|
||||
for (name, kind, values) in vars {
|
||||
let values: Map<String, Value> = modes.iter().map(|m| m.to_string()).zip(values).collect();
|
||||
let values: Map<String, Value> = modes.iter().map(ToString::to_string).zip(values).collect();
|
||||
self.run("variable.create", json!({"collection": collection, "name": name, "type": kind, "values": values}))?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
+22
-19
@@ -91,6 +91,27 @@ pub fn polylines(path: &BezPath) -> Option<Vec<Polyline>> {
|
||||
polylines_with_limit(path, MAX_SAMPLES)
|
||||
}
|
||||
|
||||
fn flatten_curve_segment(el: PathEl, seg_start: Point, limit: usize, count: &mut usize, cur: &mut Polyline) -> bool {
|
||||
let mut over = false;
|
||||
kurbo::flatten([PathEl::MoveTo(seg_start), el], FLATTEN_TOL, |fel| {
|
||||
if over {
|
||||
return;
|
||||
}
|
||||
if let PathEl::LineTo(p) = fel {
|
||||
*count += 1;
|
||||
if *count > limit {
|
||||
over = true;
|
||||
return;
|
||||
}
|
||||
if cur.points.is_empty() {
|
||||
cur.push(seg_start);
|
||||
}
|
||||
cur.push(p);
|
||||
}
|
||||
});
|
||||
!over
|
||||
}
|
||||
|
||||
/// [`polylines`] with an explicit sample budget, so tests can use a small one.
|
||||
///
|
||||
/// The budget is enforced *while* flattening, not after it (issue #204): the input element
|
||||
@@ -141,25 +162,7 @@ fn polylines_with_limit(path: &BezPath, limit: usize) -> Option<Vec<Polyline>> {
|
||||
PathEl::QuadTo(_, end) | PathEl::CurveTo(_, _, end) => {
|
||||
// Flatten this one segment on its own, so an over-budget curve aborts
|
||||
// here instead of after the whole path has been flattened.
|
||||
let seg_start = pen;
|
||||
let mut over = false;
|
||||
kurbo::flatten([PathEl::MoveTo(seg_start), *el], FLATTEN_TOL, |fel| {
|
||||
if over {
|
||||
return;
|
||||
}
|
||||
if let PathEl::LineTo(p) = fel {
|
||||
count += 1;
|
||||
if count > limit {
|
||||
over = true;
|
||||
return;
|
||||
}
|
||||
if cur.points.is_empty() {
|
||||
cur.push(seg_start);
|
||||
}
|
||||
cur.push(p);
|
||||
}
|
||||
});
|
||||
if over {
|
||||
if !flatten_curve_segment(*el, pen, limit, &mut count, &mut cur) {
|
||||
return None;
|
||||
}
|
||||
pen = end;
|
||||
|
||||
@@ -59,7 +59,7 @@ fn error(id: Value, code: i64, message: impl Into<String>) -> Value {
|
||||
}
|
||||
|
||||
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
|
||||
payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
|
||||
payload.downcast_ref::<&str>().map(ToString::to_string).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
|
||||
}
|
||||
|
||||
impl Server {
|
||||
|
||||
@@ -271,3 +271,30 @@ fn sample_files() {
|
||||
std::fs::write(format!("{stem}-{}.pdf", i + 1), export_pdf(&d, &[*f], &PdfOptions::default()).unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// A tiny PNG whose header claims 30,000 × 30,000 RGBA (3.6 GB decoded) is refused when it is read,
|
||||
/// before anything is decoded: the PDF writer is craft-libs' patched krilla.
|
||||
#[test]
|
||||
fn a_png_that_decodes_too_large_is_refused() {
|
||||
fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
|
||||
out.extend_from_slice(&u32::try_from(data.len()).unwrap().to_be_bytes());
|
||||
let mut crc = flate2::Crc::new();
|
||||
crc.update(kind);
|
||||
crc.update(data);
|
||||
out.extend_from_slice(kind);
|
||||
out.extend_from_slice(data);
|
||||
out.extend_from_slice(&crc.sum().to_be_bytes());
|
||||
}
|
||||
let mut ihdr = Vec::new();
|
||||
ihdr.extend_from_slice(&30_000u32.to_be_bytes());
|
||||
ihdr.extend_from_slice(&30_000u32.to_be_bytes());
|
||||
ihdr.extend_from_slice(&[8, 6, 0, 0, 0]);
|
||||
let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
std::io::Write::write_all(&mut z, &[0; 64]).unwrap();
|
||||
let mut png = b"\x89PNG\r\n\x1a\n".to_vec();
|
||||
chunk(&mut png, b"IHDR", &ihdr);
|
||||
chunk(&mut png, b"IDAT", &z.finish().unwrap());
|
||||
chunk(&mut png, b"IEND", &[]);
|
||||
const { assert!(krilla::CRAFT_UNWIND_SAFE_DROP) };
|
||||
assert!(krilla::image::Image::from_png(krilla::Data::from(png), true).is_err());
|
||||
}
|
||||
|
||||
@@ -831,7 +831,7 @@ fn scan_dir(dir: &std::path::Path, depth: usize, out: &mut BTreeMap<String, (Str
|
||||
if !meta.is_file() {
|
||||
continue;
|
||||
}
|
||||
let ext = path.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default();
|
||||
let ext = path.extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).unwrap_or_default();
|
||||
if !matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc") && !(path.extension().is_none() && starts_like_a_font(&path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -243,6 +243,7 @@ mod windows_qa_tests;
|
||||
mod windows_tests;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
@@ -1202,7 +1203,7 @@ impl FigApp {
|
||||
}
|
||||
let Ok(name) = self.session.doc().map(|d| d.name.clone()) else { return };
|
||||
self.pick_save(i18n::tr("Save as"), &format!("{name}.figcraft"), &["figcraft", "fig"], |app, mut p| {
|
||||
if p.extension().is_none_or(|e| e.is_empty()) {
|
||||
if p.extension().is_none_or(OsStr::is_empty) {
|
||||
if !p.set_extension("figcraft") {
|
||||
app.toast(i18n::tr("Use the .figcraft or .fig extension."));
|
||||
return;
|
||||
@@ -1219,7 +1220,7 @@ impl FigApp {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if !p.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("figcraft") || e.eq_ignore_ascii_case("fig")) {
|
||||
} else if !p.extension().and_then(OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("figcraft") || e.eq_ignore_ascii_case("fig")) {
|
||||
app.toast(i18n::tr("Use the .figcraft or .fig extension."));
|
||||
return;
|
||||
}
|
||||
@@ -1238,7 +1239,7 @@ impl FigApp {
|
||||
.docs
|
||||
.get(tab)
|
||||
.and_then(|d| d.path.as_deref())
|
||||
.is_some_and(|path| !Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("fig")));
|
||||
.is_some_and(|path| !Path::new(path).extension().and_then(OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("fig")));
|
||||
if native_path {
|
||||
if self.run("file.save", json!({})).is_some() {
|
||||
self.toast(i18n::tr("Saved"));
|
||||
@@ -1911,13 +1912,13 @@ pub(crate) const OPEN_EXTENSIONS: &[&str] = &["figcraft", "fig", "json", "svg",
|
||||
|
||||
/// Whether `path` names a file type that opens as a document (the extension, in any case).
|
||||
pub(crate) fn opens(path: &Path) -> bool {
|
||||
path.extension().and_then(|e| e.to_str()).is_some_and(|e| OPEN_EXTENSIONS.iter().any(|x| x.eq_ignore_ascii_case(e)))
|
||||
path.extension().and_then(OsStr::to_str).is_some_and(|e| OPEN_EXTENSIONS.iter().any(|x| x.eq_ignore_ascii_case(e)))
|
||||
}
|
||||
|
||||
/// Whether `path` is a whole document (`.figcraft`, `.fig`, file JSON) rather than an image or SVG:
|
||||
/// dropped on the canvas it opens as a file of its own instead of adding its pages to the open one.
|
||||
pub(crate) fn is_document(path: &Path) -> bool {
|
||||
path.extension().and_then(|e| e.to_str()).is_some_and(|e| ["figcraft", "fig", "json"].iter().any(|x| x.eq_ignore_ascii_case(e)))
|
||||
path.extension().and_then(OsStr::to_str).is_some_and(|e| ["figcraft", "fig", "json"].iter().any(|x| x.eq_ignore_ascii_case(e)))
|
||||
}
|
||||
|
||||
/// Reads the contents of dropped file `name` that has no file on disk to read by path.
|
||||
|
||||
@@ -95,7 +95,7 @@ pub fn guard(app: &mut FigApp, ctx: &Context, f: impl FnOnce(&mut FigApp)) {
|
||||
let roots = app.automation_roots.clone();
|
||||
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| figcraft_engine::file_access::confined(roots.as_ref(), || f(&mut *app))));
|
||||
if let Err(e) = res {
|
||||
let msg = e.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| e.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into());
|
||||
let msg = e.downcast_ref::<&str>().map(ToString::to_string).or_else(|| e.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into());
|
||||
log::error!("UI panic: {msg}");
|
||||
app.toast(format!("Internal error (please report): {msg}"));
|
||||
ctx.request_repaint();
|
||||
|
||||
@@ -25,6 +25,12 @@ set -euo pipefail
|
||||
HERE="$ROOT/packaging/linux"
|
||||
APP_ID=ai.storyteller.figcraft
|
||||
|
||||
verify_sha256() {
|
||||
local expected="$1"
|
||||
local file="$2"
|
||||
printf '%s %s\n' "$expected" "$file" | sha256sum -c -
|
||||
}
|
||||
|
||||
SKIP_BUILD=0
|
||||
FORMATS="appimage deb rpm tar"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -145,21 +151,21 @@ if has appimage; then
|
||||
download="$TOOL.download"
|
||||
curl --proto '=https' -fsSL -o "$download" \
|
||||
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
|
||||
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c -
|
||||
verify_sha256 "$APPIMAGETOOL_SHA256" "$download"
|
||||
chmod 755 "$download"
|
||||
mv "$download" "$TOOL"
|
||||
fi
|
||||
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c -
|
||||
verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL"
|
||||
fi
|
||||
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
|
||||
if [[ ! -f "$RUNTIME" ]]; then
|
||||
download="$RUNTIME.download"
|
||||
curl --proto '=https' -fsSL -o "$download" \
|
||||
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH"
|
||||
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c -
|
||||
verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$download"
|
||||
mv "$download" "$RUNTIME"
|
||||
fi
|
||||
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c -
|
||||
verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME"
|
||||
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
|
||||
# relative, e.g. target/agent-<name>).
|
||||
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ fn cargo() -> Command {
|
||||
}
|
||||
|
||||
fn root() -> std::path::PathBuf {
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(|p| p.to_path_buf()).unwrap_or_default()
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(std::path::Path::to_path_buf).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
|
||||
Reference in New Issue
Block a user