mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
M4.20: Document Setup with bleed, transparency grid, paper and type options
File > Document Setup is a tabbed dialog over one engine command, document.setup (no params: a query; any keys: one undo step; document.setUnits stays as the units alias). - General: units, bleed (four sides with a link toggle), Edit Artboards, Show Images in Outline Mode (greyscale pixels under the frame), Highlight Substituted Fonts/Glyphs (pink boxes on screen only). - Transparency: grid size and colour presets or custom colours, Simulate Colored Paper, the flattener preset (built-in or saved) and Discard White Overprint (Overprint Preview keeps white overprints visible). - Type: language with its quotes, Use Typographer's Quotes for the Type tool and Smart Punctuation, superscript/subscript size and position and small caps size (Character panel position/smallCaps; changing the setup restyles the text in the same undo step), text export (SVG text as text or outlines by default). - The canvas draws a red bleed outline and the grid in the document's colours and cell size as one textured quad, also in rotated views; a white Background Contents exports raster formats opaque. Model: Document.setup (DocSetup, defaulted, skipped when default) and CharStyle position/small_caps; old files load unchanged. tests_transparencygrid's helper now finds the grid by its texture (the grid is one textured mesh instead of per-cell rectangles). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
25df58e722
commit
d527349f5a
@@ -0,0 +1,295 @@
|
||||
//! File → Document Setup (`document.setup`): units, bleed, the transparency grid and simulated
|
||||
//! paper, output options and the document's type options.
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::setup::{DEFAULT_FLATTENER, GRID_COLOR_PRESETS, LANGUAGES, MAX_BLEED};
|
||||
use vectorcraft_doc::{Background, CharPosition, CharStyle, DocSetup, Document, ExportText, GridSize, NodeKind, Quotes, ScriptMetrics};
|
||||
|
||||
use super::flatten::{FlattenOptions, FlattenerPreset};
|
||||
|
||||
use super::*;
|
||||
|
||||
const C: &str = "document.setup";
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
"document.setup",
|
||||
"Document Setup",
|
||||
[],
|
||||
None,
|
||||
"{units?: \"Points\"|\"Inches\"|\"Millimeters\"|…, bleed?: pt (all sides)|[top, bottom, left, right]|{top?, bottom?, left?, right?} (0–72 pt), outlineImages?: bool (show images in Outline mode), highlightSubstitutedFonts?: bool, highlightSubstitutedGlyphs?: bool, gridSize?: \"small\"|\"medium\"|\"large\", gridColors?: \"Light\"|\"Medium\"|\"Dark\"|\"Red\"|\"Orange\"|\"Green\"|\"Blue\"|\"Purple\"|[colour, colour] (transparency grid; the first is also the paper colour), simulatePaper?: bool, flattenerPreset?: \"High Resolution\"|\"Medium Resolution\"|\"Low Resolution\"|a saved preset (flattener.presets.list), discardWhiteOverprint?: bool, language?: \"English: USA\"|\"German\"|\"French\"|… (also picks its quotes), quotes?: {double?: \"“”\", single?: \"‘’\"} (beside a language: only if they differ from the current ones), typographersQuotes?: bool (typed straight quotes become the document's quotes), superscript?: {size?: %, position?: %}, subscript?: {size?: %, position?: %}, smallCapsSize?: %, exportText?: \"editable\"|\"appearance\" (SVG text as text or as outlines), backgroundContents?: \"transparent\"|\"white\" (white: raster exports are white behind the art)} change the document setup in one undo step; no params → the current setup",
|
||||
has_doc,
|
||||
setup
|
||||
)]
|
||||
}
|
||||
|
||||
/// The setup as `document.setup` reports it (and accepts it back).
|
||||
pub(crate) fn setup_json(d: &Document) -> Value {
|
||||
let s = &d.setup;
|
||||
let pair = |p: [char; 2]| p.iter().collect::<String>();
|
||||
json!({
|
||||
"units": d.units.label(),
|
||||
"bleed": s.bleed,
|
||||
"outlineImages": s.outline_images,
|
||||
"highlightSubstitutedFonts": s.highlight_substituted_fonts,
|
||||
"highlightSubstitutedGlyphs": s.highlight_substituted_glyphs,
|
||||
"gridSize": s.grid_size.id(),
|
||||
"gridColors": s.grid_colors.map(|c| c.to_hex()),
|
||||
"gridColorsName": s.grid_colors_name(),
|
||||
"simulatePaper": s.simulate_paper,
|
||||
"flattenerPreset": s.flattener(),
|
||||
"discardWhiteOverprint": s.discard_white_overprint,
|
||||
"language": s.language,
|
||||
"quotes": {"double": pair(s.quotes.double), "single": pair(s.quotes.single)},
|
||||
"typographersQuotes": s.typographers_quotes,
|
||||
"superscript": s.superscript,
|
||||
"subscript": s.subscript,
|
||||
"smallCapsSize": s.small_caps_size,
|
||||
"exportText": s.export_text.id(),
|
||||
"backgroundContents": s.background.id(),
|
||||
})
|
||||
}
|
||||
|
||||
fn setup(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let empty = Map::new();
|
||||
let o = match p {
|
||||
Value::Null => &empty,
|
||||
Value::Object(o) => o,
|
||||
_ => return Err(bad(C, "params must be an object")),
|
||||
};
|
||||
let st = s.doc()?;
|
||||
if o.is_empty() {
|
||||
return Ok(setup_json(&st.doc));
|
||||
}
|
||||
let mut new = st.doc.setup.clone();
|
||||
let mut units = st.doc.units;
|
||||
// The language goes first: it picks its quotes, which `quotes` may then override.
|
||||
let current = st.doc.setup.quotes;
|
||||
for (k, v) in o.iter().filter(|(k, _)| *k == "language").chain(o.iter().filter(|(k, _)| *k != "language")) {
|
||||
// With a language, unchanged quotes (a report fed back) don't undo the language's quotes.
|
||||
if k == "quotes" && o.contains_key("language") && quotes_param(v, current)? == current {
|
||||
continue;
|
||||
}
|
||||
apply(&mut new, &mut units, k, v, &s.prefs.flattener_presets)?;
|
||||
}
|
||||
let st = s.doc()?;
|
||||
if new == st.doc.setup && units == st.doc.units {
|
||||
return Ok(setup_json(&st.doc));
|
||||
}
|
||||
s.edit("Document Setup", |d, _| {
|
||||
let scripts_changed =
|
||||
(new.superscript, new.subscript, new.small_caps_size) != (d.setup.superscript, d.setup.subscript, d.setup.small_caps_size);
|
||||
d.setup = new;
|
||||
d.units = units;
|
||||
if scripts_changed {
|
||||
restamp_scripts(d);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(setup_json(&s.doc()?.doc))
|
||||
}
|
||||
|
||||
fn flag(k: &str, v: &Value) -> Result<bool> {
|
||||
v.as_bool().ok_or_else(|| bad(C, format!("{k} must be true or false")))
|
||||
}
|
||||
|
||||
fn percent(k: &str, v: &Value, min: f64, max: f64) -> Result<f64> {
|
||||
v.as_f64().filter(|x| (min..=max).contains(x)).ok_or_else(|| bad(C, format!("{k} must be a number from {min} to {max}")))
|
||||
}
|
||||
|
||||
fn apply(s: &mut DocSetup, units: &mut vectorcraft_doc::Unit, k: &str, v: &Value, saved: &[FlattenerPreset]) -> Result<()> {
|
||||
match k {
|
||||
"units" => *units = v.as_str().and_then(vectorcraft_doc::Unit::named).ok_or_else(|| bad(C, "unknown units"))?,
|
||||
"bleed" => s.bleed = bleed_param(v, s.bleed, C)?,
|
||||
"outlineImages" => s.outline_images = flag(k, v)?,
|
||||
"highlightSubstitutedFonts" => s.highlight_substituted_fonts = flag(k, v)?,
|
||||
"highlightSubstitutedGlyphs" => s.highlight_substituted_glyphs = flag(k, v)?,
|
||||
"gridSize" => s.grid_size = v.as_str().and_then(GridSize::parse).ok_or_else(|| bad(C, "gridSize must be small, medium or large"))?,
|
||||
"gridColors" => s.grid_colors = grid_colors(v)?,
|
||||
"simulatePaper" => s.simulate_paper = flag(k, v)?,
|
||||
"flattenerPreset" => s.flattener_preset = flattener_preset(v, saved)?,
|
||||
"discardWhiteOverprint" => s.discard_white_overprint = flag(k, v)?,
|
||||
"language" => {
|
||||
let l = v.as_str().unwrap_or("");
|
||||
let (name, quotes) = LANGUAGES.iter().find(|(n, _)| n.eq_ignore_ascii_case(l)).ok_or_else(|| {
|
||||
bad(C, format!("unknown language `{l}` (one of {})", LANGUAGES.iter().map(|(n, _)| *n).collect::<Vec<_>>().join(", ")))
|
||||
})?;
|
||||
s.language = name.to_string();
|
||||
s.quotes = *quotes;
|
||||
}
|
||||
"quotes" => s.quotes = quotes_param(v, s.quotes)?,
|
||||
"typographersQuotes" => s.typographers_quotes = flag(k, v)?,
|
||||
"superscript" => s.superscript = script_param(k, v, s.superscript)?,
|
||||
"subscript" => s.subscript = script_param(k, v, s.subscript)?,
|
||||
"smallCapsSize" => s.small_caps_size = percent(k, v, 1.0, 200.0)?,
|
||||
"exportText" => s.export_text = v.as_str().and_then(ExportText::parse).ok_or_else(|| bad(C, "exportText must be editable or appearance"))?,
|
||||
"backgroundContents" => s.background = background_param(v, C)?,
|
||||
// Read-only values `document.setup` reports.
|
||||
"gridColorsName" => {}
|
||||
_ => return Err(bad(C, format!("unknown setting `{k}`"))),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A built-in flattener preset (by name or id) or a saved one, stored by its display name (None:
|
||||
/// the default preset).
|
||||
fn flattener_preset(v: &Value, saved: &[FlattenerPreset]) -> Result<Option<String>> {
|
||||
let name = v.as_str().unwrap_or("").trim();
|
||||
let builtin = FlattenOptions::PRESETS
|
||||
.into_iter()
|
||||
.filter_map(|id| Some((id, FlattenOptions::preset_label(id)?)))
|
||||
.find(|(id, label)| id.eq_ignore_ascii_case(name) || label.eq_ignore_ascii_case(name))
|
||||
.map(|(_, label)| label);
|
||||
let found = builtin.or_else(|| saved.iter().map(|p| p.name.as_str()).find(|n| n.eq_ignore_ascii_case(name))).ok_or_else(|| {
|
||||
bad(
|
||||
C,
|
||||
format!(
|
||||
"unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list)"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Ok((found != DEFAULT_FLATTENER).then(|| found.to_string()))
|
||||
}
|
||||
|
||||
/// `"transparent"` or `"white"`.
|
||||
pub(crate) fn background_param(v: &Value, cmd: &str) -> Result<Background> {
|
||||
v.as_str().and_then(Background::parse).ok_or_else(|| bad(cmd, "backgroundContents must be transparent or white"))
|
||||
}
|
||||
|
||||
/// `pt` (every side), `[top, bottom, left, right]` or `{top?, bottom?, left?, right?}`.
|
||||
pub(crate) fn bleed_param(v: &Value, current: [f64; 4], cmd: &str) -> Result<[f64; 4]> {
|
||||
let side = |x: &Value| x.as_f64().filter(|b| (0.0..=MAX_BLEED).contains(b));
|
||||
let err = || bad(cmd, format!("bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}}"));
|
||||
match v {
|
||||
Value::Number(_) => side(v).map(|b| [b; 4]).ok_or_else(err),
|
||||
Value::Array(a) if a.len() == 4 => {
|
||||
let mut out = [0.0; 4];
|
||||
for (o, x) in out.iter_mut().zip(a) {
|
||||
*o = side(x).ok_or_else(err)?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
Value::Object(o) => {
|
||||
let mut out = current;
|
||||
for (k, x) in o {
|
||||
let i = ["top", "bottom", "left", "right"].iter().position(|s| s == k).ok_or_else(err)?;
|
||||
out[i] = side(x).ok_or_else(err)?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
_ => Err(err()),
|
||||
}
|
||||
}
|
||||
|
||||
fn grid_colors(v: &Value) -> Result<[vectorcraft_color::Color; 2]> {
|
||||
if let Some(name) = v.as_str() {
|
||||
return GRID_COLOR_PRESETS.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)).map(|(_, c)| *c).ok_or_else(|| {
|
||||
bad(C, format!("unknown grid colours `{name}` (one of {})", GRID_COLOR_PRESETS.iter().map(|(n, _)| *n).collect::<Vec<_>>().join(", ")))
|
||||
});
|
||||
}
|
||||
match v.as_array().map(|a| a.iter().map(color_value).collect::<Option<Vec<_>>>()) {
|
||||
Some(Some(c)) if c.len() == 2 => Ok([c[0], c[1]]),
|
||||
_ => Err(bad(C, "gridColors must be a preset name or two colours")),
|
||||
}
|
||||
}
|
||||
|
||||
/// A quote pair: a two-character string or two one-character strings.
|
||||
fn quote_pair(v: &Value) -> Option<[char; 2]> {
|
||||
let chars: Vec<char> = match v {
|
||||
Value::String(s) => s.chars().collect(),
|
||||
Value::Array(a) => a.iter().map(|x| x.as_str().and_then(|s| s.chars().next().filter(|_| s.chars().count() == 1))).collect::<Option<_>>()?,
|
||||
_ => return None,
|
||||
};
|
||||
match chars[..] {
|
||||
[a, b] if !a.is_control() && !b.is_control() && !a.is_whitespace() && !b.is_whitespace() => Some([a, b]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn quotes_param(v: &Value, mut q: Quotes) -> Result<Quotes> {
|
||||
let o = v.as_object().ok_or_else(|| bad(C, "quotes must be {double?, single?}"))?;
|
||||
for (k, x) in o {
|
||||
let pair = quote_pair(x).ok_or_else(|| bad(C, format!("quotes.{k} must be an opening and a closing quote, e.g. \"“”\"")))?;
|
||||
match k.as_str() {
|
||||
"double" => q.double = pair,
|
||||
"single" => q.single = pair,
|
||||
_ => return Err(bad(C, format!("unknown quotes key `{k}` (double, single)"))),
|
||||
}
|
||||
}
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
fn script_param(k: &str, v: &Value, mut m: ScriptMetrics) -> Result<ScriptMetrics> {
|
||||
let o = v.as_object().ok_or_else(|| bad(C, format!("{k} must be {{size?, position?}}")))?;
|
||||
for (key, x) in o {
|
||||
match key.as_str() {
|
||||
"size" => m.size = percent(&format!("{k}.size"), x, 1.0, 500.0)?,
|
||||
"position" => m.position = percent(&format!("{k}.position"), x, -500.0, 500.0)?,
|
||||
_ => return Err(bad(C, format!("unknown {k} key `{key}` (size, position)"))),
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// `position?: "normal"|"superscript"|"subscript"` and `smallCaps?: bool` from a character
|
||||
/// command's params, in the document's Document Setup proportions.
|
||||
pub(crate) fn script_params(p: &Value, setup: &DocSetup, cmd: &str) -> Result<(Option<CharPosition>, Option<Option<f64>>)> {
|
||||
let position = match p.get("position").filter(|v| !v.is_null()) {
|
||||
None => None,
|
||||
Some(v) => Some(match v.as_str().map(str::to_ascii_lowercase).as_deref() {
|
||||
Some("normal") => CharPosition::Normal,
|
||||
Some("superscript") => CharPosition::Superscript(setup.superscript),
|
||||
Some("subscript") => CharPosition::Subscript(setup.subscript),
|
||||
_ => return Err(bad(cmd, "position must be normal, superscript or subscript")),
|
||||
}),
|
||||
};
|
||||
let small_caps = match p.get("smallCaps").filter(|v| !v.is_null()) {
|
||||
None => None,
|
||||
Some(v) => Some(v.as_bool().ok_or_else(|| bad(cmd, "smallCaps must be true or false"))?.then_some(setup.small_caps_size)),
|
||||
};
|
||||
Ok((position, small_caps))
|
||||
}
|
||||
|
||||
/// A superscript or subscript in `setup`'s proportions.
|
||||
fn position_in(setup: &DocSetup, p: CharPosition) -> CharPosition {
|
||||
match p {
|
||||
CharPosition::Normal => CharPosition::Normal,
|
||||
CharPosition::Superscript(_) => CharPosition::Superscript(setup.superscript),
|
||||
CharPosition::Subscript(_) => CharPosition::Subscript(setup.subscript),
|
||||
}
|
||||
}
|
||||
|
||||
/// After the superscript, subscript or small caps proportions changed: every run (and character
|
||||
/// style) using them takes the new ones.
|
||||
fn restamp_scripts(d: &mut Document) {
|
||||
let uses = |st: &CharStyle| st.position != CharPosition::Normal || st.small_caps.is_some();
|
||||
let mut ids = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind
|
||||
&& t.runs.iter().any(|r| uses(&r.style))
|
||||
{
|
||||
ids.push(n.id);
|
||||
}
|
||||
});
|
||||
let setup = d.setup.clone();
|
||||
for id in ids {
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
||||
for st in t.runs.iter_mut().map(|r| &mut r.style).filter(|st| uses(st)) {
|
||||
st.position = position_in(&setup, st.position);
|
||||
st.small_caps = st.small_caps.map(|_| setup.small_caps_size);
|
||||
}
|
||||
super::typecmd::refresh_bounds(t);
|
||||
}
|
||||
}
|
||||
// Character styles store the attributes they set by their serialized names.
|
||||
for def in &mut d.char_styles {
|
||||
if let Some(v) = def.attrs.get_mut("position")
|
||||
&& let Ok(p) = serde_json::from_value::<CharPosition>(v.clone())
|
||||
{
|
||||
*v = serde_json::to_value(position_in(&setup, p)).unwrap_or(Value::Null);
|
||||
}
|
||||
if let Some(v) = def.attrs.get_mut("small_caps").filter(|v| v.is_number()) {
|
||||
*v = json!(setup.small_caps_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,9 @@ pub fn encode_all(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
|
||||
let scale = o.scale.unwrap_or(1.0).clamp(0.01, 64.0);
|
||||
check_format_size(f, region.width() * scale, region.height() * scale)?;
|
||||
vectorcraft_render::raster_size(region, scale).map_err(|e| bad(C, e))?;
|
||||
let img = vectorcraft_render::Renderer::new().render_region(doc, region, scale, f.id == "jpg");
|
||||
// JPEG has no alpha; New Document → Background Contents: White fills the artboard too.
|
||||
let white = f.id == "jpg" || doc.setup.background == vectorcraft_doc::Background::White;
|
||||
let img = vectorcraft_render::Renderer::new().render_region(doc, region, scale, white);
|
||||
match f.id {
|
||||
"png" => img.to_png(),
|
||||
"webp" => img.to_webp(),
|
||||
|
||||
@@ -31,7 +31,7 @@ pub const OPTIONS: &[FormatOption] = &[
|
||||
name: "outlineText",
|
||||
ty: "boolean",
|
||||
default: "false",
|
||||
description: "Fonts: text as glyph outlines (viewable without the fonts) instead of <text>",
|
||||
description: "Fonts: text as glyph outlines (viewable without the fonts) instead of <text>; default: Document Setup → Type → Export (Preserve Text Appearance: true)",
|
||||
},
|
||||
FormatOption {
|
||||
name: "images",
|
||||
@@ -99,6 +99,10 @@ pub fn options_map(p: &Value) -> Result<Map<String, Value>, String> {
|
||||
/// The writer options and the artboards (`None`: the art bounds) an SVG export of `doc` covers.
|
||||
fn plan(doc: &Document, p: &Value) -> Result<(ExportOptions, Vec<Option<usize>>), String> {
|
||||
let mut m = options_map(p)?;
|
||||
// Fonts: unless chosen, Document Setup → Type → Export decides (appearance = outlines).
|
||||
if !m.contains_key("outlineText") && doc.setup.export_text == vectorcraft_doc::ExportText::Appearance {
|
||||
m.insert("outlineText".into(), Value::Bool(true));
|
||||
}
|
||||
let picks: Map<String, Value> = ARTBOARD_PARAMS.iter().chain(["useArtboards"].iter()).filter_map(|k| m.remove_entry(*k)).collect();
|
||||
let boards = Boards::deserialize(Value::Object(picks)).map_err(|e| format!("SVG options: {e}"))?;
|
||||
let opts = ExportOptions::deserialize(Value::Object(m)).map_err(|e| format!("SVG options: {e}"))?;
|
||||
|
||||
@@ -12,6 +12,7 @@ mod create;
|
||||
pub(crate) mod distortcmds;
|
||||
mod docinfo;
|
||||
mod docmenu;
|
||||
pub(crate) mod docsetup;
|
||||
mod draw2;
|
||||
mod edit;
|
||||
mod effectcmd;
|
||||
@@ -211,6 +212,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(newart::specs());
|
||||
v.extend(colortheme::specs());
|
||||
v.extend(fileio::pdf::specs());
|
||||
v.extend(docsetup::specs());
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character / Paragraph",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt, hyphenate?: bool}",
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt, hyphenate?: bool}",
|
||||
has_doc,
|
||||
set_format
|
||||
),
|
||||
@@ -93,6 +93,8 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"underline",
|
||||
"strikethrough",
|
||||
"allCaps",
|
||||
"smallCaps",
|
||||
"position",
|
||||
"leftIndent",
|
||||
"rightIndent",
|
||||
"firstLineIndent",
|
||||
@@ -103,6 +105,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !keys.iter().any(|k| p.get(*k).is_some()) {
|
||||
return Err(bad(C, "nothing to change"));
|
||||
}
|
||||
let (position, small_caps) = super::docsetup::script_params(p, &s.doc()?.doc.setup, C)?;
|
||||
s.edit("Character", |d, _| {
|
||||
for id in &ids {
|
||||
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
|
||||
@@ -132,6 +135,12 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = flag("allCaps") {
|
||||
st.all_caps = v;
|
||||
}
|
||||
if let Some(v) = position {
|
||||
st.position = v;
|
||||
}
|
||||
if let Some(v) = small_caps {
|
||||
st.small_caps = v;
|
||||
}
|
||||
}
|
||||
let para = &mut t.para;
|
||||
if let Some(v) = num("leftIndent") {
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character",
|
||||
[],
|
||||
None,
|
||||
"{id, start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, features?: [\"dlig\", \"-liga\", …]} style a character range (runs are split at the range ends) → {id, runs}",
|
||||
"{id, start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …]} style a character range (runs are split at the range ends) → {id, runs}",
|
||||
has_doc,
|
||||
set_range_style
|
||||
),
|
||||
@@ -140,6 +140,8 @@ pub(crate) struct CharChange {
|
||||
strikethrough: Option<bool>,
|
||||
all_caps: Option<bool>,
|
||||
features: Option<Vec<String>>,
|
||||
position: Option<vectorcraft_doc::CharPosition>,
|
||||
small_caps: Option<Option<f64>>,
|
||||
}
|
||||
|
||||
/// `features: ["dlig", "-liga", …]` → the canonical tag list (differences from the defaults).
|
||||
@@ -169,8 +171,10 @@ fn auto_or_num(p: &Value, k: &str, cmd: &str) -> Result<Option<Option<f64>>> {
|
||||
}
|
||||
|
||||
impl CharChange {
|
||||
pub(crate) fn parse(p: &Value, cmd: &str) -> Result<Self> {
|
||||
/// `setup` gives the superscript, subscript and small caps proportions.
|
||||
pub(crate) fn parse(p: &Value, cmd: &str, setup: &vectorcraft_doc::DocSetup) -> Result<Self> {
|
||||
let num = |k: &str| p.get(k).and_then(Value::as_f64);
|
||||
let (position, small_caps) = super::docsetup::script_params(p, setup, cmd)?;
|
||||
let flag = |k: &str| p.get(k).and_then(Value::as_bool);
|
||||
let mut stroke_opts = match p.get("strokeOptions") {
|
||||
None | Some(Value::Null) => StrokeChange::default(),
|
||||
@@ -198,6 +202,8 @@ impl CharChange {
|
||||
strikethrough: flag("strikethrough"),
|
||||
all_caps: flag("allCaps"),
|
||||
features: features_param(p, cmd)?,
|
||||
position,
|
||||
small_caps,
|
||||
};
|
||||
if c.size.is_some_and(|v| v <= 0.0) {
|
||||
return Err(bad(cmd, "size must be positive"));
|
||||
@@ -223,6 +229,8 @@ impl CharChange {
|
||||
&& self.strikethrough.is_none()
|
||||
&& self.all_caps.is_none()
|
||||
&& self.features.is_none()
|
||||
&& self.position.is_none()
|
||||
&& self.small_caps.is_none()
|
||||
}
|
||||
|
||||
pub(crate) fn apply(&self, st: &mut CharStyle) {
|
||||
@@ -287,13 +295,19 @@ impl CharChange {
|
||||
if let Some(v) = &self.features {
|
||||
st.features = v.clone();
|
||||
}
|
||||
if let Some(v) = self.position {
|
||||
st.position = v;
|
||||
}
|
||||
if let Some(v) = self.small_caps {
|
||||
st.small_caps = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.setRangeStyle";
|
||||
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
|
||||
let change = CharChange::parse(p, C)?;
|
||||
let change = CharChange::parse(p, C, &s.doc()?.doc.setup)?;
|
||||
if change.is_empty() {
|
||||
return Err(bad(C, "nothing to change"));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! commands of the Edit menu (Find and Replace, Find Next, Paste without Formatting).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{CharStyle, Document, Justify, NodeId, NodeKind, TextKind, TextRun};
|
||||
use vectorcraft_doc::{CharStyle, Document, Justify, NodeId, NodeKind, Quotes, TextKind, TextRun};
|
||||
use vectorcraft_geom::{Affine, Rect, shapes};
|
||||
|
||||
use super::typecmd::refresh_bounds;
|
||||
@@ -25,7 +25,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Smart Punctuation…",
|
||||
["Type"],
|
||||
None,
|
||||
"{quotes?: true (\"\" '' → curly), dashes?: true (-- → en dash, --- → em dash), ellipsis?: true (... → …), scope?: \"selection\"|\"document\"} → {changed}",
|
||||
"{quotes?: true (straight quotes become the Document Setup quotes), dashes?: true (-- → en dash, --- → em dash), ellipsis?: true (... → …), scope?: \"selection\"|\"document\"} → {changed}",
|
||||
has_doc,
|
||||
smart_punctuation
|
||||
),
|
||||
@@ -217,11 +217,8 @@ struct PunctState {
|
||||
prev: Option<char>,
|
||||
}
|
||||
|
||||
fn opens(prev: Option<char>) -> bool {
|
||||
prev.is_none_or(|p| p.is_whitespace() || matches!(p, '(' | '[' | '{' | '—' | '–' | '“' | '‘'))
|
||||
}
|
||||
|
||||
pub(crate) fn smarten(text: &str, prev: &mut Option<char>, quotes: bool, dashes: bool, ellipsis: bool) -> String {
|
||||
/// Dashes (`--` en, `---` em), ellipses and, with `quotes`, typographer's quotes.
|
||||
pub(crate) fn smarten(text: &str, prev: &mut Option<char>, quotes: Option<&Quotes>, dashes: bool, ellipsis: bool) -> String {
|
||||
let mut s = text.to_string();
|
||||
if dashes {
|
||||
s = s.replace("---", "—").replace("--", "–");
|
||||
@@ -229,38 +226,20 @@ pub(crate) fn smarten(text: &str, prev: &mut Option<char>, quotes: bool, dashes:
|
||||
if ellipsis {
|
||||
s = s.replace("...", "…");
|
||||
}
|
||||
if !quotes {
|
||||
*prev = s.chars().last().or(*prev);
|
||||
return s;
|
||||
match quotes {
|
||||
Some(q) => q.apply(&s, prev),
|
||||
None => {
|
||||
*prev = s.chars().last().or(*prev);
|
||||
s
|
||||
}
|
||||
}
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
let r = match c {
|
||||
'"' => {
|
||||
if opens(*prev) {
|
||||
'“'
|
||||
} else {
|
||||
'”'
|
||||
}
|
||||
}
|
||||
'\'' => {
|
||||
if opens(*prev) {
|
||||
'‘'
|
||||
} else {
|
||||
'’'
|
||||
}
|
||||
}
|
||||
c => c,
|
||||
};
|
||||
out.push(r);
|
||||
*prev = Some(r);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn smart_punctuation(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (q, d, e) = (bool_or(p, "quotes", true), bool_or(p, "dashes", true), bool_or(p, "ellipsis", true));
|
||||
let (d, e) = (bool_or(p, "dashes", true), bool_or(p, "ellipsis", true));
|
||||
let st = s.doc()?;
|
||||
// Quotes in the document's style (Document Setup → Type).
|
||||
let q = bool_or(p, "quotes", true).then_some(st.doc.setup.quotes);
|
||||
let ids = if str_param(p, "scope") == Some("document") || st.selection.is_empty() {
|
||||
let mut v = vec![];
|
||||
st.doc.walk(|n| {
|
||||
@@ -272,7 +251,7 @@ fn smart_punctuation(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
} else {
|
||||
text_ids(&st.doc, &st.selection.objects)
|
||||
};
|
||||
let n = edit_runs::<PunctState>(s, "Smart Punctuation", &ids, |text, st| smarten(text, &mut st.prev, q, d, e))?;
|
||||
let n = edit_runs::<PunctState>(s, "Smart Punctuation", &ids, |text, st| smarten(text, &mut st.prev, q.as_ref(), d, e))?;
|
||||
Ok(json!({ "changed": n }))
|
||||
}
|
||||
|
||||
|
||||
@@ -909,6 +909,8 @@ mod tests_dashalign;
|
||||
#[cfg(test)]
|
||||
mod tests_distort;
|
||||
#[cfg(test)]
|
||||
mod tests_docsetup;
|
||||
#[cfg(test)]
|
||||
mod tests_draw2;
|
||||
#[cfg(test)]
|
||||
mod tests_editcolors;
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
//! File → Document Setup (`document.setup`): validation, one undo step, round-trips, and the
|
||||
//! options other commands read (typographer's quotes, superscript/subscript, SVG text export).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{CharPosition, DocSetup, ExportText, GridSize, NodeId, NodeKind, ScriptMetrics, TextObject, TextStyleDef, Unit};
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind, ToolKey};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn setup(s: &Session) -> &DocSetup {
|
||||
&s.doc().unwrap().doc.setup
|
||||
}
|
||||
|
||||
fn text(s: &Session, id: NodeId) -> TextObject {
|
||||
match &s.doc().unwrap().doc.node(id).unwrap().kind {
|
||||
NodeKind::Text(t) => (**t).clone(),
|
||||
_ => panic!("not text"),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_text(s: &mut Session, t: &str) -> NodeId {
|
||||
NodeId(s.execute("text.create", &json!({"x": 10, "y": 50, "text": t, "size": 20})).unwrap()["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
/// Click with the Type tool at (x, y) and type `keys` one at a time; returns the new text object.
|
||||
fn type_at(s: &mut Session, x: f64, y: f64, keys: &[&str]) -> NodeId {
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("type", v).unwrap();
|
||||
for k in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(k, x, y), v).unwrap();
|
||||
}
|
||||
let id = s.doc().unwrap().selection.objects[0];
|
||||
for k in keys {
|
||||
s.tool_text(k, v).unwrap();
|
||||
}
|
||||
s.tool_key(ToolKey::Escape, Default::default(), v).unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_reports_the_defaults_and_accepts_them_back() {
|
||||
let mut s = session();
|
||||
let q = s.execute("document.setup", &json!({})).unwrap();
|
||||
assert_eq!(q["units"], "Points");
|
||||
assert_eq!(q["bleed"], json!([0.0, 0.0, 0.0, 0.0]));
|
||||
assert_eq!(q["gridColors"], json!(["#ffffff", "#cccccc"]));
|
||||
assert_eq!(q["gridColorsName"], "Light");
|
||||
assert_eq!(q["flattenerPreset"], "Medium Resolution");
|
||||
assert_eq!(q["backgroundContents"], "transparent");
|
||||
assert_eq!(q["quotes"], json!({"double": "“”", "single": "‘’"}));
|
||||
assert_eq!(q["typographersQuotes"], true);
|
||||
assert_eq!(q["superscript"], json!({"size": 58.3, "position": 33.3}));
|
||||
// Feeding the report back changes nothing (and records no undo step).
|
||||
assert_eq!(s.execute("document.setup", &q).unwrap(), q);
|
||||
assert!(s.doc().unwrap().history.undo.is_empty());
|
||||
assert_eq!(*setup(&s), DocSetup::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_settings_are_one_undo_step() {
|
||||
let mut s = session();
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let r = s
|
||||
.execute(
|
||||
"document.setup",
|
||||
&json!({
|
||||
"units": "Millimeters", "bleed": {"top": 9, "right": 4.5}, "gridSize": "large", "gridColors": "Blue",
|
||||
"simulatePaper": true, "flattenerPreset": "high", "discardWhiteOverprint": false,
|
||||
"outlineImages": true, "highlightSubstitutedGlyphs": true, "exportText": "appearance",
|
||||
"superscript": {"size": 60}, "subscript": {"position": 20}, "smallCapsSize": 75,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(r["gridColorsName"], "Blue");
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!(d.units, Unit::Millimeters);
|
||||
let st = &d.setup;
|
||||
assert_eq!(st.bleed, [9.0, 0.0, 0.0, 4.5]);
|
||||
assert_eq!((st.grid_size, st.simulate_paper, st.outline_images, st.highlight_substituted_glyphs), (GridSize::Large, true, true, true));
|
||||
assert_eq!((st.flattener(), st.discard_white_overprint, st.export_text), ("High Resolution", false, ExportText::Appearance));
|
||||
assert_eq!((st.superscript, st.subscript), (ScriptMetrics { size: 60.0, position: 33.3 }, ScriptMetrics { size: 58.3, position: 20.0 }));
|
||||
assert_eq!(st.small_caps_size, 75.0);
|
||||
let undo: Vec<&str> = s.doc().unwrap().history.undo.iter().map(|e| e.label.as_str()).collect();
|
||||
assert_eq!(undo, ["Document Setup"]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(*s.doc().unwrap().doc, *before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_values_are_rejected_and_change_nothing() {
|
||||
let mut s = session();
|
||||
for p in [
|
||||
json!({"bleed": 73}),
|
||||
json!({"bleed": [1, 2, 3]}),
|
||||
json!({"bleed": {"middle": 2}}),
|
||||
json!({"bleed": -1}),
|
||||
json!({"gridSize": "huge"}),
|
||||
json!({"gridColors": ["#ffffff"]}),
|
||||
json!({"gridColors": "Plaid"}),
|
||||
json!({"language": "Klingon"}),
|
||||
json!({"quotes": {"double": "“"}}),
|
||||
json!({"quotes": {"triple": "“”"}}),
|
||||
json!({"superscript": {"size": 0}}),
|
||||
json!({"exportText": "pdf"}),
|
||||
json!({"flattenerPreset": "Ultra"}),
|
||||
json!({"backgroundContents": "grey"}),
|
||||
json!({"simulatePaper": "yes"}),
|
||||
json!({"units": "furlongs"}),
|
||||
json!({"bleed": 9, "nonsense": true}),
|
||||
json!([1, 2]),
|
||||
] {
|
||||
assert!(s.execute("document.setup", &p).is_err(), "{p}");
|
||||
}
|
||||
assert_eq!(*setup(&s), DocSetup::default());
|
||||
assert!(s.doc().unwrap().history.undo.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_units_is_an_alias() {
|
||||
let mut s = session();
|
||||
s.execute("document.setUnits", &json!({"units": "inches"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.units, Unit::Inches);
|
||||
assert_eq!(s.execute("document.setup", &json!({})).unwrap()["units"], "Inches");
|
||||
assert!(s.execute("document.setUnits", &json!({"units": "parsecs"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_picks_its_quotes_unless_quotes_are_given() {
|
||||
let mut s = session();
|
||||
s.execute("document.setup", &json!({"language": "german"})).unwrap();
|
||||
assert_eq!(setup(&s).language, "German");
|
||||
assert_eq!(setup(&s).quotes.double, ['„', '“']);
|
||||
s.execute("document.setup", &json!({"language": "French", "quotes": {"double": ["“", "”"]}})).unwrap();
|
||||
assert_eq!((setup(&s).quotes.double, setup(&s).quotes.single), (['“', '”'], ['‹', '›']));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typographers_quotes_setting_changes_typed_quotes() {
|
||||
let mut s = session();
|
||||
let id = type_at(&mut s, 50.0, 50.0, &["\"", "H", "i", "\"", " ", "i", "t", "'", "s"]);
|
||||
assert_eq!(text(&s, id).plain_text(), "“Hi” it’s");
|
||||
// The document's quote style.
|
||||
s.execute("document.setup", &json!({"language": "German"})).unwrap();
|
||||
let id = type_at(&mut s, 50.0, 150.0, &["\"", "a", "\""]);
|
||||
assert_eq!(text(&s, id).plain_text(), "„a“");
|
||||
// Straight quotes with the option off.
|
||||
s.execute("document.setup", &json!({"typographersQuotes": false})).unwrap();
|
||||
let id = type_at(&mut s, 50.0, 250.0, &["\"", "b", "'"]);
|
||||
assert_eq!(text(&s, id).plain_text(), "\"b'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_punctuation_uses_the_document_quotes() {
|
||||
let mut s = session();
|
||||
s.execute("document.setup", &json!({"language": "French"})).unwrap();
|
||||
let id = create_text(&mut s, "\"oui\" 'non'");
|
||||
s.execute("type.smartPunctuation", &json!({"scope": "document"})).unwrap();
|
||||
assert_eq!(text(&s, id).plain_text(), "«oui» ‹non›");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn superscript_and_small_caps_use_and_follow_the_setup_sizes() {
|
||||
let mut s = session();
|
||||
let id = create_text(&mut s, "E=mc2");
|
||||
s.execute("document.setup", &json!({"superscript": {"size": 50, "position": 40}})).unwrap();
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 4, "end": 5, "position": "superscript"})).unwrap();
|
||||
s.execute("text.setFormat", &json!({"ids": [id.0], "smallCaps": true})).unwrap();
|
||||
let t = text(&s, id);
|
||||
assert_eq!(t.runs.last().unwrap().style.position, CharPosition::Superscript(ScriptMetrics { size: 50.0, position: 40.0 }));
|
||||
assert!(t.runs.iter().all(|r| r.style.small_caps == Some(70.0)));
|
||||
let narrow = t.cached_bounds.unwrap();
|
||||
// A Document Setup change restyles the text in the same undo step.
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("document.setup", &json!({"superscript": {"size": 100}, "smallCapsSize": 100})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
let t = text(&s, id);
|
||||
assert_eq!(t.runs.last().unwrap().style.position, CharPosition::Superscript(ScriptMetrics { size: 100.0, position: 40.0 }));
|
||||
assert!(t.runs.iter().all(|r| r.style.small_caps == Some(100.0)));
|
||||
assert!(t.cached_bounds.unwrap().width() > narrow.width(), "bounds follow the bigger glyphs");
|
||||
s.execute("text.setFormat", &json!({"ids": [id.0], "position": "normal", "smallCaps": false})).unwrap();
|
||||
assert!(text(&s, id).runs.iter().all(|r| r.style.position == CharPosition::Normal && r.style.small_caps.is_none()));
|
||||
assert!(s.execute("text.setFormat", &json!({"ids": [id.0], "position": "sideways"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_styles_follow_the_setup_sizes() {
|
||||
let mut s = session();
|
||||
let attrs = json!({"position": {"kind": "subscript", "size": 58.3, "position": 33.3}, "small_caps": 70.0});
|
||||
Arc::make_mut(&mut s.doc_mut().unwrap().doc).char_styles.push(TextStyleDef { name: "Note".into(), attrs: attrs.as_object().unwrap().clone() });
|
||||
s.execute("document.setup", &json!({"subscript": {"size": 40}, "smallCapsSize": 80})).unwrap();
|
||||
let def = &s.doc().unwrap().doc.char_styles[0];
|
||||
assert_eq!(def.attrs["position"], json!({"kind": "subscript", "size": 40.0, "position": 33.3}));
|
||||
assert_eq!(def.attrs["small_caps"], json!(80.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_survives_save_and_load() {
|
||||
let mut s = session();
|
||||
s.execute(
|
||||
"document.setup",
|
||||
&json!({"bleed": [9, 9, 18, 18], "gridColors": ["#ff0000", "#00ff00"], "language": "Japanese", "exportText": "appearance"}),
|
||||
)
|
||||
.unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save_file(d)).unwrap();
|
||||
assert_eq!(back.setup, d.setup);
|
||||
assert_eq!(back.setup.grid_colors_name(), "Custom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_text_export_follows_the_export_text_setting() {
|
||||
let mut s = session();
|
||||
create_text(&mut s, "Hello");
|
||||
let svg = |s: &mut Session, p: Value| {
|
||||
let mut p = p;
|
||||
p["format"] = json!("svg");
|
||||
let r = s.execute("document.export", &p).unwrap();
|
||||
String::from_utf8(vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap()).unwrap()
|
||||
};
|
||||
assert!(svg(&mut s, json!({})).contains("<text"));
|
||||
s.execute("document.setup", &json!({"exportText": "appearance"})).unwrap();
|
||||
assert!(!svg(&mut s, json!({})).contains("<text"));
|
||||
// An explicit option still wins.
|
||||
assert!(svg(&mut s, json!({"outlineText": false})).contains("<text"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discard_white_overprint_keeps_white_overprints_in_overprint_preview() {
|
||||
let mut s = session();
|
||||
let rect = |s: &mut Session, color: &str| {
|
||||
let id = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 40, "height": 40})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("paint.setFill", &json!({"ids": [id], "color": color})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"ids": [id], "none": true})).unwrap();
|
||||
id
|
||||
};
|
||||
rect(&mut s, "#ff0000");
|
||||
let white = rect(&mut s, "#ffffff");
|
||||
s.execute("object.setOverprint", &json!({"fill": true, "ids": [white]})).unwrap();
|
||||
let centre = |s: &Session| {
|
||||
let opts = vectorcraft_render::RenderOptions { background: Some([255; 4]), overprint_preview: true, ..Default::default() };
|
||||
vectorcraft_render::Renderer::new().render(&s.doc().unwrap().doc, 40, 40, vectorcraft_geom::Affine::IDENTITY, &opts).pixel(20, 20)
|
||||
};
|
||||
// On by default: the white knocks out as it will print.
|
||||
assert_eq!(centre(&s), [255, 255, 255, 255]);
|
||||
s.execute("document.setup", &json!({"discardWhiteOverprint": false})).unwrap();
|
||||
let [r, g, b, _] = centre(&s);
|
||||
assert!(r > 200 && g < 60 && b < 60, "a white overprint vanishes: {:?}", [r, g, b]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_flattener_presets_and_background_contents() {
|
||||
let mut s = session();
|
||||
s.execute("flattener.presets.save", &json!({"name": "Proofs", "preset": "high"})).unwrap();
|
||||
assert_eq!(s.execute("document.setup", &json!({"flattenerPreset": "proofs"})).unwrap()["flattenerPreset"], "Proofs");
|
||||
assert_eq!(s.execute("document.setup", &json!({"flattenerPreset": "Medium Resolution"})).unwrap()["flattenerPreset"], "Medium Resolution");
|
||||
assert_eq!(setup(&s).flattener_preset, None, "the default is not stored");
|
||||
let r = s.execute("document.setup", &json!({"backgroundContents": "white"})).unwrap();
|
||||
assert_eq!(r["backgroundContents"], "white");
|
||||
assert_eq!(setup(&s).background, vectorcraft_doc::Background::White);
|
||||
}
|
||||
Reference in New Issue
Block a user