Type: Kerning has an Optical option that spaces characters by their shapes

Squashed from #4825 onto main.
This commit is contained in:
dotCooCoo
2026-11-01 01:22:04 +07:00
committed by bflatastic
parent 012af4fd49
commit 9896f633da
47 changed files with 1165 additions and 69 deletions
+12
View File
@@ -388,6 +388,18 @@ fn char_position_round_trips() {
assert!((scale - 0.583).abs() < 1e-9 && (shift + 3.33).abs() < 1e-9);
}
#[test]
fn optical_kerning_round_trips_and_old_style_defaults_to_auto() {
let st = CharStyle { optical_kerning: true, kerning: Some(120.0), ..CharStyle::default() };
let value = serde_json::to_value(&st).unwrap();
assert_eq!(value["opticalKerning"], true);
assert_eq!(serde_json::from_value::<CharStyle>(value.clone()).unwrap(), st);
let mut old = value;
old.as_object_mut().unwrap().remove("opticalKerning");
assert!(!serde_json::from_value::<CharStyle>(old).unwrap().optical_kerning);
assert!(serde_json::to_value(CharStyle::default()).unwrap().get("opticalKerning").is_none());
}
#[test]
fn units() {
assert_eq!(Unit::Inches.parse("1"), Some(72.0));
+5 -1
View File
@@ -25,9 +25,12 @@ pub struct CharStyle {
/// Tracking in 1/1000 em.
#[serde(default)]
pub tracking: f64,
/// Kerning: None = Auto (metrics), Some(v) = manual in 1/1000 em.
/// Manual pair spacing in 1/1000 em; None uses the automatic mode.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kerning: Option<f64>,
/// Use glyph shapes for automatic pair spacing; false uses font metrics.
#[serde(default, rename = "opticalKerning", skip_serializing_if = "crate::skip::is_default")]
pub optical_kerning: bool,
#[serde(default)]
pub baseline_shift: f64,
#[serde(default = "hundred")]
@@ -266,6 +269,7 @@ impl Default for CharStyle {
leading: None,
tracking: 0.0,
kerning: None,
optical_kerning: false,
baseline_shift: 0.0,
h_scale: 100.0,
v_scale: 100.0,
+1
View File
@@ -223,6 +223,7 @@ impl ParaList {
position: CharPosition::Normal,
tracking: 0.0,
kerning: None,
optical_kerning: first.optical_kerning,
tate_chu_yoko: false,
underline: false,
strikethrough: false,
+1
View File
@@ -87,6 +87,7 @@ pub mod swatchlib;
pub mod symbollib;
pub(crate) mod tabs;
mod textedit;
pub use textedit::effective_kerning;
pub mod textstyles;
pub(crate) mod textwrap;
pub(crate) mod threads;
File diff suppressed because one or more lines are too long
+83 -14
View File
@@ -39,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Character",
[],
None,
"{id? (default: the one selected type object), 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\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, 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\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, 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\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
has_doc,
set_range_style
),
@@ -129,7 +129,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Reset Tracking and Kerning",
[],
Some("Cmd+Alt+Q"),
"{ids?: [id…]} set tracking to 0 and kerning to Auto (the font's own kerning, which the Character panel shows as 0) in all the text of the type objects in `ids`, else in the text the Type tool has selected (at a caret: only the kerning of the character before it), else in every selected type object, as one undo step → {ids}",
"{ids?: [id…]} set tracking to 0 and clear manual kerning without changing the automatic method in all the text of the type objects in `ids`, else in the text the Type tool has selected (at a caret: only the kerning of the character before it), else in every selected type object, as one undo step → {ids}",
has_selection,
reset_tracking
),
@@ -300,6 +300,7 @@ pub(crate) struct CharChange {
leading: Option<Option<f64>>,
tracking: Option<f64>,
kerning: Option<Option<f64>>,
optical_kerning: Option<bool>,
baseline_shift: Option<f64>,
h_scale: Option<f64>,
v_scale: Option<f64>,
@@ -476,7 +477,17 @@ impl CharChange {
size: num("size"),
leading: auto_or_num(p, "leading", cmd)?,
tracking: num("tracking"),
kerning: auto_or_num(p, "kerning", cmd)?,
kerning: match p.get("kerning") {
None | Some(Value::Null) => None,
Some(Value::String(v)) if v.eq_ignore_ascii_case("optical") => None,
Some(Value::String(v)) if v.eq_ignore_ascii_case("auto") => Some(None),
Some(v) => Some(Some(v.as_f64().ok_or_else(|| bad(cmd, "kerning must be a number, \"auto\", or \"optical\""))?)),
},
optical_kerning: match p.get("kerning") {
Some(Value::String(v)) if v.eq_ignore_ascii_case("optical") => Some(true),
Some(Value::String(v)) if v.eq_ignore_ascii_case("auto") => Some(false),
_ => None,
},
baseline_shift: num("baselineShift"),
h_scale: num("hScale"),
v_scale: num("vScale"),
@@ -521,6 +532,7 @@ impl CharChange {
&& self.leading.is_none()
&& self.tracking.is_none()
&& self.kerning.is_none()
&& self.optical_kerning.is_none()
&& self.baseline_shift.is_none()
&& self.h_scale.is_none()
&& self.v_scale.is_none()
@@ -583,6 +595,9 @@ impl CharChange {
if let Some(v) = self.kerning {
st.kerning = v.map(|k| k.clamp(-1000.0, 10000.0));
}
if let Some(v) = self.optical_kerning {
st.optical_kerning = v;
}
if let Some(v) = self.baseline_shift {
st.baseline_shift = v;
}
@@ -734,18 +749,53 @@ fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(w) = super::fonts::set_font_warning(set) {
out["warning"] = json!(w);
}
if let Some(kerning) = p.get("kerning") {
out["kerning"] = kerning.clone();
}
Ok(out)
}
/// The em a laid-out glyph's kerning and tracking are measured in, in text space: the size it
/// was shaped at (superscript, small caps and Shrink Text to Fit included).
fn glyph_em(t: &TextObject, g: &vectorcraft_text::PositionedGlyph) -> Option<f64> {
let face = vectorcraft_text::FontDb::global().face_by_id(g.font_id)?;
fn glyph_em(db: &vectorcraft_text::FontDb, t: &TextObject, g: &vectorcraft_text::PositionedGlyph) -> Option<f64> {
let face = db.face_by_id(g.font_id)?;
let h_scale = edit::slice_runs(&t.runs, g.byte, g.byte + g.len).first()?.style.h_scale / 100.0;
let [a, b, ..] = g.xf.as_coeffs();
Some(a.hypot(b) * face.units_per_em() / h_scale).filter(|em| em.is_finite() && *em > 0.0)
}
/// The current pair correction in thousandths of an em, including an automatic correction.
pub fn effective_kerning(db: &vectorcraft_text::FontDb, t: &TextObject, lay: &vectorcraft_text::TextLayout, x: usize, y: usize) -> f64 {
if let Some(value) = edit::slice_runs(&t.runs, x, y).first().and_then(|r| r.style.kerning) {
return value;
}
// Measure both layouts without wrapping so the same pair stays on one line.
let mut trial = t.clone();
trial.kind = TextKind::Point;
let unwrapped = (!matches!(t.kind, TextKind::Point)).then(|| vectorcraft_text::layout(db, &trial));
let original = unwrapped.as_ref().unwrap_or(lay);
edit::style_range(&mut trial.runs, x, y, |st| st.kerning = Some(0.0));
let base = vectorcraft_text::layout(db, &trial);
let owner = original.glyphs.iter().find(|g| g.byte == x);
let rtl = owner.is_some_and(|g| g.rtl);
let drawn_gap = |layout: &vectorcraft_text::TextLayout| {
let first = layout.glyphs.iter().find(|g| g.byte == if rtl { y } else { x })?;
let following = layout.glyphs.iter().find(|g| g.byte == if rtl { x } else { y })?;
if first.line != following.line {
return None;
}
let axis = vectorcraft_geom::Vec2::new(first.angle.cos(), first.angle.sin());
let along = if rtl { -axis } else { axis };
let drawn = |g: &vectorcraft_text::PositionedGlyph| {
let [_, _, _, _, tx, ty] = g.xf.as_coeffs();
Point::new(tx, ty)
};
Some((drawn(following) - drawn(first)).dot(along))
};
let em = owner.and_then(|g| glyph_em(db, t, g)).unwrap_or(0.0);
if em > 0.0 { drawn_gap(original).zip(drawn_gap(&base)).map_or(0.0, |(a, b)| (a - b) * 1000.0 / em) } else { 0.0 }
}
fn touch_type(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "text.touchType";
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
@@ -780,18 +830,21 @@ fn touch_type(s: &mut Session, p: &Value) -> Result<Value> {
// A list marker's glyphs (length 0) stand for no character.
let prev = lay.glyphs.iter().find(|h| h.line == g.line && h.len > 0 && h.byte + h.len == a);
let pr = prev.map(|h| (h.byte, h.byte + h.len));
let em = glyph_em(&t, g).ok_or_else(|| bad(C, "the character's size can't be measured"))?;
let db = vectorcraft_text::FontDb::global();
let em = glyph_em(db, &t, g).ok_or_else(|| bad(C, "the character's size can't be measured"))?;
let em_prev = match prev {
Some(h) => Some(glyph_em(&t, h).ok_or_else(|| bad(C, "the character's size can't be measured"))?),
Some(h) => Some(glyph_em(db, &t, h).ok_or_else(|| bad(C, "the character's size can't be measured"))?),
None if dx != 0.0 => return Err(bad(C, "the first character of a line has no character before it to kern: it moves only up and down (dy)")),
None => None,
};
let kerning_in = |(x, y): (usize, usize)| edit::slice_runs(&t.runs, x, y).first().and_then(|r| r.style.kerning).unwrap_or(0.0);
let kerning_in = |(x, y): (usize, usize)| effective_kerning(db, &t, &lay, x, y);
let original_before = pr.map(kerning_in);
let original_own = kerning_in((a, b));
// The move along the line, in text space, limited to what kerning from -1000 to 10000 (1/1000
// em) on the character before it and on the character itself can give.
let mut moved = dx / doc_len(along);
if let (Some(range), Some(ep)) = (pr, em_prev) {
let (kp, kc) = (kerning_in(range), kerning_in((a, b)));
if let (Some(_range), Some(ep)) = (pr, em_prev) {
let (kp, kc) = (original_before.unwrap_or(0.0), original_own);
let hi = ((10000.0 - kp) * ep).min((kc + 1000.0) * em) / 1000.0;
let lo = ((-1000.0 - kp) * ep).max((kc - 10000.0) * em) / 1000.0;
if lo <= hi {
@@ -803,7 +856,8 @@ fn touch_type(s: &mut Session, p: &Value) -> Result<Value> {
let touch = |t: &mut TextObject, before: Option<f64>, own: Option<f64>, raise: f64| {
for (range, by) in [(pr, before), (Some((a, b)), own)] {
if let (Some((x, y)), Some(by)) = (range, by) {
edit::style_range(&mut t.runs, x, y, |st| st.kerning = Some((st.kerning.unwrap_or(0.0) + by).clamp(-1000.0, 10000.0)));
let original = if (x, y) == (a, b) { original_own } else { original_before.unwrap_or(0.0) };
edit::style_range(&mut t.runs, x, y, |st| st.kerning = Some((original + by).clamp(-1000.0, 10000.0)));
}
}
edit::style_range(&mut t.runs, a, b, |st| {
@@ -1086,9 +1140,20 @@ fn type_step(s: &mut Session, p: &Value) -> Result<Value> {
}
_ => None,
})?;
let mut caret_kerning = Vec::with_capacity(ranges.len());
for &(id, a, b, caret) in &ranges {
let value = if matches!(step, Step::Kerning) && caret {
let t = text_ref(s, id)?;
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
Some(effective_kerning(vectorcraft_text::FontDb::global(), &t, &lay, a, b))
} else {
None
};
caret_kerning.push(value);
}
if !ranges.is_empty() {
s.edit("Character", |d, _| {
for &(id, a, b, _) in &ranges {
for (range_index, &(id, a, b, _)) in ranges.iter().enumerate() {
let t = text_mut(d, id).ok_or(EngineError::NoNode(id))?;
let points = if matches!(step, Step::Size | Step::Leading | Step::BaselineShift) {
t.style_scale().ok_or_else(|| bad(C, "text transform is collapsed or unrepresentable"))?.points
@@ -1127,7 +1192,10 @@ fn type_step(s: &mut Session, p: &Value) -> Result<Value> {
st.leading = Some(super::typecmd::local_type_value(t, leading * points + inc, (0.1, 5000.0), false, C)?);
}
Step::Tracking => st.tracking = (st.tracking + inc).clamp(-1000.0, 10000.0),
Step::Kerning => st.kerning = Some((st.kerning.unwrap_or(0.0) + inc).clamp(-1000.0, 10000.0)),
Step::Kerning => {
let current = caret_kerning.get(range_index).copied().flatten().unwrap_or_else(|| st.kerning.unwrap_or(0.0));
st.kerning = Some((current + inc).clamp(-1000.0, 10000.0));
}
Step::BaselineShift => {
st.baseline_shift = super::typecmd::local_type_value(t, st.baseline_shift * points + inc, (-1296.0, 1296.0), false, C)?;
}
@@ -1142,9 +1210,10 @@ fn type_step(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "ids": ranges.iter().map(|(id, ..)| id.0).collect::<Vec<_>>() }))
}
/// Cmd/Ctrl+Alt+Q: tracking 0 and kerning Auto (0 in the Character panel) in the type objects in
/// Cmd/Ctrl+Alt+Q: tracking 0 and manual kerning cleared in the type objects in
/// `ids`, else in the Type tool's selected text, else in every selected type object. At a caret,
/// only the kerning of the character before it changes, as Alt+←/→ in horizontal type or Alt+↓/↑ in vertical type kerns there.
/// The automatic method remains selected.
fn reset_tracking(s: &mut Session, p: &Value) -> Result<Value> {
let ranges = type_ranges(s, p, "type.resetTracking", |text, a| Some((edit::prev_char(text, a), a)))?;
if !ranges.is_empty() {
+1
View File
@@ -152,6 +152,7 @@ pub fn char_attrs(v: &CharStyle) -> Map<String, Value> {
("fontAxes", json!(v.font_axes)),
("leading", json!(v.leading)),
("kerning", json!(v.kerning)),
("opticalKerning", json!(v.optical_kerning)),
("features", json!(v.features)),
("overprint_fill", json!(v.overprint_fill)),
("overprint_stroke", json!(v.overprint_stroke)),
+14
View File
@@ -460,6 +460,20 @@ fn auto_leading_new_distinguishes_unset_attributes() {
assert_eq!(s.doc().unwrap().doc.char_styles[2].attrs.get("leading"), Some(&Value::Null));
}
#[test]
fn optical_character_style_captures_and_redefines_automatic_mode() {
let (mut s, id) = session_with_text("III");
s.execute("text.setRangeStyle", &json!({"id": id, "kerning": "optical"})).unwrap();
s.execute("charStyle.new", &json!({"name": "Profile", "id": id})).unwrap();
assert_eq!(s.doc().unwrap().doc.char_styles[0].attrs.get("opticalKerning"), Some(&json!(true)));
s.execute("text.setRangeStyle", &json!({"id": id, "kerning": "auto"})).unwrap();
s.execute("charStyle.apply", &json!({"name": "Profile", "id": id})).unwrap();
assert!(runs(&s, id)[0].style.optical_kerning);
s.execute("text.setRangeStyle", &json!({"id": id, "kerning": "auto"})).unwrap();
s.execute("charStyle.redefine", &json!({"name": "Profile", "id": id})).unwrap();
assert_eq!(s.doc().unwrap().doc.char_styles[0].attrs.get("opticalKerning"), Some(&json!(false)));
}
#[test]
fn paragraph_styles_and_survive_save() {
let (mut s, id) = session_with_text("Body");
+163 -1
View File
@@ -2,7 +2,7 @@
//! on-path type from paths, Fit Headline, Convert To Area/Point Type.
use serde_json::{Value, json};
use vectorcraft_doc::{NodeId, NodeKind, TextKind, TextObject};
use vectorcraft_doc::{CharStyle, NodeId, NodeKind, TextKind, TextObject};
use vectorcraft_geom::Point;
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
@@ -87,6 +87,168 @@ fn set_range_style_splits_runs() {
assert!(s.execute("text.setRangeStyle", &json!({"id": id.0, "size": -1})).is_err());
}
#[test]
fn optical_command_manual_pair_undo_redo_and_native_reopen() {
let mut s = session();
let id = text(&mut s, "III");
s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": 70})).unwrap();
let result = s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": "optical"})).unwrap();
assert_eq!(result["kerning"], "optical");
assert!(obj(&s, id).runs[0].style.optical_kerning);
assert_eq!(obj(&s, id).runs[0].style.kerning, Some(70.0));
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 1, "end": 2, "kerning": 0})).unwrap();
let styled = obj(&s, id);
assert_eq!(styled.runs.iter().map(|r| r.style.kerning).collect::<Vec<_>>(), vec![Some(70.0), Some(0.0), Some(70.0)]);
assert!(styled.runs.iter().all(|r| r.style.optical_kerning));
let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc);
let opened = vectorcraft_format::load(&bytes).unwrap();
let NodeKind::Text(reopened) = &opened.node(id).unwrap().kind else { panic!("text missing") };
assert_eq!(
reopened.runs.iter().map(|r| (r.style.optical_kerning, r.style.kerning)).collect::<Vec<_>>(),
styled.runs.iter().map(|r| (r.style.optical_kerning, r.style.kerning)).collect::<Vec<_>>()
);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(obj(&s, id).runs[0].style.kerning, Some(70.0));
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(obj(&s, id).runs[1].style.kerning, Some(0.0));
assert!(obj(&s, id).runs.iter().all(|r| r.style.optical_kerning));
}
#[test]
fn optical_set_format_keeps_existing_pair_override() {
let mut s = session();
let id = text(&mut s, "III");
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": 1, "kerning": 40})).unwrap();
let result = s.execute("text.setFormat", &json!({"ids": [id.0], "kerning": "optical"})).unwrap();
assert_eq!(result["kerning"], "optical");
let t = obj(&s, id);
assert!(t.runs.iter().all(|r| r.style.optical_kerning));
assert_eq!(t.runs[0].style.kerning, Some(40.0));
}
#[test]
fn optical_kerning_step_starts_from_the_automatic_pair() {
let mut s = session();
let id = NodeId(
s.execute("text.create", &json!({"x": 10, "y": 10, "text": "AVA", "size": 100, "font": "JetBrains Mono"})).unwrap()["id"].as_u64().unwrap(),
);
s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": "optical"})).unwrap();
let t = obj(&s, id);
let optical = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
let mut manual = t.clone();
vectorcraft_text::edit::style_range(&mut manual.runs, 0, 1, |st| st.kerning = Some(0.0));
let unkerned = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &manual);
let auto = (optical.glyphs[0].advance - unkerned.glyphs[0].advance) * 1000.0 / 100.0;
assert!(auto.abs() > 1.0, "the font needs a measurable shape correction");
s.execute("type.step", &json!({"id": id.0, "start": 1, "end": 1, "attribute": "kerning", "by": 1})).unwrap();
let after = obj(&s, id);
assert!((after.runs[0].style.kerning.unwrap() - auto - s.prefs.tracking_increment).abs() < 0.01);
assert!(after.runs.iter().all(|r| r.style.optical_kerning));
}
#[test]
fn metrics_pair_step_preserves_the_drawn_auto_position() {
use vectorcraft_text::test_fonts::{PAIR_KERNING_FAMILY, pair_kerning_font};
let db = vectorcraft_text::FontDb::global();
db.add_font(pair_kerning_font().unwrap());
let mut s = session();
let id = NodeId(
s.execute("text.create", &json!({"x": 10, "y": 10, "text": "AV", "size": 100, "font": PAIR_KERNING_FAMILY})).unwrap()["id"].as_u64().unwrap(),
);
let before = obj(&s, id);
let auto = vectorcraft_text::layout(db, &before);
let drawn_v = |layout: &vectorcraft_text::TextLayout| layout.glyphs.iter().find(|g| g.byte == 1).unwrap().xf.as_coeffs()[4];
assert!((crate::cmd::effective_kerning(db, &before, &auto, 0, 1) + 160.0).abs() < 0.01);
let mut same = before.clone();
vectorcraft_text::edit::style_range(&mut same.runs, 0, 1, |st| st.kerning = Some(-160.0));
assert!((drawn_v(&vectorcraft_text::layout(db, &same)) - drawn_v(&auto)).abs() < 0.01);
s.execute("type.step", &json!({"id": id.0, "start": 1, "end": 1, "attribute": "kerning", "by": 1})).unwrap();
let after = obj(&s, id);
assert!((after.runs[0].style.kerning.unwrap() + 160.0 - s.prefs.tracking_increment).abs() < 0.01);
let moved = drawn_v(&vectorcraft_text::layout(db, &after)) - drawn_v(&auto);
assert!((moved - s.prefs.tracking_increment * 0.1).abs() < 0.01, "V moves by one kerning increment: {moved}");
}
#[test]
fn effective_auto_kerning_at_area_line_end_survives_unkerned_wrap() {
use vectorcraft_text::test_fonts::{PAIR_KERNING_FAMILY, pair_kerning_font};
let db = vectorcraft_text::FontDb::global();
db.add_font(pair_kerning_font().unwrap());
let style = CharStyle { font_family: PAIR_KERNING_FAMILY.into(), size: 100.0, ..CharStyle::default() };
let mut text = TextObject::point(Point::ZERO, "AV", style);
let mut wrapping_pair = None;
for width in 80..160 {
text.kind = TextKind::Area { frame: vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, f64::from(width), 300.0)) };
let auto = vectorcraft_text::layout(db, &text);
let mut trial = text.clone();
vectorcraft_text::edit::style_range(&mut trial.runs, 0, 1, |st| st.kerning = Some(0.0));
let unkerned = vectorcraft_text::layout(db, &trial);
if auto.glyphs.len() >= 2
&& unkerned.glyphs.len() >= 2
&& auto.glyphs[0].line == auto.glyphs[1].line
&& unkerned.glyphs[0].line != unkerned.glyphs[1].line
{
wrapping_pair = Some((text.clone(), auto, unkerned));
break;
}
}
let (text, auto, unkerned) = wrapping_pair.expect("the kerned pair fits on a line that wraps the unkerned pair");
assert_ne!(unkerned.glyphs[0].line, unkerned.glyphs[1].line);
assert!((crate::cmd::effective_kerning(db, &text, &auto, 0, 1) + 160.0).abs() < 0.01);
}
#[test]
fn effective_optical_kerning_reads_the_right_to_left_pair() {
use vectorcraft_text::test_fonts::{RIGHT_TO_LEFT_OPTICAL_FAMILY, right_to_left_optical_font};
let db = vectorcraft_text::FontDb::with_font_dirs(vec![]);
assert_eq!(db.add_font(right_to_left_optical_font().unwrap()), 1);
let style = CharStyle { font_family: RIGHT_TO_LEFT_OPTICAL_FAMILY.into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
let text = TextObject::point(Point::ZERO, "אב", style);
let optical = vectorcraft_text::layout(&db, &text);
let mut manual = text.clone();
manual.runs[0].style.kerning = Some(0.0);
let unkerned = vectorcraft_text::layout(&db, &manual);
let drawn_gap = |lay: &vectorcraft_text::TextLayout| {
let drawn = |byte| lay.glyphs.iter().find(|g| g.byte == byte).unwrap().xf.as_coeffs()[4];
drawn(2) - drawn(0)
};
let expected = (drawn_gap(&optical) - drawn_gap(&unkerned)) * 10.0;
assert!(expected.abs() > 1.0, "the outlined pair has a measurable Optical correction");
let actual = crate::cmd::effective_kerning(&db, &text, &optical, 0, 2);
assert!((actual - expected).abs() < 0.01, "the caret reads the adjacent right-to-left pair");
}
#[test]
fn reset_tracking_clears_manual_pairs_without_changing_optical_mode() {
let mut s = session();
let id = text(&mut s, "III");
s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": "optical"})).unwrap();
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": 1, "kerning": 80})).unwrap();
s.select_tool("type", ViewInfo::default()).unwrap();
let t = obj(&s, id);
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
let (a, b) = vectorcraft_text::caret_position(&lay, 1);
let p = t.xf * a.midpoint(b);
click(&mut s, p.x, p.y);
assert!(s.tool_wants_text());
s.set_tool_option("select", &json!({"start": 1, "end": 1}));
s.execute("type.resetTracking", &json!({})).unwrap();
let caret = obj(&s, id);
assert_eq!(caret.runs[0].style.kerning, None);
assert!(caret.runs.iter().all(|r| r.style.optical_kerning));
s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": 60, "tracking": 30})).unwrap();
s.set_tool_option("select", &json!({"start": 0, "end": 3}));
s.execute("type.resetTracking", &json!({})).unwrap();
let range = obj(&s, id);
assert!(range.runs.iter().all(|r| r.style.kerning.is_none() && r.style.tracking == 0.0));
assert!(range.runs.iter().all(|r| r.style.optical_kerning));
}
/// A character style applied from byte 2^32 to 2^32 + 5 styles no character, as a range past the
/// end does. A 32-bit build read the range as bytes 0 to 5 and styled "Hello".
#[test]
+277 -5
View File
@@ -1,5 +1,6 @@
//! Shaping: text runs -> positioned glyphs in points (before line breaking).
use std::collections::HashMap;
use std::ops::Range;
use std::sync::Arc;
@@ -8,6 +9,7 @@ use unicode_script::{Script, UnicodeScript};
use unicode_segmentation::UnicodeSegmentation;
use harfrust::{Direction, Feature, ShapeOptions, Tag, UnicodeBuffer};
use kurbo::Shape;
use skrifa::MetadataProvider;
use skrifa::charmap::Charmap;
use skrifa::instance::Size;
@@ -446,6 +448,216 @@ pub(crate) fn shape_range(
{
o.sort_by_key(|g| g.byte);
}
if !feats.vertical {
optical_pairs(db, text, out.get_mut(output_start..).unwrap_or_default(), runs);
}
}
/// Line segments of a glyph outline in font units, cached for the current shaping pass.
type Profile = Vec<(f64, f64, f64, f64)>;
type CachedProfile = (Profile, (f64, f64));
fn outline_profile(db: &FontDb, g: &SGlyph, cache: &mut HashMap<(u32, u32), Option<Arc<CachedProfile>>>) -> Option<Arc<CachedProfile>> {
let key = (g.face.id(), g.gid);
cache
.entry(key)
.or_insert_with(|| {
let path = db.outline(&g.face, g.gid);
let bounds = path.bounding_box();
if path.elements().is_empty()
|| path.elements().len() > 512
|| ![bounds.x0, bounds.y0, bounds.x1, bounds.y1].iter().all(|v| v.is_finite() && v.abs() <= 100_000.0)
{
return None;
}
let mut lines = Vec::new();
let mut previous = None;
let mut contour_start = None;
kurbo::flatten(path.iter(), 2.0, |el| match el {
kurbo::PathEl::MoveTo(p) => {
previous = Some(p);
contour_start = Some(p);
}
kurbo::PathEl::LineTo(p) => {
if let Some(a) = previous
&& lines.len() < 8192
{
lines.push((a.x, a.y, p.x, p.y));
}
previous = Some(p);
}
kurbo::PathEl::ClosePath => {
if let Some((a, b)) = previous.zip(contour_start)
&& lines.len() < 8192
{
lines.push((a.x, a.y, b.x, b.y));
}
previous = None;
contour_start = None;
}
_ => {}
});
(!lines.is_empty() && lines.len() < 8192).then(|| Arc::new((lines, (bounds.y0, bounds.y1))))
})
.clone()
}
/// The left and right ink edges where a horizontal slice crosses an outline.
fn profile_edges(lines: &[(f64, f64, f64, f64)], y: f64, sx: f64, sy: f64, dx: f64, dy: f64) -> Option<(f64, f64)> {
let mut left = f64::INFINITY;
let mut right = f64::NEG_INFINITY;
for &(x0, y0, x1, y1) in lines {
let (a, b) = (y0 * sy + dy, y1 * sy + dy);
if (a <= y && y < b) || (b <= y && y < a) {
let x = (x0 + (x1 - x0) * (y - a) / (b - a)) * sx + dx;
left = left.min(x);
right = right.max(x);
}
}
(left.is_finite() && right.is_finite()).then_some((left, right))
}
#[allow(clippy::too_many_arguments)]
fn profile_correction(
a: &[(f64, f64, f64, f64)],
b: &[(f64, f64, f64, f64)],
left: (f64, f64, f64, f64),
right: (f64, f64, f64, f64),
advance: f64,
em: f64,
upem: f64,
low: f64,
high: f64,
) -> Option<f64> {
if !low.is_finite() || !high.is_finite() || high <= low || !em.is_finite() || em <= 0.0 || !upem.is_finite() || upem <= 0.0 {
return None;
}
let mut gaps = Vec::new();
for j in 0..24 {
let y = low + (high - low) * (f64::from(j) + 0.5) / 24.0;
if let (Some((_, ar)), Some((bl, _))) =
(profile_edges(a, y, left.0, left.1, left.2, left.3), profile_edges(b, y, right.0, right.1, right.2, right.3))
{
gaps.push(advance + bl - ar);
}
}
if gaps.is_empty() {
return None;
}
gaps.sort_by(f64::total_cmp);
let gap = gaps.get(gaps.len() / 2).copied().unwrap_or(0.0);
let unit = em / upem;
let correction = ((0.08 * em - gap).clamp(-0.25 * em, 0.25 * em) / unit).round() * unit;
let correction = correction.clamp(-0.25 * em, 0.25 * em);
correction.is_finite().then_some(correction)
}
/// Move each Optical pair toward an ink gap of 0.08 em, at 24 overlapping vertical slices.
/// The median avoids a single narrow tip deciding the entire pair. Corrections are rounded to
/// one font unit and limited to 1/4 em in either direction. Missing ink gives zero correction.
fn optical_pairs(db: &FontDb, text: &str, glyphs: &mut [SGlyph], runs: &[(Range<usize>, &CharStyle)]) {
let mut cache = HashMap::new();
let mut corrections: HashMap<[u64; 15], Option<f64>> = HashMap::new();
let shaped_advances: Vec<f64> = glyphs.iter().map(|g| g.adv).collect();
for i in 0..glyphs.len().saturating_sub(1) {
let Some((a, b)) = glyphs.get(i).zip(glyphs.get(i + 1)) else { continue };
if a.level != b.level {
continue;
}
let Some(owner) = runs.get(a.run).map(|(_, s)| *s) else { continue };
let invisible = |ch: char| matches!(ch, SOFT_HYPHEN | '\u{2028}') || ch.is_control();
if !owner.optical_kerning
|| a.byte == b.byte
|| invisible(a.ch)
|| invisible(b.ch)
|| text.get(a.byte..b.byte).is_some_and(|between| between.chars().skip(1).any(invisible))
|| a.rotation != 0.0
|| b.rotation != 0.0
{
continue;
}
if owner.kerning.is_some() {
continue;
}
let mut start = i;
while start > 0 && glyphs[start - 1].byte == a.byte {
start -= 1;
}
let mut end = i + 1;
while end + 1 < glyphs.len() && glyphs[end + 1].byte == b.byte {
end += 1;
}
// A mark may precede its base inside either direction's shaped cluster.
let first_base = glyphs[start..=i]
.iter()
.enumerate()
.max_by(|(a, _), (b, _)| shaped_advances[start + *a].total_cmp(&shaped_advances[start + *b]))
.map_or(start, |(offset, _)| start + offset);
let second_base = glyphs[i + 1..=end]
.iter()
.enumerate()
.max_by(|(a, _), (b, _)| shaped_advances[i + 1 + *a].total_cmp(&shaped_advances[i + 1 + *b]))
.map_or(i + 1, |(offset, _)| i + 1 + offset);
let (left_index, right_index, target, cluster_start, cluster_end) =
if a.level.is_rtl() { (second_base, first_base, end, i + 1, end) } else { (first_base, second_base, i, start, i) };
let (left, right) = (&glyphs[left_index], &glyphs[right_index]);
if left.byte == right.byte
|| left.is_space()
|| right.is_space()
|| is_mark(right.ch)
|| left.inline.is_some()
|| right.inline.is_some()
|| left.ch == '\n'
|| right.ch == '\n'
{
continue;
}
let left_y = left.dy - left.bshift;
let right_y = right.dy - right.bshift;
let Some(left_style) = runs.get(left.run).map(|(_, s)| *s) else { continue };
let em = left.sx * left.face.upem / (left_style.h_scale / 100.0);
let tracking = left_style.tracking / 1000.0 * em;
let manual = left_style.kerning.unwrap_or(0.0) / 1000.0 * em;
let advance = glyphs[cluster_start..=cluster_end].iter().map(|g| g.adv).sum::<f64>() - tracking - manual;
let key = [
u64::from(left.face.id()),
u64::from(left.gid),
u64::from(right.face.id()),
u64::from(right.gid),
left.sx.to_bits(),
left.sy.to_bits(),
left.dx.to_bits(),
left_y.to_bits(),
right.sx.to_bits(),
right.sy.to_bits(),
right.dx.to_bits(),
right_y.to_bits(),
advance.to_bits(),
em.to_bits(),
left.face.upem.to_bits(),
];
let correction = *corrections.entry(key).or_insert_with(|| {
let (Some(a), Some(b)) = (outline_profile(db, left, &mut cache), outline_profile(db, right, &mut cache)) else { return None };
let low = (a.1.0 * left.sy + left_y).max(b.1.0 * right.sy + right_y);
let high = (a.1.1 * left.sy + left_y).min(b.1.1 * right.sy + right_y);
profile_correction(
&a.0,
&b.0,
(left.sx, left.sy, left.dx, left_y),
(right.sx, right.sy, right.dx, right_y),
advance,
em,
left.face.upem,
low,
high,
)
});
if let (Some(correction), Some(glyph)) = (correction, glyphs.get_mut(target))
&& !invisible(glyph.ch)
{
glyph.adv += correction;
}
}
}
/// Index in the original run list containing `byte`, limited to one coalesced shaping span.
@@ -474,6 +686,7 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool {
leading: _,
tracking,
kerning: _,
optical_kerning: _,
baseline_shift: _,
h_scale,
v_scale,
@@ -696,6 +909,15 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
};
let manual = |byte: usize| run_at(byte).1.kerning.is_some();
let mixed_kerning = boundaries().any(|b| manual(b) != manual(range.start));
let optical = |byte: usize| run_at(byte).1.optical_kerning;
let mode_boundaries: Vec<usize> = boundaries().filter(|&b| !feats.vertical && optical(b - 1) && !optical(b)).collect();
if !mode_boundaries.is_empty() {
return shape_apart(mode_boundaries, out);
}
let manual_ends: Vec<usize> = boundaries().filter(|&b| manual(b - 1) && !manual(b)).collect();
if !manual_ends.is_empty() {
return shape_apart(manual_ends, out);
}
// Shape the segment as if all of it were in `st`, with the features `more` on top.
let shape_in = |st: &CharStyle, more: &[Feature]| {
@@ -793,17 +1015,43 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
}
shaped
};
let Shaped { raw, proportional, vpal } = if mixed_kerning {
let first_optical = if optical(range.start) { Some(range.start) } else { boundaries().find(|&b| optical(b)) };
let Shaped { raw, proportional, vpal } = if let Some(first) = first_optical.filter(|_| !feats.vertical) {
// The pair entering Optical keeps the left character's font kerning. Subsequent
// Optical pairs use outline spacing, so font pair features stop at its second character.
let optical_off = if first == range.start {
first
} else {
text.get(first..range.end).and_then(|s| s.chars().next()).map_or(range.end, |c| first + c.len_utf8())
};
let first_manual = if manual(range.start) { Some(range.start) } else { boundaries().find(|&b| manual(b)) };
if let Some(start) = first_manual.filter(|&b| b > range.start)
&& upper
&& first_char(start).to_uppercase().nth(1).is_some()
{
return shape_apart(vec![start], out);
}
let manual_off = first_manual.map_or(range.end, |start| {
if start == range.start {
start
} else {
text.get(start..range.end).and_then(|s| s.chars().next()).map_or(range.end, |c| start + c.len_utf8())
}
});
let off = optical_off.min(manual_off);
let more: Vec<Feature> = [b"kern", b"apkn", b"vkrn", b"vapk"]
.into_iter()
.filter(|_| off < range.end)
.map(|t| Feature::new(Tag::new(t), 0, off..range.end))
.collect();
shape_in(&CharStyle { kerning: None, ..run_at(range.start).1.clone() }, &more)
} else if mixed_kerning {
// A font's kerning pair applies after a character with automatic kerning and not after one
// with manual kerning. The segment is shaped apart where manual kerning ends, so each piece
// is text with automatic kerning followed by text with manual kerning. A piece is shaped
// with the font's kerning on, and off (`kern`, and the `apkn`, `vkrn` and `vapk` that
// `OtFeatures` turns off with it) from the second character with manual kerning on: a
// pair is applied only when both of its glyphs have the feature on.
let ends: Vec<usize> = boundaries().filter(|&b| manual(b - 1) && !manual(b)).collect();
if !ends.is_empty() {
return shape_apart(ends, out);
}
let first = boundaries().find(|&b| manual(b)).unwrap_or(range.end);
// A first character with manual kerning that All Caps or Small Caps sets as several letters
// (ß as SS) gives them one cluster, which a feature range cannot split, so the font's
@@ -879,3 +1127,27 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
});
}
}
#[cfg(test)]
mod optical_tests {
use super::*;
fn box_profile(left: f64, right: f64) -> Profile {
vec![(left, 0.0, left, 100.0), (right, 0.0, right, 100.0)]
}
#[test]
fn optical_side_profiles_tighten_loosen_and_stay_bounded() {
let a = box_profile(0.0, 80.0);
let b = box_profile(0.0, 20.0);
let place = (1.0, 1.0, 0.0, 0.0);
let tight = profile_correction(&a, &b, place, place, 120.0, 100.0, 1000.0, 0.0, 100.0).unwrap();
let loose = profile_correction(&a, &b, place, place, 60.0, 100.0, 1000.0, 0.0, 100.0).unwrap();
assert_eq!(tight, -25.0);
assert_eq!(loose, 25.0);
assert_eq!(profile_correction(&a, &b, place, place, 88.0, 100.0, 1000.0, 0.0, 100.0), Some(0.0));
assert_eq!(profile_correction(&a, &b, place, place, 120.0, 100.0, 1000.0, 0.0, 100.0), Some(tight));
assert_eq!(profile_correction(&a, &b, place, place, 120.0, 100.0, 1000.0, 100.0, 100.0), None);
assert_eq!(profile_correction(&[], &b, place, place, 120.0, 100.0, 1000.0, 0.0, 100.0), None);
}
}
+75
View File
@@ -412,6 +412,81 @@ pub fn lam_alef_font(feature: &[u8; 4]) -> Option<Vec<u8>> {
/// The family of [`pair_kerning_font`].
pub const PAIR_KERNING_FAMILY: &str = "Pairtest Sans";
/// The family of [`no_pair_kerning_font`].
pub const NO_PAIR_KERNING_FAMILY: &str = "No Pairtest Sans";
/// A font with the bundled Source Sans 3 outlines and no `GPOS` or `kern` table.
pub fn no_pair_kerning_font() -> Option<Vec<u8>> {
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
let font = skrifa::FontRef::new(BASE).ok()?;
let family = NO_PAIR_KERNING_FAMILY;
let name = name_table(&[(1, family), (2, "Regular"), (4, "No Pairtest Sans Regular"), (6, "NoPairtestSans-Regular")])?;
let mut builder = FontBuilder::new();
builder.add_raw(Tag::new(b"name"), name);
for r in font.table_directory.table_records() {
let tag = Tag::new(&r.tag().to_be_bytes());
if !matches!(tag, t if t == Tag::new(b"name") || t == Tag::new(b"GPOS") || t == Tag::new(b"kern")) {
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
}
}
Some(builder.build())
}
/// The family of [`right_to_left_optical_font`].
pub const RIGHT_TO_LEFT_OPTICAL_FAMILY: &str = "Right-to-left Profile Sans";
/// Two Hebrew characters mapped to the bundled Source Sans 3 A and V outlines.
pub fn right_to_left_optical_font() -> Option<Vec<u8>> {
use write_fonts::tables::cmap::Cmap;
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
let font = skrifa::FontRef::new(BASE).ok()?;
let charmap = font.charmap();
let mapping = [('א', 'A'), ('ב', 'V')];
let mappings = mapping.into_iter().map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
let family = RIGHT_TO_LEFT_OPTICAL_FAMILY;
let name = name_table(&[(1, family), (2, "Regular"), (4, "Right-to-left Profile Sans Regular"), (6, "RtlProfileSans-Regular")])?;
let mut builder = FontBuilder::new();
builder.add_table(&cmap).ok()?;
builder.add_raw(Tag::new(b"name"), name);
for r in font.table_directory.table_records() {
let tag = Tag::new(&r.tag().to_be_bytes());
if tag != Tag::new(b"GPOS") && tag != Tag::new(b"kern") && !builder.contains(tag) {
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
}
}
Some(builder.build())
}
/// The family of [`soft_hyphen_outline_font`].
pub const SOFT_HYPHEN_OUTLINE_FAMILY: &str = "Soft Hyphen Profile Sans";
/// A face that maps the invisible soft hyphen to the bundled hyphen outline.
pub fn soft_hyphen_outline_font() -> Option<Vec<u8>> {
use write_fonts::tables::cmap::Cmap;
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
let font = skrifa::FontRef::new(BASE).ok()?;
let charmap = font.charmap();
let mappings = [('A', 'A'), ('V', 'V'), ('-', '-'), ('\u{00AD}', '-')]
.into_iter()
.map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
let family = SOFT_HYPHEN_OUTLINE_FAMILY;
let name = name_table(&[(1, family), (2, "Regular"), (4, "Soft Hyphen Profile Sans Regular"), (6, "SoftHyphenProfileSans-Regular")])?;
let mut builder = FontBuilder::new();
builder.add_table(&cmap).ok()?;
builder.add_raw(Tag::new(b"name"), name);
for r in font.table_directory.table_records() {
let tag = Tag::new(&r.tag().to_be_bytes());
if tag != Tag::new(b"GPOS") && tag != Tag::new(b"kern") && !builder.contains(tag) {
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
}
}
Some(builder.build())
}
/// The bundled Source Sans 3 Regular as family [`PAIR_KERNING_FAMILY`], its `GPOS` replaced by two
/// `kern` pairs, A followed by V and S followed by S: each takes 60 units off the first glyph's
/// advance (the pair's first value record) and moves the second 100 units left (its second).
+261
View File
@@ -18,6 +18,267 @@ fn run(text: &str, size: f64) -> TextRun {
TextRun { text: text.into(), style: style(size), inline: None }
}
#[test]
fn optical_pairs_cross_paint_runs_and_manual_and_tracking_are_independent() {
let st = CharStyle { font_family: "JetBrains Mono".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
let mut t = TextObject::point(Point::ZERO, "III", st);
let advance = |t: &TextObject| layout(db(), t).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
let optical = advance(&t);
let mut plain = t.clone();
plain.runs[0].style.kerning = Some(0.0);
let unkerned = advance(&plain);
assert!((optical[0] - unkerned[0]).abs() > 0.1, "Optical changes the monospaced pair");
edit::style_range(&mut t.runs, 1, 2, |s| s.fill = vectorcraft_doc::color::Paint::solid(vectorcraft_doc::color::Color::rgb(1.0, 0.0, 0.0)));
let painted = advance(&t);
assert!((painted[0] - optical[0]).abs() < 0.01, "paint does not interrupt Optical");
edit::style_range(&mut t.runs, 0, 1, |s| s.kerning = Some(0.0));
let manual = advance(&t);
assert!((manual[0] - unkerned[0]).abs() < 0.01);
assert!((manual[1] - optical[1]).abs() < 0.01, "the next pair remains Optical");
edit::style_range(&mut t.runs, 0, 3, |s| s.tracking = 100.0);
let tracked = advance(&t);
assert!((tracked[0] - manual[0] - 10.0).abs() < 0.01);
assert!((tracked[1] - manual[1] - 10.0).abs() < 0.01);
for text in ["I I", "I\u{301}"] {
let sample = TextObject::point(Point::ZERO, text, CharStyle { kerning: None, ..t.runs[0].style.clone() });
let mut zero = sample.clone();
zero.runs[0].style.kerning = Some(0.0);
assert_eq!(advance(&sample), advance(&zero), "spaces and marks inside a cluster have no Optical correction");
}
let mut vertical = TextObject::point(Point::ZERO, "II", CharStyle { kerning: None, ..t.runs[0].style.clone() });
vertical.vertical = true;
let mut vertical_zero = vertical.clone();
vertical_zero.runs[0].style.kerning = Some(0.0);
assert_eq!(advance(&vertical), advance(&vertical_zero), "vertical type keeps a neutral correction");
let rtl = TextObject::point(Point::ZERO, "אב", CharStyle { kerning: None, ..t.runs[0].style.clone() });
let rtl_advances = advance(&rtl);
assert!(rtl_advances.len() >= 2 && rtl_advances.iter().all(|v| v.is_finite()));
let mut rtl_zero = rtl.clone();
rtl_zero.runs[0].style.kerning = Some(0.0);
assert!((rtl_advances[1] - advance(&rtl_zero)[1]).abs() <= 25.0, "right-to-left Optical corrections remain bounded");
let mut rtl_manual = rtl.clone();
edit::style_range(&mut rtl_manual.runs, 0, 2, |s| s.kerning = Some(200.0));
let mut rtl_pair_zero = rtl.clone();
edit::style_range(&mut rtl_pair_zero.runs, 0, 2, |s| s.kerning = Some(0.0));
let pair_advance = |t: &TextObject| layout(db(), t).glyphs.iter().find(|g| g.byte == 0).map(|g| g.advance).unwrap();
assert!((pair_advance(&rtl_pair_zero) - pair_advance(&rtl_zero)).abs() < 0.01, "manual zero replaces the right-to-left Optical correction");
assert!((pair_advance(&rtl_manual) - pair_advance(&rtl_pair_zero) - 20.0).abs() < 0.01, "manual spacing changes the adjacent right-to-left pair");
}
#[test]
fn optical_pair_skips_an_invisible_soft_hyphen_with_an_outline() {
use crate::test_fonts::{SOFT_HYPHEN_OUTLINE_FAMILY, soft_hyphen_outline_font};
let db = FontDb::with_font_dirs(vec![]);
assert_eq!(db.add_font(soft_hyphen_outline_font().unwrap()), 1);
let style = CharStyle { font_family: SOFT_HYPHEN_OUTLINE_FAMILY.into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
let face = db.style_face(&style).unwrap();
assert_ne!(face.glyph_for('\u{00AD}'), 0);
assert_eq!(face.glyph_for('\u{00AD}'), face.glyph_for('-'));
let paired = layout(&db, &TextObject::point(Point::ZERO, "AV", style.clone()));
let separated = layout(&db, &TextObject::point(Point::ZERO, "A\u{00AD}V", style.clone()));
let mut manual = style;
manual.kerning = Some(0.0);
let plain = layout(&db, &TextObject::point(Point::ZERO, "A\u{00AD}V", manual));
assert!((paired.glyphs[0].advance - plain.glyphs[0].advance).abs() > 0.1, "A-V has an Optical correction");
// A keeps its uncorrected advance because neither pair across the invisible glyph is measured.
assert!((separated.glyphs[0].advance - plain.glyphs[0].advance).abs() < 0.01);
assert!(separated.glyphs.iter().filter(|g| g.byte == 1).all(|g| g.advance == 0.0), "the soft hyphen remains zero width if emitted");
let v = |lay: &TextLayout| lay.glyphs.iter().find(|g| g.byte == 3).unwrap().origin.x;
assert!((v(&separated) - v(&plain)).abs() < 0.01, "the soft hyphen adds no spacing");
}
#[test]
fn optical_pair_after_separate_combining_mark_uses_base_outline() {
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
let advances = |text: &str, manual: bool| {
let mut style = st.clone();
if manual {
style.kerning = Some(0.0);
}
layout(db(), &TextObject::point(Point::ZERO, text, style)).glyphs.iter().map(|g| (g.byte, g.advance)).collect::<Vec<_>>()
};
let plain = advances("II", false);
let plain_zero = advances("II", true);
let accented = advances("I\u{30d}I", false);
let accented_zero = advances("I\u{30d}I", true);
assert_eq!(accented.len(), 3, "the accent has its own glyph");
assert_eq!(accented[0].0, accented[1].0, "the base and mark share a cluster");
assert!((accented[0].1 - accented_zero[0].1).abs() < 0.01, "the base keeps its advance");
let correction = accented[1].1 - accented_zero[1].1;
assert!((correction - (plain[0].1 - plain_zero[0].1)).abs() < 0.01, "the marked cluster uses the base outline");
assert!(correction.abs() > 0.1, "Optical adjusts the pair after the accent");
}
#[test]
fn optical_pair_after_rtl_separate_mark_uses_base_outline() {
let st = CharStyle { font_family: "Noto Sans Arabic".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
let shaped = |text: &str, manual: bool| {
let mut style = st.clone();
if manual {
style.kerning = Some(0.0);
}
layout(db(), &TextObject::point(Point::ZERO, text, style)).glyphs.iter().map(|g| (g.byte, g.advance)).collect::<Vec<_>>()
};
let plain = shaped("بب", false);
let plain_zero = shaped("بب", true);
let marked = shaped("بَب", false);
let marked_zero = shaped("بَب", true);
let marked_cluster = marked.iter().filter(|g| g.0 == 0).collect::<Vec<_>>();
assert_eq!(marked_cluster.len(), plain.iter().filter(|g| g.0 == 0).count() + 1, "the mark has its own glyph in the base cluster");
assert_eq!(marked_cluster.first().unwrap().1, 0.0, "the cluster starts with a nonspacing glyph");
assert!(marked_cluster.last().unwrap().1 > 0.0, "the base follows the mark");
let correction = marked.iter().filter(|g| g.0 == 4).map(|g| g.1).sum::<f64>() - marked_zero.iter().filter(|g| g.0 == 4).map(|g| g.1).sum::<f64>();
let plain_correction =
plain.iter().filter(|g| g.0 == 2).map(|g| g.1).sum::<f64>() - plain_zero.iter().filter(|g| g.0 == 2).map(|g| g.1).sum::<f64>();
assert!(plain_correction.abs() > 0.1, "Optical adjusts the unmarked pair");
assert!((correction - plain_correction).abs() < 0.01, "the marked cluster uses the base outline");
}
#[test]
fn recurring_optical_pairs_have_equal_corrections() {
let st = CharStyle { font_family: "JetBrains Mono".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
let text = "II".repeat(50);
let optical = layout(db(), &TextObject::point(Point::ZERO, &text, st.clone())).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
let zero = layout(db(), &TextObject::point(Point::ZERO, &text, CharStyle { kerning: Some(0.0), ..st }))
.glyphs
.iter()
.map(|g| g.advance)
.collect::<Vec<_>>();
assert_eq!(optical.len(), zero.len());
let correction = optical[0] - zero[0];
assert!(correction.abs() > 0.1);
for i in 1..optical.len() - 1 {
assert_eq!(optical[i] - zero[i], correction, "pair {i} has the same correction");
}
}
#[test]
fn metrics_pair_survives_transition_to_optical() {
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, ..CharStyle::default() };
let mut text = TextObject::point(Point::ZERO, "AVV", st);
let advances = |text: &TextObject| layout(db(), text).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
let metrics = advances(&text);
let mut unkerned = text.clone();
unkerned.runs[0].style.kerning = Some(0.0);
assert!((metrics[0] - advances(&unkerned)[0]).abs() > 0.1, "the font supplies an AV pair");
edit::style_range(&mut text.runs, 1, 3, |s| s.optical_kerning = true);
let mixed = advances(&text);
assert!((mixed[0] - metrics[0]).abs() < 0.01, "the metrics AV pair crosses the mode change");
let mut optical = TextObject::point(Point::ZERO, "VV", text.runs[1].style.clone());
let optical_pair = advances(&optical)[0];
optical.runs[0].style.kerning = Some(0.0);
assert!((mixed[1] - optical_pair).abs() < 0.01, "the first Optical V keeps its next pair");
assert!((mixed[1] - advances(&optical)[0]).abs() > 0.1, "Optical corrects the next pair");
}
#[test]
fn manual_pair_before_optical_does_not_gain_font_kerning() {
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, ..CharStyle::default() };
let mut text = TextObject::point(Point::ZERO, "AVA", st);
edit::style_range(&mut text.runs, 1, 2, |s| s.kerning = Some(0.0));
edit::style_range(&mut text.runs, 2, 3, |s| s.optical_kerning = true);
let mixed = layout(db(), &text).glyphs[1].advance;
let mut unkerned = TextObject::point(Point::ZERO, "VA", text.runs[1].style.clone());
unkerned.runs[0].style.kerning = Some(0.0);
let manual = layout(db(), &unkerned).glyphs[0].advance;
assert!((mixed - manual).abs() < 0.01, "manual V keeps its unkerned pair with Optical A");
}
#[test]
fn optical_pair_uses_drawn_baseline() {
let st = CharStyle { font_family: "JetBrains Mono".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
let mut text = TextObject::point(Point::ZERO, "II", st);
let first = |text: &TextObject| layout(db(), text).glyphs[0].advance;
let normal = first(&text);
edit::style_range(&mut text.runs, 1, 2, |s| s.baseline_shift = 80.0);
let shifted = first(&text);
assert!((shifted - normal).abs() > 0.1, "a displaced outline changes the Optical correction");
}
#[test]
fn optical_pair_skips_rotated_glyphs() {
let st = CharStyle { font_family: "JetBrains Mono".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
let mut text = TextObject::point(Point::ZERO, "II", st);
let first = |text: &TextObject| layout(db(), text).glyphs[0].advance;
let unrotated = first(&text);
let mut plain = text.clone();
plain.runs[0].style.kerning = Some(0.0);
assert!((unrotated - first(&plain)).abs() > 0.1, "the unrotated pair has an Optical correction");
edit::style_range(&mut text.runs, 1, 2, |s| s.rotation = 20.0);
let rotated = first(&text);
let mut manual_zero = text.clone();
edit::style_range(&mut manual_zero.runs, 0, 1, |s| s.kerning = Some(0.0));
assert!((rotated - first(&manual_zero)).abs() < 0.01, "rotated pairs keep their uncorrected advance");
}
#[test]
fn optical_shaping_keeps_manual_value_on_first_run() {
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, optical_kerning: true, kerning: Some(0.0), ..CharStyle::default() };
let mut text = TextObject::point(Point::ZERO, "AVA", st);
let zero = layout(db(), &text).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
edit::style_range(&mut text.runs, 0, 1, |s| s.kerning = Some(120.0));
let manual = layout(db(), &text).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
assert!((manual[0] - zero[0] - 12.0).abs() < 0.01, "the first run adds its manual value");
assert!((manual[1] - zero[1]).abs() < 0.01, "the following run keeps its advance");
}
#[test]
fn ordinary_and_vertical_segments_keep_their_spacing() {
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, kerning: Some(100.0), ..CharStyle::default() };
let plain = TextObject::point(Point::ZERO, "AVA", st);
let advances = |text: &TextObject| layout(db(), text).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
let expected = advances(&plain);
let mut split = plain.clone();
edit::style_range(&mut split.runs, 1, 2, |s| s.fill = vectorcraft_doc::color::Paint::solid(vectorcraft_doc::color::Color::rgb(1.0, 0.0, 0.0)));
assert_eq!(advances(&split), expected, "a non-Optical segment keeps the same spacing across runs");
let mut vertical = plain;
vertical.vertical = true;
let expected = advances(&vertical);
edit::style_range(&mut vertical.runs, 0, 1, |s| s.kerning = Some(120.0));
let manual = advances(&vertical);
assert!((manual[0] - expected[0] - 2.0).abs() < 0.01, "vertical manual spacing changes only its own advance");
edit::style_range(&mut vertical.runs, 0, 3, |s| s.optical_kerning = true);
assert_eq!(advances(&vertical), manual, "vertical type keeps its spacing with Optical selected");
}
#[test]
fn optical_spacing_works_without_font_pair_tables() {
use crate::test_fonts::{NO_PAIR_KERNING_FAMILY, no_pair_kerning_font};
let db = FontDb::with_font_dirs(vec![]);
assert_eq!(db.add_font(no_pair_kerning_font().unwrap()), 1);
let st = CharStyle { font_family: NO_PAIR_KERNING_FAMILY.into(), size: 100.0, ..CharStyle::default() };
let mut text = TextObject::point(Point::ZERO, "AV", st);
let metrics = layout(&db, &text).glyphs[0].advance;
text.runs[0].style.kerning = Some(0.0);
let manual_zero = layout(&db, &text).glyphs[0].advance;
assert!((metrics - manual_zero).abs() < 0.01, "the font has no automatic pair correction");
text.runs[0].style.kerning = None;
text.runs[0].style.optical_kerning = true;
let optical = layout(&db, &text).glyphs[0].advance;
assert!((optical - manual_zero).abs() > 0.1, "Optical uses outlines without a pair table");
}
#[test]
fn manual_pair_positions_match_under_auto_and_optical_in_both_directions() {
use crate::test_fonts::{RIGHT_TO_LEFT_OPTICAL_FAMILY, right_to_left_optical_font};
let db = FontDb::with_font_dirs(vec![]);
assert_eq!(db.add_font(right_to_left_optical_font().unwrap()), 1);
for (letters, family) in [("AV", "Source Sans 3"), ("אב", RIGHT_TO_LEFT_OPTICAL_FAMILY)] {
let mut text = TextObject::point(Point::ZERO, letters, CharStyle { font_family: family.into(), size: 100.0, ..CharStyle::default() });
edit::style_range(&mut text.runs, 0, letters.chars().next().unwrap().len_utf8(), |s| s.kerning = Some(200.0));
let positions = |text: &TextObject| layout(&db, text).glyphs.iter().map(|g| (g.byte, g.origin, g.advance)).collect::<Vec<_>>();
let auto = positions(&text);
edit::style_range(&mut text.runs, 0, letters.len(), |s| s.optical_kerning = true);
assert_eq!(positions(&text), auto, "{letters}: manual pair has the same positions in both modes");
}
}
fn area_path(text: &str, st: CharStyle, frame: &BezPath, justify: Justify) -> TextObject {
let mut t = TextObject::point(Point::ZERO, text, st);
t.kind = TextKind::Area { frame: PathData::from_bezpath(frame) };
+24
View File
@@ -719,5 +719,29 @@ fn proportional_metrics_kern_the_glyphs_they_respace() {
};
let (auto, manual) = (lay(true, None), lay(true, Some(0.0)));
assert!(auto.iter().zip(&manual).any(|(a, b)| (a - b).abs() > 0.01), "kerned: {auto:?} {manual:?}");
let optical = {
let s = CharStyle { font_family: "Noto Sans CJK SC".into(), optical_kerning: true, ..st(true, None) };
layout(&db, &vertical_styled("「カタカナ」とトップ、ヤッホー。", s)).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
};
assert!(auto.iter().zip(&optical).all(|(a, b)| (a - b).abs() < 0.01), "vertical Optical uses font metrics: {auto:?} {optical:?}");
assert!(lay(false, None).iter().all(|a| (a - 20.0).abs() < 0.01), "off: an em each, as before");
}
#[test]
fn vertical_optical_to_auto_keeps_the_fonts_pair() {
use crate::test_fonts::{PAIR_KERNING_FAMILY, pair_kerning_font};
let db = FontDb::with_font_dirs(vec![]);
db.add_font(pair_kerning_font().unwrap());
let style = CharStyle { font_family: PAIR_KERNING_FAMILY.into(), size: 100.0, ..CharStyle::default() };
let positions =
|t: &TextObject| layout(&db, t).glyphs.iter().map(|g| (g.advance, g.origin, g.xf.as_coeffs()[4], g.xf.as_coeffs()[5])).collect::<Vec<_>>();
let auto = vertical_styled("AV", style.clone());
let mut mixed = auto.clone();
edit::style_range(&mut mixed.runs, 0, 1, |s| s.optical_kerning = true);
let mut unkerned = auto.clone();
edit::style_range(&mut unkerned.runs, 0, 1, |s| s.kerning = Some(0.0));
let expected = positions(&auto);
assert_eq!(expected.len(), 2);
assert_ne!(expected, positions(&unkerned), "the font's A-V pair changes vertical positions");
assert_eq!(positions(&mixed), expected, "vertical Optical keeps the A-V font pair across the style boundary");
}
+3 -1
View File
@@ -1587,7 +1587,7 @@ font-technology Variable Променливи
Keep the pasted color as a new swatch with a numbered name. Запазване на поставения цвят като нов образец с номерирано име.
Keep this row's colors (exclude) Запазване на цветовете на този ред (изключване)
Keep: Запазване:
Kerning (0 = Auto) Кернинг (0 = Автоматично)
Kerning Кернинг
Keyboard Increment Стъпка с клавиатурата
Keyboard Shortcut Conflicts Конфликти на клавишни комбинации
Keyboard Shortcut Set Набор клавишни комбинации
@@ -2159,6 +2159,7 @@ font-technology Variable Променливи
Open the URL in the web browser Отваряне на URL адреса в уеб браузъра
Opened with {} note(s): {} Отворено с {} бележка(и): {}
Open… Отваряне…
Optical Оптично
Optical Margin Alignment Подравняване по оптично поле
Optimize for fast web view Оптимизиране за бърз уеб преглед
Optimized Оптимизирано
@@ -3761,6 +3762,7 @@ axis Both И двете
Float Tool Group Плаваща група инструменти
@msg no library `{id}` няма библиотека `{id}`
@msg select some type first първо изберете текст
@msg kerning must be a number, "auto", or "optical" Кернингът трябва да е число, „auto“ или „optical“
@msg {_1} has no {_2} style {_1} няма стил {_2}
@msg a layer couldn't be read слой не можа да бъде прочетен
@msg it has more than one artboard има повече от една работна област
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1411,7 +1411,8 @@ lighting Light Svjetlo
Fractional Widths Djelimične širine
Hide Options Sakrij opcije
Horizontal Scale % Vodoravna skala %
Kerning (0 = Auto) Podrezivanje (0 = automatski)
Kerning Podrezivanje
Optical Optičko
Leading Prored
No Break Bez preloma
Proportional Metrics Proporcionalna metrika
@@ -4114,6 +4115,7 @@ imageTrace Outline Obris
@plural {n} stroke|{n} strokes {n} potez|{n} poteza|{n} poteza
@msg no library `{id}` nema biblioteke `{id}`
@msg select some type first prvo odaberite tekst
@msg kerning must be a number, "auto", or "optical" Podrezivanje mora biti broj, „auto” ili „optical”
@msg the selection has no gradient fill odabir nema ispunu gradijentom
@msg the selection has no gradient stroke odabir nema potez gradijentom
@msg the selection has no pattern fill odabir nema ispunu uzorkom
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -2046,8 +2046,8 @@
Keep visible surfaces only when expanding Conserva només les superfícies visibles en expandir
Keep: Conserva:
Keeps the document editable: VectorCraft reopens the PDF with nothing lost. For files you'll edit again. Manté el document editable: VectorCraft reobre el PDF sense perdre res. Per a fitxers que editaràs de nou.
Kerning (0 = Auto) Kerning (0 = automàtic)
Key Color: Color clau:
Kerning Kerning
Keyboard Increment Increment de teclat
Keyboard Shortcut Conflicts Conflictes de dreceres de teclat
Keyboard Shortcut Set Conjunt de dreceres de teclat
@@ -2740,6 +2740,7 @@
Opens files in other apps Obre fitxers en altres aplicacions
Opens network connections Obre connexions de xarxa
Open… Obre…
Optical Òptic
Optical Margin Alignment Alineació òptica de marges
Optimize for fast web view Optimitza per a la visualització web ràpida
Optimized Optimitzat
@@ -5354,6 +5355,7 @@
@msg select paths or anchor points selecciona traçats o punts d’ancoratge
@msg select paths to join selecciona traçats per unir
@msg select some type first selecciona text primer
@msg kerning must be a number, "auto", or "optical" El kerning ha de ser un nombre, «auto» o «optical»
@msg Select text or click into text with the Type tool to insert glyphs Selecciona text o fes clic dins del text amb l’eina de text per inserir glifs
@msg select the art to export first selecciona primer la il·lustració que vols exportar
@msg select the art to repeat selecciona la il·lustració que vols repetir
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1436,7 +1436,6 @@ lighting Light Světlo
Fractional Widths Zlomkové šířky
Hide Options Skrýt možnosti
Horizontal Scale % Vodorovná změna velikosti %
Kerning (0 = Auto) Vyrovnání párů (0 = automaticky)
Leading Proklad
No Break Nezalamovat
Proportional Metrics Proporcionální metrika
@@ -3514,6 +3513,7 @@ theme Appearance Motiv
Identify Objects By Identifikovat objekty podle
Include SVG Code Zahrnout kód SVG
Italian Italština
Kerning Vyrovnání párů
Keyboard Increment Krok klávesnice
Language Options Možnosti jazyka
Large Velké
@@ -3556,6 +3556,7 @@ theme Appearance Motiv
On Paste Při vložení
On Screen Na obrazovce
Open Documents As Tabs Otevírat dokumenty jako karty
Optical Optické
Options Možnosti
Output All Blacks Accurately Ve výstupu všechny černé přesně
Output All Blacks as Rich Black Ve výstupu všechny černé jako sytou černou
@@ -4151,6 +4152,7 @@ imageTrace Outline Obrys
# Status and error messages
@msg no library `{id}` knihovna `{id}` neexistuje
@msg select some type first nejprve vyberte text
@msg kerning must be a number, "auto", or "optical" Vyrovnání párů musí být číslo, „auto“ nebo „optical“
@msg the selection has no gradient fill výběr nemá přechodovou výplň
@msg the selection has no gradient stroke výběr nemá přechodový tah
@msg the selection has no pattern fill výběr nemá vzorkovou výplň
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1351,7 +1351,6 @@ lighting Light Lys
Fractional Widths Fraktionerede bredder
Hide Options Skjul indstillinger
Horizontal Scale % Vandret skalering %
Kerning (0 = Auto) Kerning (0 = automatisk)
Leading Linjeafstand
No Break Ingen ombrydning
Proportional Metrics Proportionale metrikker
@@ -3271,6 +3270,7 @@ theme Appearance Udseende
Identify Objects By Identificér objekter ud fra
Include SVG Code Medtag SVG-kode
Italian Italiensk
Kerning Kerning
Keyboard Increment Tastaturtrin
Language Options Sprogindstillinger
Large Stor
@@ -3299,6 +3299,7 @@ theme Appearance Udseende
On Paste Ved indsættelse
On Screen På skærmen
Open Documents As Tabs Åbn dokumenter som faner
Optical Optisk
Options Indstillinger
Output All Blacks Accurately Udskriv alle sorte nøjagtigt
Output All Blacks as Rich Black Udskriv alle sorte som rich black
@@ -3881,6 +3882,7 @@ imageTrace Outline Kontur
@plural {n} stroke|{n} strokes {n} kontur|{n} konturer
@msg no library `{id}` intet bibliotek `{id}`
@msg select some type first markér først noget tekst
@msg kerning must be a number, "auto", or "optical" Kerning skal være et tal, „auto“ eller „optical“
@msg {_1} has no {_2} style {_1} har ingen {_2}-stil
@msg a layer couldn't be read et lag kunne ikke læses
@msg it has more than one artboard den har mere end én tegneflade
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1625,7 +1625,7 @@ file-filter Templates Vorlagen
Keep the pasted color as a new swatch with a numbered name. Die eingefügte Farbe als neues Farbfeld mit nummeriertem Namen behalten.
Keep this row's colors (exclude) Farben dieser Zeile beibehalten (ausschließen)
Keep: Beibehalten:
Kerning (0 = Auto) Kerning (0 = Auto)
Kerning Kerning
Keyboard Increment Schritte per Tastatur
Keyboard Shortcut Conflicts Konflikte bei Tastaturbefehlen
Keyboard Shortcut Set Tastaturbefehlssatz
@@ -2172,6 +2172,7 @@ file-filter Templates Vorlagen
Open the URL in the web browser Die URL im Webbrowser öffnen
Opened with {} note(s): {} Mit {} Hinweis(en) geöffnet: {}
Open… Öffnen…
Optical Optisch
Optical Margin Alignment Optischer Randausgleich
Optimize for fast web view Für schnelle Webanzeige optimieren
Optimized Optimiert
@@ -3763,6 +3764,7 @@ theme Appearance Erscheinungsbild
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` keine Bibliothek `{id}`
@msg select some type first wählen Sie zuerst Text aus
@msg kerning must be a number, "auto", or "optical" Kerning muss eine Zahl, „auto“ oder „optical“ sein
@msg the selection has no gradient fill die Auswahl hat keinen Flächenverlauf
@msg the selection has no gradient stroke die Auswahl hat keinen Konturverlauf
@msg the selection has no pattern fill die Auswahl hat kein Flächenmuster
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1543,7 +1543,7 @@ font-technology Variable Μεταβλητές
Keep Plain Text Διατήρηση απλού κειμένου
Keep this row's colors (exclude) Διατήρηση των χρωμάτων αυτής της γραμμής (εξαίρεση)
Keep: Διατήρηση:
Kerning (0 = Auto) Διαγραμμάτωση (0 = Αυτόματο)
Kerning Διαγραμμάτωση
Keyboard Increment Βήμα πληκτρολογίου
Keyboard Shortcut Conflicts Διενέξεις συντομεύσεων πληκτρολογίου
Keyboard Shortcut Set Σύνολο συντομεύσεων πληκτρολογίου
@@ -2080,6 +2080,7 @@ axis Both Και τα δύο
Dock Panel Αγκύρωση πάνελ
@msg no library `{id}` Δεν υπάρχει βιβλιοθήκη με αναγνωριστικό `{id}`
@msg select some type first Επιλέξτε πρώτα κάποιο κείμενο
@msg kerning must be a number, "auto", or "optical" Η διαγραμμάτωση πρέπει να είναι αριθμός, «auto» ή «optical»
@msg the selection has no gradient fill η επιλογή δεν έχει ντεγκραντέ γεμίσματος
@msg the selection has no gradient stroke η επιλογή δεν έχει ντεγκραντέ περιγράμματος
@msg the selection has no pattern fill η επιλογή δεν έχει μοτίβο γεμίσματος
@@ -2886,6 +2887,7 @@ theme Appearance Εμφάνιση
Open Swatch Library Άνοιγμα βιβλιοθήκης δειγμάτων χρωμάτων
Open the URL in the web browser Άνοιγμα της διεύθυνσης στο πρόγραμμα περιήγησης
Opened with {} note(s): {} Άνοιξε με {} σημειώσεις: {}
Optical Οπτική
Optical Margin Alignment Οπτική στοίχιση περιθωρίου
Optimize for fast web view Βελτιστοποίηση για γρήγορη προβολή στον ιστό
Options for Selection… Επιλογές για την επιλογή…
Can't render this file because it contains an unexpected character in line 164 and column 5.
+3 -1
View File
@@ -1625,7 +1625,7 @@ font-technology Variable Variables
Keep the pasted color as a new swatch with a numbered name. Conservar el color pegado como una muestra nueva con nombre numerado.
Keep this row's colors (exclude) Conservar los colores de esta fila (excluir)
Keep: Conservar:
Kerning (0 = Auto) Interletraje (0 = auto)
Kerning Interletraje
Keyboard Increment Incremento de teclado
Keyboard Shortcut Conflicts Conflictos de métodos abreviados de teclado
Keyboard Shortcut Set Conjunto de métodos abreviados de teclado
@@ -2214,6 +2214,7 @@ font-technology Variable Variables
Open the URL in the web browser Abrir la URL en el navegador web
Opened with {} note(s): {} Abierto con {} nota(s): {}
Open… Abrir…
Optical Óptico
Optical Margin Alignment Alineación óptica de márgenes
Optimize for fast web view Optimizar para vista web rápida
Optimized Optimizado
@@ -3843,6 +3844,7 @@ axis Both Ambos
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` no hay ninguna biblioteca `{id}`
@msg select some type first selecciona primero algún texto
@msg kerning must be a number, "auto", or "optical" El interletraje debe ser un número, «auto» u «optical»
@msg the selection has no gradient fill la selección no tiene un degradado de relleno
@msg the selection has no gradient stroke la selección no tiene un degradado de trazo
@msg the selection has no pattern fill la selección no tiene un motivo de relleno
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1411,7 +1411,6 @@ lighting Light Valo
Fractional Widths Murtolukuleveydet
Hide Options Piilota asetukset
Horizontal Scale % Vaakaskaalaus %
Kerning (0 = Auto) Parivälistys (0 = automaattinen)
Leading Riviväli
No Break Ei katkoa
Proportional Metrics Suhteelliset metriikat
@@ -3453,6 +3452,7 @@ theme Appearance Ulkoasu
Identify Objects By Tunnista objektit
Include SVG Code Sisällytä SVG-koodi
Italian Italia
Kerning Parivälistys
Keyboard Increment Näppäimistön lisäys
Language Options Kieliasetukset
Large Suuri
@@ -3481,6 +3481,7 @@ theme Appearance Ulkoasu
On Paste Liitettäessä
On Screen Näytöllä
Open Documents As Tabs Avaa asiakirjat välilehtinä
Optical Optinen
Options Asetukset
Output All Blacks Accurately Tulosta kaikki mustat tarkasti
Output All Blacks as Rich Black Tulosta kaikki mustat syvämustina
@@ -4067,6 +4068,7 @@ imageTrace Outline Ääriviiva
@plural {n} stroke|{n} strokes {n} viiva|{n} viivaa
@msg no library `{id}` ei kirjastoa `{id}`
@msg select some type first valitse ensin tekstiä
@msg kerning must be a number, "auto", or "optical" Parivälistyksen on oltava luku, ”auto” tai ”optical”
@msg the selection has no gradient fill valinnalla ei ole liukuväritäyttöä
@msg the selection has no gradient stroke valinnalla ei ole liukuväriviivaa
@msg the selection has no pattern fill valinnalla ei ole kuviotäyttöä
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1625,7 +1625,7 @@ font-technology Variable Variables
Keep the pasted color as a new swatch with a numbered name. Conserver la couleur collée comme nouvelle nuance avec un nom numéroté.
Keep this row's colors (exclude) Conserver les couleurs de cette ligne (exclure)
Keep: Conserver :
Kerning (0 = Auto) Crénage (0 = auto)
Kerning Crénage
Keyboard Increment Incrément clavier
Keyboard Shortcut Conflicts Conflits de raccourcis clavier
Keyboard Shortcut Set Jeu de raccourcis clavier
@@ -2212,6 +2212,7 @@ font-technology Variable Variables
Open the URL in the web browser Ouvrir l’URL dans le navigateur web
Opened with {} note(s): {} Ouvert avec {} remarque(s) : {}
Open… Ouvrir…
Optical Optique
Optical Margin Alignment Alignement optique des marges
Optimize for fast web view Optimiser pour un affichage web rapide
Optimized Optimisé
@@ -3839,6 +3840,7 @@ axis Both Les deux
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` aucune bibliothèque `{id}`
@msg select some type first sélectionnez d’abord du texte
@msg kerning must be a number, "auto", or "optical" Le crénage doit être un nombre, « auto » ou « optical »
@msg the selection has no gradient fill la sélection n’a pas de dégradé de fond
@msg the selection has no gradient stroke la sélection n’a pas de dégradé de contour
@msg the selection has no pattern fill la sélection n’a pas de motif de fond
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -2044,8 +2044,8 @@ file-filter Templates Sablonok
Keep visible surfaces only when expanding Kibontáskor csak a látható felületek megtartása
Keep: Megtartás:
Keeps the document editable: VectorCraft reopens the PDF with nothing lost. For files you'll edit again. Szerkeszthető marad a dokumentum: a VectorCraft veszteség nélkül nyitja meg újra a PDF-et. Azokhoz a fájlokhoz, amelyeket még szerkesztesz.
Kerning (0 = Auto) Alávágás (0 = automatikus)
Key Color: Kulcsszín:
Kerning Alávágás
Keyboard Increment Billentyűzetes lépésköz
Keyboard Shortcut Conflicts Billentyűparancs-ütközések
Keyboard Shortcut Set Billentyűparancs-készlet
@@ -2688,6 +2688,7 @@ file-filter Templates Sablonok
“{name}” already exists. Replace it? „{name}” már létezik. Felülírod?
The name in the save panel was “{typed}”; VectorCraft added the file type's extension. Replacing the file overwrites its contents. A mentési panelen megadott név ez volt: „{typed}”, a VectorCraft pedig hozzáadta a fájltípus kiterjesztését. A fájl felülírásakor a tartalma elvész.
Open… Megnyitás…
Optical Optikai
Optical Margin Alignment Optikai margóigazítás
Optimize for fast web view Optimalizálás gyors webes megtekintéshez
Optimized Optimalizált
@@ -5044,6 +5045,7 @@ imageTrace Outline Körvonal
@msg select paths or anchor points jelölj ki útvonalakat vagy horgonypontokat
@msg select paths to join jelöld ki az összekapcsolandó útvonalakat
@msg select some type first előbb jelölj ki szöveget
@msg kerning must be a number, "auto", or "optical" Az alávágás szám, „auto” vagy „optical” lehet
@msg the selection has no gradient fill a kijelölésnek nincs színátmenetes kitöltése
@msg the selection has no gradient stroke a kijelölésnek nincs színátmenetes körvonala
@msg the selection has no pattern fill a kijelölésnek nincs mintázatos kitöltése
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1351,7 +1351,6 @@ lighting Light Cahaya
Fractional Widths Lebar fraksional
Hide Options Sembunyikan opsi
Horizontal Scale % Skala horizontal %
Kerning (0 = Auto) Kerning (0 = Otomatis)
Leading Leading
No Break Tanpa pemenggalan
Proportional Metrics Metrik proporsional
@@ -3271,6 +3270,7 @@ theme Appearance Tampilan
Identify Objects By Identifikasi objek berdasarkan
Include SVG Code Sertakan kode SVG
Italian Italia
Kerning Kerning
Keyboard Increment Inkremen papan ketik
Language Options Opsi bahasa
Large Besar
@@ -3299,6 +3299,7 @@ theme Appearance Tampilan
On Paste Saat menempel
On Screen Di layar
Open Documents As Tabs Buka dokumen sebagai tab
Optical Optis
Options Opsi
Output All Blacks Accurately Keluarkan semua warna hitam secara akurat
Output All Blacks as Rich Black Keluarkan semua warna hitam sebagai rich black
@@ -3881,6 +3882,7 @@ imageTrace Outline Kontur
@plural {n} stroke|{n} strokes {n} goresan
@msg no library `{id}` tidak ada pustaka `{id}`
@msg select some type first pilih beberapa teks terlebih dahulu
@msg kerning must be a number, "auto", or "optical" Kerning harus berupa angka, “auto”, atau “optical”
@msg {_1} has no {_2} style {_1} tidak memiliki gaya {_2}
@msg a layer couldn't be read satu lapisan tidak dapat dibaca
@msg it has more than one artboard memiliki lebih dari satu artboard
Can't render this file because it contains an unexpected character in line 2175 and column 5.
+3 -1
View File
@@ -1650,7 +1650,7 @@ file-filter Templates Modelli
Keep the pasted color as a new swatch with a numbered name. Mantieni il colore incollato come nuovo campione con un nome numerato.
Keep this row's colors (exclude) Mantieni i colori di questa riga (escludi)
Keep: Mantieni:
Kerning (0 = Auto) Crenatura (0 = auto)
Kerning Crenatura
Keyboard Increment Incremento da tastiera
Keyboard Shortcut Conflicts Conflitti tra scelte rapide da tastiera
Keyboard Shortcut Set Set di scelte rapide da tastiera
@@ -2241,6 +2241,7 @@ file-filter Templates Modelli
Open the URL in the web browser Apri l'URL nel browser web
Opened with {} note(s): {} Aperto con note ({}): {}
Open… Apri…
Optical Ottica
Optical Margin Alignment Allineamento ottico dei margini
Optimize for fast web view Ottimizza per visualizzazione web veloce
Optimized Ottimizzato
@@ -3883,6 +3884,7 @@ axis Both Entrambi
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` nessuna libreria `{id}`
@msg select some type first seleziona prima del testo
@msg kerning must be a number, "auto", or "optical" La crenatura deve essere un numero, «auto» o «optical»
@msg the selection has no gradient fill la selezione non ha una sfumatura di riempimento
@msg the selection has no gradient stroke la selezione non ha una sfumatura di traccia
@msg the selection has no pattern fill la selezione non ha un motivo di riempimento
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1880,7 +1880,7 @@ font-technology Variable バリアブル
Keep Plain Text テキストのみを保持
Keep both sides the same width 両側を同じ幅に保つ
Keep this row's colors (exclude) この行のカラーを保持 (除外)
Kerning (0 = Auto) カーニング (0 = 自動)
Kerning カーニング
Keyboard Increment キー入力
Keyboard Shortcut Conflicts キーボードショートカットの競合
Keyboard Shortcut Set キーボードショートカットセット
@@ -2346,6 +2346,7 @@ font-technology Variable バリアブル
Open the URL in the web browser URL を Web ブラウザーで開く
Optimize for fast web view Web ビュー用に最適化
Optimized 最適化
Optical オプティカル
Options オプション
Options for Selection… 選択項目のオプション…
Options: オプション:
@@ -4573,6 +4574,7 @@ swatch Bright Violet ビビッドバイオレット
@msg choose a folder to export to 書き出し先のフォルダーを選択してください
@msg the selection has no URL (or they differ) 選択したオブジェクトに URL がありません (または URL がそれぞれ異なります)
@msg select some type first 先にテキストを選択してください
@msg kerning must be a number, "auto", or "optical" カーニングは数値、「auto」または「optical」にしてください
@msg the selection has no gradient fill 選択したオブジェクトに塗りのグラデーションがありません
@msg the selection has no gradient stroke 選択したオブジェクトに線のグラデーションがありません
@msg the selection has no pattern fill 選択したオブジェクトに塗りのパターンがありません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1268,7 +1268,6 @@ lighting Light სინათლე
Fractional Widths წილადი სიგანეები
Hide Options პარამეტრების დამალვა
Horizontal Scale % ჰორიზონტალური მასშტაბი %
Kerning (0 = Auto) კერნინგი (0 = ავტო)
Leading სტრიქონთა დაშორება
No Break გადატანის გარეშე
Proportional Metrics პროპორციული მეტრიკა
@@ -3082,6 +3081,7 @@ theme Appearance იერსახე
Identify Objects By ობიექტების იდენტიფიკაცია:
Include SVG Code SVG კოდის ჩართვა
Italian იტალიური
Kerning კერნინგი
Keyboard Increment კლავიატურის ბიჯი
Language Options ენის პარამეტრები
Large დიდი
@@ -3110,6 +3110,7 @@ theme Appearance იერსახე
On Paste ჩასმისას
On Screen ეკრანზე
Open Documents As Tabs დოკუმენტების ჩანართებად გახსნა
Optical ოპტიკური
Options პარამეტრები
Output All Blacks Accurately ყველა შავის ზუსტად გამოტანა
Output All Blacks as Rich Black ყველა შავის მდიდარ შავად გამოტანა
@@ -3647,6 +3648,7 @@ font-technology Variable ცვლადი
@plural {n} stroke|{n} strokes კონტური: {n}
@msg no library `{id}` ბიბლიოთეკა `{id}` არ არსებობს
@msg select some type first ჯერ მონიშნეთ ტექსტი
@msg kerning must be a number, "auto", or "optical" კერნინგი უნდა იყოს რიცხვი, „auto“ ან „optical“
@msg the selection has no gradient fill მონიშნულს შევსების გრადიენტი არ აქვს
@msg the selection has no gradient stroke მონიშნულს კონტურის გრადიენტი არ აქვს
@msg the selection has no pattern fill მონიშნულს შევსების პატერნი არ აქვს
Can't render this file because it is too large.
+3 -1
View File
@@ -1909,7 +1909,7 @@ font-technology Variable 가변
Keep Plain Text 일반 텍스트 유지
Keep both sides the same width 양쪽 너비를 같게 유지
Keep this row's colors (exclude) 이 행의 색상 유지 (제외)
Kerning (0 = Auto) 커닝 (0 = 자동)
Kerning 커닝
Keyboard Increment 키보드 증분
Keyboard Shortcut Conflicts 단축키 충돌
Keyboard Shortcut Set 단축키 세트
@@ -2337,6 +2337,7 @@ font-technology Variable 가변
Open the URL in the web browser 웹 브라우저에서 URL 열기
Optimize for fast web view 빠른 웹 보기용으로 최적화
Optimized 최적화됨
Optical 광학
Options 옵션
Options for Selection… 선택 항목 옵션…
Options: 옵션:
@@ -4501,6 +4502,7 @@ theme Appearance 외관
@msg choose a folder to export to 내보낼 폴더를 선택하세요
@msg the selection has no URL (or they differ) 선택 항목에 URL이 없거나 서로 다릅니다
@msg select some type first 먼저 문자를 선택하세요
@msg kerning must be a number, "auto", or "optical" 커닝은 숫자, “auto” 또는 “optical”이어야 합니다
@msg the selection has no gradient fill 선택 항목에 칠 그레이디언트가 없습니다
@msg the selection has no gradient stroke 선택 항목에 선 그레이디언트가 없습니다
@msg the selection has no pattern fill 선택 항목에 칠 패턴이 없습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1365,7 +1365,6 @@ lighting Light Licht
Fractional Widths Fractionele breedtes
Hide Options Opties verbergen
Horizontal Scale % Horizontale schaal %
Kerning (0 = Auto) Kerning (0 = Auto)
Leading Regelafstand
No Break Niet afbreken
Proportional Metrics Proportionele metingen
@@ -3285,6 +3284,7 @@ theme Appearance Vormgeving
Identify Objects By Objecten identificeren op
Include SVG Code SVG-code opnemen
Italian Italiaans
Kerning Kerning
Keyboard Increment Toetsenbordincrement
Language Options Taalopties
Large Groot
@@ -3313,6 +3313,7 @@ theme Appearance Vormgeving
On Paste Bij plakken
On Screen Op scherm
Open Documents As Tabs Documenten openen als tabbladen
Optical Optisch
Options Opties
Output All Blacks Accurately Alle zwarttinten nauwkeurig uitvoeren
Output All Blacks as Rich Black Alle zwarttinten uitvoeren als rijk zwart
@@ -3895,6 +3896,7 @@ imageTrace Outline Contour
@plural {n} stroke|{n} strokes {n} lijn|{n} lijnen
@msg no library `{id}` geen bibliotheek `{id}`
@msg select some type first selecteer eerst wat tekst
@msg kerning must be a number, "auto", or "optical" Kerning moet een getal, ‘auto’ of ‘optical’ zijn
@msg {_1} has no {_2} style {_1} heeft geen {_2}stijl
@msg a layer couldn't be read een laag kon niet worden gelezen
@msg it has more than one artboard het heeft meer dan één tekengebied
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1350,7 +1350,6 @@ lighting Light Światło
Fractional Widths Szerokości ułamkowe
Hide Options Ukryj opcje
Horizontal Scale % Skala pozioma %
Kerning (0 = Auto) Kerning (0 = auto)
Leading Interlinia
No Break Bez podziału
Proportional Metrics Metryka proporcjonalna
@@ -3392,6 +3391,7 @@ theme Appearance Wygląd
Identify Objects By Identyfikuj obiekty według
Include SVG Code Dołącz kod SVG
Italian Włoski
Kerning Kerning
Keyboard Increment Przyrost klawiatury
Language Options Opcje języka
Large Duże
@@ -3420,6 +3420,7 @@ theme Appearance Wygląd
On Paste Przy wklejaniu
On Screen Na ekranie
Open Documents As Tabs Otwieraj dokumenty jako karty
Optical Optyczny
Options Opcje
Output All Blacks Accurately Wyprowadzaj wszystkie czernie dokładnie
Output All Blacks as Rich Black Wyprowadzaj wszystkie czernie jako czerń głęboką
@@ -3975,6 +3976,7 @@ imageTrace Outline Kontur
@plural {n} stroke|{n} strokes {n} obrys|{n} obrysy|{n} obrysów
@msg no library `{id}` brak biblioteki `{id}`
@msg select some type first najpierw zaznacz tekst
@msg kerning must be a number, "auto", or "optical" Kerning musi być liczbą, „auto” lub „optical”
@msg the selection has no gradient fill zaznaczenie nie ma gradientu wypełnienia
@msg the selection has no gradient stroke zaznaczenie nie ma gradientu obrysu
@msg the selection has no pattern fill zaznaczenie nie ma wzorka wypełnienia
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1400,7 +1400,7 @@
Keep Plain Text Manter Texto Simples
Keep both sides the same width Manter os dois lados com a mesma largura
Keep this row's colors (exclude) Manter as cores desta linha (excluir)
Kerning (0 = Auto) Kerning (0 = Automático)
Kerning Kerning
Keyboard Increment Incremento do Teclado
Keyboard Shortcut Conflicts Conflitos de Atalhos de Teclado
Keyboard Shortcut Set Conjunto de Atalhos de Teclado
@@ -1810,6 +1810,7 @@
Open password: Senha de abertura:
Open the URL in the web browser Abra a URL no navegador da web
Open… Abrir…
Optical Óptico
Optical Margin Alignment Alinhamento de Margem Óptica
Optimize for fast web view Otimizar para visualização rápida na web
Optimized Otimizado
@@ -3430,6 +3431,7 @@ axis Both Ambos
Drag to the window's left edge to dock Arraste até a borda esquerda da janela para encaixar
@msg no library `{id}` nenhuma biblioteca `{id}`
@msg select some type first selecione algum texto primeiro
@msg kerning must be a number, "auto", or "optical" O kerning deve ser um número, “auto” ou “optical”
@msg the selection has no gradient fill a seleção não tem gradiente de preenchimento
@msg the selection has no gradient stroke a seleção não tem gradiente de traçado
@msg the selection has no pattern fill a seleção não tem padrão de preenchimento
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -1
View File
@@ -1648,7 +1648,7 @@ file-filter Templates Шаблоны
Keep the pasted color as a new swatch with a numbered name. Сохранять вставленный цвет как новый образец с номерным именем.
Keep this row's colors (exclude) Сохранить цвета этой строки (исключить)
Keep: Сохранять:
Kerning (0 = Auto) Кернинг (0 = авто)
Kerning Кернинг
Keyboard Increment Приращение с клавиатуры
Keyboard Shortcut Conflicts Конфликты сочетаний клавиш
Keyboard Shortcut Set Набор сочетаний клавиш
@@ -2237,6 +2237,7 @@ file-filter Templates Шаблоны
Open the URL in the web browser Открыть URL в веб-браузере
Opened with {} note(s): {} Открыто с примечаниями ({}) : {}
Open… Открыть…
Optical Оптический
Optical Margin Alignment Оптическое выравнивание полей
Optimize for fast web view Оптимизировать для быстрого просмотра в web
Optimized Оптимизировано
@@ -3879,6 +3880,7 @@ axis Both Оба
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
@msg no library `{id}` нет библиотеки `{id}`
@msg select some type first сначала выделите текст
@msg kerning must be a number, "auto", or "optical" Кернинг должен быть числом, «auto» или «optical»
@msg the selection has no gradient fill у выделения нет градиентной заливки
@msg the selection has no gradient stroke у выделения нет градиентной обводки
@msg the selection has no pattern fill у выделения нет узорной заливки
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1587,7 +1587,7 @@ font-technology Variable Variabla
Keep the pasted color as a new swatch with a numbered name. Behåll den inklistrade färgen som en ny färgruta med numrerat namn.
Keep this row's colors (exclude) Behåll radens färger (undanta)
Keep: Behåll:
Kerning (0 = Auto) Kerning (0 = auto)
Kerning Kerning
Keyboard Increment Tangentbordssteg
Keyboard Shortcut Conflicts Konflikter mellan kortkommandon
Keyboard Shortcut Set Uppsättning kortkommandon
@@ -2159,6 +2159,7 @@ font-technology Variable Variabla
Open the URL in the web browser Öppna URL:en i webbläsaren
Opened with {} note(s): {} Öppnad med {} anteckning(ar): {}
Open… Öppna…
Optical Optisk
Optical Margin Alignment Optisk marginaljustering
Optimize for fast web view Optimera för snabb webbvisning
Optimized Optimerad
@@ -3761,6 +3762,7 @@ axis Both Båda
Float Tool Group Gör verktygsgruppen flytande
@msg no library `{id}` inget bibliotek `{id}`
@msg select some type first markera någon text först
@msg kerning must be a number, "auto", or "optical" Kerning måste vara ett tal, ”auto” eller ”optical”
@msg the selection has no gradient fill markeringen har ingen fyllnadsgradient
@msg the selection has no gradient stroke markeringen har ingen konturgradient
@msg the selection has no pattern fill markeringen har inget fyllnadsmönster
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1 -1
View File
@@ -576,7 +576,7 @@ fn character_panel_terms_agree_within_each_catalog() {
for l in LANGUAGES.iter().filter(|l| !l.source.is_empty()) {
let (entries, _) = parse_entries(l.source);
let row = |src: &str| entries.iter().find(|(ctx, s, _)| ctx.is_empty() && s == src).map(|(_, _, tr)| tr.to_lowercase());
let kerning = row("Kerning (0 = Auto)").map(|tr| tr.split(" (").next().unwrap_or_default().trim().to_string());
let kerning = row("Kerning");
let leading = row("Leading");
let tracking = row("Tracking");
for (a, b) in [(&kerning, &leading), (&kerning, &tracking), (&leading, &tracking)] {
+3 -1
View File
@@ -1474,7 +1474,7 @@ font-technology Variable Değişken
Keep the pasted color as a new swatch with a numbered name. Yapıştırılan rengi numaralı bir adla yeni bir renk örneği olarak koruyun.
Keep this row's colors (exclude) Bu satırın renklerini koru (hariç tut)
Keep: Sakla:
Kerning (0 = Auto) Karakter Aralığı (0 = Otomatik)
Kerning Karakter aralığı
Keyboard Increment Klavye Artışı
Keyboard Shortcut Conflicts Klavye Kısayolu Çakışmaları
Keyboard Shortcut Set Klavye Kısayol Seti
@@ -2038,6 +2038,7 @@ shape Open Açık
Open the URL in the web browser URL'yi web tarayıcısında açın
Opened with {} note(s): {} {} notla açıldı: {}
Open… Aç…
Optical Optik
Optical Margin Alignment Optik Kenar Boşluğu Hizalaması
Optimize for fast web view Hızlı web görünümü için optimize edin
Optimized Optimize edilmiş
@@ -3655,6 +3656,7 @@ axis Both Her İkisi
Float Tool Group Araç Grubunu Yüzer Yap
@msg no library `{id}` `{id}` kütüphanesi yok
@msg select some type first önce bir tür seçin
@msg kerning must be a number, "auto", or "optical" Karakter aralığı sayı, “auto” veya “optical” olmalıdır
@msg the selection has no gradient fill seçimin degrade dolgusu yok
@msg the selection has no gradient stroke seçimin degrade konturu yok
@msg the selection has no pattern fill seçimin desen dolgusu yok
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3 -1
View File
@@ -1644,7 +1644,7 @@ file-filter Templates Шаблони
Keep the pasted color as a new swatch with a numbered name. Зберігати вставлений колір як новий зразок із номерним ім’ям.
Keep this row's colors (exclude) Зберегти кольори цього рядка (виключити)
Keep: Зберігати:
Kerning (0 = Auto) Кернінг (0 = авто)
Kerning Кернінг
Keyboard Increment Крок клавіатури
Keyboard Shortcut Conflicts Конфлікти поєднань клавіш
Keyboard Shortcut Set Набір клавіш
@@ -2233,6 +2233,7 @@ file-filter Templates Шаблони
Open the URL in the web browser Відкрити URL у веб-браузері
Opened with {} note(s): {} Відкрито із примітками ({}) : {}
Open… Відкрити…
Optical Оптичний
Optical Margin Alignment Оптичне вирівнювання полів
Optimize for fast web view Оптимізувати для швидкого перегляду web
Optimized Оптимізовано
@@ -3867,6 +3868,7 @@ axis Both Обидві
Float Tool Group Від’єднати групу інструментів
@msg no library `{id}` немає бібліотеки `{id}`
@msg select some type first спочатку виділіть текст
@msg kerning must be a number, "auto", or "optical" Кернінг має бути числом, «auto» або «optical»
@msg the selection has no gradient fill виділення не має градієнтної заливки
@msg the selection has no gradient stroke виділення не має градієнтного обведення
@msg the selection has no pattern fill виділення не має візерункової заливки
Can't render this file because it contains an unexpected character in line 178 and column 5.
+3 -1
View File
@@ -1922,8 +1922,8 @@ file-filter Templates Mẫu
Keep this row's colors (exclude) Giữ màu của hàng này (loại trừ)
Keep visible surfaces only when expanding Chỉ giữ bề mặt hiển thị khi mở rộng
Keep: Giữ:
Kerning (0 = Auto) Kerning (0 = Tự động)
Key Color: Màu chủ:
Kerning Kerning
Keyboard Increment Bước nhảy bàn phím
Keyboard Shortcut Conflicts Xung đột phím tắt
Keyboard Shortcut Set Bộ phím tắt
@@ -2499,6 +2499,7 @@ file-filter Templates Mẫu
“{name}” already exists. Replace it? “{name}” đã tồn tại. Thay thế tệp này?
The name in the save panel was “{typed}”; VectorCraft added the file type's extension. Replacing the file overwrites its contents. Tên trong bảng lưu là “{typed}”, và VectorCraft đã thêm phần mở rộng của loại tệp. Thay thế tệp sẽ ghi đè nội dung của tệp.
Open… Mở…
Optical Quang học
Optical Margin Alignment Căn lề quang học
Optimize for fast web view Tối ưu để xem web nhanh
Optimized Đã tối ưu
@@ -4696,6 +4697,7 @@ theme Appearance Giao diện
@msg select paths or anchor points chọn đường dẫn hoặc điểm neo
@msg select paths to join hãy chọn đường dẫn để nối
@msg select some type first hãy chọn chữ trước
@msg kerning must be a number, "auto", or "optical" Kerning phải là số, “auto” hoặc “optical”
@msg the selection has no gradient fill vùng chọn không có tô chuyển sắc
@msg the selection has no gradient stroke vùng chọn không có nét chuyển sắc
@msg the selection has no pattern fill vùng chọn không có tô họa tiết
Can't render this file because it contains an unexpected character in line 200 and column 5.
+3 -1
View File
@@ -1575,7 +1575,7 @@ file-filter Templates 模板
Keep the pasted color as a new swatch with a numbered name. 将粘贴的颜色保留为新色板,并以编号命名。
Keep this row's colors (exclude) 保留此行的颜色 (排除)
Keep: 保留:
Kerning (0 = Auto) 字偶距 (0 = 自动)
Kerning 字偶距
Keyboard Increment 键盘增量
Keyboard Shortcut Conflicts 键盘快捷键冲突
Keyboard Shortcut Set 键盘快捷键组
@@ -2113,6 +2113,7 @@ file-filter Templates 模板
Opened with {} note(s): {} 已打开,附 {} 条附注:{}
OpenType OpenType
Open… 打开…
Optical 视觉
Optical Margin Alignment 视觉边界对齐
Optimize for fast web view 优化快速 Web 查看
Optimized 优化
@@ -4290,6 +4291,7 @@ imageTrace Outline 轮廓
@msg choose a folder to export to 请选择要导出到的文件夹
@msg the selection has no URL (or they differ) 所选对象没有 URL (或其 URL 各不相同)
@msg select some type first 请先选择一些文字
@msg kerning must be a number, "auto", or "optical" 字偶距必须是数字、“auto”或“optical”
@msg the selection has no gradient fill 所选对象没有渐变填色
@msg the selection has no gradient stroke 所选对象没有渐变描边
@msg the selection has no pattern fill 所选对象没有图案填色
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3 -1
View File
@@ -1614,7 +1614,7 @@ file-filter Templates 範本
Keep the pasted color as a new swatch with a numbered name. 將貼上的顏色保留為新色票,並以編號命名。
Keep this row's colors (exclude) 保留此列的顏色 (排除)
Keep: 保留:
Kerning (0 = Auto) 字距微調 (0 = 自動)
Kerning 字距微調
Keyboard Increment 鍵盤增量
Keyboard Shortcut Conflicts 鍵盤快速鍵衝突
Keyboard Shortcut Set 鍵盤快速鍵組
@@ -2228,6 +2228,7 @@ file-filter Templates 範本
@msg choose a folder to export to 請選擇要匯出至的資料夾
@msg the selection has no URL (or they differ) 選取範圍沒有 URL (或其 URL 各不相同)
@msg select some type first 請先選取一些文字
@msg kerning must be a number, "auto", or "optical" 字距微調必須是數字、「auto」或「optical」
@msg the selection has no gradient fill 選取範圍沒有漸層填色
@msg the selection has no gradient stroke 選取範圍沒有漸層筆畫
@msg the selection has no pattern fill 選取範圍沒有圖樣填色
@@ -3047,6 +3048,7 @@ file-filter Templates 範本
Opened with {} note(s): {} 已開啟,附 {} 則附註:{}
OpenType OpenType
Open… 開啟…
Optical 視覺
Optical Margin Alignment 視覺邊界對齊
Optimize for fast web view 最佳化以便快速網頁檢視
Optimized 最佳化
Can't render this file because it contains an unexpected character in line 6 and column 47.
+55 -5
View File
@@ -222,7 +222,7 @@ fn char_cmd(app: &mut VectorcraftApp, cmd: &str, p: Value) {
range_style(app, p);
}
/// Set the Kerning field's `value` (a number or "auto"). At a caret in text the Type tool edits
/// Set the Kerning field's `value` (a number, "auto", or "optical"). At a caret in text the Type tool edits
/// it is the pair kerning after the character before the caret, as Alt+←/→ steps it
/// (`type.step`); nowhere to set at the start of the text. Else it is a character attribute like
/// the others: the selected range, or all the text.
@@ -244,6 +244,37 @@ fn set_kerning(app: &mut VectorcraftApp, value: Value) {
app.run("text.setRangeStyle", json!({"id": id.0, "start": start, "end": a, "kerning": value})).ok();
}
/// The pair before the caret, in 1/1000 em, using the layout the canvas draws.
fn caret_kerning(app: &VectorcraftApp, ui: &Ui) -> Option<(f64, bool)> {
let (id, a, b) = text_editing(app)?;
if a != b {
return None;
}
let NodeKind::Text(t) = &app.session.active()?.doc.node(id)?.kind else { return None };
let text = t.plain_text();
let start = vectorcraft_text::edit::prev_char(&text, a);
if start >= a {
return None;
}
let style = vectorcraft_text::edit::style_at(&t.runs, start + 1);
if let Some(v) = style.kerning {
return Some((v, false));
}
let state = app.session.active()?;
let key = (state.uid, state.revision, id, a);
let slot = ui.scope_id().with("caret-kerning");
if let Some((cached_key, value)) = ui.ctx().data(|d| d.get_temp::<((u64, u64, NodeId, usize), f64)>(slot))
&& cached_key == key
{
return Some((value, true));
}
let original = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
let value = vectorcraft_engine::cmd::effective_kerning(vectorcraft_text::FontDb::global(), t, &original, start, a);
let value = value.round();
ui.ctx().data_mut(|d| d.insert_temp(slot, (key, value)));
Some((value, true))
}
/// The faces Type › Bold and Type › Italic switch to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Face {
@@ -637,14 +668,33 @@ pub(crate) fn metrics_grid(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, s: &
match widgets::spin_field_auto(ui, "ch-lead", leading, Some(auto), unit, fw, 1.0, 0.1, &SIZE_PRESETS) {
Some(SpinPick::Value(v)) => style(app, json!({"leading": v})),
Some(SpinPick::Auto) => style(app, json!({"leading": "auto"})),
Some(SpinPick::Optical) => {}
None => {}
}
});
ui.end_row();
cell(ui, "VA", tl!("Kerning (0 = Auto)"), |ui| {
let kerning = shared(app, |s| s.kerning.unwrap_or(0.0));
if let Some(v) = widgets::spin_plain(ui, "ch-kern", kerning, "", 0, fw, 10.0, -1000.0, &TRACKING_PRESETS) {
set_kerning(app, if v == 0.0 { json!("auto") } else { json!(v) });
cell(ui, "VA", tl!("Kerning"), |ui| {
let mode = shared(app, |s| (s.optical_kerning, s.kerning));
let caret = text_editing(app).is_some_and(|(_, a, b)| a == b);
let at_caret = if caret { caret_kerning(app, ui) } else { None };
let kerning = at_caret.map(|(v, _)| v).or_else(|| mode.map(|(_, manual)| manual.unwrap_or(0.0)));
let automatic = mode.map(|(optical, manual)| !optical && manual.is_none());
let optical = mode.map(|(optical, _)| optical);
match widgets::spin_kerning(
ui,
"ch-kern",
kerning,
automatic,
optical,
tl!("Optical"),
at_caret.is_some_and(|(_, auto)| auto),
fw,
&TRACKING_PRESETS,
) {
Some(SpinPick::Value(v)) => set_kerning(app, json!(v)),
Some(SpinPick::Auto) => set_kerning(app, json!("auto")),
Some(SpinPick::Optical) => set_kerning(app, json!("optical")),
None => {}
}
});
cell(ui, "VA↔", tl!("Tracking"), |ui| {
@@ -130,6 +130,38 @@ fn kerning_at_a_caret_sets_the_pair_not_the_whole_text() {
assert_eq!((kerning(&app, 1), kerning(&app, 2), kerning(&app, 3)), (Some(-50.0), Some(30.0), Some(30.0)));
}
#[test]
fn character_kerning_setter_and_automatic_pair_value() {
let german = crate::i18n::Lang::from_code("de").unwrap();
assert!(crate::i18n::has(german, "Optical"));
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.session.select_tool("type", Default::default()).unwrap();
app.run("text.create", json!({"x": 20, "y": 50, "text": "III", "size": 100, "font": "JetBrains Mono"})).unwrap();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 1, "end": 1}));
set_kerning(&mut app, json!("optical"));
assert!(text_style(&app).unwrap().0.optical_kerning);
let ctx = egui::Context::default();
let mut shown = None;
ctx.run_ui(Default::default(), |ui| {
shown = caret_kerning(&app, ui);
})
.textures_delta
.clear();
let (value, automatic) = shown.unwrap();
assert!(automatic && value != 0.0);
set_kerning(&mut app, json!(50));
ctx.run_ui(Default::default(), |ui| {
shown = caret_kerning(&app, ui);
})
.textures_delta
.clear();
assert_eq!(shown, Some((50.0, false)));
app.session.set_tool_option("select", &json!({"start": 0, "end": 2}));
assert_eq!(shared(&app, |s| (s.optical_kerning, s.kerning)), None, "a mixed range shows a blank Kerning field");
}
/// Highlight Kerning and Highlight Tracking show the Character panel and give its field the
/// keyboard focus with the value selected: what is typed replaces it, and Enter applies it.
/// Highlight Font Name opens the font family menu with its search field focused. Without
+1 -1
View File
@@ -51,7 +51,7 @@ pub(crate) use numeric::{ANY, round_to, set_bare_numbers_are_points, typed_unit}
pub use numeric::{
SpinPick, mixed_field, mixed_field_named, num_field, num_field_named, num_field_within, opt_field, opt_field_named, range_field,
range_field_named, range_field_stepped, size_field, size_field_named, slider, spin_field, spin_field_auto, spin_field_auto_named,
spin_field_named, spin_field_stops_named, spin_plain, spin_plain_named,
spin_field_named, spin_field_stops_named, spin_kerning, spin_plain, spin_plain_named,
};
#[cfg(test)]
use numeric::{decimal_point, share_of, spinner_step, step_through, step_to_multiple, within_a_millionth};
+50 -9
View File
@@ -346,7 +346,7 @@ pub fn range_field_named(
let places = decimals.min(6) as i32;
let value = value.into().map(|v| within(v, &range));
let label = take_previous_label(ui);
bounded_field(ui, id, label, name, value, suffix, decimals, width, &range, None).map(|v| within(round_to(v, places), &range))
bounded_field(ui, id, label, name, value, suffix, decimals, width, &range, None, false).map(|v| within(round_to(v, places), &range))
}
/// [`range_field`] whose ↑/↓ and wheel take `step`'s steps instead of whole numbers (see
@@ -366,7 +366,7 @@ pub fn range_field_stepped(
let places = decimals.min(6) as i32;
let value = value.into().map(|v| within(v, &range));
let label = take_previous_label(ui);
bounded_field(ui, id, label, None, value, suffix, decimals, width, &range, Some(step)).map(|v| within(round_to(v, places), &range))
bounded_field(ui, id, label, None, value, suffix, decimals, width, &range, Some(step), false).map(|v| within(round_to(v, places), &range))
}
/// [`plain_field`] for a value the selected objects may not share: `None` shows a blank field.
@@ -385,7 +385,7 @@ pub fn mixed_field_named(
width: f32,
) -> Option<f64> {
let label = take_previous_label(ui);
bounded_field(ui, id, label, name, value, suffix, decimals, width, &ANY, None)
bounded_field(ui, id, label, name, value, suffix, decimals, width, &ANY, None, false)
}
/// [`mixed_field`] whose ↑/↓, wheel and scrub stop at `bounds`; ↑/↓ and the wheel take `step`'s steps
@@ -403,6 +403,7 @@ fn bounded_field(
width: f32,
bounds: &RangeInclusive<f64>,
step: Option<fn(f64, f64) -> f64>,
parentheses: bool,
) -> Option<f64> {
let armed = take_armed_focus(ui, &id);
let id = id_with(ui.scope_id(), id);
@@ -412,9 +413,9 @@ fn bounded_field(
let suffix = suffix.trim();
let buf = decimal_point(buf);
let bare = if suffix.is_empty() { buf.to_string() } else { buf.replace(suffix, "") };
vectorcraft_doc::parse_number(&bare.replace(['%', '°'], ""))
vectorcraft_doc::parse_number(&bare.replace(['%', '°', '(', ')'], ""))
};
let shown = value.map(show).unwrap_or_default();
let shown = value.map(|v| if parentheses { format!("({})", show(v)) } else { show(v) }).unwrap_or_default();
take_dialog_focus(ui, id, &shown);
if armed {
focus_field(ui, id, &shown);
@@ -508,7 +509,7 @@ pub fn spin_field_stops_named(
presets: &[f64],
) -> Option<f64> {
let label = take_previous_label(ui);
spin_generic(ui, label, value, None, width, step, min, presets, presets, &|v| unit.format(v), &mut |ui, fw, label| {
spin_generic(ui, label, value, None, None, None, width, step, min, presets, presets, &|v| unit.format(v), &mut |ui, fw, label| {
unit_field(ui, id, label, name, value, unit, fw, false, min..=f64::INFINITY, presets)
})
.and_then(SpinPick::value)
@@ -519,6 +520,7 @@ pub fn spin_field_stops_named(
pub enum SpinPick {
Value(f64),
Auto,
Optical,
}
impl SpinPick {
@@ -527,6 +529,7 @@ impl SpinPick {
match self {
SpinPick::Value(v) => Some(v),
SpinPick::Auto => None,
SpinPick::Optical => None,
}
}
}
@@ -563,7 +566,7 @@ pub fn spin_field_auto_named(
presets: &[f64],
) -> Option<SpinPick> {
let label = take_previous_label(ui);
spin_generic(ui, label, value, auto, width, step, min, presets, &[], &|v| unit.format(v), &mut |ui, fw, label| {
spin_generic(ui, label, value, auto, None, None, width, step, min, presets, &[], &|v| unit.format(v), &mut |ui, fw, label| {
unit_field(ui, id, label, name, value, unit, fw, false, min..=f64::INFINITY, &[])
})
}
@@ -602,12 +605,43 @@ pub fn spin_plain_named(
let label = take_previous_label(ui);
let value = value.into();
let fmt = |v: f64| format!("{v}{suffix}");
spin_generic(ui, label, value, None, width, step, min, presets, &[], &fmt, &mut |ui, fw, label| {
bounded_field(ui, id, label, name, value, suffix, decimals, fw, &(min..=f64::INFINITY), None)
spin_generic(ui, label, value, None, None, None, width, step, min, presets, &[], &fmt, &mut |ui, fw, label| {
bounded_field(ui, id, label, name, value, suffix, decimals, fw, &(min..=f64::INFINITY), None, false)
})
.and_then(SpinPick::value)
}
/// Kerning's numeric field and automatic method choices.
#[allow(clippy::too_many_arguments)]
pub fn spin_kerning(
ui: &mut Ui,
id: impl std::hash::Hash + Copy,
value: Option<f64>,
automatic: Option<bool>,
optical: Option<bool>,
optical_label: &str,
parentheses: bool,
width: f32,
presets: &[f64],
) -> Option<SpinPick> {
let label = take_previous_label(ui);
spin_generic(
ui,
label,
value,
automatic,
optical,
Some(optical_label),
width,
10.0,
-1000.0,
presets,
&[],
&|v| format!("{v}"),
&mut |ui, fw, label| bounded_field(ui, id, label, None, value, "", 0, fw, &(-1000.0..=f64::INFINITY), None, parentheses),
)
}
type SpinField<'a> = dyn FnMut(&mut Ui, f32, Option<PreviousLabel>) -> Option<f64> + 'a;
#[allow(clippy::too_many_arguments)]
@@ -616,6 +650,8 @@ fn spin_generic(
label: Option<PreviousLabel>,
value: Option<f64>,
auto: Option<bool>,
optical: Option<bool>,
optical_label: Option<&str>,
width: f32,
step: f64,
min: f64,
@@ -674,6 +710,11 @@ fn spin_generic(
{
out = Some(SpinPick::Auto);
}
if let Some(on) = optical
&& ui.selectable_label(on, optical_label.unwrap_or_default()).clicked()
{
out = Some(SpinPick::Optical);
}
let current = value.filter(|_| auto != Some(true));
for p in presets {
if ui.selectable_label(current.is_some_and(|v| (v - p).abs() < 1e-6), fmt(*p)).clicked() {
+12 -1
View File
@@ -3217,7 +3217,8 @@ Pen's second anchor at (150, 431), in line with the square's centre.
leading (↑/↓ in horizontal type, ←/→ in vertical type), and baseline shift (Shift+↑/↓ in horizontal type,
Shift+→/← in vertical type). Mixed selected text objects use the horizontal mapping; Cmd/Ctrl makes five steps.
`type.resetTracking` (Cmd+Alt+Q, `press_key {key: "Q", mods: {cmd: true, alt: true}}`) sets tracking to 0 and
kerning to Auto in the type objects in `ids`, the Type tool's selected text or the selected type objects; at a caret
clears manual kerning in the type objects in `ids`, the Type tool's selected text or the selected type objects,
keeping Optical; at a caret
it resets only the kerning of the character before it. `type.resetHorizontalScale` (Cmd+Shift+X,
`press_key {key: "X", mods: {cmd: true, shift: true}}`) sets the horizontal scale to 100%, as the Character panel
shows it, in the same text; at a caret it leaves the text as it is. In the app, `type.highlightKerning` (Cmd+Alt+K),
@@ -4918,6 +4919,16 @@ the paragraph's text now starts; when the text before `at` starts no list, nothi
{"name":"run_command","arguments":{"command":"text.convertListToText","params":{}}}
```
## Kerning
`text.setRangeStyle {id, start, end, kerning}` (a range) and `text.setFormat {ids?, kerning}`
(the selected type) accept `kerning: "auto" | "optical" | number` (1/1000 em). Auto uses the font's
pair metrics; Optical spaces horizontal pairs from their glyph shapes. Vertical text uses font
metrics in either mode. A number sets a manual pair value,
replacing that pair's automatic correction while tracking remains additive. Choosing Optical keeps
existing manual pair values; choosing `"auto"` clears them. New text and files without an Optical setting use Auto. The Character
panel offers both automatic modes.
## Small Caps, Superscript and Subscript
`text.setFormat {smallCaps, position}` (the selected type, or `ids`) and `text.setRangeStyle {id, start, end, smallCaps,
position}` (a range) set what the Character panel's Small Caps, Superscript and Subscript buttons and menu items set.
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -24,7 +24,7 @@ can use installed system fonts, while the web app uses the fonts supplied by its
6. Adjust **Font Size** (the field beside **T**) and **Tracking** (beside **VA↔**) in the Character
panel. Hover a field's label to see its name. For example, enter `72 pt` for a large heading and
try tracking `50` to spread its letters slightly. Positive tracking adds space; negative tracking
tightens it. **Leading** controls the spacing between lines; **Kerning (0 = Auto)** adjusts pairs.
tightens it. **Leading** controls the spacing between lines; **Kerning** adjusts pairs.
7. To change only part of the text, select **Type Tool**, click into the text and drag across the
characters before using the Character panel. Press **Escape** when finished. Keep the text
editable until its wording, font and spacing are settled.