mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
F: Load/Save Swatches (.ase) — swatch.load, swatch.save, Swatches panel menu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9dd44f63cf
commit
67dd4852ca
10 files changed
+326
-7
No files matched your search
@@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- Load Swatches / Save Swatches for Exchange (.ase: CMYK, RGB, Lab, Gray; spot or process) from the Swatches panel menu; `swatch.load` / `swatch.save`.
|
||||
- Pathfinder (Object › Pathfinder and a Pathfinder panel): Add, Subtract, Intersect, Exclude Overlap, Minus Back; curves stay curves (flo_curves); `object.pathfinder`.
|
||||
- Make / Release Compound Path (⌘8; nested paths become holes), Create Outlines (⇧⌘O; one path per text colour), Scissors tool (C) with `path.split`.
|
||||
- Transform Again / Individually / Sequence Again (⌥⌘3; a moved copy repeats like Step and Repeat), Clear Transformations; Layers: Merge, Delete Unused, Hide/Lock Others, Show/Unlock All; Break Link to Style (paragraph and character).
|
||||
|
||||
@@ -98,7 +98,9 @@ fn services(inbox: Inbox, ctx: egui::Context) -> Services {
|
||||
open_async: Some(Box::new(move |purpose: &str| {
|
||||
let inbox = open_inbox.clone();
|
||||
let ctx = ctx.clone();
|
||||
let dialog = if purpose == "place" {
|
||||
let dialog = if purpose == "swatches" {
|
||||
rfd::AsyncFileDialog::new().add_filter("Swatch Exchange (ASE)", &["ase"])
|
||||
} else if purpose == "place" {
|
||||
rfd::AsyncFileDialog::new().add_filter("Graphics", IMAGE_EXTS)
|
||||
} else {
|
||||
rfd::AsyncFileDialog::new().add_filter("DesignCraft", DOC_EXTS)
|
||||
|
||||
@@ -81,7 +81,9 @@ fn services() -> Services {
|
||||
Services {
|
||||
pick_open: Some(Box::new(|purpose: &str| {
|
||||
let d = rfd::FileDialog::new();
|
||||
let d = if purpose == "place" {
|
||||
let d = if purpose == "swatches" {
|
||||
d.add_filter("Swatch Exchange (ASE)", &["ase"])
|
||||
} else if purpose == "place" {
|
||||
d.add_filter(
|
||||
"Graphics and text",
|
||||
&["png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "psd", "svg", "pdf", "ai", "txt", "docx", "rtf", "md"],
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
//! Swatch exchange files (`.ase`): read and write colour swatches so they can move between
|
||||
//! documents and applications. Big-endian: a signature, version 1.0, a block count, then blocks —
|
||||
//! group start/end and colour entries (UTF-16 name, colour model, f32 values, colour type).
|
||||
|
||||
use crate::swatch::{ColorType, Swatch, SwatchValue};
|
||||
use crate::{Color, cms};
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
pub enum AseError {
|
||||
#[error("not a swatch exchange file")]
|
||||
Signature,
|
||||
#[error("the file ends early")]
|
||||
Truncated,
|
||||
}
|
||||
|
||||
const COLOR: u16 = 0x0001;
|
||||
#[cfg(test)]
|
||||
const GROUP_START: u16 = 0xC001;
|
||||
#[cfg(test)]
|
||||
const GROUP_END: u16 = 0xC002;
|
||||
|
||||
struct R<'a> {
|
||||
b: &'a [u8],
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl R<'_> {
|
||||
fn take(&mut self, n: usize) -> Result<&[u8], AseError> {
|
||||
let s = self.b.get(self.i..self.i + n).ok_or(AseError::Truncated)?;
|
||||
self.i += n;
|
||||
Ok(s)
|
||||
}
|
||||
fn u16(&mut self) -> Result<u16, AseError> {
|
||||
let s = self.take(2)?;
|
||||
Ok(u16::from_be_bytes([s[0], s[1]]))
|
||||
}
|
||||
fn u32(&mut self) -> Result<u32, AseError> {
|
||||
let s = self.take(4)?;
|
||||
Ok(u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
|
||||
}
|
||||
fn f32(&mut self) -> Result<f32, AseError> {
|
||||
Ok(f32::from_bits(self.u32()?))
|
||||
}
|
||||
fn name(&mut self) -> Result<String, AseError> {
|
||||
let n = self.u16()? as usize;
|
||||
let mut units = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
units.push(self.u16()?);
|
||||
}
|
||||
while units.last() == Some(&0) {
|
||||
units.pop();
|
||||
}
|
||||
Ok(String::from_utf16_lossy(&units))
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour swatches in an `.ase` file (groups are flattened).
|
||||
pub fn read(bytes: &[u8]) -> Result<Vec<Swatch>, AseError> {
|
||||
let mut r = R { b: bytes, i: 0 };
|
||||
if r.take(4)? != b"ASEF" {
|
||||
return Err(AseError::Signature);
|
||||
}
|
||||
r.take(4)?; // version
|
||||
let blocks = r.u32()?;
|
||||
let mut out = Vec::new();
|
||||
for _ in 0..blocks {
|
||||
let kind = r.u16()?;
|
||||
let len = r.u32()? as usize;
|
||||
let body = r.take(len)?;
|
||||
// Group start/end blocks are skipped: groups are flattened.
|
||||
if kind != COLOR {
|
||||
continue;
|
||||
}
|
||||
let mut b = R { b: body, i: 0 };
|
||||
let name = b.name()?;
|
||||
let model = b.take(4)?.to_vec();
|
||||
let color = match &model[..] {
|
||||
b"CMYK" => Color::Cmyk { c: b.f32()?, m: b.f32()?, y: b.f32()?, k: b.f32()? },
|
||||
b"RGB " => Color::Rgb { r: b.f32()?, g: b.f32()?, b: b.f32()? },
|
||||
b"Gray" => Color::Gray { k: 1.0 - b.f32()? },
|
||||
b"LAB " => {
|
||||
let (l, a, bb) = (b.f32()?, b.f32()?, b.f32()?);
|
||||
let [r, g, bl] = cms::lab::lab_to_srgb(cms::Lab { l: l * 100.0, a, b: bb });
|
||||
Color::Rgb { r: r.clamp(0.0, 1.0), g: g.clamp(0.0, 1.0), b: bl.clamp(0.0, 1.0) }
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
let color_type = if b.u16().unwrap_or(2) == 1 { ColorType::Spot } else { ColorType::Process };
|
||||
out.push(Swatch { name, value: SwatchValue::Color { color, color_type }, locked: false, named: true, hidden: false });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// An `.ase` file of the colour swatches (special swatches, tints and gradients are left out).
|
||||
pub fn write(swatches: &[Swatch]) -> Vec<u8> {
|
||||
let mut blocks: Vec<Vec<u8>> = Vec::new();
|
||||
for s in swatches {
|
||||
let SwatchValue::Color { color, color_type } = &s.value else { continue };
|
||||
if s.locked || s.hidden {
|
||||
continue;
|
||||
}
|
||||
let mut b = Vec::new();
|
||||
let units: Vec<u16> = s.name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
b.extend((units.len() as u16).to_be_bytes());
|
||||
for u in units {
|
||||
b.extend(u.to_be_bytes());
|
||||
}
|
||||
let (model, vals): (&[u8; 4], Vec<f32>) = match *color {
|
||||
Color::Cmyk { c, m, y, k } => (b"CMYK", vec![c, m, y, k]),
|
||||
Color::Rgb { r, g, b } => (b"RGB ", vec![r, g, b]),
|
||||
Color::Gray { k } => (b"Gray", vec![1.0 - k]),
|
||||
};
|
||||
b.extend(model);
|
||||
for v in vals {
|
||||
b.extend(v.to_bits().to_be_bytes());
|
||||
}
|
||||
b.extend((if *color_type == ColorType::Spot { 1u16 } else { 2 }).to_be_bytes());
|
||||
blocks.push(b);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
out.extend(b"ASEF");
|
||||
out.extend(1u16.to_be_bytes());
|
||||
out.extend(0u16.to_be_bytes());
|
||||
out.extend((blocks.len() as u32).to_be_bytes());
|
||||
for b in blocks {
|
||||
out.extend(COLOR.to_be_bytes());
|
||||
out.extend((b.len() as u32).to_be_bytes());
|
||||
out.extend(b);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip_and_lab() {
|
||||
let sw = vec![
|
||||
Swatch {
|
||||
name: "Sky".into(),
|
||||
value: SwatchValue::Color { color: Color::Rgb { r: 0.2, g: 0.5, b: 1.0 }, color_type: ColorType::Process },
|
||||
locked: false,
|
||||
named: true,
|
||||
hidden: false,
|
||||
},
|
||||
Swatch {
|
||||
name: "PANTONE-ish 1".into(),
|
||||
value: SwatchValue::Color { color: Color::Cmyk { c: 0.0, m: 0.5, y: 1.0, k: 0.0 }, color_type: ColorType::Spot },
|
||||
locked: false,
|
||||
named: true,
|
||||
hidden: false,
|
||||
},
|
||||
Swatch { name: "[Paper]".into(), value: SwatchValue::Paper { color: Color::WHITE }, locked: true, named: true, hidden: false },
|
||||
];
|
||||
let bytes = write(&sw);
|
||||
let back = read(&bytes).unwrap();
|
||||
assert_eq!(back.len(), 2);
|
||||
assert_eq!(back[0].name, "Sky");
|
||||
assert_eq!(back[0].value, sw[0].value);
|
||||
assert_eq!(back[1].value, sw[1].value);
|
||||
assert_eq!(read(b"nope"), Err(AseError::Signature));
|
||||
assert_eq!(read(&bytes[..bytes.len() - 3]), Err(AseError::Truncated));
|
||||
// A group around a Lab colour.
|
||||
let mut f = Vec::new();
|
||||
f.extend(b"ASEF\x00\x01\x00\x00");
|
||||
f.extend(3u32.to_be_bytes());
|
||||
let mut g = Vec::new();
|
||||
g.extend(2u16.to_be_bytes());
|
||||
g.extend([0, b'G', 0, 0]);
|
||||
f.extend(GROUP_START.to_be_bytes());
|
||||
f.extend((g.len() as u32).to_be_bytes());
|
||||
f.extend(&g);
|
||||
let mut c = Vec::new();
|
||||
c.extend(2u16.to_be_bytes());
|
||||
c.extend([0, b'W', 0, 0]);
|
||||
c.extend(b"LAB ");
|
||||
for v in [1.0f32, 0.0, 0.0] {
|
||||
c.extend(v.to_bits().to_be_bytes());
|
||||
}
|
||||
c.extend(2u16.to_be_bytes());
|
||||
f.extend(COLOR.to_be_bytes());
|
||||
f.extend((c.len() as u32).to_be_bytes());
|
||||
f.extend(&c);
|
||||
f.extend(GROUP_END.to_be_bytes());
|
||||
f.extend(0u32.to_be_bytes());
|
||||
let w = read(&f).unwrap();
|
||||
assert_eq!(w.len(), 1);
|
||||
assert_eq!(w[0].name, "W");
|
||||
let SwatchValue::Color { color, .. } = &w[0].value else { panic!() };
|
||||
assert!(color.to_rgb().iter().all(|v| *v > 0.98), "Lab white: {color:?}");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
//! documents don't drift when converting back and forth. Rendering asks for [`Color::to_rgba`].
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod ase;
|
||||
pub mod blend;
|
||||
pub mod cms;
|
||||
pub mod gradient;
|
||||
|
||||
@@ -60,6 +60,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
super::has_selection,
|
||||
apply_gradient
|
||||
),
|
||||
cmd!(
|
||||
"swatch.load",
|
||||
"Load Swatches…",
|
||||
[],
|
||||
None,
|
||||
"{path?|base64?, replace?: bool} — colour swatches from a swatch exchange (.ase) file; names already in the document are kept unless `replace` → {added, replaced}",
|
||||
has_doc,
|
||||
load_swatches
|
||||
),
|
||||
cmd!(
|
||||
noundo "swatch.save",
|
||||
"Save Swatches for Exchange…",
|
||||
[],
|
||||
None,
|
||||
"{path?, names?: [swatch names] (default: all colour swatches)} → {base64} or writes `path` (.ase)",
|
||||
has_doc,
|
||||
save_swatches
|
||||
),
|
||||
cmd!(
|
||||
"object.color",
|
||||
"Apply Color",
|
||||
@@ -389,6 +407,51 @@ fn value_name(color: designcraft_color::Color) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_swatches(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let bytes = match (str_param(p, "path"), str_param(p, "base64")) {
|
||||
(_, Some(b)) => super::file::base64_decode(b),
|
||||
(Some(path), None) => std::fs::read(path).map_err(|e| bad("swatch.load", format!("{path}: {e}")))?,
|
||||
_ => return Err(bad("swatch.load", "give `path` or `base64`")),
|
||||
};
|
||||
let incoming = designcraft_color::ase::read(&bytes).map_err(|e| bad("swatch.load", e.to_string()))?;
|
||||
let replace = p.get("replace").and_then(Value::as_bool).unwrap_or(false);
|
||||
s.edit(|d, _| {
|
||||
let (mut added, mut replaced) = (0, 0);
|
||||
for w in incoming {
|
||||
match d.swatches.iter_mut().find(|x| x.name == w.name) {
|
||||
Some(x) if x.locked => {}
|
||||
Some(x) => {
|
||||
if replace {
|
||||
x.value = w.value;
|
||||
x.hidden = false;
|
||||
replaced += 1;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
d.swatches.push(w);
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(json!({"added": added, "replaced": replaced}))
|
||||
})
|
||||
}
|
||||
|
||||
fn save_swatches(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let names: Option<Vec<String>> =
|
||||
p.get("names").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect());
|
||||
let chosen: Vec<Swatch> = d.swatches.iter().filter(|w| names.as_ref().is_none_or(|n| n.contains(&w.name))).cloned().collect();
|
||||
let bytes = designcraft_color::ase::write(&chosen);
|
||||
match str_param(p, "path") {
|
||||
Some(path) => {
|
||||
std::fs::write(path, &bytes).map_err(|e| bad("swatch.save", format!("{path}: {e}")))?;
|
||||
Ok(json!({"path": path, "bytes": bytes.len()}))
|
||||
}
|
||||
None => Ok(json!({"base64": super::file::base64_encode(&bytes)})),
|
||||
}
|
||||
}
|
||||
|
||||
fn gradient_json(name: &str, g: &designcraft_color::Gradient) -> Value {
|
||||
let stops: Vec<Value> = g
|
||||
.stops
|
||||
@@ -580,6 +643,21 @@ fn rename_in(it: &mut designcraft_doc::Item, from: &str, to: &str) {
|
||||
mod color_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn swatches_save_and_load_ase() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("swatch.create", &json!({"name": "Brand Teal", "color": "#108080"})).unwrap();
|
||||
let b64 = s.execute("swatch.save", &json!({"names": ["Brand Teal"]})).unwrap()["base64"].as_str().unwrap().to_string();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("swatch.load", &json!({"base64": b64})).unwrap();
|
||||
assert_eq!(r["added"], 1);
|
||||
assert!(s.doc().unwrap().doc.swatch("Brand Teal").is_some());
|
||||
let r = s.execute("swatch.load", &json!({"base64": b64})).unwrap();
|
||||
assert_eq!((r["added"].as_u64(), r["replaced"].as_u64()), (Some(0), Some(0)));
|
||||
assert!(s.execute("swatch.load", &json!({"base64": "AAAA"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed_colors_then_add_to_swatches() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -359,7 +359,9 @@ impl DesignApp {
|
||||
for (name, bytes) in files {
|
||||
let b64 = designcraft_engine::cmd::base64_encode(&bytes);
|
||||
let lower = name.to_ascii_lowercase();
|
||||
let r = if lower.ends_with(".designcraft") || lower.ends_with(".idml") {
|
||||
let r = if lower.ends_with(".ase") {
|
||||
self.run("swatch.load", json!({"base64": b64}))
|
||||
} else if lower.ends_with(".designcraft") || lower.ends_with(".idml") {
|
||||
let title = name.rsplit_once('.').map_or(name.as_str(), |(stem, _)| stem);
|
||||
self.run("file.openBytes", json!({"name": title, "base64": b64}))
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,8 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
||||
("app.saveDialog", "Save As…", Some("Cmd+Shift+S"), "{}"),
|
||||
("app.save", "Save", Some("Cmd+S"), "{}"),
|
||||
("app.saveCopyDialog", "Save a Copy…", Some("Cmd+Alt+S"), "{} — choose where to write a copy (the document stays as it is)"),
|
||||
("app.loadSwatches", "Load Swatches…", None, "{} — pick a swatch exchange (.ase) file"),
|
||||
("app.saveSwatches", "Save Swatches for Exchange…", None, "{path?} — write the colour swatches as .ase"),
|
||||
("app.exportPng", "Export Page as PNG…", Some("Cmd+E"), "{}"),
|
||||
("app.exportIdml", "Export IDML…", None, "{path?} — InDesign Markup (IDML) package"),
|
||||
("app.exportPdf", "Export PDF…", None, "{path?, …file.exportPdf options} — asks for a path when none is given"),
|
||||
@@ -472,6 +474,34 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
}
|
||||
"app.exportPng" => export_png(app, p),
|
||||
"app.exportIdml" => export_idml(app, p),
|
||||
"app.loadSwatches" => {
|
||||
if let Some(pick) = app.services.pick_open.as_mut() {
|
||||
return Some(match pick("swatches") {
|
||||
Some(path) => app.run("swatch.load", json!({"path": path})),
|
||||
None => Ok(Value::Null),
|
||||
});
|
||||
}
|
||||
if let Some(open) = app.services.open_async.as_mut() {
|
||||
open("swatches");
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"app.saveSwatches" => {
|
||||
let path = match p.get("path").and_then(Value::as_str) {
|
||||
Some(s) => Some(s.to_string()),
|
||||
None => app.services.pick_save.as_mut().and_then(|f| f("Swatches.ase")),
|
||||
};
|
||||
let Some(path) = path else { return Some(Ok(Value::Null)) };
|
||||
let r = match app.run("swatch.save", json!({})) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
let bytes = designcraft_engine::cmd::base64_decode(r["base64"].as_str().unwrap_or_default());
|
||||
match app.services.write.as_mut() {
|
||||
Some(w) => w(&path, &bytes).map(|_| json!({"path": path, "bytes": bytes.len()})),
|
||||
None => Err("no writer".into()),
|
||||
}
|
||||
}
|
||||
"app.storyEditor" => {
|
||||
let sid = p.get("story").and_then(Value::as_u64).map(designcraft_doc::StoryId).or_else(|| {
|
||||
let st = app.session.active()?;
|
||||
|
||||
@@ -66,6 +66,16 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if icons::button(ui, "plus", 20.0, false, "New Swatch").clicked() {
|
||||
let _ = app.run("swatch.create", json!({"color": {"c": 0, "m": 50, "y": 100, "k": 0}}));
|
||||
}
|
||||
ui.menu_button("☰", |ui| {
|
||||
if ui.button("Load Swatches…").clicked() {
|
||||
let _ = app.run("app.loadSwatches", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
if ui.button("Save Swatches for Exchange…").clicked() {
|
||||
let _ = app.run("app.saveSwatches", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+4
-4
@@ -5,12 +5,12 @@ The summary is generated by `cargo xtask parity` from the table at the bottom of
|
||||
Each row of the InDesign feature catalogue is scored **D** (done), **P** (partial: works but lacks options or UI) or **M** (missing), weighted by priority (**P0** core = 3, **P1** = 2, **P2** = 1). Out-of-scope rows (Adobe cloud services, plug-ins, Typekit, Flash) are excluded.
|
||||
|
||||
<!-- SUMMARY -->
|
||||
**Weighted breadth parity: 73%** over 275 features.
|
||||
**Weighted breadth parity: 74%** over 275 features.
|
||||
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 43 | 23 | 31 | 56% |
|
||||
| P1 | 97 | 44 | 23 | 30 | 57% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
@@ -20,7 +20,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Layers | 95% |
|
||||
| Frames, shapes & paths | 85% |
|
||||
| Transform | 93% |
|
||||
| Fill, stroke, colour | 69% |
|
||||
| Fill, stroke, colour | 74% |
|
||||
| Effects & transparency | 50% |
|
||||
| Placing & links | 66% |
|
||||
| Type & text frames | 80% |
|
||||
@@ -148,7 +148,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Fill, stroke, colour | Special swatches | P0 | D |
|
||||
| Fill, stroke, colour | Spot colours | P1 | D |
|
||||
| Fill, stroke, colour | Ink Manager | P2 | M |
|
||||
| Fill, stroke, colour | Load/Save Swatches (.ase) | P1 | M |
|
||||
| Fill, stroke, colour | Load/Save Swatches (.ase) | P1 | D |
|
||||
| Fill, stroke, colour | Color groups | P2 | M |
|
||||
| Fill, stroke, colour | Unnamed colour to swatch | P1 | D |
|
||||
| Fill, stroke, colour | Stroke panel | P0 | D |
|
||||
|
||||
Reference in new issue
Block a user