diff --git a/crates/color/src/cms/icc.rs b/crates/color/src/cms/icc.rs index 565ae0b7..53efb67a 100644 --- a/crates/color/src/cms/icc.rs +++ b/crates/color/src/cms/icc.rs @@ -125,7 +125,7 @@ impl IccProfile { /// Built-in RGB working spaces from `moxcms`'s standard primaries. pub fn builtin_rgb(name: &str) -> Option { let p = match name { - super::ADOBE_RGB => ColorProfile::new_adobe_rgb(), + super::WIDE_GAMUT_RGB => ColorProfile::new_adobe_rgb(), super::DISPLAY_P3 => ColorProfile::new_display_p3(), super::PROPHOTO_RGB => ColorProfile::new_pro_photo_rgb(), super::SRGB => ColorProfile::new_srgb(), diff --git a/crates/color/src/cms/mod.rs b/crates/color/src/cms/mod.rs index 9fc84d1b..7960d44f 100644 --- a/crates/color/src/cms/mod.rs +++ b/crates/color/src/cms/mod.rs @@ -1,8 +1,8 @@ //! Colour management: working spaces, RGB ↔ CMYK ↔ Lab conversions with rendering intents, //! gamut checks and soft-proof transforms. //! -//! * **RGB working spaces** are ICC matrix/shaper profiles via [`moxcms`] (sRGB, Adobe RGB (1998) -//! compatible, Display P3, ProPhoto RGB) or user `.icc` files. Display output is sRGB. +//! * **RGB working spaces** are ICC matrix/shaper profiles via [`moxcms`] (sRGB, Wide Gamut RGB +//! (1998 primaries), Display P3, ProPhoto RGB) or user `.icc` files. Display output is sRGB. //! * **CMYK spaces:** [`GENERIC_CMYK`] is our own documented parametric press model //! ([`generic`]); [`DEVICE_CMYK`] is the old profile-free formula (kept for exact legacy numbers); //! any CMYK `.icc` the user loads goes through `moxcms` (LUT-based profiles). @@ -25,7 +25,8 @@ pub use lab::{Lab, delta_e76, delta_e2000}; use crate::Color; pub const SRGB: &str = "sRGB IEC61966-2.1"; -pub const ADOBE_RGB: &str = "Adobe RGB (1998) compatible"; +/// The wide-gamut RGB space with the 1998 primaries (γ 2.2, D65). +pub const WIDE_GAMUT_RGB: &str = "Wide Gamut RGB (1998 primaries)"; pub const DISPLAY_P3: &str = "Display P3"; pub const PROPHOTO_RGB: &str = "ProPhoto RGB"; /// Our parametric press model (see [`generic`]). Not an Adobe/ECI profile. @@ -33,6 +34,14 @@ pub const GENERIC_CMYK: &str = "VectorCraft Generic CMYK (SWOP-like)"; /// Profile-free CMYK (`rgb = (1−c)(1−k)` …): uncalibrated, kept for legacy numbers. pub const DEVICE_CMYK: &str = "Device CMYK (uncalibrated)"; +/// Built-in RGB and CMYK working spaces, in menu order. +const BUILTIN_RGB: [&str; 4] = [SRGB, WIDE_GAMUT_RGB, DISPLAY_P3, PROPHOTO_RGB]; +const BUILTIN_CMYK: [&str; 2] = [GENERIC_CMYK, DEVICE_CMYK]; + +/// Names earlier versions gave built-in profiles: (old name, current name). Settings, documents +/// and commands that still use an old name get the current profile. +pub const LEGACY_NAMES: &[(&str, &str)] = &[("Adobe RGB (1998) compatible", WIDE_GAMUT_RGB)]; // brand-ok: legacy alias + /// ΔE2000 above which a colour counts as out of the CMYK gamut (the gamut warning). pub const GAMUT_THRESHOLD: f32 = 2.0; @@ -169,7 +178,7 @@ fn user_profile(name: &str) -> Option> { } fn cmyk_space(name: &str) -> Result { - match name { + match canonical_name(name) { GENERIC_CMYK => Ok(CmykSpace::Generic(generic())), DEVICE_CMYK => Ok(CmykSpace::Device), _ => match user_profile(name) { @@ -194,12 +203,23 @@ fn rgb_space(name: &str) -> Result { } } +/// The current name of a profile: names earlier versions used resolve to today's name. +pub fn canonical_name(name: &str) -> &str { + LEGACY_NAMES.iter().find(|(old, _)| *old == name).map_or(name, |(_, new)| new) +} + +/// An available profile by its current or legacy name. +pub fn profile(name: &str) -> Option { + let name = canonical_name(name); + profiles().into_iter().find(|p| p.name == name) +} + /// Every profile available (built-in first, then user-loaded). pub fn profiles() -> Vec { - let mut v: Vec = [SRGB, ADOBE_RGB, DISPLAY_P3, PROPHOTO_RGB] + let mut v: Vec = BUILTIN_RGB .iter() .map(|n| ProfileInfo { name: (*n).into(), kind: ProfileKind::Rgb, builtin: true }) - .chain([GENERIC_CMYK, DEVICE_CMYK].iter().map(|n| ProfileInfo { name: (*n).into(), kind: ProfileKind::Cmyk, builtin: true })) + .chain(BUILTIN_CMYK.iter().map(|n| ProfileInfo { name: (*n).into(), kind: ProfileKind::Cmyk, builtin: true })) .collect(); for p in USER.read().unwrap_or_else(|e| e.into_inner()).iter() { v.push(ProfileInfo { name: p.name.clone(), kind: p.kind, builtin: false }); @@ -214,7 +234,8 @@ pub fn register_icc(bytes: &[u8], name: Option) -> Result Result { - Ok(Self { settings: settings.clone(), rgb: rgb_space(&settings.rgb)?, cmyk: cmyk_space(&settings.cmyk)? }) + let settings = ColorSettings { rgb: canonical_name(&settings.rgb).into(), cmyk: canonical_name(&settings.cmyk).into(), ..settings.clone() }; + Ok(Self { rgb: rgb_space(&settings.rgb)?, cmyk: cmyk_space(&settings.cmyk)?, settings }) } pub fn settings(&self) -> &ColorSettings { &self.settings diff --git a/crates/color/src/cms/tests.rs b/crates/color/src/cms/tests.rs index 4e4f1d8b..a5b211ca 100644 --- a/crates/color/src/cms/tests.rs +++ b/crates/color/src/cms/tests.rs @@ -138,11 +138,11 @@ fn convert_between_models() { #[test] fn wide_gamut_working_space() { - let c = Cms::new(&ColorSettings { rgb: ADOBE_RGB.into(), ..Default::default() }).unwrap(); + let c = Cms::new(&ColorSettings { rgb: WIDE_GAMUT_RGB.into(), ..Default::default() }).unwrap(); assert!(!c.rgb_is_srgb()); let grey = c.rgb_to_srgb([0.5, 0.5, 0.5]); assert!(de(grey, [0.5, 0.5, 0.5]) < 1.5, "{grey:?}"); - // Adobe RGB green is outside sRGB: it clips to (nearly) full sRGB green. + // Wide-gamut green is outside sRGB: it clips to (nearly) full sRGB green. let g = c.rgb_to_srgb([0.0, 1.0, 0.0]); assert!(g[1] > 0.95 && g[0] < 0.1, "{g:?}"); let back = c.srgb_to_rgb(c.rgb_to_srgb([0.4, 0.5, 0.3])); @@ -150,6 +150,21 @@ fn wide_gamut_working_space() { assert!(matches!(Cms::new(&ColorSettings { rgb: "Nope".into(), ..Default::default() }), Err(CmsError::UnknownProfile(_)))); } +#[test] +fn legacy_profile_name_resolves() { + let (old, _) = LEGACY_NAMES[0]; + assert_eq!(canonical_name(old), WIDE_GAMUT_RGB); + assert_eq!(canonical_name(SRGB), SRGB); + assert_eq!(profile(old).map(|p| (p.name, p.kind, p.builtin)), Some((WIDE_GAMUT_RGB.to_string(), ProfileKind::Rgb, true))); + assert!(profile("Nope").is_none()); + assert!(profiles().iter().all(|p| p.name != old), "only the current name is listed"); + // Settings saved with the old name load as the current profile, under its current name. + let c = Cms::new(&ColorSettings { rgb: old.into(), ..Default::default() }).unwrap(); + assert_eq!(c.settings().rgb, WIDE_GAMUT_RGB); + let same = Cms::new(&ColorSettings { rgb: WIDE_GAMUT_RGB.into(), ..Default::default() }).unwrap(); + assert_eq!(c.rgb_to_srgb([0.0, 1.0, 0.0]), same.rgb_to_srgb([0.0, 1.0, 0.0])); +} + #[test] fn proof_lut_matches_direct_proof() { let c = cms(); diff --git a/crates/engine/src/cmd/colormgmt.rs b/crates/engine/src/cmd/colormgmt.rs index be83a99e..230ed000 100644 --- a/crates/engine/src/cmd/colormgmt.rs +++ b/crates/engine/src/cmd/colormgmt.rs @@ -157,10 +157,10 @@ fn color_settings(s: &mut Session, p: &Value) -> Result { let mut st = cms::active_settings(); let before = st.clone(); if let Some(v) = str_param(p, "rgb") { - st.rgb = v.to_string(); + st.rgb = cms::canonical_name(v).to_string(); } if let Some(v) = str_param(p, "cmyk") { - st.cmyk = v.to_string(); + st.cmyk = cms::canonical_name(v).to_string(); } if let Some(i) = intent_param(p, C)? { st.intent = i; @@ -189,24 +189,23 @@ fn load_profile(_: &mut Session, _: &Value) -> Result { /// The document's assigned profiles (`None` = working space). pub fn doc_profiles(d: &vectorcraft_doc::Document) -> (Option, Option) { let o = d.unknown.get(PROFILES_KEY); - let get = |k: &str| o.and_then(|o| o.get(k)).and_then(Value::as_str).map(str::to_string); + let get = |k: &str| o.and_then(|o| o.get(k)).and_then(Value::as_str).map(|n| cms::canonical_name(n).to_string()); (get("rgb"), get("cmyk")) } fn assign_profile(s: &mut Session, p: &Value) -> Result { const C: &str = "edit.assignProfile"; let (mut rgb, mut cmyk) = doc_profiles(&s.doc()?.doc); - let known = cms::profiles(); let pick = |key: &str, cur: &mut Option, kind: cms::ProfileKind| -> Result { match p.get(key) { None => Ok(false), Some(Value::Null) => Ok(cur.take().is_some()), Some(Value::String(n)) => { - if !known.iter().any(|k| &k.name == n && k.kind == kind) { + let Some(info) = cms::profile(n).filter(|k| k.kind == kind) else { return Err(bad(C, format!("unknown {key} profile `{n}`"))); - } - let changed = cur.as_deref() != Some(n.as_str()); - *cur = Some(n.clone()); + }; + let changed = cur.as_deref() != Some(info.name.as_str()); + *cur = Some(info.name); Ok(changed) } Some(_) => Err(bad(C, format!("`{key}` must be a profile name or null"))), @@ -363,7 +362,7 @@ fn proof_setup(s: &mut Session, p: &Value) -> Result { if let Some(t) = str_param(p, "target") { let t = ProofTarget::parse(t).ok_or_else(|| bad(C, format!("unknown proof target `{t}`")))?; if let ProofTarget::Cmyk(name) = &t - && !cms::profiles().iter().any(|k| &k.name == name && k.kind == cms::ProfileKind::Cmyk) + && !cms::profile(name).is_some_and(|k| k.kind == cms::ProfileKind::Cmyk) { return Err(bad(C, format!("unknown CMYK profile `{name}`"))); } diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index 1cdbe340..4b65b3f3 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -269,12 +269,12 @@ pub const PREF_SPECS: &[PrefSpec] = &[ // Clipboard Handling p!("copyAsSvg", "Clipboard Handling", "On Copy", "Include SVG Code", bool), p!("copyAsPdf", "Clipboard Handling", "On Copy", "PDF", bool), - p!("copyAicb", "Clipboard Handling", "On Copy", "AICB (no transparency support)", bool), + p!("copyAicb", "Clipboard Handling", "On Copy", "Legacy vector clipboard (no transparency)", bool), p!( "aicbMode", "Clipboard Handling", "On Copy", - "AICB", + "Legacy vector clipboard", choice(&[("preservePaths", "Preserve Paths"), ("preserveAppearance", "Preserve Appearance and Overprints")]) ), p!( diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 571c74df..02ef3cff 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -44,7 +44,7 @@ pub fn specs() -> Vec { "Area / Path Type", [], None, - "{path: id, mode: \"area\"|\"onPath\", text?: \"\", at?: [x, y] (on-path start: nearest point), size?, font?} turn a path into an area-type frame or a type-on-a-path baseline (the path's paint is dropped, like Illustrator) → {id}", + "{path: id, mode: \"area\"|\"onPath\", text?: \"\", at?: [x, y] (on-path start: nearest point), size?, font?} turn a path into an area-type frame or a type-on-a-path baseline (the path's paint is dropped) → {id}", has_doc, create_in_path ), diff --git a/crates/engine/src/tests_colormgmt.rs b/crates/engine/src/tests_colormgmt.rs index 02f25c27..42906c58 100644 --- a/crates/engine/src/tests_colormgmt.rs +++ b/crates/engine/src/tests_colormgmt.rs @@ -13,7 +13,7 @@ use vectorcraft_render::{RenderOptions, Renderer}; use super::*; -/// Tests that touch the process-wide proof view or colour settings run one at a time. +/// Every test here reads or changes the process-wide proof view or colour settings: one at a time. static GLOBAL: Mutex<()> = Mutex::new(()); fn session() -> Session { @@ -45,6 +45,7 @@ fn render(doc: &Document, opts: RenderOptions) -> vectorcraft_render::Rendered { #[test] fn document_mode_converts_through_cms() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); let a = rect(&mut s, 0.0, 0.0, 10.0, 10.0, json!("#ff0000")); let r = s.execute("object.convertDocumentColorMode", &json!({"mode": "cmyk"})).unwrap(); @@ -63,6 +64,7 @@ fn document_mode_converts_through_cms() { #[test] fn document_mode_conversion_is_idempotent() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); rect(&mut s, 0.0, 0.0, 10.0, 10.0, json!("#336699")); rect(&mut s, 20.0, 0.0, 10.0, 10.0, json!({"gray": 40})); @@ -79,6 +81,7 @@ fn document_mode_conversion_is_idempotent() { #[test] fn rgb_cmyk_rgb_document_roundtrip_within_tolerance() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); let cols = ["#cc9980", "#808080", "#4d7359", "#e6d94d"]; let ids: Vec = cols.iter().enumerate().map(|(i, h)| rect(&mut s, i as f64 * 20.0, 0.0, 10.0, 10.0, json!(h))).collect(); @@ -95,6 +98,7 @@ fn rgb_cmyk_rgb_document_roundtrip_within_tolerance() { #[test] fn swatch_links_survive_mode_conversion() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); s.execute("swatch.new", &json!({"name": "Brand", "color": "#2a6fb0", "global": true})).unwrap(); let a = NodeId(s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap()); @@ -109,6 +113,7 @@ fn swatch_links_survive_mode_conversion() { #[test] fn gamut_check_flags_unprintable_colours() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); rect(&mut s, 0.0, 0.0, 10.0, 10.0, json!("#0000ff")); rect(&mut s, 20.0, 0.0, 10.0, 10.0, json!("#cc9980")); @@ -124,6 +129,7 @@ fn gamut_check_flags_unprintable_colours() { #[test] fn convert_query_and_intents() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); let q = |s: &mut Session, intent: &str| s.execute("color.convert", &json!({"color": "#0000ff", "to": "cmyk", "intent": intent})).unwrap(); let p = q(&mut s, "perceptual"); @@ -170,6 +176,7 @@ fn color_settings_lists_profiles_and_validates() { #[test] fn proof_colors_simulate_press_on_screen() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); rect(&mut s, 0.0, 0.0, 100.0, 100.0, json!("#0000ff")); let doc = s.doc().unwrap().doc.clone(); @@ -186,6 +193,7 @@ fn proof_colors_simulate_press_on_screen() { #[test] fn separation_plates_render_ink_coverage() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); rect(&mut s, 0.0, 0.0, 50.0, 100.0, json!({"c": 100, "m": 0, "y": 0, "k": 0})); rect(&mut s, 50.0, 0.0, 50.0, 100.0, json!({"c": 0, "m": 50, "y": 0, "k": 0})); @@ -223,6 +231,7 @@ fn separation_plates_render_ink_coverage() { #[test] fn rgb_art_separates_with_gcr() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); rect(&mut s, 0.0, 0.0, 100.0, 100.0, json!("#000000")); let doc = s.doc().unwrap().doc.clone(); @@ -274,6 +283,7 @@ fn spot_colours_get_their_own_plate() { #[test] fn overprint_black_multiplies_in_preview() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); rect(&mut s, 0.0, 0.0, 100.0, 100.0, json!({"c": 100, "m": 0, "y": 0, "k": 0})); let k = rect(&mut s, 25.0, 25.0, 50.0, 50.0, json!({"c": 0, "m": 0, "y": 0, "k": 100})); @@ -304,6 +314,8 @@ fn view_toggles_redraw_without_dirtying() { assert_eq!(r["target"], "deuteranopia"); assert_eq!(vectorcraft_render::proof::active_proof().unwrap().target, ProofTarget::Deuteranopia); assert!(s.execute("view.proofSetup", &json!({"target": "cmyk:Nope"})).is_err()); + assert!(s.execute("view.proofSetup", &json!({"target": format!("cmyk:{}", cms::WIDE_GAMUT_RGB)})).is_err(), "an RGB profile"); + s.execute("view.proofSetup", &json!({"target": format!("cmyk:{}", cms::DEVICE_CMYK)})).unwrap(); s.execute("view.proofSetup", &json!({"target": "workingCmyk", "intent": "relative"})).unwrap(); let r = s.execute("view.proofColors", &json!({"on": false})).unwrap(); assert_eq!(r["proofColors"], false); @@ -325,6 +337,7 @@ fn pdf_text(doc: &Document) -> String { #[test] fn pdf_cmyk_document_writes_device_cmyk() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut s = session(); rect(&mut s, 0.0, 0.0, 10.0, 10.0, json!("#ff0000")); let rgb_pdf = pdf_text(&s.doc().unwrap().doc); @@ -356,6 +369,7 @@ fn pdf_spot_colour_is_a_separation() { #[test] fn intents_reach_the_document_conversion() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); let mut a = session(); rect(&mut a, 0.0, 0.0, 10.0, 10.0, json!("#00ff00")); let mut b = session(); @@ -377,3 +391,24 @@ fn intents_reach_the_document_conversion() { assert_ne!(first(&a), first(&b)); let _ = Intent::Perceptual; } + +#[test] +fn legacy_profile_name_resolves_in_commands_and_files() { + let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner()); + let (old, _) = cms::LEGACY_NAMES[0]; + let mut s = session(); + let before = cms::active_settings(); + let r = s.execute("edit.colorSettings", &json!({"rgb": old})).unwrap(); + assert_eq!(r["rgb"], cms::WIDE_GAMUT_RGB); + assert_eq!(cms::active_settings().rgb, cms::WIDE_GAMUT_RGB); + let names: Vec<&str> = r["profiles"].as_array().unwrap().iter().map(|p| p["name"].as_str().unwrap()).collect(); + assert!(names.contains(&cms::WIDE_GAMUT_RGB) && !names.contains(&old)); + let r = s.execute("edit.assignProfile", &json!({"rgb": old})).unwrap(); + assert_eq!(r["rgb"], cms::WIDE_GAMUT_RGB, "stored under the current name"); + // A document saved by an older version names the profile the old way. + let mut d = (*s.doc().unwrap().doc).clone(); + d.unknown.insert(cmd::colormgmt::PROFILES_KEY.into(), json!({"rgb": old, "cmyk": null})); + let back = vectorcraft_format::load(&vectorcraft_format::save(&d, false)).unwrap(); + assert_eq!(cmd::colormgmt::doc_profiles(&back), (Some(cms::WIDE_GAMUT_RGB.to_string()), None)); + cms::set_active(&before).unwrap(); +} diff --git a/crates/mcp/src/server.rs b/crates/mcp/src/server.rs index 57ebb88c..6b168481 100644 --- a/crates/mcp/src/server.rs +++ b/crates/mcp/src/server.rs @@ -18,7 +18,7 @@ const INVALID_PARAMS: i64 = -32602; const INTERNAL_ERROR: i64 = -32603; const RESOURCE_NOT_FOUND: i64 = -32002; -const INSTRUCTIONS: &str = "VectorCraft is a vector illustration app (an Illustrator clone). Coordinates are points in \ +const INSTRUCTIONS: &str = "VectorCraft is a professional vector illustration app. Coordinates are points in \ document space (y down, origin at the first artboard's top-left; a new document is 612×792). Draw with draw_shape / \ draw_path, change colours with set_paint, look with screenshot and inspect_document. Every menu action is a command: \ find it with list_commands and run it with run_command. New objects become the selection, and most commands act on the \ diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index 09b90610..c8366db6 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -302,7 +302,7 @@ pub fn tool_definitions() -> Vec { obj( json!({ "effect": {"type": "string", "description": "Effect id (see the catalogue)"}, - "params": {"type": "object", "description": "Effect parameters; missing keys take Illustrator's dialog defaults"}, + "params": {"type": "object", "description": "Effect parameters; missing keys take the dialog defaults"}, "ids": {"type": "array", "items": {"type": "integer"}}, }), &[], @@ -347,7 +347,7 @@ pub fn tool_definitions() -> Vec { tool( "create_graph", "Create graph", - "Create a graph (Illustrator graph tools) in the plot rectangle. Data as `csv` (first row: empty cell then series names; then one row per category: label, values…) or `series`/`categories`/`rows`. Edit later with run_command graph.setData / graph.setType.", + "Create a graph (graph tools) in the plot rectangle. Data as `csv` (first row: empty cell then series names; then one row per category: label, values…) or `series`/`categories`/`rows`. Edit later with run_command graph.setData / graph.setType.", obj( json!({ "type": {"type": "string", "enum": ["column", "stackedColumn", "bar", "stackedBar", "line", "area", "scatter", "pie", "radar"]}, diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 889f2224..ed602a51 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -21,13 +21,13 @@ use vectorcraft_doc::Document; /// PDF standard / version to target. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum Compatibility { - /// Acrobat 5 (PDF 1.4). + /// PDF 1.4. Pdf14, - /// Acrobat 6 (PDF 1.5). + /// PDF 1.5. Pdf15, - /// Acrobat 7 (PDF 1.6). + /// PDF 1.6. Pdf16, - /// Acrobat 8+ (PDF 1.7). The default. + /// PDF 1.7. The default. #[default] Pdf17, /// PDF 2.0. diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 7de7893d..1d56029b 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -3,7 +3,7 @@ name = "vectorcraft-ui-egui" version.workspace = true edition.workspace = true license.workspace = true -description = "VectorCraft egui frontend (Illustrator-style UI); swappable — nothing below depends on it" +description = "VectorCraft egui frontend (menus, panels, tools and canvas); swappable — nothing below depends on it" [lints] workspace = true diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 6495d65d..08a13ff2 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -31,6 +31,9 @@ pub mod unsaved; pub mod widgets; pub mod workspaces; +#[cfg(test)] +mod tests_labels; + use std::sync::mpsc::{Receiver, Sender}; use std::sync::{Arc, Mutex}; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index e1fc19d7..cf7fc14a 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -20,7 +20,7 @@ pub enum Item { Todo(&'static str, &'static str), Sub(&'static str, Vec), Sep, - /// Section header (disabled label, e.g. "Illustrator Effects"). + /// Section header (disabled label, e.g. "Vector Effects"). Header(&'static str), } @@ -39,6 +39,10 @@ fn todos(label: &'static str, sc: &'static str) -> Item { fn sub(label: &'static str, items: Vec) -> Item { Item::Sub(label, items) } +/// Window → … Libraries until the code-generated libraries land (disabled entries). +fn library_placeholders() -> Vec { + vec![todo("Built-in Libraries"), todo("User Defined"), Sep, todo("Other Library…")] +} use Item::Sep; /// UI-level commands: (id, label, shortcut, params doc). @@ -94,7 +98,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ ("shortcuts.list", "List Keyboard Shortcuts", "", "{query?} → [{id, label, group, shortcut, default, overridden}]"), ("shortcuts.conflicts", "Keyboard Shortcut Conflicts", "", "{} → [{shortcut, ids}]"), ("shortcuts.reset", "Reset Keyboard Shortcuts", "", "{}"), - ("shortcuts.preset", "Keyboard Shortcut Set", "", "{name: \"VectorCraft Defaults\" | \"Illustrator Defaults\"}"), + ( + "shortcuts.preset", + "Keyboard Shortcut Set", + "", + "{name: \"VectorCraft Defaults\" | \"Classic Defaults\"} (names of earlier versions are accepted)", + ), ("shortcuts.export", "Export Keyboard Shortcuts…", "", "{path?}"), ("shortcuts.import", "Import Keyboard Shortcuts…", "", "{path? | data?}"), ("view.outline", "Outline", "Cmd+Y", "{} toggle Outline/Preview"), @@ -746,7 +755,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { Sep, todo("Transparency Flattener Presets…"), todo("Print Presets…"), - todo("Adobe PDF Presets…"), + todo("PDF Presets…"), cp("Perspective Grid Presets…", "perspective.grid.preset", json!({"kind": 2})), Sep, c("Color Settings…", "edit.colorSettings"), @@ -1218,10 +1227,10 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { ), todo("Variables"), Sep, - sub("Brush Libraries", vec![todo("Arrows"), todo("Artistic"), todo("Borders"), todo("Decorative")]), - sub("Graphic Style Libraries", vec![todo("Additive"), todo("Artistic Effects"), todo("Buttons and Rollovers")]), - sub("Swatch Libraries", vec![todo("Art History"), todo("Celebration"), todo("Color Properties"), todo("Nature"), todo("Web")]), - sub("Symbol Libraries", vec![todo("Arrows"), todo("Charts"), todo("Web Buttons and Bars")]), + sub("Brush Libraries", library_placeholders()), + sub("Graphic Style Libraries", library_placeholders()), + sub("Swatch Libraries", library_placeholders()), + sub("Symbol Libraries", library_placeholders()), ], ), ( @@ -1576,7 +1585,7 @@ fn font_items() -> Vec { .collect() } -/// The Effect menu, built from the effects catalogue (Illustrator Effects), plus raster effects. +/// The Effect menu, built from the effects catalogue (vector effects), plus raster effects. fn effect_menu() -> Vec { let cat = vectorcraft_effects::effect_catalog(); let mut out = vec![ @@ -1585,7 +1594,7 @@ fn effect_menu() -> Vec { Sep, c("Document Raster Effects Settings…", "document.rasterEffectsSettings"), Sep, - Item::Header("Illustrator Effects"), + Item::Header("Vector Effects"), ]; // Submenus in Illustrator's order. let order = ["3D and Materials", "Convert to Shape", "Distort & Transform", "Path", "Pathfinder", "Stylize", "SVG Filters", "Warp", "Blur"]; diff --git a/crates/ui-egui/src/panels/gradient.rs b/crates/ui-egui/src/panels/gradient.rs index adc7973a..a789e066 100644 --- a/crates/ui-egui/src/panels/gradient.rs +++ b/crates/ui-egui/src/panels/gradient.rs @@ -157,9 +157,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.horizontal(|ui| { widgets::dim_label(ui, "Stroke:"); for (icon, tip) in [ - ("dc-stroke-center", "Apply gradient within stroke"), - ("dc-stroke-outside", "Apply gradient along stroke (on the roadmap)"), - ("dc-stroke-inside", "Apply gradient across stroke (on the roadmap)"), + ("dc-stroke-center", "Gradient inside stroke"), + ("dc-stroke-outside", "Gradient along stroke (on the roadmap)"), + ("dc-stroke-inside", "Gradient across stroke (on the roadmap)"), ] { widgets::icon_button_enabled(ui, icon, tip, icon == "dc-stroke-center" && !app.session.fill_active, false, 22.0); } diff --git a/crates/ui-egui/src/panels/stroke.rs b/crates/ui-egui/src/panels/stroke.rs index a0c932a5..90b456c5 100644 --- a/crates/ui-egui/src/panels/stroke.rs +++ b/crates/ui-egui/src/panels/stroke.rs @@ -14,8 +14,7 @@ pub const WEIGHT_PRESETS: [f64; 22] = [0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]; /// Width profile presets: (id for `stroke.set`, label). -pub const PROFILES: [(&str, &str); 4] = - [("uniform", "Uniform"), ("lens", "Width Profile 1"), ("taperStart", "Width Profile 4"), ("taperEnd", "Width Profile 5")]; +pub const PROFILES: [(&str, &str); 4] = [("uniform", "Uniform"), ("lens", "Lens"), ("taperStart", "Taper Start"), ("taperEnd", "Taper End")]; /// Which preset a stroke's profile is (None profile = uniform). pub fn profile_id(p: Option<&WidthProfile>) -> &'static str { @@ -165,19 +164,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.add_space((ui.available_width() - 56.0).max(0.0)); let on = dash.is_some(); - if widgets::icon_button_enabled(ui, "dc-dash-exact", "Preserves exact dash and gap lengths", on && !align_corners, on, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "dc-dash-exact", "Exact dash lengths", on && !align_corners, on, 24.0).clicked() { set(app, json!({"dash": dash_pattern(&fields), "alignDashes": false})); } - if widgets::icon_button_enabled( - ui, - "dc-dash-align", - "Aligns dashes to corners and path ends, adjusting lengths to fit", - on && align_corners, - on, - 24.0, - ) - .clicked() - { + if widgets::icon_button_enabled(ui, "dc-dash-align", "Fit dashes to corners and ends", on && align_corners, on, 24.0).clicked() { set(app, json!({"dash": dash_pattern(&fields), "alignDashes": true})); } }); @@ -242,8 +232,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { }); ui.horizontal(|ui| { row_label(ui, "Align:"); - widgets::icon_button_enabled(ui, "dc-cap-square", "Extend arrow tip beyond end of path (on the roadmap)", true, false, 22.0); - widgets::icon_button_enabled(ui, "dc-cap-butt", "Place arrow tip at end of path (on the roadmap)", false, false, 22.0); + widgets::icon_button_enabled(ui, "dc-cap-square", "Tip extends past the end (on the roadmap)", true, false, 22.0); + widgets::icon_button_enabled(ui, "dc-cap-butt", "Tip on the end point (on the roadmap)", false, false, 22.0); }); widgets::divider(ui); // Profile. diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index ab9165de..2320f8f7 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -431,7 +431,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { menu_item(ui, "Spot Colors…", false, false); ui.separator(); menu_item(ui, "Open Swatch Library", false, false); - menu_item(ui, "Export Swatches Library as ASE…", false, false); + menu_item(ui, "Save Swatch Library…", false, false); } #[cfg(test)] diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index 6f9381ca..e145c7de 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -17,8 +17,20 @@ use crate::state::{Dialog, UiState}; use crate::theme::{self, Tokens}; use crate::{VectorcraftApp, menus, widgets}; -pub const PRESETS: &[&str] = &["VectorCraft Defaults", "Illustrator Defaults"]; +pub const PRESETS: &[&str] = &["VectorCraft Defaults", "Classic Defaults"]; pub const CUSTOM: &str = "Custom"; +/// Set names earlier versions saved: (old name, current name). +pub const LEGACY_PRESETS: &[(&str, &str)] = &[("Illustrator Defaults", "Classic Defaults")]; // brand-ok: legacy preference value + +/// The current name of a shortcut set: names earlier versions saved map to today's. +pub fn set_name(name: &str) -> &str { + LEGACY_PRESETS.iter().find(|(old, _)| *old == name).map_or(name, |(_, new)| new) +} + +/// Serde reader for a saved set name (UI preferences written by earlier versions). +pub fn deserialize_set_name<'de, D: serde::Deserializer<'de>>(d: D) -> Result { + ::deserialize(d).map(|s| set_name(&s).to_string()) +} /// Bumped whenever overrides or the workspace list change (the native menu rebuilds on it). pub static GENERATION: AtomicU64 = AtomicU64::new(0); @@ -281,7 +293,7 @@ pub fn reset_one(overrides: &mut BTreeMap, key: &str) { /// Overrides of a preset (both presets are the registry defaults for now). pub fn preset(name: &str) -> Option> { - PRESETS.contains(&name).then(BTreeMap::new) + PRESETS.contains(&set_name(name)).then(BTreeMap::new) } /// Export format: `{"format": "vectorcraft-shortcuts", "set": name, "overrides": {...}}`. @@ -299,7 +311,7 @@ pub fn import_json(v: &Value) -> Result<(String, BTreeMap), Stri } out.insert(k.clone(), normalize(s).unwrap_or_default()); } - Ok((v.get("set").and_then(Value::as_str).unwrap_or(CUSTOM).to_string(), out)) + Ok((v.get("set").and_then(Value::as_str).map_or(CUSTOM, set_name).to_string(), out)) } // ---------- UI commands ---------- @@ -370,7 +382,7 @@ pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option match s("name").as_deref().and_then(|n| preset(n).map(|p| (n.to_string(), p))) { + "shortcuts.preset" => match s("name").as_deref().map(set_name).and_then(|n| preset(n).map(|p| (n.to_string(), p))) { Some((n, p)) => { app.ui.shortcut_overrides = p; app.ui.shortcut_set = n; diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 72c26630..5cdbbcdd 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -229,7 +229,7 @@ pub struct UiState { #[serde(default)] pub shortcut_overrides: std::collections::BTreeMap, /// Name of the shortcut set (preset or "Custom"). - #[serde(default = "default_shortcut_set")] + #[serde(default = "default_shortcut_set", deserialize_with = "crate::shortcut_editor::deserialize_set_name")] pub shortcut_set: String, /// Current workspace (Window → Workspace). #[serde(default = "default_workspace")] diff --git a/crates/ui-egui/src/tests_labels.rs b/crates/ui-egui/src/tests_labels.rs new file mode 100644 index 00000000..f17a0d6a --- /dev/null +++ b/crates/ui-egui/src/tests_labels.rs @@ -0,0 +1,107 @@ +//! Neutral wording in menus and panels: what users read names VectorCraft's own features. + +use egui::epaint::Shape; +use serde_json::json; +use vectorcraft_engine::Session; + +use crate::VectorcraftApp; +use crate::menus::{self, Item}; + +fn app() -> VectorcraftApp { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + app +} + +/// Every string painted by `f` in one headless frame, one per line. +fn painted_text(app: &mut VectorcraftApp, mut f: impl FnMut(&mut VectorcraftApp, &mut egui::Ui)) -> String { + fn collect(s: &Shape, out: &mut String) { + match s { + Shape::Text(t) => { + out.push_str(t.galley.text()); + out.push('\n'); + } + Shape::Vec(v) => v.iter().for_each(|s| collect(s, out)), + _ => {} + } + } + let ctx = egui::Context::default(); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| f(app, ui)); + out.textures_delta.clear(); + let mut text = String::new(); + for c in &out.shapes { + collect(&c.shape, &mut text); + } + text +} + +#[test] +fn effect_menu_has_a_vector_effects_section() { + let tree = menus::menu_tree(); + let (_, effect) = tree.iter().find(|(t, _)| *t == "Effect").expect("Effect menu"); + assert!(effect.iter().any(|i| matches!(i, Item::Header("Vector Effects")))); +} + +#[test] +fn library_submenus_are_disabled_placeholders() { + let app = app(); + let entries = menus::menu_entries(&app); + for lib in ["Brush Libraries", "Graphic Style Libraries", "Swatch Libraries", "Symbol Libraries"] { + let items: Vec<_> = entries.iter().filter(|e| e.path == ["Window", lib]).collect(); + let labels: Vec<&str> = items.iter().map(|e| e.label.as_str()).collect(); + assert_eq!(labels, ["Built-in Libraries", "User Defined", "Other Library…"], "{lib}"); + assert!(items.iter().all(|e| !e.enabled && e.command.is_none()), "{lib}"); + } +} + +#[test] +fn stroke_profiles_have_descriptive_names() { + let labels: Vec<&str> = crate::panels::stroke::PROFILES.iter().map(|p| p.1).collect(); + assert_eq!(labels, ["Uniform", "Lens", "Taper Start", "Taper End"]); +} + +#[test] +fn swatches_menu_offers_save_swatch_library() { + let mut app = app(); + let text = painted_text(&mut app, crate::panels::swatches::menu); + assert!(text.contains("Save Swatch Library…"), "{text}"); + assert!(!text.contains("ASE"), "{text}"); +} + +#[test] +fn edit_menu_lists_pdf_presets() { + let app = app(); + assert!(menus::menu_entries(&app).iter().any(|e| e.path == ["Edit"] && e.label == "PDF Presets…" && !e.enabled)); +} + +#[test] +fn clipboard_preferences_name_the_legacy_format_neutrally() { + let label = |key: &str| vectorcraft_engine::cmd::prefscmds::PREF_SPECS.iter().find(|p| p.key == key).unwrap().label; + assert_eq!(label("copyAicb"), "Legacy vector clipboard (no transparency)"); + assert_eq!(label("aicbMode"), "Legacy vector clipboard"); +} + +#[test] +fn shortcut_set_old_name_is_accepted() { + use crate::shortcut_editor::{PRESETS, import_json}; + let (old, _) = crate::shortcut_editor::LEGACY_PRESETS[0]; + assert_eq!(PRESETS, ["VectorCraft Defaults", "Classic Defaults"]); + // UI preferences saved by an earlier version load (and save again) under the current name. + let ui: crate::state::UiState = serde_json::from_value(json!({ "shortcut_set": old })).unwrap(); + assert_eq!(ui.shortcut_set, "Classic Defaults"); + let saved = serde_json::to_value(&ui).unwrap(); + assert_eq!(saved["shortcut_set"], "Classic Defaults"); + let back: crate::state::UiState = serde_json::from_value(saved).unwrap(); + assert_eq!(back.shortcut_set, "Classic Defaults"); + let default: crate::state::UiState = serde_json::from_value(json!({})).unwrap(); + assert_eq!(default.shortcut_set, PRESETS[0]); + // The command and imported sets take the old name too. + let mut app = app(); + app.run("shortcuts.preset", json!({ "name": old })).unwrap(); + assert_eq!(app.ui.shortcut_set, "Classic Defaults"); + app.run("shortcuts.preset", json!({ "name": "VectorCraft Defaults" })).unwrap(); + assert_eq!(app.ui.shortcut_set, "VectorCraft Defaults"); + assert!(app.run("shortcuts.preset", json!({ "name": "Nope" })).is_err()); + let (set, _) = import_json(&json!({ "set": old, "overrides": {} })).unwrap(); + assert_eq!(set, "Classic Defaults"); +} diff --git a/docs/development.md b/docs/development.md index d77f990f..7fe3f9a3 100644 --- a/docs/development.md +++ b/docs/development.md @@ -23,3 +23,7 @@ How the web shell (`apps/vectorcraft-web/src/web.rs`) differs from desktop: - **Drag-and-drop:** `WebShell` takes the frame's `dropped_files` before the app sees them, reads each with `DroppedFile::bytes_async` and pushes the bytes into the inbox. (The app's synchronous drop path is compiled out on wasm32.) - **No control server:** browsers can't listen on TCP. To automate the web build, drive headless Chrome with `--remote-debugging-port`. - Quick smoke test: `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --enable-unsafe-webgpu --screenshot=web.png --window-size=1440,900 --virtual-time-budget=15000 http://127.0.0.1:8766/` (headless Chrome on macOS gets a real WebGPU adapter). + +## Vendor names gate + +`cargo xtask brands` (part of `cargo xtask ci`) fails when user-visible text names another vendor's products or company: string literals in Rust sources (command labels and params docs, menus, panels, MCP tool definitions), `Cargo.toml` descriptions and every text file under `docs/` and `packaging/`. Comments and test code are not checked. Say "the reference app" or name the feature itself. A line that must keep an old name, such as an alias that files or preferences from earlier versions still use, carries a `brand-ok` comment. diff --git a/docs/mcp.md b/docs/mcp.md index 305924c6..56c83df2 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -64,7 +64,7 @@ or a full `paint.setFill` params object (`{"gradient": …}`, `{"swatch": "name" | `apply_effect` | `{effect?, params?, ids?}` | Appends a live effect. Without `effect`, returns the effect catalogue with parameters and defaults. | | `pathfinder` | `{operation, ids?}` | `unite`, `minusFront`, `intersect`, `exclude`, `divide`, `trim`, `merge`, `crop`, `outline`, `minusBack`. For a live version, apply the `pathfinder.*` effect to a group. | | `transform` | `{ids?, dx?, dy?, rotate?, scale?, scaleX?, scaleY?, reflect?, shear?, origin?, copy?}` | Runs move, rotate, scale, reflect, shear in that order. With `copy`, the first step duplicates. | -| `create_graph` | `{type?, x, y, width, height, csv? \| series?, categories?, rows?}` | The nine Illustrator graph types. Edit later with `graph.setData` / `graph.setType` via `run_command`. | +| `create_graph` | `{type?, x, y, width, height, csv? \| series?, categories?, rows?}` | Nine graph types: column, stacked column, bar, stacked bar, line, area, scatter, pie and radar. Edit later with `graph.setData` / `graph.setType` via `run_command`. | | `text_wrap` | `{ids?, offset?, invert?, release?}` | Area type below the objects (same layer) flows around them. | | `undo` / `redo` | `{}` | | diff --git a/packaging/linux/ai.storyteller.vectorcraft.desktop b/packaging/linux/ai.storyteller.vectorcraft.desktop index 82999eb0..af345530 100644 --- a/packaging/linux/ai.storyteller.vectorcraft.desktop +++ b/packaging/linux/ai.storyteller.vectorcraft.desktop @@ -10,5 +10,5 @@ Terminal=false StartupNotify=true StartupWMClass=ai.storyteller.vectorcraft Categories=Graphics;2DGraphics;VectorGraphics; -Keywords=vector;illustration;svg;pdf;drawing;bezier;illustrator; +Keywords=vector;illustration;svg;pdf;drawing;bezier; MimeType=image/svg+xml;application/pdf; diff --git a/packaging/linux/ai.storyteller.vectorcraft.metainfo.xml.in b/packaging/linux/ai.storyteller.vectorcraft.metainfo.xml.in index ade490d3..a19681a9 100644 --- a/packaging/linux/ai.storyteller.vectorcraft.metainfo.xml.in +++ b/packaging/linux/ai.storyteller.vectorcraft.metainfo.xml.in @@ -12,7 +12,9 @@

- VectorCraft is a vector graphics editor. Draw and edit vector illustrations, SVG and PDF + VectorCraft is an open-source vector illustration app. Draw with pen, shape and brush tools, + style art with fills, strokes, gradients and live effects, lay out artboards and type, and + open and save SVG and PDF.

ai.storyteller.vectorcraft.desktop @@ -24,19 +26,21 @@ https://discord.gg/artcraft Graphics - RasterGraphics - Photography + 2DGraphics + VectorGraphics - photo - image editor - psd - layers + vector + illustration + svg + pdf + drawing + bezier - Editing The Great Wave off Kanagawa with type layers and adjustment layers - https://raw.githubusercontent.com/storytold/vectorcraft/main/docs/images/vectorcraft-demo.jpg + Editing the Neon Drive poster with a live glow effect in the Appearance panel + https://raw.githubusercontent.com/storytold/vectorcraft/main/docs/images/shot-1-neon.png diff --git a/packaging/linux/nfpm.yaml b/packaging/linux/nfpm.yaml index 572ed6fa..5661d843 100644 --- a/packaging/linux/nfpm.yaml +++ b/packaging/linux/nfpm.yaml @@ -17,9 +17,9 @@ vendor: Learning Machines LLC homepage: https://getartcraft.com/apps/vectorcraft license: MIT OR Apache-2.0 description: |- - Open-source, native image editor that works the way Photoshop users expect. - Layers, masks, adjustment layers, layer styles, type and brushes; opens and - saves layered PSD/PSB files; 8/16/32-bit documents; GPU compositing (wgpu). + Open-source, native vector illustration app. + Pen, shape and brush tools, artboards, type, gradients, symbols and live + effects; opens and saves SVG and PDF. Includes vectorcraft-cli, a headless converter and command runner. # The binaries link only glibc and libgcc_s directly. Windowing (X11/Wayland, xkbcommon) and the diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in index 7df0f93b..c6eda447 100644 --- a/packaging/macos/Info.plist.in +++ b/packaging/macos/Info.plist.in @@ -45,7 +45,7 @@ NSHumanReadableCopyright Copyright © the VectorCraft authors. MIT OR Apache-2.0. - + UTExportedTypeDeclarations @@ -63,48 +63,11 @@ UTTypeTagSpecification public.filename-extension - pcraft + vectorcraftdrawcraft public.mime-type application/x-vectorcraft - - UTImportedTypeDeclarations - - - UTTypeIdentifier - com.adobe.photoshop-large-image - UTTypeDescription - Photoshop Large Document - UTTypeConformsTo - - public.data - public.image - - UTTypeTagSpecification - - public.filename-extension - psb - - - - UTTypeIdentifier - ai.storyteller.vectorcraft.qoi - UTTypeDescription - QOI Image - UTTypeConformsTo - - public.data - public.image - - UTTypeTagSpecification - - public.filename-extension - qoi - - - - diff --git a/packaging/web/README.md b/packaging/web/README.md index b39ec0e9..bfb81ca3 100644 --- a/packaging/web/README.md +++ b/packaging/web/README.md @@ -55,7 +55,7 @@ Local test: `python3 -m http.server 8765` inside the folder, then open http://lo ```html