use super::para_style::ParaDirection; use super::*; /// Character attributes (the Character panel). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct CharStyle { pub font_family: String, #[serde(default = "regular")] pub font_style: String, /// Which installed version of the font (its version string) when several versions of the /// family and style are installed and the text was set in one the family and style alone /// wouldn't pick (an imported file's type matched to it); None = the one they pick. #[serde(default, skip_serializing_if = "Option::is_none")] pub font_version: Option, /// Variation axis settings of a variable font (Character panel › Variable Font), by axis tag /// (`wght`, `wdth`), in the axis's own units. They replace the named instance's setting of /// each axis they name; the other axes keep the style's settings. Empty: the style as it is. #[serde(default, rename = "fontAxes", skip_serializing_if = "std::collections::BTreeMap::is_empty")] pub font_axes: std::collections::BTreeMap, /// Size in points. pub size: f64, /// Leading in points; None = Auto (120% of size). #[serde(default, skip_serializing_if = "Option::is_none")] pub leading: Option, /// Tracking in 1/1000 em. #[serde(default)] pub tracking: f64, /// Kerning: None = Auto (metrics), Some(v) = manual in 1/1000 em. #[serde(default, skip_serializing_if = "Option::is_none")] pub kerning: Option, #[serde(default)] pub baseline_shift: f64, #[serde(default = "hundred")] pub h_scale: f64, #[serde(default = "hundred")] pub v_scale: f64, #[serde(default)] pub rotation: f64, pub fill: Paint, #[serde(default)] pub stroke: Paint, #[serde(default)] pub stroke_width: f64, #[serde(default)] pub underline: bool, #[serde(default)] pub strikethrough: bool, #[serde(default)] pub all_caps: bool, /// OpenType features that differ from the defaults (OpenType panel), as tags: `"dlig"` turns /// a feature on, `"-liga"` off. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub features: Vec, /// Character style (Character Styles panel) these attributes come from; None = Normal. #[serde(default, skip_serializing_if = "Option::is_none")] pub style_name: Option, /// Overprint Fill / Overprint Stroke of these characters (see [`crate::FillLayer::overprint`]). #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub overprint_fill: bool, #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub overprint_stroke: bool, /// Cap, join, miter limit and dashes of the character stroke (Stroke panel, with type /// selected); defaults as for object strokes. #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub stroke_cap: LineCap, #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub stroke_join: LineJoin, #[serde(default = "ten", skip_serializing_if = "is_ten")] pub stroke_miter_limit: f64, #[serde(default, skip_serializing_if = "Option::is_none")] pub stroke_dash: Option, /// Align Stroke of the character stroke (Stroke panel, with type selected): centred on the /// glyph outlines, inside or outside them, as for object strokes; the type stays live. #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub stroke_align: StrokeAlign, /// Superscript or subscript (Character panel). #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub position: CharPosition, /// Small Caps (Character panel): lowercase letters drawn as capitals at this percentage of the /// size (Document Setup → Type → Small Caps); None = off. #[serde(default, skip_serializing_if = "Option::is_none")] pub small_caps: Option, /// Character Alignment (Character panel menu): where characters smaller than the largest on /// their line line up with it. #[serde(default, rename = "charAlign", skip_serializing_if = "crate::skip::is_default")] pub char_align: CharAlign, /// Character Direction (Character panel menu, Middle Eastern options): these characters are /// set left to right or right to left inside their paragraph, each stretch of them a Unicode /// bidi embedding (UAX #9 LRE or RLE); None = Default, the direction the bidi algorithm gives /// them. #[serde(default, rename = "charDirection", skip_serializing_if = "Option::is_none")] pub char_direction: Option, /// Proportional Metrics (Character panel menu, East Asian options): full-width glyphs take the /// font's own proportional widths, OpenType `palt` (#966); in vertical type upright glyphs take /// its proportional heights, `vpal`. #[serde(default, rename = "proportionalMetrics", skip_serializing_if = "crate::skip::is_default")] pub proportional_metrics: bool, /// Tate-chu-yoko (Character panel menu, East Asian options): in vertical type these characters /// stand side by side across one em of the column. Horizontal type keeps the mark and does not /// move them. Two or three half-width digits do this without the mark. /// #[serde(default, rename = "tateChuYoko", skip_serializing_if = "crate::skip::is_default")] pub tate_chu_yoko: bool, /// Digits (Character panel menu, Middle Eastern options): the numeral system the digits of /// these characters are drawn in, whatever digits were typed. #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub digits: Digits, /// No Break (Character panel menu): these characters are not broken across lines, including /// at a space or a hyphenation point inside the run. A run longer than the line stays together /// and can overflow it. /// #[serde(default, rename = "noBreak", skip_serializing_if = "crate::skip::is_default")] pub no_break: bool, } /// The numeral system digits are drawn in ([`CharStyle::digits`]): as typed, or every digit of /// the Western, Arabic-Indic and Eastern Arabic-Indic sets drawn in one of them. The text keeps /// the digits that were typed; only the glyphs change (like All Caps). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Digits { /// Each digit as typed. #[default] Default, /// Western digits 0–9 (the digits Arabic typography calls Arabic). Arabic, /// Arabic-Indic digits ٠–٩ (U+0660–U+0669), used in Arabic. Hindi, /// Eastern Arabic-Indic digits ۰–۹ (U+06F0–U+06F9), used in Persian and Urdu. Farsi, } impl Digits { /// The digit `c` is drawn as in this numeral system: the same digit in the chosen set, or `c` /// itself when `c` isn't a digit of the Western, Arabic-Indic or Eastern Arabic-Indic set. pub fn map(self, c: char) -> char { let value = match c { '0'..='9' => c as u32 - '0' as u32, '\u{660}'..='\u{669}' => c as u32 - 0x660, '\u{6F0}'..='\u{6F9}' => c as u32 - 0x6F0, _ => return c, }; let base = match self { Digits::Default => return c, Digits::Arabic => '0' as u32, Digits::Hindi => 0x660, Digits::Farsi => 0x6F0, }; char::from_u32(base + value).unwrap_or(c) } } /// Where a character smaller than the largest on its line lines up with it: on the Roman /// baseline, at the top (right, in vertical type), centre or bottom (left) of the ideographic /// em boxes, or at the top (right) or bottom (left) of the ideographic character faces (ICF, the /// average box of the ideographs inside the em box: OpenType's `icft` and `icfb` baselines). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum CharAlign { #[default] RomanBaseline, EmBoxTop, EmBoxCenter, EmBoxBottom, IcfTop, IcfBottom, } /// Superscript or subscript proportions in percent of the font size (Document Setup → Type). #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct ScriptMetrics { /// Glyph size. pub size: f64, /// Baseline offset: up for superscript, down for subscript. pub position: f64, } impl ScriptMetrics { pub const DEFAULT: Self = Self { size: 58.3, position: 33.3 }; /// Returns these proportions with the size kept to 1 to 500% and the position kept to -500 to /// 500%, the ranges `document.setup` takes for `superscript` and `subscript`. A value outside /// its range becomes the nearer end. pub fn in_range(self) -> Self { Self { size: self.size.clamp(1.0, 500.0), position: self.position.clamp(-500.0, 500.0) } } } /// Returns a small caps size kept to 1 to 200%, the range `document.setup` takes for /// `smallCapsSize`. A size outside it becomes the nearer end. fn small_caps_in_range(size: f64) -> f64 { size.clamp(1.0, 200.0) } impl Default for ScriptMetrics { fn default() -> Self { Self::DEFAULT } } /// Character position (Character panel Superscript / Subscript). The proportions are the /// document's when the position is applied, and follow later Document Setup changes. #[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum CharPosition { #[default] Normal, Superscript(ScriptMetrics), Subscript(ScriptMetrics), } impl CharPosition { /// Glyph scale and baseline shift (points, positive = up) for text of `size` points. pub fn scale_shift(self, size: f64) -> (f64, f64) { match self { CharPosition::Normal => (1.0, 0.0), CharPosition::Superscript(m) => (m.size / 100.0, m.position / 100.0 * size), CharPosition::Subscript(m) => (m.size / 100.0, -m.position / 100.0 * size), } } /// `normal`, `superscript` or `subscript`. pub fn id(self) -> &'static str { match self { CharPosition::Normal => "normal", CharPosition::Superscript(_) => "superscript", CharPosition::Subscript(_) => "subscript", } } /// Returns this position with its proportions kept by [`ScriptMetrics::in_range`]. pub fn in_range(self) -> Self { match self { CharPosition::Normal => CharPosition::Normal, CharPosition::Superscript(m) => CharPosition::Superscript(m.in_range()), CharPosition::Subscript(m) => CharPosition::Subscript(m.in_range()), } } } fn ten() -> f64 { 10.0 } fn is_ten(v: &f64) -> bool { *v == 10.0 } fn regular() -> String { "Regular".into() } fn hundred() -> f64 { 100.0 } pub(super) fn yes() -> bool { true } impl Default for CharStyle { fn default() -> Self { Self { font_family: "Source Sans 3".into(), font_style: "Regular".into(), font_version: None, font_axes: std::collections::BTreeMap::new(), size: 12.0, leading: None, tracking: 0.0, kerning: None, baseline_shift: 0.0, h_scale: 100.0, v_scale: 100.0, rotation: 0.0, fill: Paint::solid(Color::BLACK), stroke: Paint::None, stroke_width: 0.0, underline: false, strikethrough: false, all_caps: false, features: vec![], style_name: None, overprint_fill: false, overprint_stroke: false, stroke_cap: LineCap::Butt, stroke_join: LineJoin::Miter, stroke_miter_limit: 10.0, stroke_dash: None, stroke_align: StrokeAlign::Center, position: CharPosition::Normal, char_align: CharAlign::RomanBaseline, char_direction: None, proportional_metrics: false, tate_chu_yoko: false, small_caps: None, digits: Digits::Default, no_break: false, } } } impl CharStyle { pub fn effective_leading(&self) -> f64 { self.leading.unwrap_or(self.size * 1.2) } /// Keeps the size, leading, baseline shift and horizontal and vertical scales in range: a size /// or scale of 0 or less becomes the default new type takes (12 pt, 100%), a negative leading /// becomes Auto, and a value past [`crate::MAX_COORD`] (a baseline shift past it either way) /// becomes the nearer end. A leading of 0, which legacy `.ai` import stores, stays, and a /// baseline shift that is not a number becomes 0. pub fn keep_dimensions_in_range(&mut self) { let default = Self::default(); let within = |v: f64, or: f64| if v > 0.0 { v.min(crate::MAX_COORD) } else { or }; self.size = within(self.size, default.size); self.h_scale = within(self.h_scale, default.h_scale); self.v_scale = within(self.v_scale, default.v_scale); self.leading = self.leading.filter(|l| *l >= 0.0).map(|l| l.min(crate::MAX_COORD)); self.baseline_shift = if self.baseline_shift.is_nan() { 0.0 } else { self.baseline_shift.clamp(-crate::MAX_COORD, crate::MAX_COORD) }; } /// Do the characters draw a stroke (a paint and a positive weight)? pub fn has_stroke(&self) -> bool { !self.stroke.is_none() && self.stroke_width > 0.0 } /// The character stroke as a stroke layer (paint, weight, cap, join, miter limit, dashes, /// alignment and overprint), so type strokes share the object strokes' geometry, rendering /// and export. pub fn stroke_layer(&self) -> StrokeLayer { StrokeLayer { overprint: self.overprint_stroke, cap: self.stroke_cap, join: self.stroke_join, miter_limit: self.stroke_miter_limit, dash: self.stroke_dash.clone(), align: self.stroke_align, ..StrokeLayer::new(self.stroke.clone(), self.stroke_width) } } /// Take the paint, weight, cap, join, miter limit, dashes and alignment of `st` as the /// character stroke (the options characters have no use for, such as arrowheads and width /// profiles, are dropped). pub fn set_stroke_layer(&mut self, st: &StrokeLayer) { self.stroke = st.paint.clone(); self.stroke_width = st.width; self.stroke_cap = st.cap; self.stroke_join = st.join; self.stroke_miter_limit = st.miter_limit; self.stroke_dash = st.dash.clone(); self.stroke_align = st.align; } /// How far the character stroke paints past the glyph outlines (text space): half the weight /// centred, the whole weight outside, nothing inside (miter spikes aside). pub fn stroke_outset(&self) -> f64 { if !self.has_stroke() { return 0.0; } match self.stroke_align { StrokeAlign::Center => self.stroke_width / 2.0, StrokeAlign::Outside => self.stroke_width, StrokeAlign::Inside => 0.0, } } /// The character fill as a fill layer, overprinting as the characters do. fn fill_layer(&self) -> FillLayer { FillLayer { overprint: self.overprint_fill, ..FillLayer::new(self.fill.clone()) } } /// The characters' paint as an object appearance (their outlines'): the fill, plus the /// stroke when it draws one, overprinting as the characters do. pub fn appearance(&self) -> Appearance { let mut a = Appearance { items: vec![AppearanceItem::Fill(self.fill_layer())], ..Default::default() }; if self.has_stroke() { a.items.push(AppearanceItem::Stroke(self.stroke_layer())); } a } /// The characters' paint as the basic fill and stroke rows (the stroke row even when it has /// no paint): the appearance the Eyedropper and graphic styles take from type. pub fn basic_appearance(&self) -> Appearance { Appearance { items: vec![AppearanceItem::Fill(self.fill_layer()), AppearanceItem::Stroke(self.stroke_layer())], ..Default::default() } } /// Keeps the superscript or subscript proportions in range with [`CharPosition::in_range`] and /// keeps the small caps size to 1 to 200%, the range `document.setup` takes for `smallCapsSize`. pub fn keep_scripts_in_range(&mut self) { self.position = self.position.in_range(); self.small_caps = self.small_caps.map(small_caps_in_range); } } impl crate::Document { /// Keeps the superscript, subscript and small caps proportions to the ranges `document.setup` /// takes ([`CharStyle::keep_scripts_in_range`]) in the type of the layers, symbols, graph /// designs, pattern swatches and the pattern definition that pattern editing's Cancel puts /// back (opacity-mask art included), in graphs' label styles and in the character styles. It /// recurses through the art: call it on a document no deeper than [`crate::depth::MAX_DEPTH`]. pub fn keep_scripts_in_range(&mut self) { let mut fix = |n: &mut crate::Node| { if let crate::NodeKind::Text(t) = &mut n.kind { for r in &mut t.runs { r.style.keep_scripts_in_range(); } } if let Some(g) = n.extras_if_any().and_then(|e| e.graph.as_deref_mut()) { let l = &mut g.label_styles; for st in [&mut l.values, &mut l.categories, &mut l.legend].into_iter().flatten() { st.keep_scripts_in_range(); } } }; let roots = self .layers .iter_mut() .chain(self.symbols.iter_mut().map(|s| &mut s.art)) .chain(self.graph_designs.iter_mut().map(|g| &mut g.art)) .chain(self.patterns.iter_mut().flat_map(|p| p.art.iter_mut())) .chain(self.pattern_edit.iter_mut().filter_map(|pe| pe.original.as_mut()).flat_map(|p| p.art.iter_mut())); for root in roots { crate::placed_document::each_mut(std::sync::Arc::make_mut(root), &mut fix); } // Character styles store the attributes they set by their serialized names. for def in self.char_styles.iter_mut() { if let Some(v) = def.attrs.get_mut("position") && let Ok(p) = serde_json::from_value::(v.clone()) && p.in_range() != p { *v = serde_json::to_value(p.in_range()).unwrap_or(serde_json::Value::Null); } if let Some(v) = def.attrs.get_mut("small_caps") && let Some(size) = v.as_f64() && small_caps_in_range(size) != size { *v = small_caps_in_range(size).into(); } } } }