diff --git a/crates/cad/Cargo.toml b/crates/cad/Cargo.toml index 4886d9ea5..2ea6dea4f 100644 --- a/crates/cad/Cargo.toml +++ b/crates/cad/Cargo.toml @@ -21,5 +21,6 @@ image = { workspace = true } [dev-dependencies] serde_json = { workspace = true } -# The tests lay type out in the bundled fonts (the workspace leaves turning them on to the engine). -vectorcraft-text = { workspace = true, features = ["bundled-fonts"] } +# The tests lay type out in the bundled fonts (the workspace leaves turning them on to the engine) +# and in fonts built in code (`vectorcraft_text::test_fonts`). +vectorcraft-text = { workspace = true, features = ["bundled-fonts", "test-fonts"] } diff --git a/crates/cad/src/scene.rs b/crates/cad/src/scene.rs index 6ae29487a..ea3b8558d 100644 --- a/crates/cad/src/scene.rs +++ b/crates/cad/src/scene.rs @@ -1062,7 +1062,8 @@ impl<'a> Scene<'a> { } /// Type: glyph outlines (Preserve Appearance, Outline Text, type on a path), else one text - /// entity per run of each line. + /// entity per run of each line, whose height (group 40) is [`CAP_HEIGHT`] times the em size its + /// run is drawn at ([`vectorcraft_text::FontDb::font_heights_resolved`]). fn text(&mut self, n: &Node, t: &TextObject) { let outline = self.o.preserve == Preserve::Appearance || self.o.outline_text || matches!(t.kind, TextKind::OnPath { .. }); if outline { @@ -1074,6 +1075,8 @@ impl<'a> Scene<'a> { if !n.appearance.items.is_empty() { self.warn(TYPE_ITEMS); } + let resolved = vectorcraft_text::FontDb::global().font_heights_resolved(t); + let t = &*resolved; let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); let plain = t.plain_text(); let xf = self.to_dxf * t.xf; diff --git a/crates/cad/src/tests.rs b/crates/cad/src/tests.rs index ba0a13b76..f1b537b70 100644 --- a/crates/cad/src/tests.rs +++ b/crates/cad/src/tests.rs @@ -906,6 +906,23 @@ fn text_outside_ascii_is_escaped_before_2007() { assert_eq!(text(DxfVersion::R2018), "Größe"); } +/// Type whose font size measures the cap height is written at the em size it is drawn at: 14 pt +/// measuring a 0.7 em cap height is 20 pt type, whose TEXT height is 14. +#[test] +fn a_font_height_reference_is_written_as_the_size_drawn() { + use vectorcraft_text::test_fonts::{HEIGHT_FAMILY, height_font}; + vectorcraft_text::FontDb::global().add_font(height_font(true).unwrap()); + let mut d = text_doc("Plan A", 14.0); + let id = d.layers[0].children().and_then(|c| c.first()).map(|n| n.id).unwrap(); + if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) { + t.runs[0].style.font_family = HEIGHT_FAMILY.into(); + t.runs[0].style.font_height_reference = vectorcraft_doc::FontHeightReference::CapHeight; + } + let editable = export(&d, &DxfOptions { preserve: Preserve::Editability, ..opts(&d) }).unwrap(); + let e = entities(&editable); + assert!((all(&e[0], 40)[0] - 14.0).abs() < 1e-6, "{:?}", all(&e[0], 40)); +} + #[test] fn images_link_png_or_jpeg_files() { let mut png = vec![]; diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 26e2bfebf..8d3b2a883 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -106,6 +106,7 @@ pub use selection::{AnchorRef, SegmentRef, Selection}; pub use setup::{Background, DocSetup, ExportText, GridSize, Quotes}; pub use slices::{CellAlign, CellVAlign, Slice, SliceArea, SliceKind, SliceOptions, SliceSource}; pub use style_libs::StyleLibrary; +pub use text::FontHeightReference; pub use text::{ AreaFit, AreaOptions, Burasagari, CharAlign, CharPosition, CharStyle, Composer, Digits, FirstBaseline, HyphenationOptions, InlineArt, JustificationOptions, Justify, Kinsoku, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, RubyAlign, RubyPosition, diff --git a/crates/doc/src/text.rs b/crates/doc/src/text.rs index c1a36e058..675245b56 100644 --- a/crates/doc/src/text.rs +++ b/crates/doc/src/text.rs @@ -15,7 +15,7 @@ mod tabs_lists; pub use area::{AreaFit, AreaOptions, PathAlign, PathEffect, TextFlow, TextWrap, VerticalAlign, WrapShape}; use char_style::yes; -pub use char_style::{CharAlign, CharPosition, CharStyle, Digits, ScriptMetrics}; +pub use char_style::{CharAlign, CharPosition, CharStyle, Digits, FontHeightReference, ScriptMetrics}; pub use para_style::{ Burasagari, Composer, FirstBaseline, HyphenationOptions, JustificationOptions, Justify, Kashida, Kinsoku, LeadingModel, Mojikumi, ParaDirection, ParaStyle, SingleWordJustify, diff --git a/crates/doc/src/text/char_style.rs b/crates/doc/src/text/char_style.rs index dd84912f0..b5bc7e065 100644 --- a/crates/doc/src/text/char_style.rs +++ b/crates/doc/src/text/char_style.rs @@ -116,6 +116,10 @@ pub struct CharStyle { /// these characters are drawn in, whatever digits were typed. #[serde(default, skip_serializing_if = "crate::skip::is_default")] pub digits: Digits, + /// Font height reference (Character panel menu › Show Font Height Options): the height of the + /// font that [`Self::size`] measures, the em box by default. + #[serde(default, rename = "fontHeightReference", skip_serializing_if = "crate::skip::is_default")] + pub font_height_reference: FontHeightReference, /// No Break (Character panel menu): these characters are not broken across lines, including /// at a space or a hyphenation point inside the run. A run longer than the line stays together /// and can overflow it. @@ -130,6 +134,21 @@ pub struct CharStyle { pub language: Option, } +/// What a character's font size measures ([`CharStyle::font_height_reference`]): the em square +/// of the font, the height of its capitals or of its lowercase letters (the cap height and x +/// height of its OS/2 table), or its ideographic character face (ICF, OpenType's `icfb` and +/// `icft` baselines). Layout sets the characters at the em size at which the chosen height equals +/// the font size; with a font that has no such value, the size measures the em box. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum FontHeightReference { + #[default] + EmBox, + CapHeight, + XHeight, + IcfBox, +} + /// The numeral system digits are drawn in ([`CharStyle::digits`]): as typed, or every digit of /// the Western, Arabic-Indic and Eastern Arabic-Indic sets drawn in one of them or in the digits /// of an Indic script. The text keeps the digits that were typed; only the glyphs change (like @@ -380,6 +399,7 @@ impl Default for CharStyle { standard_vertical_roman: false, small_caps: None, digits: Digits::Default, + font_height_reference: FontHeightReference::EmBox, no_break: false, language: None, } diff --git a/crates/doc/src/text/tests.rs b/crates/doc/src/text/tests.rs index bd53024b1..04deae8ee 100644 --- a/crates/doc/src/text/tests.rs +++ b/crates/doc/src/text/tests.rs @@ -60,6 +60,24 @@ fn digits_map_between_the_numeral_systems() { assert_eq!(serde_json::from_value::(serde_json::to_value(&farsi).unwrap()).unwrap(), farsi); } +/// The font height reference is the em box by default and is saved only when it is another. +#[test] +fn font_height_reference_is_saved_only_when_not_the_em_box() { + let st = CharStyle::default(); + assert_eq!(st.font_height_reference, FontHeightReference::EmBox); + assert!(serde_json::to_value(&st).unwrap().get("fontHeightReference").is_none()); + for (r, key) in + [(FontHeightReference::CapHeight, "capHeight"), (FontHeightReference::XHeight, "xHeight"), (FontHeightReference::IcfBox, "icfBox")] + { + let set = CharStyle { font_height_reference: r, ..CharStyle::default() }; + assert_eq!(serde_json::to_value(&set).unwrap()["fontHeightReference"], key); + assert_eq!(serde_json::from_value::(serde_json::to_value(&set).unwrap()).unwrap(), set); + } + // Styles written before the attribute existed read as the em box. + let old = serde_json::to_value(&st).unwrap(); + assert_eq!(serde_json::from_value::(old).unwrap().font_height_reference, FontHeightReference::EmBox); +} + /// Each Indic numeral system draws 0–9 (and the Arabic-Indic sets) as the ten digits of its /// script's block; every system saves under its id, which reads back, and other names are refused. #[test] diff --git a/crates/engine/src/cmd/panelcmds.rs b/crates/engine/src/cmd/panelcmds.rs index 7f504dcc4..7934dbe69 100644 --- a/crates/engine/src/cmd/panelcmds.rs +++ b/crates/engine/src/cmd/panelcmds.rs @@ -43,7 +43,7 @@ pub fn specs() -> Vec { "Character / Paragraph", [], None, - "{ids?|id?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), list?: \"none\"|\"bullet\"|\"number\", bullet?: \"none\"|\"dot\"|\"dash\"|\"circle\"|\"square\", number?: \"none\"|\"arabic\"|\"upperLetters\"|\"lowerLetters\"|\"upperRoman\"|\"lowerRoman\"|\"twoDigits\"|\"threeDigits\" (Paragraph panel › Bullets and Numbered List: each paragraph of a list starts with its marker, drawn in the font, size and color of the paragraph's first character, and its text starts at the next tab stop after the marker, the left indent counting as a stop; a bulleted list's marker is its bullet, and a numbered list's is the paragraph's number and a period: 1, 2, 3; A to Z, then AA, BB; a to z, then aa, bb; I, II, III; i, ii, iii; 01 to 99, then 100; 001 to 999, then 1000; letters and roman numerals past 3999 in Arabic digits; the numbers count the story's numbered paragraphs, its threaded frames included, from 1 after a paragraph in no list, and an empty or bulleted paragraph is not counted; list \"bullet\" keeps a list's bullet, else a dot, and list \"number\" keeps a numbered list's style, else arabic; without list, a number style makes a numbered list and a bullet makes a bulleted one; \"none\" in any of the three turns the list off; a paragraph that joins a list with left and first-line indents of 0 gets 18 and -18 unless the call sets an indent; a horizontal right-to-left list paragraph has its indents mirrored, measured from the right), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), wordSpacing?: % 0..1000 (80, 100, 133), letterSpacing?: % of a word space -100..500 (0, 0, 0), glyphScaling?: % 50..200 (100, 100, 100): each [min, desired, max] or {min?, desired?, max?} (Paragraph panel menu › Justification…: Desired applies to every line, Minimum and Maximum bound what justified lines may use; values are clamped to their range, and Minimum ≤ Desired ≤ Maximum must hold afterwards or nothing changes), autoLeading?: % of the size 0..500 (120; the leading of characters whose leading is Auto), singleWordJustification?: \"fullJustify\"|\"alignLeft\"|\"alignCenter\"|\"alignRight\" (a justified line holding one word), mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), 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), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, as text.setRangeStyle), 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\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), language?: name|tag (Character panel › Language, as text.setRangeStyle), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), kashida?: \"none\"|\"short\"|\"medium\"|\"long\"|\"stylistic\" (Paragraph panel › Insert Kashidas: justified lines of horizontal area type lengthen each Arabic word with a kashida between its last pair of joined letters, at most a quarter, half or whole em long, or whole tatweels up to an em for stylistic, before they widen their word spaces; none by default), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)", + "{ids?|id?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), list?: \"none\"|\"bullet\"|\"number\", bullet?: \"none\"|\"dot\"|\"dash\"|\"circle\"|\"square\", number?: \"none\"|\"arabic\"|\"upperLetters\"|\"lowerLetters\"|\"upperRoman\"|\"lowerRoman\"|\"twoDigits\"|\"threeDigits\" (Paragraph panel › Bullets and Numbered List: each paragraph of a list starts with its marker, drawn in the font, size and color of the paragraph's first character, and its text starts at the next tab stop after the marker, the left indent counting as a stop; a bulleted list's marker is its bullet, and a numbered list's is the paragraph's number and a period: 1, 2, 3; A to Z, then AA, BB; a to z, then aa, bb; I, II, III; i, ii, iii; 01 to 99, then 100; 001 to 999, then 1000; letters and roman numerals past 3999 in Arabic digits; the numbers count the story's numbered paragraphs, its threaded frames included, from 1 after a paragraph in no list, and an empty or bulleted paragraph is not counted; list \"bullet\" keeps a list's bullet, else a dot, and list \"number\" keeps a numbered list's style, else arabic; without list, a number style makes a numbered list and a bullet makes a bulleted one; \"none\" in any of the three turns the list off; a paragraph that joins a list with left and first-line indents of 0 gets 18 and -18 unless the call sets an indent; a horizontal right-to-left list paragraph has its indents mirrored, measured from the right), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), wordSpacing?: % 0..1000 (80, 100, 133), letterSpacing?: % of a word space -100..500 (0, 0, 0), glyphScaling?: % 50..200 (100, 100, 100): each [min, desired, max] or {min?, desired?, max?} (Paragraph panel menu › Justification…: Desired applies to every line, Minimum and Maximum bound what justified lines may use; values are clamped to their range, and Minimum ≤ Desired ≤ Maximum must hold afterwards or nothing changes), autoLeading?: % of the size 0..500 (120; the leading of characters whose leading is Auto), singleWordJustification?: \"fullJustify\"|\"alignLeft\"|\"alignCenter\"|\"alignRight\" (a justified line holding one word), mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), 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), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, as text.setRangeStyle), 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\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference, as text.setRangeStyle: the height of the font that the size measures), language?: name|tag (Character panel › Language, as text.setRangeStyle), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), kashida?: \"none\"|\"short\"|\"medium\"|\"long\"|\"stylistic\" (Paragraph panel › Insert Kashidas: justified lines of horizontal area type lengthen each Arabic word with a kashida between its last pair of joined letters, at most a quarter, half or whole em long, or whole tatweels up to an em for stylistic, before they widen their word spaces; none by default), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)", has_doc, set_format ), @@ -373,6 +373,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result { "standardVerticalRoman", "noBreak", "digits", + "fontHeightReference", "language", "fontAxes", "burasagari", @@ -397,6 +398,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result { let digits = super::textedit::digits_param(p, C)?; let language = super::textedit::language_param(p, C)?; let font_axes = super::textedit::font_axes_param(p, C)?; + let font_height_reference = super::textedit::font_height_reference_param(p, C)?; let mojikumi = match p.get("mojikumi") { None => None, Some(v) => Some(match v.as_str() { @@ -545,6 +547,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result { if let Some(v) = &font_axes { super::textedit::change_font_axes(st, v); } + if let Some(v) = font_height_reference { + st.font_height_reference = v; + } }; s.edit("Character", |d, _| { for id in &ids { diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 8cfdaf47f..5f5f653a9 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -39,7 +39,7 @@ pub fn specs() -> Vec { "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\"|\"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), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, with Show East Asian Options: in vertical type those half-width characters, such as Latin letters and digits, stand upright one below the other instead of being rotated; spaces and tate-chu-yoko blocks are not changed; kept on the characters and ignored in horizontal type; off by default), 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\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), 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), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, with Show East Asian Options: in vertical type those half-width characters, such as Latin letters and digits, stand upright one below the other instead of being rotated; spaces and tate-chu-yoko blocks are not changed; kept on the characters and ignored in horizontal type; off by default), 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\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), fontHeightReference?: \"emBox\"|\"capHeight\"|\"xHeight\"|\"icfBox\" (Character panel › Set Font Height Reference: the height of the font that size measures, the em box by default; with a font that has no such height, or one outside 0.1 to 2 em, the em box), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), 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 ), @@ -325,6 +325,7 @@ pub(crate) struct CharChange { /// The language to set: a tag, or `None` for the default. language: Option>, font_axes: Option, + font_height_reference: Option, } /// `charDirection: "default"|"leftToRight"|"rightToLeft"` → the Character Direction to set (`None` @@ -452,6 +453,20 @@ pub(crate) fn char_align_param(p: &Value, cmd: &str) -> Result Result> { + use vectorcraft_doc::FontHeightReference; + let Some(v) = p.get("fontHeightReference").filter(|v| !v.is_null()) else { return Ok(None) }; + Ok(Some(match v.as_str() { + Some("emBox") => FontHeightReference::EmBox, + Some("capHeight") => FontHeightReference::CapHeight, + Some("xHeight") => FontHeightReference::XHeight, + Some("icfBox") => FontHeightReference::IcfBox, + _ => return Err(bad(cmd, "`fontHeightReference` must be \"emBox\", \"capHeight\", \"xHeight\" or \"icfBox\"")), + })) +} + fn paint_param(p: &Value, k: &str, cmd: &str) -> Result> { match p.get(k) { None | Some(Value::Null) => Ok(None), @@ -523,6 +538,7 @@ impl CharChange { digits: digits_param(p, cmd)?, language: language_param(p, cmd)?, font_axes: font_axes_param(p, cmd)?, + font_height_reference: font_height_reference_param(p, cmd)?, }; if c.size.is_some_and(|v| v <= 0.0) { return Err(bad(cmd, "size must be positive")); @@ -570,6 +586,7 @@ impl CharChange { && self.digits.is_none() && self.language.is_none() && self.font_axes.is_none() + && self.font_height_reference.is_none() } fn localize(&mut self, t: &TextObject, cmd: &str) -> Result<()> { @@ -680,6 +697,9 @@ impl CharChange { if let Some(v) = self.digits { st.digits = v; } + if let Some(v) = self.font_height_reference { + st.font_height_reference = v; + } if let Some(v) = &self.language { st.language.clone_from(v); } @@ -1216,7 +1236,11 @@ fn type_step(s: &mut Session, p: &Value) -> Result { match step { Step::Size => st.size = super::typecmd::local_type_value(t, st.size * points + inc, (0.1, 1296.0), false, C)?, Step::Leading => { - let leading = t.para_at(paras.get(i).copied().unwrap_or(0)).effective_leading(st); + // With Auto leading the step starts from the leading the lines are set with: + // the paragraph's Auto Leading share of the em size the font height reference + // gives the characters. An inline graphic keeps its size. + let em = if run.inline.is_some() { st.size } else { vectorcraft_text::FontDb::global().em_size(st, t.vertical) }; + let leading = t.para_at(paras.get(i).copied().unwrap_or(0)).effective_leading(&CharStyle { size: em, ..st.clone() }); 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), diff --git a/crates/engine/src/cmd/textstyles.rs b/crates/engine/src/cmd/textstyles.rs index 5995c490d..ee8ab1d3c 100644 --- a/crates/engine/src/cmd/textstyles.rs +++ b/crates/engine/src/cmd/textstyles.rs @@ -178,6 +178,7 @@ pub fn char_attrs(v: &CharStyle) -> Map { ("standardVerticalRoman", json!(v.standard_vertical_roman)), ("noBreak", json!(v.no_break)), ("digits", json!(v.digits)), + ("fontHeightReference", json!(v.font_height_reference)), ("language", json!(v.language)), ] { m.insert(k.into(), x); diff --git a/crates/engine/src/cmd/typecmd/area_tests.rs b/crates/engine/src/cmd/typecmd/area_tests.rs index 65febdf0b..4a1200c25 100644 --- a/crates/engine/src/cmd/typecmd/area_tests.rs +++ b/crates/engine/src/cmd/typecmd/area_tests.rs @@ -1306,6 +1306,97 @@ fn digits_are_a_character_attribute_saved_only_when_set() { assert_eq!(set, [("صفحة ".to_string(), Digits::Default), ("12".to_string(), Digits::Farsi)]); } +/// Font height reference (Character panel › Set Font Height Reference): a character attribute, +/// saved only when not the em box; the type is laid out again at the em size it gives, so 20 pt +/// type measuring a 0.7 em cap height grows by 1 / 0.7, and a leading step from Auto starts at +/// 120% of that em size. +#[test] +fn font_height_reference_is_a_character_attribute_saved_only_when_not_the_em_box() { + use vectorcraft_doc::FontHeightReference as R; + use vectorcraft_text::test_fonts::{HEIGHT_CAP, HEIGHT_FAMILY, height_font}; + vectorcraft_text::FontDb::global().add_font(height_font(true).unwrap()); + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + let id = + s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hx Hx", "font": HEIGHT_FAMILY, "size": 20})).unwrap()["id"].as_u64().unwrap(); + let text = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind { + NodeKind::Text(t) => t.clone(), + _ => panic!("text"), + }; + let height = |s: &Session| text(s).cached_bounds.unwrap().height(); + let em_box = height(&s); + assert_eq!(text(&s).runs[0].style.font_height_reference, R::EmBox); + s.execute("select.set", &json!({"ids": [id]})).unwrap(); + for (key, want) in [("capHeight", R::CapHeight), ("xHeight", R::XHeight), ("icfBox", R::IcfBox), ("emBox", R::EmBox)] { + s.execute("text.setFormat", &json!({"fontHeightReference": key})).unwrap(); + assert!(text(&s).runs.iter().all(|r| r.style.font_height_reference == want && r.style.size == 20.0)); + let saved = serde_json::to_value(&text(&s).runs[0].style).unwrap(); + assert_eq!(saved.get("fontHeightReference").and_then(|v| v.as_str()), (want != R::EmBox).then_some(key)); + } + s.execute("text.setFormat", &json!({"fontHeightReference": "capHeight"})).unwrap(); + let grown = height(&s) / em_box; + assert!((grown - 1000.0 / f64::from(HEIGHT_CAP)).abs() < 1e-6, "{grown}"); + // A leading step from Auto starts at the leading the lines are set with: 120% of the em size + // (the paragraph's Auto Leading). + s.execute("type.step", &json!({"attribute": "leading"})).unwrap(); + let stepped = text(&s).runs[0].style.leading.unwrap(); + assert!((stepped - (1.2 * 20.0 * 1000.0 / f64::from(HEIGHT_CAP) + 2.0)).abs() < 1e-9, "{stepped}"); + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(text(&s).runs[0].style.font_height_reference, R::EmBox); + assert!((height(&s) - em_box).abs() < 1e-9); + let err = s.execute("text.setFormat", &json!({"fontHeightReference": "baseline"})).unwrap_err().to_string(); + assert!(err.contains("`fontHeightReference` must be"), "{err}"); + // Characters selected with the Type tool: only the range takes it. + s.execute("text.setRangeStyle", &json!({"id": id, "start": 3, "end": 5, "fontHeightReference": "xHeight"})).unwrap(); + let set: Vec<_> = text(&s).runs.iter().map(|r| (r.text.clone(), r.style.font_height_reference)).collect(); + assert_eq!(set, [("Hx ".to_string(), R::EmBox), ("Hx".to_string(), R::XHeight)]); +} + +/// A leading step from Auto starts at the leading each paragraph's lines are set with, its +/// Auto Leading share of the em size: 20 pt type in paragraphs at 120% and 150% steps from 24 pt +/// and from 30 pt. +#[test] +fn a_leading_step_from_auto_starts_at_each_paragraphs_auto_leading() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ab\nCd", "size": 20})).unwrap()["id"].as_u64().unwrap(); + s.execute("text.setFormat", &json!({"id": id, "start": 3, "end": 5, "autoLeading": 150})).unwrap(); + s.execute("select.set", &json!({"ids": [id]})).unwrap(); + s.execute("type.step", &json!({"attribute": "leading"})).unwrap(); + let NodeKind::Text(t) = &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind else { + panic!("text"); + }; + let leading: Vec<_> = t.runs.iter().map(|r| (r.text.as_str(), r.style.leading)).collect(); + assert_eq!(leading, [("Ab\n", Some(26.0)), ("Cd", Some(32.0))]); +} + +/// An inline graphic keeps its size whatever the font height reference, so a leading step from +/// Auto starts at 120% of that size: 20 pt Cap Height type steps from 120% of 20 / 0.7 pt and +/// the graphic in it from 24 pt. +#[test] +fn a_leading_step_from_auto_starts_at_an_inline_graphics_own_size() { + use vectorcraft_text::test_fonts::{HEIGHT_CAP, HEIGHT_FAMILY, height_font}; + vectorcraft_text::FontDb::global().add_font(height_font(true).unwrap()); + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + s.execute("shape.ellipse", &json!({"x": 300, "y": 300, "width": 10, "height": 10})).unwrap(); + s.execute("symbol.new", &json!({"name": "Dot"})).unwrap(); + s.execute("edit.clear", &json!({})).unwrap(); + let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hx", "font": HEIGHT_FAMILY, "size": 20})).unwrap()["id"].as_u64().unwrap(); + s.execute("text.insertInline", &json!({"id": id, "at": 2, "symbol": "Dot"})).unwrap(); + s.execute("select.set", &json!({"ids": [id]})).unwrap(); + s.execute("text.setFormat", &json!({"fontHeightReference": "capHeight"})).unwrap(); + s.execute("type.step", &json!({"attribute": "leading"})).unwrap(); + let NodeKind::Text(t) = &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind else { + panic!("text"); + }; + let leading: Vec<_> = t.runs.iter().map(|r| (r.inline.is_some(), r.style.leading.unwrap())).collect(); + let text = 1.2 * 20.0 * 1000.0 / f64::from(HEIGHT_CAP) + 2.0; + assert_eq!(leading.len(), 2, "{leading:?}"); + assert!(leading.iter().all(|&(inline, l)| (l - if inline { 26.0 } else { text }).abs() < 1e-9), "{leading:?}"); +} + /// The Indic digits (#4942): each set by its id through text.setFormat and text.setRangeStyle, /// saved under it, kept by save and reopen and by character styles; the text stays as typed. #[test] diff --git a/crates/eps/src/legacy_ai/tests.rs b/crates/eps/src/legacy_ai/tests.rs index ee55b0a5d..1c750d983 100644 --- a/crates/eps/src/legacy_ai/tests.rs +++ b/crates/eps/src/legacy_ai/tests.rs @@ -1017,6 +1017,39 @@ fn variable_font_settings_are_warned_as_left_out() { assert!(String::from_utf8(out.bytes).unwrap().contains("(H\\351llo \\2005\\r) Tx\n"), "still written as text"); } +/// Type whose font size measures the cap height is written at the em size it is drawn at, with +/// no warning: 14 pt measuring a 0.7 em cap height reads back as 20 pt type. +#[test] +fn a_font_height_reference_is_written_as_the_size_drawn() { + use vectorcraft_text::test_fonts::{HEIGHT_FAMILY, height_font}; + vectorcraft_text::FontDb::global().add_font(height_font(true).unwrap()); + let mut doc = typed(); + let id = texts(&doc)[0].0.id; + if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) { + let st = &mut t.runs[0].style; + (st.font_family, st.size) = (HEIGHT_FAMILY.into(), 14.0); + st.font_height_reference = vectorcraft_doc::FontHeightReference::CapHeight; + } + let out = write(&doc); + assert!(!out.warnings.iter().any(|w| w == TYPE_ATTRIBUTES || w == TYPE), "{:?}", out.warnings); + let imported = crate::import(&out.bytes).unwrap(); + let size = texts(&imported.document)[0].1.runs[0].style.size; + assert!((size - 20.0).abs() < 1e-6, "{size}"); +} + +/// Characters with Auto leading are written with their paragraph's Auto Leading share of the +/// size: 18 pt type in paragraphs at 150% is written with 27 pt leading. +#[test] +fn auto_leading_is_written_at_the_paragraphs_share() { + let mut doc = typed(); + let id = texts(&doc)[0].0.id; + if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) { + t.edit_paras(None, |p| p.justification.auto_leading = 150.0); + } + let text = String::from_utf8(write(&doc).bytes).unwrap(); + assert!(text.contains("\n27 0 Tl\n"), "{text}"); +} + /// A legacy `.ai` paragraph has no list: type with a bulleted or numbered list is written as its /// text, with the warning that the bullets and numbers are left out. #[test] diff --git a/crates/eps/src/legacy_ai/writer.rs b/crates/eps/src/legacy_ai/writer.rs index b0ba5508f..9879a51ab 100644 --- a/crates/eps/src/legacy_ai/writer.rs +++ b/crates/eps/src/legacy_ai/writer.rs @@ -719,11 +719,15 @@ impl<'a> Writer<'a> { // ---------- type ---------- - /// Type: a text object when the format can hold it as text, else its outlines. + /// Type: a text object when the format can hold it as text, else its outlines. A run whose + /// font size measures a height other than the em box is written at the em size it is drawn + /// at ([`vectorcraft_text::FontDb::font_heights_resolved`]). fn text(&mut self, t: &TextObject) { if !invertible(t.xf) { return; } + let resolved = vectorcraft_text::FontDb::global().font_heights_resolved(t); + let t = &*resolved; if !self.outline_text { if writable(t) { return self.text_object(t); @@ -849,7 +853,8 @@ impl<'a> Writer<'a> { ps::push_num(&mut self.body, finite(st.tracking, 0.0)); self.line(" Tt"); self.line(if st.kerning.is_none() { "1 TA" } else { "0 TA" }); - ps::push_nums(&mut self.body, &[finite(st.effective_leading(), size * 1.2), finite(p.space_before, 0.0)]); + let leading = st.leading.unwrap_or_else(|| p.justification.auto_leading_for(st.size)); + ps::push_nums(&mut self.body, &[finite(leading, size * 1.2), finite(p.space_before, 0.0)]); self.line("Tl"); } diff --git a/crates/svg/src/css.rs b/crates/svg/src/css.rs index 178528887..747dae25b 100644 --- a/crates/svg/src/css.rs +++ b/crates/svg/src/css.rs @@ -567,8 +567,12 @@ impl Rules<'_> { } } - /// Type: its first character style's font, colour and spacing, its paragraph's alignment. + /// Type: its first character style's font, colour and spacing, its paragraph's alignment. The + /// font size is the em size the characters are drawn at + /// ([`vectorcraft_text::FontDb::font_heights_resolved`]). fn type_art(&mut self, n: &Node, t: &vectorcraft_doc::TextObject, art: &mut Art) { + let resolved = vectorcraft_text::FontDb::global().font_heights_resolved(t); + let t = &*resolved; if matches!(t.kind, TextKind::OnPath { .. }) { art.unsupported("type on a path"); } diff --git a/crates/svg/src/export/text.rs b/crates/svg/src/export/text.rs index b94a7449c..c8b6ced72 100644 --- a/crates/svg/src/export/text.rs +++ b/crates/svg/src/export/text.rs @@ -73,11 +73,14 @@ impl Writer<'_> { /// instances of their symbols (a `` of the symbol's def where it can be shared), all in /// one group carrying the object's transparency. The live text skips their characters and /// places the text after each one; type on a path with inline graphics is written as outlines - /// (a `` couldn't leave room for the art). + /// (a `` couldn't leave room for the art). A run whose font size measures a height + /// other than the em box is written at the em size it is drawn at + /// ([`vectorcraft_text::FontDb::font_heights_resolved`]). pub(super) fn text_node(&mut self, n: &Node, t: &TextObject) { let doc = self.doc; let resolved = doc.inline_resolved(t); - let t = &*resolved; + let heights = vectorcraft_text::FontDb::global().font_heights_resolved(&resolved); + let t = &*heights; let inline = t.runs.iter().any(|r| r.inline.is_some()); // Ruby is written as outlines with its base text: a reader's can't place it. So are // small capitals, superscript and subscript and tab leaders, which SVG text doesn't draw (#3869). diff --git a/crates/svg/tests/it/css_rules.rs b/crates/svg/tests/it/css_rules.rs index 1c385a095..88e96cddf 100644 --- a/crates/svg/tests/it/css_rules.rs +++ b/crates/svg/tests/it/css_rules.rs @@ -148,6 +148,26 @@ fn ellipses_round_fully_and_options_change_units_and_names() { assert!(prop(&r, "width").is_none() && prop(&r, "height").is_none()); } +/// Type whose font size measures the cap height gives the em size it is drawn at: 14 pt measuring +/// a 0.7 em cap height is `font-size: 20px`, and its Auto leading is not written. +#[test] +fn type_gives_the_font_size_it_is_drawn_at() { + use vectorcraft_text::test_fonts::{HEIGHT_FAMILY, height_font}; + vectorcraft_text::FontDb::global().add_font(height_font(true).unwrap()); + let mut d = doc(); + let st = CharStyle { + font_family: HEIGHT_FAMILY.into(), + size: 14.0, + font_height_reference: vectorcraft_doc::FontHeightReference::CapHeight, + ..CharStyle::default() + }; + let n = text(&mut d, "Hello", st); + let id = add(&mut d, n); + let r = one(&d, id, &CssOptions::default()); + assert_eq!(prop(&r, "font-size"), Some("20px")); + assert!(prop(&r, "line-height").is_none()); +} + #[test] fn type_gives_font_properties() { let mut d = doc(); diff --git a/crates/svg/tests/it/text_layout.rs b/crates/svg/tests/it/text_layout.rs index 76c30a107..3107a1f44 100644 --- a/crates/svg/tests/it/text_layout.rs +++ b/crates/svg/tests/it/text_layout.rs @@ -176,6 +176,23 @@ fn digits_are_written_as_drawn() { assert!(!text.contains("page 4") && !text.contains("page ٥"), "{svg}"); } +/// A run whose font size measures the cap height is written at the em size it is drawn at: 14 pt +/// measuring a 0.7 em cap height is `font-size="20"`, after a 12 pt run measuring the em box, +/// which takes the `` element's 12. +#[test] +fn font_height_reference_writes_the_size_drawn() { + use vectorcraft_doc::FontHeightReference; + use vectorcraft_text::test_fonts::{HEIGHT_FAMILY, height_font}; + vectorcraft_text::FontDb::global().add_font(height_font(true).unwrap()); + let em_box = CharStyle { font_family: HEIGHT_FAMILY.into(), ..style(12.0) }; + let cap = CharStyle { size: 14.0, font_height_reference: FontHeightReference::CapHeight, ..em_box.clone() }; + let d = area_doc(vec![run("Hx ", em_box), run("Hx", cap)], 280.0, Justify::Left); + let svg = export(&d, &ExportOptions::default()); + assert!(svg.contains("font-size=\"12\" fill"), "{svg}"); + let sizes: Vec<_> = tspans(&svg).iter().map(|t| attr(t, "font-size")).collect(); + assert_eq!(sizes, [None, Some(20.0)], "{svg}"); +} + #[test] fn fewer_tspans_writes_one_per_line() { let bold = CharStyle { font_style: "Bold".into(), ..style(12.0) }; diff --git a/crates/text/src/fontdb/face.rs b/crates/text/src/fontdb/face.rs index 758acc2c2..0df8c2e33 100644 --- a/crates/text/src/fontdb/face.rs +++ b/crates/text/src/fontdb/face.rs @@ -279,6 +279,26 @@ impl FontFace { *self.icf_margins.get_or_init(|| self.icf_from_base().or_else(|| self.icf_from_glyphs()).unwrap_or_default()) } + /// The height of the face that a font size measures with `reference`, in ems: 1 for the em + /// box; the cap height or x height of its OS/2 table; the height of its ideographic character + /// face ([`Self::icf_margins`]), or across a `vertical` line its width. None for a face whose + /// font has no such value (an OS/2 table without the heights, no ICF), or one outside 0.1 to + /// 2 ems. + pub fn font_height(&self, reference: vectorcraft_doc::FontHeightReference, vertical: bool) -> Option { + use vectorcraft_doc::FontHeightReference as R; + let units = |v: Option| v.map(f64::from).map(|v| v / self.upem); + let h = match reference { + R::EmBox => Some(1.0), + R::CapHeight => units(self.skrifa()?.metrics(Size::unscaled(), self.location()).cap_height), + R::XHeight => units(self.skrifa()?.metrics(Size::unscaled(), self.location()).x_height), + R::IcfBox => { + let m = self.icf_margins(); + (m != IcfMargins::default()).then_some(if vertical { 1.0 - m.left - m.right } else { 1.0 - m.top - m.bottom }) + } + }; + h.filter(|h| (0.1..=2.0).contains(h)) + } + /// [`Self::icf_margins`] from the BASE table: the ICF's bottom edge `icfb` (top edge `icft`, else /// as far below the em box top) against the em box `ideo` .. `idtp` (else the OS/2 typographic /// descender, and one em above it); across vertical lines, `icfb` and `icft` of the vertical diff --git a/crates/text/src/fontdb/lookup.rs b/crates/text/src/fontdb/lookup.rs index e01a47493..010f96ffa 100644 --- a/crates/text/src/fontdb/lookup.rs +++ b/crates/text/src/fontdb/lookup.rs @@ -247,6 +247,37 @@ impl FontDb { Some(self.instance(&face, &st.font_axes)) } + /// The em size characters of style `st` are set at, in `vertical` type or not: its size, or + /// with a font height reference other than the em box ([`vectorcraft_doc::FontHeightReference`]) + /// the size at which its face ([`Self::style_face`]) has that height + /// ([`FontFace::font_height`]). The size itself when the face has no such height. + pub fn em_size(&self, st: &vectorcraft_doc::CharStyle, vertical: bool) -> f64 { + let reference = st.font_height_reference; + if reference == vectorcraft_doc::FontHeightReference::EmBox { + return st.size; + } + self.style_face(st).and_then(|f| f.font_height(reference, vertical)).map_or(st.size, |h| st.size / h) + } + + /// `t` as layout sets it: each run takes its [`Self::em_size`] and the em box as its font + /// height reference; an inline graphic keeps its size. Explicit leading and baseline shift + /// stay in points; Auto leading, tracking and kerning follow the em size. `t` itself when every + /// run measures the em box. + pub fn font_heights_resolved<'a>(&self, t: &'a vectorcraft_doc::TextObject) -> std::borrow::Cow<'a, vectorcraft_doc::TextObject> { + use vectorcraft_doc::FontHeightReference; + if t.runs.iter().all(|r| r.style.font_height_reference == FontHeightReference::EmBox) { + return std::borrow::Cow::Borrowed(t); + } + let mut out = t.clone(); + for r in &mut out.runs { + if r.inline.is_none() { + r.style.size = self.em_size(&r.style, t.vertical); + } + r.style.font_height_reference = FontHeightReference::EmBox; + } + std::borrow::Cow::Owned(out) + } + /// `face` with the axis settings `axes` (by tag, in the axes' own units) in place of its own, /// each clamped to its axis's range: its outlines, metrics and advances taken there, and its /// file embedded instanced there. Tags the font has no axis for are left out. `face` itself diff --git a/crates/text/src/layout/driver.rs b/crates/text/src/layout/driver.rs index 4cb518d0d..58ca0a525 100644 --- a/crates/text/src/layout/driver.rs +++ b/crates/text/src/layout/driver.rs @@ -106,8 +106,11 @@ pub fn decorations(layout: &TextLayout, db: &FontDb, t: &TextObject) -> Vec<(usi const SHRINK_PASSES: usize = 11; /// Lay out a text object with explicit options (area type rows/columns, inset, first baseline, -/// fit, composer, OpenType features). +/// fit, composer, OpenType features). Each run is set at the em size its font height reference +/// gives it ([`FontDb::font_heights_resolved`]). pub fn layout_with(db: &FontDb, t: &TextObject, opts: &LayoutOptions) -> TextLayout { + let resolved = db.font_heights_resolved(t); + let t = &*resolved; let first = layout_once(db, t, opts); match (opts.fit.min_scale(), &t.kind) { (Some(min), TextKind::Area { .. }) if first.overflow => shrink_to_fit(db, t, opts, min).unwrap_or(first), diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index e04c6c6dc..6f22baa7f 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -718,6 +718,7 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool { tate_chu_yoko, standard_vertical_roman: _, digits, + font_height_reference, no_break, language, } = a; @@ -735,6 +736,7 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool { && *proportional_metrics == b.proportional_metrics && *tate_chu_yoko == b.tate_chu_yoko && *digits == b.digits + && *font_height_reference == b.font_height_reference && *no_break == b.no_break && *language == b.language && ligatures_suppressed_by(*tracking) == ligatures_suppressed_by(b.tracking) diff --git a/crates/text/src/test_fonts.rs b/crates/text/src/test_fonts.rs index 817f1689f..a4e0fcde6 100644 --- a/crates/text/src/test_fonts.rs +++ b/crates/text/src/test_fonts.rs @@ -254,6 +254,86 @@ pub fn vertical_font(vorg: bool) -> Option> { Some(builder.build()) } +/// The family of [`height_font`] with its heights. +pub const HEIGHT_FAMILY: &str = "Heighttest Sans"; + +/// The family of [`height_font`] without them. +pub const HEIGHTLESS_FAMILY: &str = "Heightlesstest Sans"; + +/// The cap height and x height [`height_font`] gives, in font units (1,000 to the em). +pub const HEIGHT_CAP: i16 = 700; +pub const HEIGHT_X: i16 = 500; + +/// The height (and the width across a vertical line) of [`height_font`]'s ideographic character +/// face, in ems. +pub const HEIGHT_ICF: f64 = 0.9; + +/// The family of [`height_outside_font`]. +pub const HEIGHT_OUTSIDE_FAMILY: &str = "Heightoutsidetest Sans"; + +/// The cap height and x height [`height_outside_font`] gives, in font units (1,000 to the em): +/// 2.5 em and 0.05 em, outside the 0.1 to 2 ems a font size measures. +pub const HEIGHT_OUTSIDE_CAP: i16 = 2500; +pub const HEIGHT_OUTSIDE_X: i16 = 50; + +/// The bundled Source Sans 3 Regular as family [`HEIGHT_FAMILY`], with the cap height +/// [`HEIGHT_CAP`] and x height [`HEIGHT_X`] in its OS/2 table and a `BASE` table whose Han +/// baselines put the ideographic character face [`HEIGHT_ICF`] em high inside the em box (`ideo` +/// -120, `idtp` 880, `icfb` -70, `icft` 830). Without `heights`: family [`HEIGHTLESS_FAMILY`], +/// with a version 1 OS/2 table, which holds neither height, and no `BASE` table. `None` when it +/// can't be built. +pub fn height_font(heights: bool) -> Option> { + if heights { heights_font(HEIGHT_FAMILY, Some((HEIGHT_X, HEIGHT_CAP)), true) } else { heights_font(HEIGHTLESS_FAMILY, None, false) } +} + +/// The bundled Source Sans 3 Regular as family [`HEIGHT_OUTSIDE_FAMILY`], with the cap height +/// [`HEIGHT_OUTSIDE_CAP`] and x height [`HEIGHT_OUTSIDE_X`] in its OS/2 table and no `BASE` +/// table. `None` when it can't be built. +pub fn height_outside_font() -> Option> { + heights_font(HEIGHT_OUTSIDE_FAMILY, Some((HEIGHT_OUTSIDE_X, HEIGHT_OUTSIDE_CAP)), false) +} + +/// The bundled Source Sans 3 Regular as `family`: with the x height and cap height `heights` in +/// its OS/2 table, else a version 1 OS/2 table, which holds neither; with `base`, the `BASE` +/// table of [`height_font`]. +fn heights_font(family: &str, heights: Option<(i16, i16)>, base: bool) -> Option> { + use write_fonts::tables::base::{Axis, Base, BaseCoord, BaseScript, BaseScriptList, BaseScriptRecord, BaseTagList, BaseValues}; + + const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf"); + let font = skrifa::FontRef::new(BASE).ok()?; + let full = format!("{family} Regular"); + let postscript = format!("{}-Regular", family.replace(' ', "")); + let name = name_table(&[(1, family), (2, "Regular"), (4, &full), (6, &postscript)])?; + let mut builder = FontBuilder::new(); + builder.add_raw(Tag::new(b"name"), name); + if base { + let tags = BaseTagList::new([b"icfb", b"icft", b"ideo", b"idtp"].map(Tag::new).to_vec()); + let values = BaseValues::new(2, [-70, 830, -120, 880].map(BaseCoord::format_1).to_vec()); + let hani = BaseScriptRecord::new(Tag::new(b"hani"), BaseScript::new(Some(values), None, vec![])); + let axis = Axis::new(Some(tags), BaseScriptList::new(vec![hani])); + builder.add_table(&Base::new(Some(axis), None)).ok()?; + } + for r in font.table_directory.table_records() { + let tag = Tag::new(&r.tag().to_be_bytes()); + if builder.contains(tag) { + continue; + } + let mut data = font.table_data(r.tag())?.as_bytes().to_vec(); + if tag == Tag::new(b"OS/2") { + if let Some((x, cap)) = heights { + // sxHeight and sCapHeight, at bytes 86 and 88 of a version 2 or later table. + data.get_mut(86..90)?.copy_from_slice(&[x.to_be_bytes(), cap.to_be_bytes()].concat()); + } else { + // Version 1 ends before sxHeight. + data.truncate(86); + data.get_mut(0..2)?.copy_from_slice(&1_u16.to_be_bytes()); + } + } + builder.add_raw(tag, data); + } + Some(builder.build()) +} + /// The family of [`vertical_punctuation_font`]. pub const VERTICAL_PUNCTUATION_FAMILY: &str = "Punctuationtest Sans"; diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 7ed2eb48c..db28b2013 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1347,6 +1347,62 @@ fn character_alignment_on_the_icf_lines_small_characters_up_with_the_largest_fac } } +/// The font height reference: 20 pt type that measures the cap height is set at the em size at +/// which the font's cap height (OS/2) is 20 pt, and the same for the x height and for the ideographic +/// character face's height (its width in vertical type); the em box keeps 20 pt to the em, and +/// so does every reference in a font without these values. Auto leading follows the em size, +/// and two runs that measure different heights each take their own em size. +#[test] +fn font_size_measures_the_font_height_reference() { + use crate::test_fonts::{HEIGHT_CAP, HEIGHT_FAMILY, HEIGHT_ICF, HEIGHT_X, HEIGHTLESS_FAMILY, height_font}; + use vectorcraft_doc::FontHeightReference as R; + let db = FontDb::with_font_dirs(vec![]); + db.add_font(height_font(true).unwrap()); + db.add_font(height_font(false).unwrap()); + let st = |family: &str, r: R| CharStyle { font_family: family.into(), font_height_reference: r, ..style(20.0) }; + // The em size each glyph of `t` is drawn at (its transform's scale times 1,000 units). + let ems = |t: &TextObject| layout(&db, t).glyphs.iter().map(|g| g.xf.determinant().abs().sqrt() * 1000.0).collect::>(); + let near = |a: f64, b: f64| (a - b).abs() < 1e-6; + let (cap, x) = (f64::from(HEIGHT_CAP) / 1000.0, f64::from(HEIGHT_X) / 1000.0); + for vertical_type in [false, true] { + for (r, h) in [(R::EmBox, 1.0), (R::CapHeight, cap), (R::XHeight, x), (R::IcfBox, HEIGHT_ICF)] { + let mut t = point("Hx", st(HEIGHT_FAMILY, r)); + t.vertical = vertical_type; + assert!(ems(&t).iter().all(|e| near(*e, 20.0 / h)), "{r:?} vertical {vertical_type}: {:?}", ems(&t)); + t.runs[0].style.font_family = HEIGHTLESS_FAMILY.into(); + assert!(ems(&t).iter().all(|e| near(*e, 20.0)), "{r:?} without the value, vertical {vertical_type}: {:?}", ems(&t)); + } + } + let face = db.face(HEIGHT_FAMILY, "Regular").unwrap(); + assert_eq!(face.font_height(R::CapHeight, false), Some(cap)); + assert_eq!(db.face(HEIGHTLESS_FAMILY, "Regular").unwrap().font_height(R::XHeight, false), None); + // Auto leading: 120% of the em size the line is set at. + let lines = layout(&db, &point("H\nH", st(HEIGHT_FAMILY, R::CapHeight))).lines; + assert!(near(lines[1].baseline - lines[0].baseline, 1.2 * 20.0 / cap), "{lines:?}"); + // One run at the em box, the next at the cap height. + let mut t = point("H", st(HEIGHT_FAMILY, R::EmBox)); + t.runs.push(TextRun { text: "H".into(), style: st(HEIGHT_FAMILY, R::CapHeight), inline: None }); + let set = ems(&t); + assert!(set.len() == 2 && near(set[0], 20.0) && near(set[1], 20.0 / cap), "{set:?}"); +} + +/// A font height outside 0.1 to 2 ems is not used: 20 pt type that measures a font's 2.5 em cap +/// height or its 0.05 em x height is set at 20 pt to the em. +#[test] +fn a_font_height_outside_a_tenth_to_two_ems_measures_the_em_box() { + use crate::test_fonts::{HEIGHT_OUTSIDE_FAMILY, height_outside_font}; + use vectorcraft_doc::FontHeightReference as R; + let db = FontDb::with_font_dirs(vec![]); + db.add_font(height_outside_font().unwrap()); + let face = db.face(HEIGHT_OUTSIDE_FAMILY, "Regular").unwrap(); + for r in [R::CapHeight, R::XHeight] { + assert_eq!(face.font_height(r, false), None, "{r:?}"); + let st = CharStyle { font_family: HEIGHT_OUTSIDE_FAMILY.into(), font_height_reference: r, ..style(20.0) }; + let ems = layout(&db, &point("Hx", st)).glyphs.iter().map(|g| g.xf.determinant().abs().sqrt() * 1000.0).collect::>(); + assert!(!ems.is_empty() && ems.iter().all(|e| (e - 20.0).abs() < 1e-6), "{r:?}: {ems:?}"); + } +} + /// Burasagari leaves Latin punctuation alone: in a measure of exactly "abcd", the full stop of /// "abcd. ef" doesn't hang with Standard or Forced (the line breaks as it does with None), and a /// Latin line ending in a full stop isn't shortened by Forced. diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 801ed44b5..904c220ba 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4500,6 +4500,12 @@ axis Both И двете Insert Inline Symbol Вграждане на вграден символ ICF Top/Right ICF горе/дясно ICF Bottom/Left ICF долу/ляво + Show Font Height Options Показване на опциите за височина на шрифта + Hide Font Height Options Скриване на опциите за височина на шрифта + Set Font Height Reference Задаване на референтна височина на шрифта + Em Box Квадратна касетка (Em Box) + x-Height Височина на x + ICF Box Касетка ICF @msg `id` is the step reference {r}, which only `run` and `command_batch` resolve `id` е референцията за стъпка {r}, която се разрешава само от `run` и `command_batch` @msg `ids` must be an array of object ids, not {v} `ids` трябва да бъде масив от идентификатори на обекти, а не {v} @msg `corners` must be an array of anchor indices, not {v} `corners` трябва да бъде масив от индекси на опорни точки, а не {v} diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index d2e67c11d..d9e96915f 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -1454,6 +1454,12 @@ digits Malayalam Malajalam Axis Osa ICF Top/Right ICF vrh/desno ICF Bottom/Left ICF dno/lijevo + Show Font Height Options Prikaži opcije visine fonta + Hide Font Height Options Sakrij opcije visine fonta + Set Font Height Reference Postavi referentnu visinu fonta + Em Box Em okvir + x-Height Visina malih slova + ICF Box ICF okvir Copy Color Value (Hex) Kopiraj vrijednost boje (hex) Create New Swatch… Kreiraj novi uzorak boje… Name 40% Naziv 40% diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index ffb752f8a..5cbf470f6 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5735,6 +5735,12 @@ theme Appearance Aparença Glyph Bounds Límits del glif Proximity Guides Guies de proximitat Angular Guides Guies angulars + Show Font Height Options Mostra les opcions d’alçada de la font + Hide Font Height Options Amaga les opcions d’alçada de la font + Set Font Height Reference Estableix la referència d’alçada de la font + Em Box Caixa em + x-Height Alçada de la x + ICF Box Caixa ICF # Average & Join (Object › Path, Cmd+Alt+Shift+J). Average & Join… Mitjana i unió… Average & Join Mitjana i unió diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 97ab1c407..86c2c2ef4 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1478,6 +1478,12 @@ digits Malayalam Malajálamské Axis Osa ICF Top/Right ICF horní/pravý ICF Bottom/Left ICF dolní/levý + Show Font Height Options Zobrazit možnosti výšky písma + Hide Font Height Options Skrýt možnosti výšky písma + Set Font Height Reference Nastavit referenční výšku písma + Em Box Čtverčík + x-Height Výška x + ICF Box Rámeček ICF Copy Color Value (Hex) Kopírovat hodnotu barvy (hex) Create New Swatch… Vytvořit novou položku vzorníku… Name 40% Název 40 % @@ -4209,6 +4215,7 @@ imageTrace Outline Obrys @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer vrstvy souboru nebyly načteny ({_1}): soubor se otevře jako jeho stránka v jedné vrstvě @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part vrstvy souboru nebyly načteny z jeho dat pro úpravy ({_1}): pocházejí z jeho části PDF @msg `to` must be "corner" or "smooth" `to` musí být "corner" nebo "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` musí být "emBox", "capHeight", "xHeight" nebo "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray soubory BMP jsou RGB nebo ve stupních šedi: colorModel rgb nebo gray @msg Blend colors: select at least three objects with filled colours Prolnout barvy: vyberte alespoň tři objekty s barevnou výplní @msg Combine Slices: the slices have no bounds Spojit řezy: řezy nemají hranice diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 1d1741662..bee5e7270 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -1388,6 +1388,12 @@ digits Malayalam Malayalam Axis Akse ICF Top/Right ICF øverst/højre ICF Bottom/Left ICF nederst/venstre + Show Font Height Options Vis indstillinger for skrifthøjde + Hide Font Height Options Skjul indstillinger for skrifthøjde + Set Font Height Reference Angiv reference for skrifthøjde + Em Box Em-felt + x-Height x-højde + ICF Box ICF-felt Copy Color Value (Hex) Kopiér farveværdi (hex) Create New Swatch… Opret ny farveprøve… Name 40% Navn 40% diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 804542013..92ca0c239 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3821,6 +3821,7 @@ theme Appearance Erscheinungsbild @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer die Ebenen der Datei wurden nicht gelesen ({_1}): sie wird als ihre Seite in einer Ebene geöffnet @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part die Ebenen der Datei wurden nicht aus ihren Bearbeitungsdaten gelesen ({_1}): sie stammen aus ihrem PDF-Teil @msg `to` must be "corner" or "smooth" `to` muss "corner" oder "smooth" sein +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` muss "emBox", "capHeight", "xHeight" oder "icfBox" sein @msg BMP files are RGB or grayscale: colorModel rgb or gray BMP-Dateien sind RGB oder Graustufen: colorModel rgb oder gray @msg Blend colors: select at least three objects with filled colours Farben angleichen: Wählen Sie mindestens drei Objekte mit Flächenfarbe aus @msg Combine Slices: the slices have no bounds Slices kombinieren: Die Slices haben keine Begrenzung @@ -4555,6 +4556,13 @@ theme Appearance Erscheinungsbild Insert Inline Symbol Inline-Symbol einfügen ICF Top/Right ICF oben/rechts ICF Bottom/Left ICF unten/links + Show Font Height Options Optionen für Schrifthöhe einblenden + Hide Font Height Options Optionen für Schrifthöhe ausblenden + Set Font Height Reference Bezugsgröße der Schrifthöhe festlegen + Em Box Geviert + Cap Height Versalhöhe + x-Height x-Höhe + ICF Box ICF-Rahmen @msg `id` is the step reference {r}, which only `run` and `command_batch` resolve `id` ist der Schrittverweis {r}, den nur `run` und `command_batch` auflösen @msg `ids` must be an array of object ids, not {v} `ids` muss ein Array von Objekt-IDs sein, nicht {v} @msg `corners` must be an array of anchor indices, not {v} `corners` muss ein Array von Ankerpunkt-Indizes sein, nicht {v} diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index ce061787a..30ab097ec 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2123,6 +2123,7 @@ axis Both Και τα δύο @msg the art on them differs from the page's in {_1}% of it: they come from its editing data all the same Τα γραφικά τους διαφέρουν από εκείνα της σελίδας κατά {_1}%, αλλά θα χρησιμοποιηθούν τα δεδομένα επεξεργασίας @msg `Custom` stands for changed settings: save the preset under another name Το `Custom` αντιστοιχεί σε αλλαγμένες ρυθμίσεις: αποθηκεύστε την προεπιλογή με άλλο όνομα @msg `to` must be "corner" or "smooth" Το `to` πρέπει να είναι "corner" ή "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" Το `fontHeightReference` πρέπει να είναι "emBox", "capHeight", "xHeight" ή "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray Τα αρχεία BMP πρέπει να είναι RGB ή σε κλίμακα του γκρι: colorModel rgb ή gray @msg Blend colors: select at least three objects with filled colours Ανάμειξη χρωμάτων: επιλέξτε τουλάχιστον τρία αντικείμενα με χρώμα γεμίσματος @msg Combine Slices: the slices have no bounds Συνδυασμός τομών: οι τομές δεν έχουν όρια @@ -4580,6 +4581,12 @@ digits Malayalam Μαλαγιάλαμ Insert Inline Symbol Εισαγωγή ενσωματωμένου συμβόλου ICF Top/Right ICF επάνω/δεξιά ICF Bottom/Left ICF κάτω/αριστερά + Show Font Height Options Εμφάνιση επιλογών ύψους γραμματοσειράς + Hide Font Height Options Απόκρυψη επιλογών ύψους γραμματοσειράς + Set Font Height Reference Ορισμός αναφοράς ύψους γραμματοσειράς + Em Box Τετράγωνο em + x-Height Ύψος x + ICF Box Πλαίσιο ICF @msg `id` is the step reference {r}, which only `run` and `command_batch` resolve το `id` είναι η αναφορά βήματος {r}, την οποία αναλύουν μόνο τα `run` και `command_batch` @msg `ids` must be an array of object ids, not {v} το `ids` πρέπει να είναι πίνακας αναγνωριστικών αντικειμένων, όχι {v} @msg `corners` must be an array of anchor indices, not {v} το `corners` πρέπει να είναι πίνακας δεικτών αγκύρωσης, όχι {v} diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 3f389ff27..48797306c 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -949,6 +949,12 @@ font-technology Variable Variables Em Box Top/Right Arriba/derecha del cuadro eme ICF Top/Right Arriba/derecha del ICF ICF Bottom/Left Abajo/izquierda del ICF + Show Font Height Options Mostrar opciones de altura de fuente + Hide Font Height Options Ocultar opciones de altura de fuente + Set Font Height Reference Definir la referencia de altura de fuente + Em Box Cuadro eme + x-Height Altura de la x + ICF Box Cuadro ICF Em Dash Raya Em Space Espacio eme Embed Incrustar @@ -3901,6 +3907,7 @@ axis Both Ambos @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer no se leyeron las capas del archivo ({_1}): se abre como su página, en una sola capa @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part no se leyeron las capas del archivo desde sus datos de edición ({_1}): proceden de su parte PDF @msg `to` must be "corner" or "smooth" `to` debe ser "corner" o "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` debe ser "emBox", "capHeight", "xHeight" o "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray los archivos BMP son RGB o en escala de grises: colorModel rgb o gray @msg Blend colors: select at least three objects with filled colours Fusionar colores: selecciona al menos tres objetos con relleno de color @msg Combine Slices: the slices have no bounds Combinar sectores: los sectores no tienen límites diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index d3cf99cd8..1f7de4be1 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1451,6 +1451,12 @@ digits Malayalam Malajalam Axis Akseli ICF Top/Right ICF ylä/oikea ICF Bottom/Left ICF ala/vasen + Show Font Height Options Näytä fontin korkeuden asetukset + Hide Font Height Options Piilota fontin korkeuden asetukset + Set Font Height Reference Aseta fontin korkeuden viite + Em Box Em-laatikko + x-Height x-korkeus + ICF Box ICF-laatikko Copy Color Value (Hex) Kopioi väriarvo (heksa) Create New Swatch… Luo uusi värimalli… Name 40% Nimi 40% @@ -4123,6 +4129,7 @@ imageTrace Outline Ääriviiva @msg the art on them differs from the page's in {_1}% of it: they come from its editing data all the same niiden kuvitus eroaa sivun kuvituksesta {_1} %:n osalta: ne tuodaan silti sen muokkaustiedoista @msg `Custom` stands for changed settings: save the preset under another name `Custom` tarkoittaa muutettuja asetuksia: tallenna esiasetus toisella nimellä @msg `to` must be "corner" or "smooth" `to` on oltava "corner" tai "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` on oltava "emBox", "capHeight", "xHeight" tai "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray BMP-tiedostot ovat RGB- tai harmaasävykuvia: colorModel rgb tai gray @msg Blend colors: select at least three objects with filled colours Sulauta värit: valitse vähintään kolme objektia, joilla on väritäyttö @msg Combine Slices: the slices have no bounds Yhdistä viipaleet: viipaleilla ei ole rajoja diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index e82972fbc..523c06bb5 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3897,6 +3897,7 @@ axis Both Les deux @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer les calques du fichier n’ont pas été lus ({_1}) : il est ouvert tel que sa page, sur un seul calque @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part les calques du fichier n’ont pas été lus depuis ses données de modification ({_1}) : ils proviennent de sa partie PDF @msg `to` must be "corner" or "smooth" `to` doit être "corner" ou "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` doit être "emBox", "capHeight", "xHeight" ou "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray Les fichiers BMP sont en RVB ou en niveaux de gris : colorModel rgb ou gray @msg Blend colors: select at least three objects with filled colours Fusion des couleurs : sélectionnez au moins trois objets dotés d’un fond de couleur @msg Combine Slices: the slices have no bounds Combiner les tranches : les tranches n’ont pas de limites @@ -4628,6 +4629,12 @@ axis Both Les deux Free Transform Transformation manuelle Perspective Distort Distorsion de perspective Insert Inline Symbol Insérer un symbole dans le texte + Show Font Height Options Afficher les options de hauteur de police + Hide Font Height Options Masquer les options de hauteur de police + Set Font Height Reference Définir la référence de hauteur de police + Em Box Cadratin + x-Height Hauteur d’x + ICF Box Cadre ICF ICF Top/Right ICF haut/droite ICF Bottom/Left ICF bas/gauche @msg `id` is the step reference {r}, which only `run` and `command_batch` resolve `id` est la référence d’étape {r}, que seuls `run` et `command_batch` résolvent diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 1f7317fad..af31ed39c 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1937,6 +1937,12 @@ file-filter Templates Sablonok Hyphenate Capitalized Words Nagybetűs szavak elválasztása ICC Profile ICC-profil ICF Bottom/Left ICF alja/bal oldala + Show Font Height Options Betűmagasság-beállítások megjelenítése + Hide Font Height Options Betűmagasság-beállítások elrejtése + Set Font Height Reference Betűmagasság viszonyítási alapjának beállítása + Em Box Kvirtmező + x-Height x-magasság + ICF Box ICF-mező ICF Top/Right ICF teteje/jobb oldala Identify Objects By Objektumok azonosítása Ignore Kihagy @@ -4518,6 +4524,7 @@ imageTrace Outline Körvonal @msg `stops` must be an array `stops`: tömb kell @msg `svg` must be an object of SVG options (see document.formats) `svg`: SVG-beállításokat tartalmazó objektum kell (lásd: document.formats) @msg `to` must be "corner" or "smooth" `to`: "corner" vagy "smooth" kell +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference`: "emBox", "capHeight", "xHeight" vagy "icfBox" kell @msg `{_1}` is no save command nem mentési parancs: `{_1}` @msg `{area}` is no recovery area name nem helyreállítási terület neve: `{area}` @msg `{color}` is not a colour (#rrggbb or a preset name) nem szín: `{color}` (#rrggbb vagy előbeállítás neve) diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index b90aadc98..a15105ef9 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1388,6 +1388,12 @@ digits Malayalam Malayalam Axis Sumbu ICF Top/Right ICF atas/kanan ICF Bottom/Left ICF bawah/kiri + Show Font Height Options Tampilkan opsi tinggi font + Hide Font Height Options Sembunyikan opsi tinggi font + Set Font Height Reference Atur acuan tinggi font + Em Box Em Box + x-Height Tinggi x + ICF Box Kotak ICF Copy Color Value (Hex) Salin nilai warna (Hex) Create New Swatch… Buat swatch baru… Name 40% Nama 40% diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 9a9709b8d..e94ffaf0b 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -949,6 +949,13 @@ font-technology Variable Variabili Em Box Top/Right Riquadro em in alto/a destra ICF Top/Right ICF in alto/a destra ICF Bottom/Left ICF in basso/a sinistra + Show Font Height Options Mostra opzioni altezza font + Hide Font Height Options Nascondi opzioni altezza font + Set Font Height Reference Imposta riferimento altezza font + Em Box Riquadro em + Cap Height Altezza maiuscole + x-Height Altezza x + ICF Box Riquadro ICF Em Dash Lineetta Em Space Spazio em Embed Incorpora @@ -3941,6 +3948,7 @@ axis Both Entrambi @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer i livelli del file non sono stati letti ({_1}): si apre come la sua pagina, in un solo livello @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part i livelli del file non sono stati letti dai suoi dati di modifica ({_1}): provengono dalla sua parte PDF @msg `to` must be "corner" or "smooth" `to` deve essere "corner" o "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` deve essere "emBox", "capHeight", "xHeight" o "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray I file BMP sono RGB o in scala di grigi: colorModel rgb o gray @msg Blend colors: select at least three objects with filled colours Fondi colori: seleziona almeno tre oggetti con riempimento colore @msg Combine Slices: the slices have no bounds Combina sezioni: le sezioni non hanno limiti diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index b901b9336..902df6a9d 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -42,6 +42,13 @@ font-technology Variable バリアブル Em Box Top/Right 仮想ボディの上 / 右 ICF Top/Right 平均字面の上 / 右 ICF Bottom/Left 平均字面の下 / 左 + Show Font Height Options フォントの高さオプションを表示 + Hide Font Height Options フォントの高さオプションを隠す + Set Font Height Reference フォントの高さの基準を設定 + Em Box 仮想ボディ + Cap Height キャップハイト + x-Height x ハイト + ICF Box 平均字面 Proportional Metrics プロポーショナルメトリクス Character Direction 文字の方向 Digits 数字 @@ -3983,6 +3990,7 @@ swatch Bright Violet ビビッドバイオレット @msg Saving {_1} {_1} を保存中 @msg Sent to {_1} {_1} に送信しました @msg `to` must be "corner" or "smooth" `to` には "corner" または "smooth" を指定してください +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` には "emBox"、"capHeight"、"xHeight"、"icfBox" のいずれかを指定してください @msg a layer couldn't be read 読み込めなかったレイヤーがあります @msg it has more than one artboard アートボードが複数あります @msg its editing data has no layers 編集データにレイヤーがありません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index be58e0b36..d6dcb9e03 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1305,6 +1305,12 @@ digits Malayalam მალაიალამური Axis ღერძი ICF Top/Right ICF ზედა/მარჯვენა ICF Bottom/Left ICF ქვედა/მარცხენა + Show Font Height Options შრიფტის სიმაღლის პარამეტრების ჩვენება + Hide Font Height Options შრიფტის სიმაღლის პარამეტრების დამალვა + Set Font Height Reference შრიფტის სიმაღლის საზომის დაყენება + Em Box Em-ბლოკი + x-Height x-სიმაღლე + ICF Box ICF-ბლოკი Copy Color Value (Hex) ფერის მნიშვნელობის კოპირება (Hex) Create New Swatch… ახალი ფერის ნიმუშის შექმნა… Name 40% სახელი 40% @@ -3692,6 +3698,7 @@ font-technology Variable ცვლადი @msg the art on them differs from the page's in {_1}% of it მათზე არსებული ნახატი გვერდისას {_1}%-ით განსხვავდება @msg `Custom` stands for changed settings: save the preset under another name `Custom` შეცვლილ პარამეტრებს ნიშნავს: შეინახეთ პრესეტი სხვა სახელით @msg `to` must be "corner" or "smooth" `to` უნდა იყოს "corner" ან "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` უნდა იყოს "emBox", "capHeight", "xHeight" ან "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray BMP ფაილები RGB-ა ან ნაცრისფერი: colorModel rgb ან gray @msg Blend colors: select at least three objects with filled colours ფერების შერევა: მონიშნეთ მინიმუმ სამი ობიექტი ფერადი შევსებით @msg Combine Slices: the slices have no bounds ნაჭრების გაერთიანება: ნაჭრებს საზღვრები არ აქვს diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index b979e2613..e2da7c92a 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -36,6 +36,13 @@ font-technology Variable 가변 Em Box Top/Right 엠 박스 위/오른쪽 ICF Top/Right ICF 위/오른쪽 ICF Bottom/Left ICF 아래/왼쪽 + Show Font Height Options 글꼴 높이 옵션 표시 + Hide Font Height Options 글꼴 높이 옵션 숨기기 + Set Font Height Reference 글꼴 높이 기준 설정 + Em Box 엠 박스 + Cap Height 대문자 높이 + x-Height x 높이 + ICF Box ICF 박스 Proportional Metrics 비례 메트릭 Character Direction 문자 방향 Digits 숫자 @@ -3910,6 +3917,7 @@ theme Appearance 외관 @msg Saving {_1} {_1} 저장 중 @msg Sent to {_1} {_1}(으)로 보냈습니다 @msg `to` must be "corner" or "smooth" `to`는 "corner" 또는 "smooth"여야 합니다 +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference`는 "emBox", "capHeight", "xHeight" 또는 "icfBox"여야 합니다 @msg a layer couldn't be read 레이어를 읽을 수 없습니다 @msg it has more than one artboard 아트보드가 둘 이상입니다 @msg its editing data has no layers 편집 데이터에 레이어가 없습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 60b73cb0f..9d3d2c7c8 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -1402,6 +1402,12 @@ digits Malayalam Malayalam Axis As ICF Top/Right ICF boven/rechts ICF Bottom/Left ICF onder/links + Show Font Height Options Opties voor letterhoogte tonen + Hide Font Height Options Opties voor letterhoogte verbergen + Set Font Height Reference Referentie voor letterhoogte instellen + Em Box Em-vak + x-Height x-hoogte + ICF Box ICF-vak Copy Color Value (Hex) Kleurwaarde kopiëren (Hex) Create New Swatch… Nieuw staal maken… Name 40% Naam 40% diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index e32cb1fa1..24c3685b3 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1391,6 +1391,12 @@ digits Malayalam Malajalam Axis Oś ICF Top/Right ICF góra/prawo ICF Bottom/Left ICF dół/lewo + Show Font Height Options Pokaż opcje wysokości czcionki + Hide Font Height Options Ukryj opcje wysokości czcionki + Set Font Height Reference Ustaw odniesienie wysokości czcionki + Em Box Ramka firetowa + x-Height Wysokość x + ICF Box Ramka ICF Copy Color Value (Hex) Kopiuj wartość koloru (hex) Create New Swatch… Utwórz nową próbkę… Name 40% Nazwa 40% @@ -4032,6 +4038,7 @@ imageTrace Outline Kontur @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer nie odczytano warstw pliku ({_1}): plik jest wczytywany jako jego strona, na jednej warstwie @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part nie odczytano warstw pliku z jego danych edycyjnych ({_1}): pochodzą z jego części PDF @msg `to` must be "corner" or "smooth" `to` musi mieć wartość "corner" lub "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` musi mieć wartość "emBox", "capHeight", "xHeight" lub "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray Pliki BMP są w RGB lub skali szarości: colorModel rgb lub gray @msg Blend colors: select at least three objects with filled colours Przejście kolorów: zaznacz co najmniej trzy obiekty z wypełnieniem kolorem @msg Combine Slices: the slices have no bounds Połącz plasterki: plasterki nie mają granic diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index af74d69dd..152cb4b41 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -840,6 +840,12 @@ Em Box Top/Right Topo/Direita da Caixa Eme ICF Top/Right Topo/Direita do ICF ICF Bottom/Left Base/Esquerda do ICF + Show Font Height Options Mostrar Opções de Altura da Fonte + Hide Font Height Options Ocultar Opções de Altura da Fonte + Set Font Height Reference Definir Referência de Altura da Fonte + Em Box Caixa Eme + x-Height Altura do x + ICF Box Caixa ICF Em Dash Travessão Em Space Espaço Em Embed Incorporar @@ -3485,6 +3491,7 @@ axis Both Ambos @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer as camadas do arquivo não foram lidas ({_1}): ele entra como sua página, em uma só camada @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part as camadas do arquivo não foram lidas dos seus dados de edição ({_1}): elas vêm da sua parte PDF @msg `to` must be "corner" or "smooth" `to` deve ser "corner" ou "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` deve ser "emBox", "capHeight", "xHeight" ou "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray arquivos BMP são RGB ou em tons de cinza: colorModel rgb ou gray @msg Blend colors: select at least three objects with filled colours Mesclar cores: selecione pelo menos três objetos com preenchimento de cor @msg Combine Slices: the slices have no bounds Combinar fatias: as fatias não têm limites diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index c497b2ad2..f60e17a75 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -947,6 +947,12 @@ font-technology Variable Вариативные Em Box Top/Right Верх/право кегельной площадки ICF Top/Right Верх/право ICF ICF Bottom/Left Низ/лево ICF + Show Font Height Options Показать параметры высоты шрифта + Hide Font Height Options Скрыть параметры высоты шрифта + Set Font Height Reference Задать опорную высоту шрифта + Em Box Кегельная площадка + x-Height Высота строчных + ICF Box Рамка ICF Em Dash Длинное тире Em Space Круглая шпация Embed Внедрить @@ -3937,6 +3943,7 @@ axis Both Оба @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer слои файла не прочитаны ({_1}): он открывается как его страница, на одном слое @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part слои файла не прочитаны из его данных для редактирования ({_1}): они взяты из его PDF-части @msg `to` must be "corner" or "smooth" `to` должно быть "corner" или "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` должно быть "emBox", "capHeight", "xHeight" или "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray файлы BMP бывают RGB или в оттенках серого: colorModel rgb или gray @msg Blend colors: select at least three objects with filled colours Смешивание цветов: выберите не менее трёх объектов с цветными заливками @msg Combine Slices: the slices have no bounds Объединение фрагментов: у фрагментов нет границ diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 4b8742d00..91595dfa2 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3818,6 +3818,7 @@ axis Both Båda @msg the art on them differs from the page's in {_1}% of it: they come from its editing data all the same grafiken på dem skiljer sig från sidans i {_1} % av ytan: de hämtas ändå från dess redigeringsdata @msg `Custom` stands for changed settings: save the preset under another name `Custom` står för ändrade inställningar: spara förinställningen med ett annat namn @msg `to` must be "corner" or "smooth" `to` måste vara "corner" eller "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` måste vara "emBox", "capHeight", "xHeight" eller "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray BMP-filer är RGB eller gråskala: colorModel rgb eller gray @msg Blend colors: select at least three objects with filled colours Blanda färger: markera minst tre objekt med färgfyllning @msg Combine Slices: the slices have no bounds Kombinera skivor: skivorna har inga gränser @@ -4537,6 +4538,12 @@ axis Both Båda Insert Inline Symbol Infoga symbol i texten ICF Top/Right ICF överkant/höger ICF Bottom/Left ICF nederkant/vänster + Show Font Height Options Visa alternativ för teckenhöjd + Hide Font Height Options Dölj alternativ för teckenhöjd + Set Font Height Reference Ange referens för teckenhöjd + Em Box Em-ruta + x-Height x-höjd + ICF Box ICF-ruta @msg `id` is the step reference {r}, which only `run` and `command_batch` resolve `id` är stegreferensen {r}, som bara `run` och `command_batch` löser upp @msg `ids` must be an array of object ids, not {v} `ids` måste vara en array med objekt-id:n, inte {v} @msg `corners` must be an array of anchor indices, not {v} `corners` måste vara en array med index för ankarpunkter, inte {v} diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index e6517df49..7e5ca938c 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -144,6 +144,17 @@ fn catalan_reads_as_catalan() { assert_eq!(message(ca, nested), "l’ordre `object.group` no està disponible ara mateix: no hi ha res seleccionat"); } +#[test] +fn font_height_reference_labels_are_in_complete_catalogs() { + for lang in LANGUAGES.iter().filter(|lang| lang.complete_menus) { + for label in + ["Show Font Height Options", "Hide Font Height Options", "Set Font Height Reference", "Em Box", "Cap Height", "x-Height", "ICF Box"] + { + assert!(lang.catalog().plain(label).is_some(), "{} is missing {label}", lang.code); + } + } +} + /// Each language counts with its own plural rule: the form a count picks (`one`, then `few`, then /// `other`/`many`). #[test] diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 5aeae9118..18449679b 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3703,6 +3703,7 @@ axis Both Her İkisi @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer dosyanın katmanları okunmadı ({_1}): tek katmanda kendi sayfası olarak geliyor @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part dosyanın katmanları düzenleme verilerinden okunmadı ({_1}): PDF kısmından geliyorlar @msg `to` must be "corner" or "smooth" "to" "köşe" veya "pürüzsüz" olmalıdır +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` "emBox", "capHeight", "xHeight" veya "icfBox" olmalıdır @msg BMP files are RGB or grayscale: colorModel rgb or gray BMP dosyaları RGB veya gri tonlamalıdır: colorModel rgb veya gri @msg Blend colors: select at least three objects with filled colours Renkleri karıştır: dolgulu renklere sahip en az üç nesne seçin @msg Combine Slices: the slices have no bounds Dilimleri Birleştir: dilimlerin sınırları yoktur @@ -4380,6 +4381,12 @@ axis Both Her İkisi Insert Inline Symbol Satır İçi Sembol Ekle ICF Top/Right ICF Üst/Sağ ICF Bottom/Left ICF Alt/Sol + Show Font Height Options Yazı Tipi Yüksekliği Seçeneklerini Göster + Hide Font Height Options Yazı Tipi Yüksekliği Seçeneklerini Gizle + Set Font Height Reference Yazı Tipi Yüksekliği Referansını Ayarla + Em Box Em Kutusu + x-Height x-Yüksekliği + ICF Box ICF Kutusu @msg `id` is the step reference {r}, which only `run` and `command_batch` resolve "id", yalnızca "run" ve "command_batch" çözümleyen {r} adım referansıdır @msg `ids` must be an array of object ids, not {v} "kimlikler" {v} değil, bir nesne kimlikleri dizisi olmalıdır @msg `corners` must be an array of anchor indices, not {v} "köşeler" {v} değil, bir bağlantı indeksleri dizisi olmalıdır diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 53fe1f462..ba2ae98bc 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -943,6 +943,13 @@ font-technology Variable Варіативні Em Box Top/Right Верх/право кегельного майданчика ICF Top/Right Верх/право ICF ICF Bottom/Left Нижній/лівий край ICF + Show Font Height Options Показати параметри висоти шрифту + Hide Font Height Options Приховати параметри висоти шрифту + Set Font Height Reference Задати опорну висоту шрифту + Em Box Кегельний майданчик + Cap Height Висота великих літер + x-Height Висота малих літер + ICF Box Рамка ICF Em Dash Довге тире Em Space Кругла шпація Embed Вбудувати @@ -3925,6 +3932,7 @@ axis Both Обидві @msg the file's layers weren't read ({_1}): it comes in as its page, in one layer шари файлу не прочитано ({_1}): він відкривається як його сторінка, на одному шарі @msg the file's layers weren't read from its editing data ({_1}): they come from its PDF part шари файлу не прочитано з його даних для редагування ({_1}): їх взято з його PDF-частини @msg `to` must be "corner" or "smooth" `to` має бути "corner" або "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` має бути "emBox", "capHeight", "xHeight" або "icfBox" @msg BMP files are RGB or grayscale: colorModel rgb or gray Файли BMP бувають RGB або у відтінках сірого: colorModel rgb або gray @msg Blend colors: select at least three objects with filled colours Змішування кольорів: виберіть не менше трьох об’єктів із кольоровими заливками @msg Combine Slices: the slices have no bounds Об’єднання фрагментів: фрагменти не мають кордонів diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index ea692ca64..e215bb2eb 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1781,6 +1781,13 @@ digits Hindi Hindi Hyphenate Capitalized Words Gạch nối từ viết hoa chữ đầu ICC Profile Hồ sơ ICC ICF Bottom/Left ICF Dưới/Trái + Show Font Height Options Hiện tùy chọn chiều cao phông chữ + Hide Font Height Options Ẩn tùy chọn chiều cao phông chữ + Set Font Height Reference Đặt tham chiếu chiều cao phông chữ + Em Box Ô em + Cap Height Chiều cao chữ hoa + x-Height Chiều cao chữ x + ICF Box Ô ICF ICF Top/Right ICF Trên/Phải Identify Objects By Nhận diện đối tượng theo Ignore Bỏ qua @@ -4169,6 +4176,7 @@ theme Appearance Giao diện @msg `stops` must be an array `stops` phải là một mảng @msg `svg` must be an object of SVG options (see document.formats) `svg` phải là đối tượng tùy chọn SVG (xem document.formats) @msg `to` must be "corner" or "smooth" `to` phải là "corner" hoặc "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` phải là "emBox", "capHeight", "xHeight" hoặc "icfBox" @msg `{_1}` is no save command `{_1}` không phải là lệnh lưu @msg `{area}` is no recovery area name `{area}` không phải là tên vùng phục hồi @msg `{color}` is not a colour (#rrggbb or a preset name) `{color}` không phải là màu (#rrggbb hoặc tên thiết lập sẵn) diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 431eb3355..acdfaf1b1 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -889,6 +889,13 @@ Em Box Top/Right 全角字框上 / 右 ICF Top/Right ICF 上 / 右 ICF Bottom/Left ICF 下 / 左 + Show Font Height Options 显示字体高度选项 + Hide Font Height Options 隐藏字体高度选项 + Set Font Height Reference 设置字体高度参考 + Em Box 全角字框 + Cap Height 大写字母高度 + x-Height x 字高 + ICF Box ICF 框 Em Dash 长破折号 Em Space 全角空格 Embed 嵌入 @@ -3720,6 +3727,7 @@ imageTrace Outline 轮廓 @msg Saving {_1} 正在保存 {_1} @msg Sent to {_1} 已发送到 {_1} @msg `to` must be "corner" or "smooth" `to` 必须是 "corner" 或 "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` 必须是 "emBox"、"capHeight"、"xHeight" 或 "icfBox" @msg a layer couldn't be read 无法读取某个图层 @msg it has more than one artboard 它有多个画板 @msg its editing data has no layers 其编辑数据中没有图层 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index c55958717..460926d92 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -911,6 +911,13 @@ font-technology Variable 可變 Em Box Top/Right 全形字框上 / 右 ICF Top/Right ICF 上 / 右 ICF Bottom/Left ICF 下 / 左 + Show Font Height Options 顯示字體高度選項 + Hide Font Height Options 隱藏字體高度選項 + Set Font Height Reference 設定字體高度參考 + Em Box 全形字框 + Cap Height 大寫字母高度 + x-Height x 字高 + ICF Box ICF 框 Em Dash 長破折號 Em Space 全形空格 Embed 嵌入 @@ -1645,6 +1652,7 @@ file-filter Templates 範本 @msg Saving {_1} 正在儲存 {_1} @msg Sent to {_1} 已傳送至 {_1} @msg `to` must be "corner" or "smooth" `to` 必須是 "corner" 或 "smooth" +@msg `fontHeightReference` must be "emBox", "capHeight", "xHeight" or "icfBox" `fontHeightReference` 必須是 "emBox"、"capHeight"、"xHeight" 或 "icfBox" @msg a layer couldn't be read 有一個圖層無法讀取 @msg it has more than one artboard 它有多個工作區域 @msg its editing data has no layers 其編輯資料中沒有圖層 diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index b3a72b9d3..f00c33d70 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -91,20 +91,27 @@ pub(crate) fn shared_text_flag(app: &VectorcraftApp, f: impl Fn(&vectorcraft_doc /// caret's style at a caret), else every run of the selected type, groups' included. `None` (a /// blank field) where they differ, or with no type. pub(crate) fn shared(app: &VectorcraftApp, f: impl Fn(&CharStyle) -> T) -> Option { - shared_styles(app, |_, _, style| Some(f(style))) + shared_styles(app, |_, _, style, _| Some(f(style))) } /// Dimensional controls use document units and tolerate arithmetic roundoff when comparing /// compensated runs. Keep raw styles available to font/paragraph controls and style definitions, /// even if a collapsed affine prevents editing dimensions. fn shared_effective(app: &VectorcraftApp, f: impl Fn(&CharStyle) -> f64) -> Option { - shared_styles(app, |t, _, style| t.effective_char_style(style).map(|s| Metric(f(&s)))).map(|v| v.0) + shared_styles(app, |t, _, style, _| t.effective_char_style(style).map(|s| Metric(f(&s)))).map(|v| v.0) } -/// The Leading shown: each character's own, or for Auto leading the share of its size its -/// paragraph's Justification › Auto Leading sets, as the layout uses. +/// The Leading shown: each character's own, or for Auto leading the share of the em size their +/// font height reference gives them ([`vectorcraft_text::FontDb::em_size`]) that their paragraph's +/// Justification › Auto Leading sets, as the layout uses. An inline graphic keeps its size. fn shared_leading(app: &VectorcraftApp) -> Option { - shared_styles(app, |t, para, style| t.effective_char_style(style).map(|s| Metric(para.effective_leading(&s)))).map(|v| v.0) + let db = vectorcraft_text::FontDb::global(); + shared_styles(app, |t, para, style, inline| { + let s = t.effective_char_style(style)?; + let em = if inline { s.size } else { db.em_size(&s, t.vertical) }; + Some(Metric(para.effective_leading(&CharStyle { size: em, ..s }))) + }) + .map(|v| v.0) } pub(crate) struct Metric(pub(crate) f64); @@ -115,11 +122,12 @@ impl PartialEq for Metric { } } -/// `f` of each character style in the selection, with its text object and the paragraph the -/// characters are in (a run that spans paragraphs counts once for each). +/// `f` of each character style in the selection, with its text object, the paragraph the +/// characters are in (a run that spans paragraphs counts once for each) and whether the run is an +/// inline graphic. fn shared_styles( app: &VectorcraftApp, - f: impl Fn(&vectorcraft_doc::TextObject, &vectorcraft_doc::ParaStyle, &CharStyle) -> Option, + f: impl Fn(&vectorcraft_doc::TextObject, &vectorcraft_doc::ParaStyle, &CharStyle, bool) -> Option, ) -> Option { let st = app.session.active()?; let text = |id: NodeId| match st.doc.node(id).map(|n| &n.kind) { @@ -131,14 +139,14 @@ fn shared_styles( let t = text(id)?; if b <= a { let para = t.plain_text().get(..a).map_or(0, |s| s.bytes().filter(|&c| c == b'\n').count()); - return f(t, t.para_at(para), &vectorcraft_text::edit::insertion_style(&t.runs, a, b)); + return f(t, t.para_at(para), &vectorcraft_text::edit::insertion_style(&t.runs, a, b), false); } - each_paragraph_run(t, Some((a, b)), |para, style| acc.add(f(t, para, style))); + each_paragraph_run(t, Some((a, b)), |para, style, inline| acc.add(f(t, para, style, inline))); } else { for n in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) { n.walk(&mut |c| { if let NodeKind::Text(t) = &c.kind { - each_paragraph_run(t, None, |para, style| acc.add(f(t, para, style))); + each_paragraph_run(t, None, |para, style, inline| acc.add(f(t, para, style, inline))); } }); } @@ -147,13 +155,14 @@ fn shared_styles( } /// `f` of the paragraph and style of each piece of `t`'s runs that lies in one paragraph (a -/// paragraph's closing line break belongs to it): the pieces within bytes `a..b`, or with no -/// range every run's, empty ones included. With no range, a final empty paragraph (the text ends -/// in a line break) counts too, with the style of the run before it, as the caret there types. +/// paragraph's closing line break belongs to it), and whether the run is an inline graphic: the +/// pieces within bytes `a..b`, or with no range every run's, empty ones included. With no range, a +/// final empty paragraph (the text ends in a line break) counts too, with the style of the run +/// before it, as the caret there types. fn each_paragraph_run<'a>( t: &'a vectorcraft_doc::TextObject, range: Option<(usize, usize)>, - mut f: impl FnMut(&'a vectorcraft_doc::ParaStyle, &'a CharStyle), + mut f: impl FnMut(&'a vectorcraft_doc::ParaStyle, &'a CharStyle, bool), ) { let (mut end, mut para) = (0, 0); // The style of the last character and whether it is a line break: the paragraph after it has @@ -162,7 +171,7 @@ fn each_paragraph_run<'a>( for r in &t.runs { if r.text.is_empty() { if range.is_none_or(|(a, b)| a < end && end < b) { - f(t.para_at(para), &r.style); + f(t.para_at(para), &r.style, r.inline.is_some()); } continue; } @@ -170,7 +179,7 @@ fn each_paragraph_run<'a>( let start = end; end += piece.len(); if range.is_none_or(|(a, b)| start < b && end > a) { - f(t.para_at(para), &r.style); + f(t.para_at(para), &r.style, r.inline.is_some()); } para += usize::from(piece.ends_with('\n')); last = Some((&r.style, piece.ends_with('\n'))); @@ -179,7 +188,7 @@ fn each_paragraph_run<'a>( if range.is_none() && let Some((style, true)) = last { - f(t.para_at(para), style); + f(t.para_at(para), style, false); } } @@ -657,7 +666,8 @@ fn cell(ui: &mut Ui, label: &str, tip: &str, add: impl FnOnce(&mut Ui)) { /// The Character panel's grid of numeric fields, `w` wide: Font Size, Leading, Kerning and /// Tracking, then with `more` the scales, Baseline Shift and Character Rotation (`s`: the style -/// shown). The Properties panel shows its first rows. +/// shown). With Show Font Height Options on, a row under Font Size holds the Set Font Height +/// Reference dropdown. The Properties panel shows its first rows. pub(crate) fn metrics_grid(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, s: &CharStyle, w: f32, more: bool) { let fw = ((w - 66.0) / 2.0).clamp(60.0, 100.0); let unit = app.session.type_unit(); @@ -674,6 +684,10 @@ pub(crate) fn metrics_grid(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, s: & } }); ui.end_row(); + if pstate::(ui.ctx(), FONT_HEIGHT_OPTIONS) { + cell(ui, "T↕", tl!("Set Font Height Reference"), |ui| font_height_reference(app, ui, fw)); + ui.end_row(); + } 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); @@ -741,6 +755,30 @@ pub(crate) fn size_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, width: } } +/// The panel state of the Character panel menu's Show Font Height Options. +const FONT_HEIGHT_OPTIONS: &str = "ch-font-height-options"; + +/// The font height references the Set Font Height Reference dropdown lists, with their +/// `text.setFormat` values. +const FONT_HEIGHT_REFERENCES: [(vectorcraft_doc::FontHeightReference, &str); 4] = [ + (vectorcraft_doc::FontHeightReference::EmBox, "emBox"), + (vectorcraft_doc::FontHeightReference::CapHeight, "capHeight"), + (vectorcraft_doc::FontHeightReference::XHeight, "xHeight"), + (vectorcraft_doc::FontHeightReference::IcfBox, "icfBox"), +]; + +/// The Set Font Height Reference dropdown, `width` wide: the height of the font that the Font Size +/// measures, blank where the characters the panel acts on differ. +fn font_height_reference(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) { + // The labels of [`FONT_HEIGHT_REFERENCES`], in its order. + let labels = [tl!("Em Box"), tl!("Cap Height"), tl!("x-Height"), tl!("ICF Box")]; + let current = shared(app, |s| s.font_height_reference); + let shown = FONT_HEIGHT_REFERENCES.iter().zip(labels).find(|(r, _)| Some(r.0) == current).map_or("", |(_, l)| l); + if let Some((_, key)) = widgets::dropdown_names(ui, "ch-font-height", shown, &labels, width).and_then(|i| FONT_HEIGHT_REFERENCES.get(i)) { + format(app, json!({"fontHeightReference": key})); + } +} + /// The font family menu and the font style dropdown of style `s` (Character and Properties panels, /// Control bar), with their `ids` and `widths`. pub(crate) fn font_pickers(app: &mut VectorcraftApp, ui: &mut Ui, s: &CharStyle, ids: (&str, &str), widths: (f32, f32)) { @@ -1065,6 +1103,11 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if menu_item(ui, if glyph { tl!("Hide Snap to Glyph Options") } else { tl!("Show Snap to Glyph Options") }, true, false) { set_pstate(ui.ctx(), SNAP_TO_GLYPH_OPTIONS, !glyph); } + // Show Font Height Options shows the Set Font Height Reference dropdown under Font Size. + let heights: bool = pstate(ui.ctx(), FONT_HEIGHT_OPTIONS); + if menu_item(ui, if heights { tl!("Hide Font Height Options") } else { tl!("Show Font Height Options") }, true, false) { + set_pstate(ui.ctx(), FONT_HEIGHT_OPTIONS, !heights); + } ui.separator(); let s = st.map(|x| x.0); for (label, key, on) in [ @@ -1191,8 +1234,9 @@ fn fractional_widths_on(app: &VectorcraftApp) -> bool { /// kerning, no baseline shift, 100% scales, no rotation, no underline or strikethrough, All Caps /// and Small Caps off, the normal position (neither superscript nor subscript), Roman Baseline /// character alignment, Proportional Metrics, Tate-chu-yoko, Standard Vertical Roman Alignment and -/// No Break off, the Default character direction and the Default digits, whether or not Show East -/// Asian Options and Show Indic Options are on. +/// No Break off, the Default character direction, the Default digits and the Em Box font height +/// reference, whether or not Show East Asian Options, Show Indic Options and Show Font Height +/// Options are on. fn reset_panel(app: &mut VectorcraftApp) { let d = CharStyle::default(); style(app, json!({"tracking": 0, "leading": "auto"})); @@ -1203,7 +1247,7 @@ fn reset_panel(app: &mut VectorcraftApp) { "allCaps": false, "smallCaps": d.small_caps.is_some(), "position": d.position.id(), "charAlign": d.char_align, "proportionalMetrics": d.proportional_metrics, "tateChuYoko": d.tate_chu_yoko, "standardVerticalRoman": d.standard_vertical_roman, "noBreak": d.no_break, "charDirection": "default", - "digits": d.digits + "digits": d.digits, "fontHeightReference": d.font_height_reference }), ); } diff --git a/crates/ui-egui/src/panels/character/tests.rs b/crates/ui-egui/src/panels/character/tests.rs index 92809a2a4..da7a3dc09 100644 --- a/crates/ui-egui/src/panels/character/tests.rs +++ b/crates/ui-egui/src/panels/character/tests.rs @@ -89,6 +89,102 @@ fn scaled_character_fields_show_document_sizes_and_mixed_group_values() { assert_eq!(shared_effective(&app, |s| s.h_scale), Some(100.0)); } +/// The Leading field shows the Auto leading the characters are set with, their paragraph's Auto +/// Leading share (120% by default) of their em size: 20 pt type measuring a 0.7 em cap height +/// is set at 20 / 0.7 pt. Paragraphs whose Auto leading differs show a blank field, and an +/// explicit leading shows as it is. +#[test] +fn leading_shows_the_auto_leading_of_the_em_size() { + use vectorcraft_text::test_fonts::{HEIGHT_CAP, HEIGHT_FAMILY, height_font}; + vectorcraft_text::FontDb::global().add_font(height_font(true).unwrap()); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "Hx", "size": 20, "font": HEIGHT_FAMILY})).unwrap()["id"].clone(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + let near = |l: Option, want: f64| l.is_some_and(|l| (l - want).abs() < 1e-9); + assert!(near(shared_leading(&app), 24.0), "{:?}", shared_leading(&app)); + app.run("text.setFormat", json!({"fontHeightReference": "capHeight"})).unwrap(); + let cap = f64::from(HEIGHT_CAP) / 1000.0; + assert!(near(shared_leading(&app), 1.2 * 20.0 / cap), "{:?}", shared_leading(&app)); + app.run("text.setFormat", json!({"autoLeading": 150})).unwrap(); + assert!(near(shared_leading(&app), 1.5 * 20.0 / cap), "{:?}", shared_leading(&app)); + // The second paragraph ends in a two-byte character. + let two = app.run("text.create", json!({"x": 20, "y": 120, "text": "Hx\nHé", "size": 20, "font": HEIGHT_FAMILY})).unwrap()["id"].clone(); + app.run("select.set", json!({"ids": [two]})).unwrap(); + assert!(near(shared_leading(&app), 24.0), "{:?}", shared_leading(&app)); + app.run("text.setFormat", json!({"id": two, "start": 3, "end": 6, "autoLeading": 150})).unwrap(); + assert_eq!(shared_leading(&app), None, "the second paragraph's Auto leading is 30 pt"); + app.run("select.set", json!({"ids": [id]})).unwrap(); + app.run("text.setStyle", json!({"leading": 30})).unwrap(); + assert!(near(shared_leading(&app), 30.0), "{:?}", shared_leading(&app)); +} + +/// An inline graphic keeps its size whatever the font height reference, so the Leading field +/// shows 120% of that size for it: blank for 20 pt Cap Height type with a graphic in it, and +/// 24 pt for the graphic alone. +#[test] +fn leading_of_an_inline_graphic_follows_its_own_size() { + use vectorcraft_text::test_fonts::{HEIGHT_FAMILY, height_font}; + vectorcraft_text::FontDb::global().add_font(height_font(true).unwrap()); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + app.run("shape.ellipse", json!({"x": 200, "y": 150, "width": 10, "height": 10})).unwrap(); + app.run("symbol.new", json!({"name": "Dot"})).unwrap(); + app.run("edit.clear", json!({})).unwrap(); + app.session.select_tool("type", Default::default()).unwrap(); + let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "Hx", "size": 20, "font": HEIGHT_FAMILY})).unwrap()["id"].clone(); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + app.run("text.insertInline", json!({"id": id, "at": 2, "symbol": "Dot"})).unwrap(); + app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 5, "fontHeightReference": "capHeight"})).unwrap(); + app.session.set_tool_option("select", &json!({"start": 0, "end": 5})); + assert_eq!(shared_leading(&app), None, "the type is set with 34.3 pt Auto leading and the graphic with 24 pt"); + app.session.set_tool_option("select", &json!({"start": 2, "end": 5})); + assert_eq!(text_editing(&app).map(|(_, a, b)| (a, b)), Some((2, 5))); + let shown = shared_leading(&app); + assert!(shown.is_some_and(|l| (l - 24.0).abs() < 1e-9), "{shown:?}"); +} + +/// A selected text object that ends in a line break has an empty last paragraph, which the +/// Leading field takes in; a Type tool selection of all the text does not. +#[test] +fn leading_of_a_whole_object_takes_in_the_empty_last_paragraph() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "Ab\n", "size": 20})).unwrap()["id"].clone(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + let near = |l: Option, want: f64| l.is_some_and(|l| (l - want).abs() < 1e-9); + assert!(near(shared_leading(&app), 24.0), "{:?}", shared_leading(&app)); + app.run("text.setFormat", json!({"id": id, "start": 3, "end": 3, "autoLeading": 150})).unwrap(); + assert_eq!(shared_leading(&app), None, "the empty last paragraph's Auto leading is 30 pt"); + app.session.select_tool("type", Default::default()).unwrap(); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + app.session.set_tool_option("select", &json!({"start": 0, "end": 3})); + assert_eq!(text_editing(&app).map(|(_, a, b)| (a, b)), Some((0, 3))); + assert!(near(shared_leading(&app), 24.0), "a line break belongs to the paragraph it ends: {:?}", shared_leading(&app)); +} + +/// With the Type tool, the Leading field shows the Auto leading of the paragraphs the selection +/// covers (a line break belongs to the paragraph it ends), and at a caret that of the caret's +/// paragraph. +#[test] +fn leading_follows_the_paragraphs_the_type_tool_selection_covers() { + 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(); + // é is two bytes, 4..6. + let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "Ab\nCé", "size": 20})).unwrap()["id"].clone(); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + app.run("text.setFormat", json!({"id": id, "start": 3, "end": 6, "autoLeading": 150})).unwrap(); + let ranges = [((0, 2), Some(24.0)), ((0, 3), Some(24.0)), ((3, 6), Some(30.0)), ((1, 4), None), ((1, 6), None), ((4, 4), Some(30.0))]; + for ((start, end), want) in ranges { + app.session.set_tool_option("select", &json!({"start": start, "end": end})); + assert_eq!(text_editing(&app).map(|(_, a, b)| (a, b)), Some((start, end))); + let shown = shared_leading(&app); + assert_eq!(shown.is_some(), want.is_some(), "{start}..{end}: {shown:?}"); + assert!(shown.zip(want).is_none_or(|(l, w)| (l - w).abs() < 1e-9), "{start}..{end}: {shown:?}"); + } +} + #[test] fn scaled_range_fields_follow_the_type_tool_selection() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); @@ -498,6 +594,48 @@ fn character_alignment_shows_with_the_east_asian_options() { assert_eq!(shown(&mut app), (true, true)); } +/// The panel menu's Show Font Height Options shows the Set Font Height Reference dropdown, +/// which shows the selected type's reference and sets the one picked (one undo step); Hide +/// Font Height Options hides it again. +#[test] +fn show_font_height_options_shows_the_font_height_reference() { + use super::super::tests_appearance::{click, frame_events, text_rect}; + use vectorcraft_doc::FontHeightReference as R; + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "Hx", "size": 12})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + let reference = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind { + NodeKind::Text(t) => t.runs.iter().map(|r| r.style.font_height_reference).collect::>(), + _ => panic!("text"), + }; + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let texts = |ctx: &egui::Context, app: &mut VectorcraftApp, f: fn(&mut VectorcraftApp, &mut Ui)| { + frame_events(ctx, app, vec![], f); + frame_events(ctx, app, vec![], f) + }; + // A menu item's label is drawn after its check column (a check mark or spaces). + let item = |texts: &[(String, egui::Rect)], s: &str| texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == s).map(|(_, r)| r.center()); + assert!(item(&texts(&ctx, &mut app, show), "Em Box").is_none()); + let at = item(&texts(&ctx, &mut app, menu), "Show Font Height Options").unwrap(); + click(&ctx, &mut app, at, menu); + assert!(item(&texts(&ctx, &mut app, menu), "Hide Font Height Options").is_some()); + let panel = texts(&ctx, &mut app, show); + assert!(item(&panel, "Em Box").is_some(), "{panel:?}"); + // Pick Cap Height from the dropdown. + click(&ctx, &mut app, text_rect(&panel, "Em Box").center(), show); + let undo = app.session.active().unwrap().history.undo.len(); + let open = texts(&ctx, &mut app, show); + click(&ctx, &mut app, text_rect(&open, "Cap Height").center(), show); + assert_eq!(reference(&app), [R::CapHeight]); + assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1); + assert!(item(&texts(&ctx, &mut app, show), "Cap Height").is_some()); + let at = item(&texts(&ctx, &mut app, menu), "Hide Font Height Options").unwrap(); + click(&ctx, &mut app, at, menu); + assert!(item(&texts(&ctx, &mut app, show), "Cap Height").is_none()); +} + /// Tate-chu-yoko is in the panel menu with the East Asian options, and choosing it marks the /// selected characters. #[test] @@ -723,8 +861,8 @@ fn no_break_menu_item_toggles_the_selected_characters() { /// Reset Panel sets every character attribute the panel shows, except the font family, style /// and size, back to its default (Small Caps, the position, Character Alignment, Proportional -/// Metrics, Tate-chu-yoko, No Break, Character Direction and Digits included), in the selected -/// type and in the text the Type tool has selected while it edits. +/// Metrics, Tate-chu-yoko, No Break, Character Direction, Digits and the font height reference +/// included), in the selected type and in the text the Type tool has selected while it edits. #[test] fn reset_panel_resets_every_attribute_but_the_font() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); @@ -755,6 +893,7 @@ fn reset_panel_resets_every_attribute_but_the_font() { s.no_break != d.no_break, s.char_direction != d.char_direction, s.digits != d.digits, + s.font_height_reference != d.font_height_reference, ] }; let custom = |app: &mut VectorcraftApp| { @@ -764,16 +903,16 @@ fn reset_panel_resets_every_attribute_but_the_font() { "id": id, "tracking": 50, "leading": 30, "kerning": 20, "baselineShift": 3, "hScale": 120, "vScale": 90, "rotation": 15, "underline": true, "strikethrough": true, "allCaps": true, "smallCaps": true, "position": "superscript", "charAlign": "emBoxCenter", "proportionalMetrics": true, "tateChuYoko": true, "standardVerticalRoman": true, - "noBreak": true, "charDirection": "rightToLeft", "digits": "hindi" + "noBreak": true, "charDirection": "rightToLeft", "digits": "hindi", "fontHeightReference": "capHeight" }), ) .unwrap(); }; let plain = runs(&app)[0].1.clone(); - assert_eq!(differs(&plain, &CharStyle::default()), [false; 19]); + assert_eq!(differs(&plain, &CharStyle::default()), [false; 20]); custom(&mut app); let styled = runs(&app)[0].1.clone(); - assert_eq!(differs(&styled, &CharStyle::default()), [true; 19]); + assert_eq!(differs(&styled, &CharStyle::default()), [true; 20]); app.run("select.set", json!({"ids": [id]})).unwrap(); reset_panel(&mut app); assert_eq!(runs(&app), [("One 12".to_string(), plain.clone())]); diff --git a/docs/mcp.md b/docs/mcp.md index ddd23ecfa..0f586deb3 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5119,6 +5119,27 @@ A digit the font lacks comes from a fallback font. Default and saved only when s {"name":"run_command","arguments":{"command":"text.setFormat","params":{"digits":"hindi"}}} ``` +## Font height reference + +`text.setFormat {fontHeightReference: "emBox"|"capHeight"|"xHeight"|"icfBox"}` (the selected type, or `ids`) and +`text.setRangeStyle {id, start, end, fontHeightReference}` (a range) set the height of the font that the characters' font +size measures, as the Character panel's Set Font Height Reference dropdown does (panel menu › Show Font Height Options). +With `emBox`, the default, the font size is the size of the em square. With `capHeight`, `xHeight` or `icfBox`, the +characters are set at the em size at which the font's cap height or x height (OS/2 `sCapHeight`, `sxHeight`), or the +height of its ideographic character face (the BASE table's `icfb` and `icft`, else the ink of some ideographs and kana; +across a vertical line, its width), equals the font size: 20 pt Cap Height type in a font whose capitals are 0.7 em +tall is set at about 28.6 pt, and its font size stays 20 pt. With a font that has no such value, or one outside 0.1 to 2 +em, the size measures the em box. Auto leading, tracking and kerning follow the em size the characters are set at (the +Character panel's Leading field shows that Auto leading, and `type.step {attribute: "leading"}` starts from it); +explicit leading and baseline shift stay in points, and an inline graphic keeps its size. +SVG, PDF and legacy `.ai` export and the CSS Properties rules write the em size. DXF export writes 0.7 times the em +size as each TEXT entity's height (group 40, the height of its capitals). +Saved only when not `emBox`. One undo step. + +```json +{"name":"run_command","arguments":{"command":"text.setFormat","params":{"fontHeightReference":"capHeight"}}} +``` + ## Language `text.setFormat {language}` (the selected type, or `ids`) and `text.setRangeStyle {id, start, end, language}` (a range) diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index ddcee05b2..581f4bd9f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -855,6 +855,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Kerning across style runs:** characters that differ only in fill, stroke, underline, strikethrough, overprint, character style name, leading, baseline shift, rotation, Character Alignment, kerning, or tracking that keeps or drops ligatures alike are shaped together, so restyling one character keeps the font's kerning pairs with its neighbors. A character with manual kerning that follows one with automatic kerning keeps the font's kerning with it (unless All Caps or Small Caps sets it as two letters, as ß becomes SS), and its value replaces the font's kerning after it. A ligature does not form across a change of style, and a change of font, size or OpenType features still shapes the characters apart. Behavior reference: [Line and character spacing](https://web.archive.org/web/20260916050333/https://helpx.adobe.com/illustrator/using/line-character-spacing.html) (public documentation). Tests: `crates/text/src/tests/typo.rs`, `crates/text/src/tests/bidi.rs`. - **Recent font pickers (#3515):** The shared Character, Properties, Glyphs and Find Font family picker lists recently applied families first, once each, in most-recent order and limited by Preferences › Type › Number of Recent Fonts. Search and filters apply to both sections; unavailable families are omitted. Section headings cannot be selected or previewed. Confirmed character-range font edits update the same saved history; previews and failed edits do not. Requirement: [issue #3515](https://github.com/storytold/vectorcraft/issues/3515). - **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, Text Flow by rows or by columns (#4978), vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`), and an anchor of the path of type on a path outside perspective, the type reflowing along it (`text.reshapePath`, #3980). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. The Character panel's Variable Font sliders set each axis (see Fonts). Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). Cmd+Shift+C (Align center) works on Windows and Linux too (#1940). A double-click on area type's bottom middle handle turns on Auto Size (#1940). Print and Print Preview warn about the area type and type on a path that prints without the text that doesn't fit, and give their ids. A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). + The Character panel menu's Show Font Height Options shows a Set Font Height Reference dropdown under Font Size (`text.setFormat` or `text.setRangeStyle` `{fontHeightReference}`). With Em Box, the default, the font size is the size of the em square; with Cap Height, x-Height or ICF Box, the characters are set at the em size at which the font's OS/2 cap height, its x height or the height of its ideographic character face equals the font size. With a font that has no such value, or one outside 0.1 to 2 em, the size measures the em box. SVG, PDF and legacy `.ai` export and the CSS Properties rules write the em size the characters are drawn at, and DXF export writes 0.7 times that em size as each TEXT entity's height (group 40, the height of its capitals). Behavior reference: [Vary font height](https://web.archive.org/web/20251118174722/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/vary-font-height.html). The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in rectangular area type, rows and columns included. In area type of another shape, or with text wrap, such a paragraph is broken with the Single-line Composer. The Character panel's Small Caps, Superscript and Subscript buttons and the panel menu's items of the same names turn small caps and the superscript or subscript position on and off for the selected type, or for the text the Type tool has selected (`text.setFormat` or `text.setRangeStyle` `{smallCaps, position}`), in Document Setup's proportions. Turning Superscript on turns Subscript off, and turning Subscript on turns Superscript off. A button or item shows as on when the first selected character (at a caret, the character before it) has its attribute. The OpenType panel's Stylistic Alternates and Titling Alternates checkboxes turn `salt` and `titl` on and off for the selected type, and its Figure menu lists Default Figure, Tabular Lining, Proportional Lining, Proportional Oldstyle and Tabular Oldstyle; Proportional Lining sets `pnum` and `lnum` and turns `tnum` and `onum` off, Tabular Lining turns `pnum` off and keeps `lnum`, Proportional Oldstyle turns `lnum` off and keeps `pnum`, and Default Figure and Tabular Oldstyle turn `pnum` and `lnum` off. Behavior reference: [Format text with OpenType features](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/format-text-with-opentype-features.html) and [Use swashes, titling alternates, or stylistic alternates](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/use-swashes-titling-alternates-or-stylistic-alternates.html) (public documentation). Tests: `panels::opentype::tests`.