mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Type: composite fonts take custom character sets
Type > Composite Fonts... has Custom: named character sets, each setting the characters it lists in its own font, style, size, baseline and scales ahead of their class. The later of two sets that list a character sets it. Characters are added by Direct Entry or by Unicode code and deleted one at a time, and the parts table scrolls when it grows. - type.compositeFonts.set takes `custom`; part keys left out take the Kanji part's values. Sets are saved with the document and in .vccomposite files, and Export refuses a file over 4 MB. - VectorCraft's own limits: 64 sets per font, 8,192 characters per set, each character once per set, up to U+FFFF and no control characters. - The font database caches each font's character lookup. Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
This commit is contained in:
@@ -128,7 +128,7 @@ pub use text::{
|
||||
RubyPosition, RubySpan, ScriptMetrics, SingleWordJustify, TabAlign, TabStop, TextFlow, TextKind, TextObject, TextRun, TextStyleDef, TextWrap,
|
||||
VerticalAlign, WrapShape,
|
||||
};
|
||||
pub use text::{CharClass, CompositeFont, CompositePart, CompositeParts};
|
||||
pub use text::{CharClass, CompositeClass, CompositeFont, CompositePart, CompositeParts, CustomCharSet};
|
||||
pub use trace::TraceView;
|
||||
pub use variables::{DataSet, DataValue, Variable, VariableKind, Variables};
|
||||
pub use vectorcraft_color as color;
|
||||
|
||||
@@ -105,8 +105,13 @@ impl Document {
|
||||
n += shared(&self.para_styles, &newer.para_styles, styles);
|
||||
n += shared(&self.composite_fonts, &newer.composite_fonts, |c| {
|
||||
let part = |p: &crate::CompositePart| p.font.capacity() + p.style.capacity();
|
||||
let custom = |f: &crate::CompositeFont| {
|
||||
vec_bytes(&f.custom) + f.custom.iter().map(|s| s.name.capacity() + s.characters.capacity() + part(&s.part)).sum::<usize>()
|
||||
};
|
||||
vec_bytes(c)
|
||||
+ c.iter().map(|f| f.name.capacity() + crate::CharClass::ALL.into_iter().map(|k| part(f.parts.get(k))).sum::<usize>()).sum::<usize>()
|
||||
+ c.iter()
|
||||
.map(|f| f.name.capacity() + crate::CharClass::ALL.into_iter().map(|k| part(f.parts.get(k))).sum::<usize>() + custom(f))
|
||||
.sum::<usize>()
|
||||
});
|
||||
n += shared(&self.saved_selections, &newer.saved_selections, |s| {
|
||||
vec_bytes(s) + s.iter().map(|s: &SavedSelection| s.name.capacity() + vec_bytes(&s.objects)).sum::<usize>()
|
||||
|
||||
@@ -22,8 +22,8 @@ pub use char_style::{
|
||||
WARICHU_LINES, WARICHU_SCALE, WarichuAlign,
|
||||
};
|
||||
pub use composite::{
|
||||
COMPOSITE_BASELINE, COMPOSITE_FONTS_MAX, COMPOSITE_NAME_MAX, COMPOSITE_SCALE, COMPOSITE_SIZE, CharClass, CompositeFont, CompositePart,
|
||||
CompositeParts,
|
||||
COMPOSITE_BASELINE, COMPOSITE_FONTS_MAX, COMPOSITE_NAME_MAX, COMPOSITE_SCALE, COMPOSITE_SIZE, CUSTOM_CHARS_MAX, CUSTOM_SETS_MAX, CharClass,
|
||||
CompositeClass, CompositeClasses, CompositeFont, CompositePart, CompositeParts, CustomCharSet,
|
||||
};
|
||||
pub use para_style::{
|
||||
Burasagari, Composer, FirstBaseline, HyphenationOptions, JustificationOptions, Justify, Kashida, Kinsoku, KinsokuShoriType, LeadingModel,
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
//! Type › Composite Fonts…: a font made of other fonts, one for each class of character (the
|
||||
//! Kanji font is the base; kana, punctuation, symbols, Roman letters and numerals each take a font
|
||||
//! of their own, sized, raised and scaled against it). Type set in a composite font names it as its
|
||||
//! family; the layout sets each character in its class's font.
|
||||
//! of their own, sized, raised and scaled against it). Custom character sets give characters the
|
||||
//! user picks a font of their own too, ahead of their class; of two sets that list a character,
|
||||
//! the later one sets it. Type set in a composite font names it as its family; the layout sets each
|
||||
//! character in its set's or its class's font.
|
||||
//!
|
||||
//! Behavior reference (public documentation):
|
||||
//! <https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=347>.
|
||||
//! Which characters fall in which class, the ranges and the defaults are VectorCraft's own.
|
||||
//! <https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=347>,
|
||||
//! custom character sets:
|
||||
//! <https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=348>.
|
||||
//! Which characters fall in which class, the ranges, the limits and the defaults are VectorCraft's
|
||||
//! own.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -179,12 +184,76 @@ impl CompositeParts {
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom character set of a composite font: characters chosen by the user, set in a part of
|
||||
/// their own instead of their class's part.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CustomCharSet {
|
||||
/// The set's name, unique in its composite font in any case.
|
||||
pub name: String,
|
||||
/// The set's characters, each once, in the order they were added.
|
||||
#[serde(default)]
|
||||
pub characters: String,
|
||||
/// The font, style, size, baseline and scales its characters take.
|
||||
#[serde(flatten)]
|
||||
pub part: CompositePart,
|
||||
}
|
||||
|
||||
impl CustomCharSet {
|
||||
/// An empty set named `name`, its characters set in `part`.
|
||||
pub fn new(name: impl Into<String>, part: CompositePart) -> Self {
|
||||
Self { name: name.into(), characters: String::new(), part }
|
||||
}
|
||||
|
||||
/// Whether `c` can be in a custom character set: not a control character, and in the Basic
|
||||
/// Multilingual Plane (U+0000 to U+FFFF).
|
||||
pub fn takes(c: char) -> bool {
|
||||
!c.is_control() && (c as u32) <= 0xFFFF
|
||||
}
|
||||
|
||||
/// The character a Unicode code names: hexadecimal digits, optionally after `U+` or `0x`
|
||||
/// (`3042`, `U+3042`). `None` for anything else or a code [`Self::takes`] refuses.
|
||||
pub fn char_from_code(code: &str) -> Option<char> {
|
||||
let code = code.trim();
|
||||
let hex = ["U+", "u+", "0x", "0X"].iter().find_map(|p| code.strip_prefix(p)).unwrap_or(code);
|
||||
if hex.is_empty() || hex.len() > 6 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
return None;
|
||||
}
|
||||
u32::from_str_radix(hex, 16).ok().and_then(char::from_u32).filter(|&c| Self::takes(c))
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a composite font sets a character: the part of one of its six classes, or the part of
|
||||
/// one of its custom character sets (an index into [`CompositeFont::custom`]).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum CompositeClass {
|
||||
Fixed(CharClass),
|
||||
Custom(usize),
|
||||
}
|
||||
|
||||
/// The custom character set each character of a composite font's sets falls in
|
||||
/// ([`CompositeFont::classes`]): the last set that lists it.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CompositeClasses {
|
||||
custom: std::collections::HashMap<char, usize>,
|
||||
}
|
||||
|
||||
impl CompositeClasses {
|
||||
/// Where the composite font sets `c`: the last custom set that lists it, else its class.
|
||||
pub fn of(&self, c: char) -> CompositeClass {
|
||||
self.custom.get(&c).map_or(CompositeClass::Fixed(CharClass::of(c)), |&i| CompositeClass::Custom(i))
|
||||
}
|
||||
}
|
||||
|
||||
/// A composite font (Type › Composite Fonts…), saved with the document. Its name is the family
|
||||
/// that type set in it names.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CompositeFont {
|
||||
pub name: String,
|
||||
pub parts: CompositeParts,
|
||||
/// Custom character sets, checked before the classes. A character two sets list is set in the
|
||||
/// later set's part.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub custom: Vec<CustomCharSet>,
|
||||
}
|
||||
|
||||
/// The size a part can take, in percent of the type's size.
|
||||
@@ -197,11 +266,40 @@ pub const COMPOSITE_SCALE: std::ops::RangeInclusive<f64> = 10.0..=400.0;
|
||||
pub const COMPOSITE_NAME_MAX: usize = 255;
|
||||
/// The most composite fonts a document or a file keeps.
|
||||
pub const COMPOSITE_FONTS_MAX: usize = 256;
|
||||
/// The most custom character sets a composite font keeps.
|
||||
pub const CUSTOM_SETS_MAX: usize = 64;
|
||||
/// The most characters a custom character set keeps.
|
||||
pub const CUSTOM_CHARS_MAX: usize = 8192;
|
||||
|
||||
impl CompositeFont {
|
||||
/// A composite font named `name` setting every class in `font` and `style`.
|
||||
pub fn new(name: impl Into<String>, font: impl Into<String>, style: impl Into<String>) -> Self {
|
||||
Self { name: name.into(), parts: CompositeParts::all(CompositePart::new(font, style)) }
|
||||
Self { name: name.into(), parts: CompositeParts::all(CompositePart::new(font, style)), custom: Vec::new() }
|
||||
}
|
||||
|
||||
/// The part that sets the characters of `class`, `None` for a custom set the font doesn't have.
|
||||
pub fn part(&self, class: CompositeClass) -> Option<&CompositePart> {
|
||||
match class {
|
||||
CompositeClass::Fixed(c) => Some(self.parts.get(c)),
|
||||
CompositeClass::Custom(i) => self.custom.get(i).map(|s| &s.part),
|
||||
}
|
||||
}
|
||||
|
||||
/// Where this font sets character `c`: the last custom set that lists it, else its class. For
|
||||
/// many characters, [`Self::classes`] looks each up faster.
|
||||
pub fn class_of(&self, c: char) -> CompositeClass {
|
||||
self.custom.iter().rposition(|s| s.characters.contains(c)).map_or(CompositeClass::Fixed(CharClass::of(c)), CompositeClass::Custom)
|
||||
}
|
||||
|
||||
/// The custom set each character of this font's sets falls in, to look up many characters.
|
||||
pub fn classes(&self) -> CompositeClasses {
|
||||
let mut custom = std::collections::HashMap::new();
|
||||
for (i, s) in self.custom.iter().enumerate() {
|
||||
for c in s.characters.chars() {
|
||||
custom.insert(c, i);
|
||||
}
|
||||
}
|
||||
CompositeClasses { custom }
|
||||
}
|
||||
|
||||
/// Do `a` and `b` name the same composite font: equal in any case?
|
||||
@@ -223,36 +321,50 @@ impl CompositeFont {
|
||||
}
|
||||
|
||||
/// Why this composite font can't be used, if it can't: a name [`Self::clean_name`] refuses, a
|
||||
/// part without a font, a value out of its range, or a scale or Scale at Center on a class
|
||||
/// that has none.
|
||||
/// part without a font, a value out of its range, a scale or Scale at Center on a class that
|
||||
/// has none, or a custom character set [`Self::check_custom`] refuses.
|
||||
pub fn check(&self) -> Result<(), String> {
|
||||
if Self::clean_name(&self.name).as_deref() != Some(self.name.as_str()) {
|
||||
return Err(format!("`{}` can't name a composite font", self.name));
|
||||
}
|
||||
for class in CharClass::ALL {
|
||||
let p = self.parts.get(class);
|
||||
let k = class.key();
|
||||
if p.font.trim().is_empty() {
|
||||
return Err(format!("{k}: give a font"));
|
||||
check_part(self.parts.get(class), class.key(), class.scales(), class == CharClass::Kana)?;
|
||||
}
|
||||
self.check_custom()
|
||||
}
|
||||
|
||||
/// Why this font's custom character sets can't be used, if they can't: more than
|
||||
/// [`CUSTOM_SETS_MAX`], a name [`Self::clean_name`] refuses or two sets share (in any case), a
|
||||
/// character [`CustomCharSet::takes`] refuses or a set lists twice, more than
|
||||
/// [`CUSTOM_CHARS_MAX`] characters in a set, or a part [`Self::check`] would refuse. A set's
|
||||
/// characters can be scaled across and up; only kana are scaled at the center.
|
||||
pub fn check_custom(&self) -> Result<(), String> {
|
||||
if self.custom.len() > CUSTOM_SETS_MAX {
|
||||
return Err(format!("a composite font keeps at most {CUSTOM_SETS_MAX} character sets"));
|
||||
}
|
||||
let mut names = std::collections::HashSet::new();
|
||||
for s in &self.custom {
|
||||
let name = &s.name;
|
||||
if Self::clean_name(name).as_deref() != Some(name.as_str()) {
|
||||
return Err(format!("`{name}` can't name a character set"));
|
||||
}
|
||||
let within = |v: f64, r: &std::ops::RangeInclusive<f64>| v.is_finite() && r.contains(&v);
|
||||
if !within(p.size, &COMPOSITE_SIZE) {
|
||||
return Err(format!("{k}: size must be {} to {}%", COMPOSITE_SIZE.start(), COMPOSITE_SIZE.end()));
|
||||
if !names.insert(name.to_lowercase()) {
|
||||
return Err(format!("two character sets are named `{name}`"));
|
||||
}
|
||||
if !within(p.baseline, &COMPOSITE_BASELINE) {
|
||||
return Err(format!("{k}: baseline must be {} to {}%", COMPOSITE_BASELINE.start(), COMPOSITE_BASELINE.end()));
|
||||
}
|
||||
for v in [p.h_scale, p.v_scale] {
|
||||
if !within(v, &COMPOSITE_SCALE) {
|
||||
return Err(format!("{k}: scale must be {} to {}%", COMPOSITE_SCALE.start(), COMPOSITE_SCALE.end()));
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for (n, c) in s.characters.chars().enumerate() {
|
||||
if n == CUSTOM_CHARS_MAX {
|
||||
return Err(format!("`{name}`: a character set keeps at most {CUSTOM_CHARS_MAX} characters"));
|
||||
}
|
||||
if v != 100.0 && !class.scales() {
|
||||
return Err(format!("{k}: only kana, roman and numerals can be scaled"));
|
||||
let code = c as u32;
|
||||
if !CustomCharSet::takes(c) {
|
||||
return Err(format!("`{name}`: U+{code:04X} is a control character or past U+FFFF"));
|
||||
}
|
||||
if !seen.insert(c) {
|
||||
return Err(format!("`{name}`: U+{code:04X} is listed twice"));
|
||||
}
|
||||
}
|
||||
if p.scale_at_center && class != CharClass::Kana {
|
||||
return Err(format!("{k}: only kana can be scaled at the center"));
|
||||
}
|
||||
check_part(&s.part, name, true, false)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -264,9 +376,41 @@ impl CompositeFont {
|
||||
let mut names = std::collections::HashSet::new();
|
||||
let mut kept: Vec<CompositeFont> =
|
||||
fonts.iter().filter(|f| f.check().is_ok() && names.insert(f.name.to_lowercase())).take(COMPOSITE_FONTS_MAX).cloned().collect();
|
||||
kept.retain(|f| CharClass::ALL.iter().all(|&c| !names.contains(&f.parts.get(c).font.to_lowercase())));
|
||||
kept.retain(|f| f.part_fonts().all(|font| !names.contains(&font.to_lowercase())));
|
||||
(kept.len() != fonts.len()).then_some(kept)
|
||||
}
|
||||
|
||||
/// The fonts of the parts, each class's then each custom set's.
|
||||
pub fn part_fonts(&self) -> impl Iterator<Item = &str> {
|
||||
CharClass::ALL.into_iter().map(|c| self.parts.get(c).font.as_str()).chain(self.custom.iter().map(|s| s.part.font.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Why part `p` (of the class or set named `k`) can't be used, if it can't: no font, a value out
|
||||
/// of its range, a scale when `scales` is false, or Scale at Center when `center` is false.
|
||||
fn check_part(p: &CompositePart, k: &str, scales: bool, center: bool) -> Result<(), String> {
|
||||
if p.font.trim().is_empty() {
|
||||
return Err(format!("{k}: give a font"));
|
||||
}
|
||||
let within = |v: f64, r: &std::ops::RangeInclusive<f64>| v.is_finite() && r.contains(&v);
|
||||
if !within(p.size, &COMPOSITE_SIZE) {
|
||||
return Err(format!("{k}: size must be {} to {}%", COMPOSITE_SIZE.start(), COMPOSITE_SIZE.end()));
|
||||
}
|
||||
if !within(p.baseline, &COMPOSITE_BASELINE) {
|
||||
return Err(format!("{k}: baseline must be {} to {}%", COMPOSITE_BASELINE.start(), COMPOSITE_BASELINE.end()));
|
||||
}
|
||||
for v in [p.h_scale, p.v_scale] {
|
||||
if !within(v, &COMPOSITE_SCALE) {
|
||||
return Err(format!("{k}: scale must be {} to {}%", COMPOSITE_SCALE.start(), COMPOSITE_SCALE.end()));
|
||||
}
|
||||
if v != 100.0 && !scales {
|
||||
return Err(format!("{k}: only kana, roman and numerals can be scaled"));
|
||||
}
|
||||
}
|
||||
if p.scale_at_center && !center {
|
||||
return Err(format!("{k}: only kana can be scaled at the center"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -341,4 +485,81 @@ mod tests {
|
||||
spaced.name = " Mixed".into();
|
||||
assert!(spaced.check().is_err());
|
||||
}
|
||||
|
||||
/// A character a custom set lists falls in that set ahead of its class, the later of two sets
|
||||
/// that list it wins, and the quick lookup agrees with the one-character one.
|
||||
#[test]
|
||||
fn custom_sets_take_their_characters_before_the_classes() {
|
||||
let mut f = CompositeFont::new("Mixed", "Inter", "Regular");
|
||||
let mut a = CustomCharSet::new("Marks", CompositePart::new("Source Sans 3", "Bold"));
|
||||
a.characters = "※A".into();
|
||||
let mut b = CustomCharSet::new("Letters", CompositePart::new("Inter", "Italic"));
|
||||
b.characters = "AB".into();
|
||||
f.custom = vec![a, b];
|
||||
let classes = f.classes();
|
||||
for (c, want) in [
|
||||
('※', CompositeClass::Custom(0)),
|
||||
('A', CompositeClass::Custom(1)),
|
||||
('B', CompositeClass::Custom(1)),
|
||||
('C', CompositeClass::Fixed(CharClass::Roman)),
|
||||
('漢', CompositeClass::Fixed(CharClass::Kanji)),
|
||||
] {
|
||||
assert_eq!(f.class_of(c), want, "{c:?}");
|
||||
assert_eq!(classes.of(c), want, "{c:?}");
|
||||
}
|
||||
assert_eq!(f.part(CompositeClass::Custom(0)).map(|p| p.style.as_str()), Some("Bold"));
|
||||
assert_eq!(f.part(CompositeClass::Custom(2)), None);
|
||||
assert_eq!(f.check(), Ok(()));
|
||||
// Read and written with the part's fields beside the set's name and characters.
|
||||
let json = serde_json::to_value(&f).unwrap();
|
||||
assert_eq!(json["custom"][1]["characters"], "AB");
|
||||
assert_eq!(json["custom"][1]["style"], "Italic");
|
||||
assert_eq!(serde_json::from_value::<CompositeFont>(json).unwrap(), f);
|
||||
// A font without sets is written as before.
|
||||
assert!(serde_json::to_value(CompositeFont::new("Plain", "Inter", "Regular")).unwrap().get("custom").is_none());
|
||||
}
|
||||
|
||||
/// Set names, characters and limits are checked; sets scale like Roman letters.
|
||||
#[test]
|
||||
fn custom_sets_check_their_names_and_characters() {
|
||||
let ok = CompositeFont::new("Mixed", "Inter", "Regular");
|
||||
let with = |sets: Vec<(&str, &str)>, edit: fn(&mut CompositePart)| {
|
||||
let mut f = ok.clone();
|
||||
for (name, chars) in sets {
|
||||
let mut s = CustomCharSet::new(name, CompositePart::new("Inter", "Regular"));
|
||||
s.characters = chars.into();
|
||||
edit(&mut s.part);
|
||||
f.custom.push(s);
|
||||
}
|
||||
f.check()
|
||||
};
|
||||
let none = |_: &mut CompositePart| {};
|
||||
assert_eq!(with(vec![("Empty", "")], none), Ok(()));
|
||||
assert_eq!(with(vec![("Wide", "AB")], |p| p.h_scale = 80.0), Ok(()));
|
||||
assert!(with(vec![("Wide", "AB")], |p| p.scale_at_center = true).is_err());
|
||||
assert!(with(vec![("Big", "A")], |p| p.size = 401.0).is_err());
|
||||
assert!(with(vec![("Same", "A"), ("same", "B")], none).unwrap_err().contains("two character sets"));
|
||||
assert!(with(vec![(" Pad", "A")], none).is_err());
|
||||
assert!(with(vec![("Twice", "ABA")], none).unwrap_err().contains("U+0041 is listed twice"));
|
||||
assert!(with(vec![("Tab", "A\t")], none).unwrap_err().contains("U+0009"));
|
||||
assert!(with(vec![("Astral", "𠀋")], none).unwrap_err().contains("U+2000B"));
|
||||
let many: String = (0x4E00u32..).filter_map(char::from_u32).take(CUSTOM_CHARS_MAX + 1).collect();
|
||||
let mut f = ok.clone();
|
||||
f.custom.push(CustomCharSet { name: "Many".into(), characters: many, part: CompositePart::new("Inter", "Regular") });
|
||||
assert!(f.check().unwrap_err().contains("at most"));
|
||||
let mut f = ok.clone();
|
||||
f.custom = (0..=CUSTOM_SETS_MAX).map(|i| CustomCharSet::new(format!("S{i}"), CompositePart::new("Inter", "Regular"))).collect();
|
||||
assert!(f.check().is_err());
|
||||
}
|
||||
|
||||
/// Codes are hexadecimal, with or without `U+` or `0x`, inside the Basic Multilingual Plane.
|
||||
#[test]
|
||||
fn custom_set_codes_name_characters() {
|
||||
assert_eq!(CustomCharSet::char_from_code("3042"), Some('あ'));
|
||||
assert_eq!(CustomCharSet::char_from_code(" U+203B "), Some('※'));
|
||||
assert_eq!(CustomCharSet::char_from_code("0x41"), Some('A'));
|
||||
for bad in ["", "U+", "zz", "20000B", "D800", "0009", "1F600", "1234567", "+41"] {
|
||||
assert_eq!(CustomCharSet::char_from_code(bad), None, "{bad:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
//! Type › Composite Fonts…: the document's composite fonts ([`vectorcraft_doc::CompositeFont`]),
|
||||
//! which set each class of character (Kanji, Kana, Punctuation, Symbols, Roman, Numerals) in a
|
||||
//! font of its own. Type names a composite font as its family, and the font menus list them first.
|
||||
//! The font database holds the active document's composite fonts ([`sync`]); a file exported here
|
||||
//! carries them to another document.
|
||||
//! font of its own, and the characters of each custom character set in the set's font. Type names
|
||||
//! a composite font as its family, and the font menus list them first. The font database holds the
|
||||
//! active document's composite fonts ([`sync`]); a file exported here carries them to another
|
||||
//! document.
|
||||
//!
|
||||
//! Behavior reference (public documentation):
|
||||
//! <https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=347>.
|
||||
//! <https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=347>,
|
||||
//! custom character sets:
|
||||
//! <https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=348>.
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::text::{COMPOSITE_FONTS_MAX, COMPOSITE_NAME_MAX};
|
||||
use vectorcraft_doc::{CharClass, CompositeFont, CompositePart, CompositeParts};
|
||||
use vectorcraft_doc::text::{COMPOSITE_FONTS_MAX, COMPOSITE_NAME_MAX, CUSTOM_CHARS_MAX, CUSTOM_SETS_MAX};
|
||||
use vectorcraft_doc::{CharClass, CompositeFont, CompositePart, CompositeParts, CustomCharSet};
|
||||
use vectorcraft_text::FontDb;
|
||||
|
||||
use super::fileio::{read_file_head, write_file};
|
||||
@@ -26,6 +29,10 @@ const MAX_FILE_BYTES: usize = 4 << 20;
|
||||
/// The keys a part may give.
|
||||
const PART_KEYS: [&str; 7] = ["font", "style", "size", "baseline", "hScale", "vScale", "scaleAtCenter"];
|
||||
|
||||
/// The keys a custom character set may give: its name and characters, then its part's values
|
||||
/// (a set is never scaled at the center).
|
||||
const SET_KEYS: [&str; 8] = ["name", "characters", "font", "style", "size", "baseline", "hScale", "vScale"];
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
@@ -33,7 +40,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Composite Fonts",
|
||||
[],
|
||||
None,
|
||||
"{} the active document's composite fonts → {fonts: [{name, parts: {kanji, kana, punctuation, symbols, roman, numerals: {font, style, size: % of the type size, baseline: % of the type size (positive raises), hScale: %, vScale: %, scaleAtCenter?: bool}}}]}",
|
||||
"{} the active document's composite fonts → {fonts: [{name, parts: {kanji, kana, punctuation, symbols, roman, numerals: {font, style, size: % of the type size, baseline: % of the type size (positive raises), hScale: %, vScale: %, scaleAtCenter?: bool}}, custom?: [{name, characters, font, style, size, baseline, hScale, vScale}]}]}",
|
||||
has_doc,
|
||||
list
|
||||
),
|
||||
@@ -42,7 +49,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Composite Fonts…",
|
||||
["Type"],
|
||||
None,
|
||||
"{fonts: [{name, parts: {kanji: {font, style?: \"Regular\", size?: % 10..400 = 100, baseline?: % -100..100 = 0}, kana?, punctuation?, symbols?, roman?, numerals?: {font, style?, size?, baseline?, hScale?, vScale?: % 10..400 = 100, scaleAtCenter?: bool = false}}}]} replace the active document's composite fonts (Type › Composite Fonts…'s OK), as one undo step, none when nothing changes. A composite font sets each class of character in its part's font: kanji (the base: ideographs, full-width letters and digits and anything no other class takes), kana (hiragana and katakana, both widths), punctuation (East Asian and full-width punctuation, quotes and dashes), symbols (arrows, math, shapes and other symbols), roman (half-width letters, their punctuation and spaces) and numerals (the digits 0 to 9); a part left out is the kanji part. size and baseline are percent of the type's size, hScale and vScale scale kana, roman and numerals only, and scaleAtCenter (kana only) scales them from the em box's center instead of the baseline. Names are trimmed, unique in any case, at most 255 characters, and may not name an installed font; a part's font may not be a composite font; at most 256. Type set in a composite font names it as its family (text.setStyle {font: name}), and the font menus list composite fonts first. A font taken out stays named by its type, which then shows as missing → {fonts}",
|
||||
"{fonts: [{name, parts: {kanji: {font, style?: \"Regular\", size?: % 10..400 = 100, baseline?: % -100..100 = 0}, kana?, punctuation?, symbols?, roman?, numerals?: {font, style?, size?, baseline?, hScale?, vScale?: % 10..400 = 100, scaleAtCenter?: bool = false}}, custom?: [{name, characters?: string = \"\", font?, style?, size?, baseline?, hScale?, vScale?}]}]} replace the active document's composite fonts (Type › Composite Fonts…'s OK), as one undo step, none when nothing changes. A composite font sets each class of character in its part's font: kanji (the base: ideographs, full-width letters and digits and anything no other class takes), kana (hiragana and katakana, both widths), punctuation (East Asian and full-width punctuation, quotes and dashes), symbols (arrows, math, shapes and other symbols), roman (half-width letters, their punctuation and spaces) and numerals (the digits 0 to 9); a part left out is the kanji part. size and baseline are percent of the type's size, hScale and vScale scale kana, roman and numerals only, and scaleAtCenter (kana only) scales them from the em box's center instead of the baseline. custom lists custom character sets: each sets the characters it lists in its own font, style, size, baseline and scales (values left out are the kanji part's), ahead of their class; a character two sets list is set by the later one. A set's name is trimmed and unique in the font in any case; its characters are each listed once, at most 8192, none a control character or past U+FFFF; at most 64 sets. Names are trimmed, unique in any case, at most 255 characters, and may not name an installed font; a part's font may not be a composite font; at most 256. Type set in a composite font names it as its family (text.setStyle {font: name}), and the font menus list composite fonts first. A font taken out stays named by its type, which then shows as missing → {fonts}",
|
||||
has_doc,
|
||||
set
|
||||
),
|
||||
@@ -51,7 +58,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export Composite Fonts",
|
||||
[],
|
||||
None,
|
||||
"{path?: .vccomposite file, names?: [name] (default: all), fonts?: [{name, parts}] (as type.compositeFonts.set takes them)} write the active document's composite fonts (all, or those named, any case), or the fonts given, to a file type.compositeFonts.import reads; without path the file's text comes back as data → {path, count} | {data, count}",
|
||||
"{path?: .vccomposite file, names?: [name] (default: all), fonts?: [{name, parts}] (as type.compositeFonts.set takes them)} write the active document's composite fonts (all, or those named, any case), or the fonts given, to a file type.compositeFonts.import reads (a file over 4 MB is refused); without path the file's text comes back as data → {path, count} | {data, count}",
|
||||
has_doc,
|
||||
export
|
||||
),
|
||||
@@ -121,8 +128,8 @@ fn list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
/// copy of the Kanji part, and keys or values a part can't take refused.
|
||||
fn parse_font(v: &Value, c: &str) -> Result<CompositeFont> {
|
||||
let o = v.as_object().ok_or_else(|| bad(c, "each font must be {name, parts}"))?;
|
||||
if let Some(k) = o.keys().find(|k| !["name", "parts"].contains(&k.as_str())) {
|
||||
return Err(bad(c, format!("unknown key `{k}`; a font takes name and parts")));
|
||||
if let Some(k) = o.keys().find(|k| !["name", "parts", "custom"].contains(&k.as_str())) {
|
||||
return Err(bad(c, format!("unknown key `{k}`; a font takes name, parts and custom")));
|
||||
}
|
||||
let raw = o.get("name").and_then(Value::as_str).ok_or_else(|| bad(c, "each font needs a name"))?;
|
||||
let name = CompositeFont::clean_name(raw)
|
||||
@@ -140,7 +147,17 @@ fn parse_font(v: &Value, c: &str) -> Result<CompositeFont> {
|
||||
serde_json::from_value(p.clone()).map(Some).map_err(|e| bad(c, format!("`{name}`: {}: {e}", class.key())))
|
||||
};
|
||||
let kanji = part(CharClass::Kanji)?.ok_or_else(|| bad(c, format!("`{name}`: give the kanji part")))?;
|
||||
let mut f = CompositeFont { name: name.clone(), parts: CompositeParts::all(kanji) };
|
||||
let custom = match o.get("custom") {
|
||||
None => Vec::new(),
|
||||
Some(Value::Array(sets)) => {
|
||||
if sets.len() > CUSTOM_SETS_MAX {
|
||||
return Err(bad(c, format!("`{name}`: a composite font keeps at most {CUSTOM_SETS_MAX} character sets")));
|
||||
}
|
||||
sets.iter().map(|s| parse_set(s, &name, &kanji, c)).collect::<Result<Vec<_>>>()?
|
||||
}
|
||||
Some(_) => return Err(bad(c, format!("`{name}`: custom must be a list of character sets"))),
|
||||
};
|
||||
let mut f = CompositeFont { name: name.clone(), parts: CompositeParts::all(kanji), custom };
|
||||
for class in CharClass::ALL.into_iter().skip(1) {
|
||||
if let Some(p) = part(class)? {
|
||||
*f.parts.get_mut(class) = p;
|
||||
@@ -150,6 +167,38 @@ fn parse_font(v: &Value, c: &str) -> Result<CompositeFont> {
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// Custom character set `v` of composite font `font` from command parameters or a file: its name
|
||||
/// trimmed, the part values it leaves out those of `kanji`, and keys or values it can't take
|
||||
/// refused. [`CompositeFont::check`] checks its characters and values.
|
||||
fn parse_set(v: &Value, font: &str, kanji: &CompositePart, c: &str) -> Result<CustomCharSet> {
|
||||
let o = v.as_object().ok_or_else(|| bad(c, format!("`{font}`: each character set must be {{name, characters, font…}}")))?;
|
||||
if let Some(k) = o.keys().find(|k| !SET_KEYS.contains(&k.as_str())) {
|
||||
return Err(bad(c, format!("`{font}`: unknown key `{k}` in a character set")));
|
||||
}
|
||||
let raw = o.get("name").and_then(Value::as_str).ok_or_else(|| bad(c, format!("`{font}`: each character set needs a name")))?;
|
||||
let name = CompositeFont::clean_name(raw)
|
||||
.ok_or_else(|| bad(c, format!("`{raw}` can't name a character set (1 to {COMPOSITE_NAME_MAX} characters, no control characters)")))?;
|
||||
let characters = match o.get("characters") {
|
||||
None => String::new(),
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(_) => return Err(bad(c, format!("`{font}`: `{name}`: characters must be a string"))),
|
||||
};
|
||||
// Checked here as well as by `check`, so a huge list is refused before it is copied again.
|
||||
if characters.chars().nth(CUSTOM_CHARS_MAX).is_some() {
|
||||
return Err(bad(c, format!("`{font}`: `{name}`: a character set keeps at most {CUSTOM_CHARS_MAX} characters")));
|
||||
}
|
||||
let mut part = serde_json::to_value(kanji).map_err(|e| bad(c, e.to_string()))?;
|
||||
if let Some(p) = part.as_object_mut() {
|
||||
for k in &SET_KEYS[2..] {
|
||||
if let Some(given) = o.get(*k) {
|
||||
p.insert(k.to_string(), given.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let part: CompositePart = serde_json::from_value(part).map_err(|e| bad(c, format!("`{font}`: `{name}`: {e}")))?;
|
||||
Ok(CustomCharSet { name, characters, part })
|
||||
}
|
||||
|
||||
/// Error unless `fonts` can stand together in a document: names unique in any case, none naming
|
||||
/// an installed font, no part set in a composite font, and at most [`COMPOSITE_FONTS_MAX`].
|
||||
fn check_set(fonts: &[CompositeFont], c: &str) -> Result<()> {
|
||||
@@ -167,10 +216,10 @@ fn check_set(fonts: &[CompositeFont], c: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
for f in fonts {
|
||||
for class in CharClass::ALL {
|
||||
let font = &f.parts.get(class).font;
|
||||
let classes = CharClass::ALL.into_iter().map(|class| (class.key(), &f.parts.get(class).font));
|
||||
for (part, font) in classes.chain(f.custom.iter().map(|s| (s.name.as_str(), &s.part.font))) {
|
||||
if names.contains(&font.to_lowercase()) || db.composite_font(font).is_some() {
|
||||
return Err(bad(c, format!("`{}`: {} is set in `{font}`, a composite font", f.name, class.key())));
|
||||
return Err(bad(c, format!("`{}`: {} is set in `{font}`, a composite font", f.name, part)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +309,9 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut file = Map::new();
|
||||
file.insert("compositeFonts".into(), json!(fonts));
|
||||
let text = serde_json::to_string_pretty(&file).map_err(|e| bad(C, e.to_string()))?;
|
||||
if text.len() > MAX_FILE_BYTES {
|
||||
return Err(bad(C, "the composite fonts make a file larger than 4 MB, which Import can't read; export fewer of them"));
|
||||
}
|
||||
match path {
|
||||
Some(path) => {
|
||||
write_file(path, text.as_bytes())?;
|
||||
|
||||
@@ -856,3 +856,127 @@ fn composite_fonts_export_and_import() {
|
||||
assert!(s.execute("type.compositeFonts.export", &json!({"path": " "})).is_err());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// `font(name)` with two custom character sets: "Marks" (x and A) in Inter at 150%, and "A only"
|
||||
/// (A) in Source Sans 3, which sets A since it comes later.
|
||||
fn font_with_sets(name: &str) -> Value {
|
||||
let mut v = font(name);
|
||||
v["custom"] = json!([
|
||||
{"name": " Marks ", "characters": "xA", "font": "Inter", "size": 150, "hScale": 90},
|
||||
{"name": "A only", "characters": "A"},
|
||||
]);
|
||||
v
|
||||
}
|
||||
|
||||
/// Custom character sets are set with the font as one undo step, take values they leave out from
|
||||
/// the Kanji part, set the characters they list ahead of their class (the later set winning), are
|
||||
/// saved with the document, and measure type again when they change.
|
||||
#[test]
|
||||
fn custom_character_sets_set_their_characters() {
|
||||
let mut s = session();
|
||||
let before = steps(&s);
|
||||
let r = s.execute("type.compositeFonts.set", &json!({"fonts": [font_with_sets("CF Sets")]})).unwrap();
|
||||
assert_eq!(r["fonts"][0]["custom"][0]["name"], "Marks");
|
||||
assert_eq!(steps(&s), before + 1);
|
||||
let f = s.doc().unwrap().doc.composite_fonts[0].clone();
|
||||
assert_eq!(f.custom.len(), 2);
|
||||
assert_eq!((f.custom[0].part.font.as_str(), f.custom[0].part.size, f.custom[0].part.h_scale), ("Inter", 150.0, 90.0));
|
||||
assert_eq!((f.custom[1].part.font.as_str(), f.custom[1].part.size), ("Source Sans 3", 100.0));
|
||||
// The same sets again change nothing.
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font_with_sets("CF Sets")]})).unwrap();
|
||||
assert_eq!(steps(&s), before + 1);
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save_file(&s.doc().unwrap().doc)).unwrap();
|
||||
assert_eq!(*back.composite_fonts, *s.doc().unwrap().doc.composite_fonts);
|
||||
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "xAB", "size": 20})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("text.setStyle", &json!({"ids": [id], "font": "CF Sets"})).unwrap();
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let face = |family: &str| db.face(family, "Regular").unwrap().id();
|
||||
let lay = vectorcraft_text::layout(db, &text(&s, id));
|
||||
let fonts: Vec<u32> = lay.glyphs.iter().map(|g| g.font_id).collect();
|
||||
assert_eq!(fonts, [face("Inter"), face("Source Sans 3"), face("JetBrains Mono")]);
|
||||
// Taking x out of Marks sets it in its class's font again, measured in the same step.
|
||||
let wide = text(&s, id).cached_bounds.unwrap().width();
|
||||
let mut fewer = font_with_sets("CF Sets");
|
||||
fewer["custom"][0]["characters"] = json!("A");
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [fewer]})).unwrap();
|
||||
assert!(text(&s, id).cached_bounds.unwrap().width() < wide - 1.0);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!((text(&s, id).cached_bounds.unwrap().width() - wide).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// Bad custom character sets are refused with an error and no undo step.
|
||||
#[test]
|
||||
fn bad_custom_character_sets_are_refused() {
|
||||
let mut s = session();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font_with_sets("CF Sets Base")]})).unwrap();
|
||||
let before = steps(&s);
|
||||
let with = |f: &dyn Fn(&mut Value)| {
|
||||
let mut v = font_with_sets("CF Sets Refused");
|
||||
f(&mut v);
|
||||
json!({"fonts": [v]})
|
||||
};
|
||||
let many: String = (0x4E00u32..).filter_map(char::from_u32).take(8193).collect();
|
||||
let sets: Vec<Value> = (0..65).map(|i| json!({"name": format!("S{i}")})).collect();
|
||||
for bad in [
|
||||
with(&|v| v["custom"] = json!("Marks")),
|
||||
with(&|v| v["custom"] = Value::Null),
|
||||
with(&|v| v["custom"][0]["characters"] = Value::Null),
|
||||
with(&|v| v["custom"][0] = json!("Marks")),
|
||||
with(&|v| v["custom"][0]["name"] = json!(" ")),
|
||||
with(&|v| {
|
||||
v["custom"][0].as_object_mut().unwrap().remove("name");
|
||||
}),
|
||||
with(&|v| v["custom"][1]["name"] = json!("marks")),
|
||||
with(&|v| v["custom"][0]["characters"] = json!(["※"])),
|
||||
with(&|v| v["custom"][0]["characters"] = json!("※※")),
|
||||
with(&|v| v["custom"][0]["characters"] = json!("A\n")),
|
||||
with(&|v| v["custom"][0]["characters"] = json!("😀")),
|
||||
with(&|v| v["custom"][0]["characters"] = json!(many.clone())),
|
||||
with(&|v| v["custom"] = json!(sets.clone())),
|
||||
with(&|v| v["custom"][0]["size"] = json!(5)),
|
||||
with(&|v| v["custom"][0]["size"] = json!("50")),
|
||||
with(&|v| v["custom"][0]["scaleAtCenter"] = json!(true)),
|
||||
with(&|v| v["custom"][0]["scaleAtCenter"] = json!(false)),
|
||||
with(&|v| v["custom"][0]["font"] = json!("")),
|
||||
with(&|v| v["custom"][0]["font"] = json!("CF Sets Base")),
|
||||
with(&|v| v["custom"][0]["width"] = json!(3)),
|
||||
] {
|
||||
let e = s.execute("type.compositeFonts.set", &bad);
|
||||
assert!(e.is_err(), "accepted {bad}");
|
||||
assert_eq!(names(&s), ["CF Sets Base"]);
|
||||
assert_eq!(steps(&s), before);
|
||||
}
|
||||
// A set without characters is kept: it sets nothing.
|
||||
let ok = with(&|v| v["custom"][0]["characters"] = json!(""));
|
||||
s.execute("type.compositeFonts.set", &ok).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.composite_fonts[0].custom[0].characters, "");
|
||||
}
|
||||
|
||||
/// Export writes the custom character sets and Import reads them back; a file whose set is bad is
|
||||
/// refused.
|
||||
#[test]
|
||||
fn custom_character_sets_export_and_import() {
|
||||
let mut s = session();
|
||||
s.execute("type.compositeFonts.set", &json!({"fonts": [font_with_sets("CF Sets IO")]})).unwrap();
|
||||
let data = s.execute("type.compositeFonts.export", &json!({})).unwrap();
|
||||
let mut t = session();
|
||||
t.execute("type.compositeFonts.import", &json!({"data": data["data"]})).unwrap();
|
||||
assert_eq!(*t.doc().unwrap().doc.composite_fonts, *s.doc().unwrap().doc.composite_fonts);
|
||||
let text = data["data"].as_str().unwrap().replace("\"A only\"", "\"marks\"");
|
||||
let mut u = session();
|
||||
assert!(u.execute("type.compositeFonts.import", &json!({"data": text})).is_err());
|
||||
assert!(names(&u).is_empty());
|
||||
// Fonts whose file would be larger than Import reads are refused.
|
||||
let full: String = (0x4E00u32..).filter_map(char::from_u32).take(8192).collect();
|
||||
let sets: Vec<Value> = (0..64).map(|i| json!({"name": format!("S{i}"), "characters": full})).collect();
|
||||
let big: Vec<Value> = (0..3)
|
||||
.map(|i| {
|
||||
let mut f = font(&format!("CF Sets Big {i}"));
|
||||
f["custom"] = json!(sets);
|
||||
f
|
||||
})
|
||||
.collect();
|
||||
let e = s.execute("type.compositeFonts.export", &json!({"fonts": big})).unwrap_err();
|
||||
assert!(e.to_string().contains("4 MB"), "{e}");
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ fn composite_font_parts_are_not_highlighted_as_substituted_glyphs() {
|
||||
let mut parts = CompositeParts::all(CompositePart::new("Source Sans 3", "Regular"));
|
||||
parts.roman = CompositePart::new("JetBrains Mono", "Regular");
|
||||
parts.numerals = CompositePart::new("Inter", "Regular");
|
||||
db.set_composite_font(&CompositeFont { name: "CF Highlight".into(), parts });
|
||||
db.set_composite_font(&CompositeFont { name: "CF Highlight".into(), parts, custom: Vec::new() });
|
||||
let style = CharStyle { size: 40.0, font_family: "CF Highlight".into(), ..Default::default() };
|
||||
let t = TextObject::point(Point::new(10.0, 60.0), "Ab12", style);
|
||||
let g = text_geom(&t);
|
||||
@@ -175,7 +175,7 @@ fn a_documents_own_composite_font_draws_without_being_registered() {
|
||||
let (mut composite, id) = typed(NAME);
|
||||
let mut parts = CompositeParts::all(CompositePart::new("Source Sans 3", "Regular"));
|
||||
parts.roman = CompositePart::new("JetBrains Mono", "Regular");
|
||||
composite.composite_fonts.push(CompositeFont { name: NAME.into(), parts });
|
||||
composite.composite_fonts.push(CompositeFont { name: NAME.into(), parts, custom: Vec::new() });
|
||||
let (plain, plain_id) = typed("JetBrains Mono");
|
||||
assert!(db.composite_font(NAME).is_none());
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
@@ -236,7 +236,7 @@ fn a_placed_document_draws_its_own_definition_of_a_name_the_page_defines() {
|
||||
let definition = |roman: &str| {
|
||||
let mut parts = CompositeParts::all(CompositePart::new("Source Sans 3", "Regular"));
|
||||
parts.roman = CompositePart::new(roman, "Regular");
|
||||
CompositeFont { name: NAME.into(), parts }
|
||||
CompositeFont { name: NAME.into(), parts, custom: Vec::new() }
|
||||
};
|
||||
let typed = |family: &str| {
|
||||
let mut d = Document::new(300.0, 150.0);
|
||||
@@ -319,7 +319,7 @@ fn a_document_draws_type_in_another_documents_composite_font_as_missing() {
|
||||
};
|
||||
let mut parts = CompositeParts::all(CompositePart::new("Source Sans 3", "Regular"));
|
||||
parts.roman = CompositePart::new("JetBrains Mono", "Regular");
|
||||
db.set_composite_font(&CompositeFont { name: NAME.into(), parts });
|
||||
db.set_composite_font(&CompositeFont { name: NAME.into(), parts, custom: Vec::new() });
|
||||
let (drawn, as_missing, in_front) = (draw(&back, id), draw(&missing, missing_id), draw(&plain, plain_id));
|
||||
db.remove_composite_font(NAME);
|
||||
assert!(drawn.0 != in_front.0, "not in the front document's definition");
|
||||
|
||||
@@ -119,6 +119,9 @@ pub struct FontDb {
|
||||
/// type naming one as its family is set in its parts' fonts. A thread's guards can lend others
|
||||
/// in their place ([`FontDb::lend_composite_fonts`]).
|
||||
composites: RwLock<HashMap<String, Arc<vectorcraft_doc::CompositeFont>>>,
|
||||
/// The lookups of the custom character sets of the composite fonts held or lent, each built
|
||||
/// when first used.
|
||||
custom_classes: Mutex<composite::CustomClasses>,
|
||||
}
|
||||
|
||||
/// Where a database's system font scan looks: folders (read with their subfolders) and font files.
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
//! Composite fonts (Type › Composite Fonts…) in the font database: each is listed first in the
|
||||
//! font menus and resolves to its base (Kanji) font, and layout sets each character of type in one
|
||||
//! in the font of the character's class ([`FontDb::composite_split`]).
|
||||
//! in the font of the custom character set that lists it, else of its class
|
||||
//! ([`FontDb::composite_split`]).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Weak;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use vectorcraft_doc::{CharClass, CompositeFont, TextObject, TextRun};
|
||||
use vectorcraft_doc::text::CompositeClasses;
|
||||
use vectorcraft_doc::{CharClass, CompositeClass, CompositeFont, CompositePart, TextObject, TextRun};
|
||||
|
||||
use super::face::FALLBACK_FAMILY;
|
||||
use super::{Arc, FontDb, FontFace, FontMatch};
|
||||
@@ -17,6 +20,13 @@ use super::{Arc, FontDb, FontFace, FontMatch};
|
||||
/// A face [`FontDb::resolve`] found and how it matched.
|
||||
type Resolved = Option<(Arc<FontFace>, FontMatch)>;
|
||||
|
||||
/// The base class: type without characters and anything no part names takes its part.
|
||||
const KANJI: CompositeClass = CompositeClass::Fixed(CharClass::Kanji);
|
||||
|
||||
/// Lookups of the custom character sets of composite fonts, by the address of the font's `Arc`,
|
||||
/// each with a weak reference that tells when that font has gone and the address may be reused.
|
||||
pub(super) type CustomClasses = HashMap<usize, (Weak<CompositeFont>, Arc<CompositeClasses>)>;
|
||||
|
||||
thread_local! {
|
||||
/// The composite fonts the guards of this thread lend ([`FontDb::lend_composite_fonts`]), the
|
||||
/// latest loan last. Only the latest loan of a database is looked in.
|
||||
@@ -41,15 +51,25 @@ struct Loan {
|
||||
print: u64,
|
||||
}
|
||||
|
||||
/// Feed composite font `f`'s name and parts to `h`.
|
||||
/// Feed composite font `f`'s name, parts and custom character sets to `h`.
|
||||
fn hash_font(f: &CompositeFont, h: &mut DefaultHasher) {
|
||||
f.name.hash(h);
|
||||
let p = &f.parts;
|
||||
for part in [&p.kanji, &p.kana, &p.punctuation, &p.symbols, &p.roman, &p.numerals] {
|
||||
(&part.font, &part.style, part.scale_at_center).hash(h);
|
||||
for v in [part.size, part.baseline, part.h_scale, part.v_scale] {
|
||||
v.to_bits().hash(h);
|
||||
}
|
||||
hash_part(part, h);
|
||||
}
|
||||
f.custom.len().hash(h);
|
||||
for s in &f.custom {
|
||||
(&s.name, &s.characters).hash(h);
|
||||
hash_part(&s.part, h);
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed part `part`'s font and values to `h`.
|
||||
fn hash_part(part: &CompositePart, h: &mut DefaultHasher) {
|
||||
(&part.font, &part.style, part.scale_at_center).hash(h);
|
||||
for v in [part.size, part.baseline, part.h_scale, part.v_scale] {
|
||||
v.to_bits().hash(h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,8 +156,8 @@ impl FontDb {
|
||||
.unwrap_or(g)
|
||||
}
|
||||
|
||||
/// The face composite font `name` sets character `c` in (its class's part's), and whether
|
||||
/// that part's font is missing. `None` when `name` is no composite font.
|
||||
/// The face composite font `name` sets character `c` in (its custom set's or its class's
|
||||
/// part's), and whether that part's font is missing. `None` when `name` is no composite font.
|
||||
pub fn composite_face(&self, name: &str, c: char) -> Option<(bool, u32)> {
|
||||
self.composite_resolve(name, c).map(|(face, m)| (m == FontMatch::Missing, face.id()))
|
||||
}
|
||||
@@ -149,30 +169,58 @@ impl FontDb {
|
||||
self.composite_resolve(name, first)
|
||||
}
|
||||
|
||||
/// The face composite font `name` sets character `c` in (its class's part's), and how that
|
||||
/// part's font matched ([`FontDb::resolve`]). `None` when `name` is no composite font, or no
|
||||
/// font at all is loaded.
|
||||
/// The face composite font `name` sets character `c` in (its custom set's or its class's
|
||||
/// part's), and how that part's font matched ([`FontDb::resolve`]). `None` when `name` is no
|
||||
/// composite font, or no font at all is loaded.
|
||||
pub fn composite_resolve(&self, name: &str, c: char) -> Option<(Arc<FontFace>, FontMatch)> {
|
||||
let f = self.composite_font(name)?;
|
||||
let (family, style) = self.part_font(&f, CharClass::of(c));
|
||||
let (family, style) = self.part_font(&f, self.composite_class(&f, c));
|
||||
self.resolve(&family, &style)
|
||||
}
|
||||
|
||||
/// Where composite font `f` sets character `c`: the last custom set that lists it, else its
|
||||
/// class. The lookup of `f`'s sets is built once and kept while `f` lives.
|
||||
fn composite_class(&self, f: &Arc<CompositeFont>, c: char) -> CompositeClass {
|
||||
if f.custom.is_empty() {
|
||||
return CompositeClass::Fixed(CharClass::of(c));
|
||||
}
|
||||
self.custom_lookup(f).of(c)
|
||||
}
|
||||
|
||||
/// The lookup of composite font `f`'s custom character sets, built when first asked for.
|
||||
fn custom_lookup(&self, f: &Arc<CompositeFont>) -> Arc<CompositeClasses> {
|
||||
if f.custom.is_empty() {
|
||||
return Arc::default();
|
||||
}
|
||||
let key = Arc::as_ptr(f) as usize;
|
||||
let mut cache = self.custom_classes.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some((font, lookup)) = cache.get(&key)
|
||||
&& font.upgrade().is_some_and(|g| Arc::ptr_eq(&g, f))
|
||||
{
|
||||
return lookup.clone();
|
||||
}
|
||||
cache.retain(|_, (font, _)| font.strong_count() > 0);
|
||||
let lookup = Arc::new(f.classes());
|
||||
cache.insert(key, (Arc::downgrade(f), lookup.clone()));
|
||||
lookup
|
||||
}
|
||||
|
||||
/// The fonts (family, style) composite font `name` sets `text` in: those of the parts its
|
||||
/// grapheme clusters fall in (each by its first character, as layout splits them), each once,
|
||||
/// in the order first used; the Kanji part's for text without characters. `None` when `name` is
|
||||
/// no composite font.
|
||||
pub fn composite_fonts_for(&self, name: &str, text: &str) -> Option<Vec<(String, String)>> {
|
||||
let f = self.composite_font(name)?;
|
||||
let mut classes: Vec<CharClass> = Vec::new();
|
||||
let lookup = self.custom_lookup(&f);
|
||||
let mut classes: Vec<CompositeClass> = Vec::new();
|
||||
for g in text.graphemes(true) {
|
||||
let c = g.chars().next().map_or(CharClass::Kanji, CharClass::of);
|
||||
let c = g.chars().next().map_or(KANJI, |c| lookup.of(c));
|
||||
if !classes.contains(&c) {
|
||||
classes.push(c);
|
||||
}
|
||||
}
|
||||
if classes.is_empty() {
|
||||
classes.push(CharClass::Kanji);
|
||||
classes.push(KANJI);
|
||||
}
|
||||
let mut fonts: Vec<(String, String)> = Vec::with_capacity(classes.len());
|
||||
for c in classes {
|
||||
@@ -196,8 +244,9 @@ impl FontDb {
|
||||
FontMatch::Style => 1,
|
||||
FontMatch::Missing => 2,
|
||||
};
|
||||
let mut faces: Vec<(CharClass, Resolved)> = Vec::new();
|
||||
let mut face_of = |class: CharClass| {
|
||||
let lookup = self.custom_lookup(&f);
|
||||
let mut faces: Vec<(CompositeClass, Resolved)> = Vec::new();
|
||||
let mut face_of = |class: CompositeClass| {
|
||||
if let Some((_, found)) = faces.iter().find(|(c, _)| *c == class) {
|
||||
return found.clone();
|
||||
}
|
||||
@@ -213,7 +262,7 @@ impl FontDb {
|
||||
graphemes.push("");
|
||||
}
|
||||
for g in graphemes {
|
||||
let class = g.chars().next().map_or(CharClass::Kanji, CharClass::of);
|
||||
let class = g.chars().next().map_or(KANJI, |c| lookup.of(c));
|
||||
let found = face_of(class);
|
||||
let m = found.as_ref().map_or(FontMatch::Missing, |(_, m)| *m);
|
||||
if worst.as_ref().is_none_or(|(w, _)| rank(m) > rank(*w)) {
|
||||
@@ -307,10 +356,11 @@ impl FontDb {
|
||||
LentCompositeFonts { number: pushed.then_some(number), thread: PhantomData }
|
||||
}
|
||||
|
||||
/// The family and style composite font `f` sets characters of `class` in: its part's, or the
|
||||
/// fallback font's when that part names a composite font.
|
||||
fn part_font(&self, f: &CompositeFont, class: CharClass) -> (String, String) {
|
||||
let p = f.parts.get(class);
|
||||
/// The family and style composite font `f` sets characters of `class` in: its part's (the
|
||||
/// Kanji part's for a custom set it doesn't have), or the fallback font's when that part names
|
||||
/// a composite font.
|
||||
fn part_font(&self, f: &CompositeFont, class: CompositeClass) -> (String, String) {
|
||||
let p = f.part(class).unwrap_or(&f.parts.kanji);
|
||||
if self.composite_font(&p.font).is_some() { (FALLBACK_FAMILY.to_string(), "Regular".to_string()) } else { (p.font.clone(), p.style.clone()) }
|
||||
}
|
||||
|
||||
@@ -326,11 +376,13 @@ impl FontDb {
|
||||
/// names a composite font too. `None` when `name` is no composite font.
|
||||
pub(super) fn composite_base(&self, name: &str) -> Option<(String, String)> {
|
||||
let f = self.composite_font(name)?;
|
||||
Some(self.part_font(&f, CharClass::Kanji))
|
||||
Some(self.part_font(&f, KANJI))
|
||||
}
|
||||
|
||||
/// `t` with each run set in a composite font split into runs of one [`CharClass`] each, set in
|
||||
/// that class's font: its size and baseline shift from the part's size and baseline (in
|
||||
/// `t` with each run set in a composite font split into runs of one custom character set or
|
||||
/// [`CharClass`] each (a set listing a character takes it ahead of its class, the font's last
|
||||
/// such set when several do), set in that set's or class's font: its size and baseline shift
|
||||
/// from the part's size and baseline (in
|
||||
/// percent of the run's size), its scales multiplied by the part's, and Kana scaled at the
|
||||
/// center raised so the em box's center stays where it was. Each grapheme cluster goes with
|
||||
/// its first character. Also the index of the run of `t` each run came from. `None` when no run
|
||||
@@ -352,10 +404,11 @@ impl FontDb {
|
||||
from.push(i);
|
||||
continue;
|
||||
};
|
||||
let lookup = self.custom_lookup(font);
|
||||
let mut start = 0;
|
||||
let mut class = None;
|
||||
for (at, g) in r.text.grapheme_indices(true) {
|
||||
let c = g.chars().next().map_or(CharClass::Kanji, CharClass::of);
|
||||
let c = g.chars().next().map_or(KANJI, |c| lookup.of(c));
|
||||
if let Some(k) = class
|
||||
&& k != c
|
||||
{
|
||||
@@ -365,16 +418,16 @@ impl FontDb {
|
||||
}
|
||||
class = Some(c);
|
||||
}
|
||||
runs.push(self.composite_run(r.text.get(start..).unwrap_or_default(), r, font, class.unwrap_or(CharClass::Kanji)));
|
||||
runs.push(self.composite_run(r.text.get(start..).unwrap_or_default(), r, font, class.unwrap_or(KANJI)));
|
||||
from.push(i);
|
||||
}
|
||||
Some((TextObject { runs, ..t.clone() }, from))
|
||||
}
|
||||
|
||||
/// Characters `text` of run `r` (set in composite font `font`), all of class `class`, as a run
|
||||
/// set in that class's part.
|
||||
fn composite_run(&self, text: &str, r: &TextRun, font: &CompositeFont, class: CharClass) -> TextRun {
|
||||
let p = font.parts.get(class);
|
||||
/// Characters `text` of run `r` (set in composite font `font`), all of class or set `class`,
|
||||
/// as a run set in that class's or set's part.
|
||||
fn composite_run(&self, text: &str, r: &TextRun, font: &CompositeFont, class: CompositeClass) -> TextRun {
|
||||
let p = font.part(class).unwrap_or(&font.parts.kanji);
|
||||
let mut st = r.style.clone();
|
||||
let base = st.size;
|
||||
let (family, style) = self.part_font(font, class);
|
||||
@@ -386,7 +439,7 @@ impl FontDb {
|
||||
st.baseline_shift += base * p.baseline / 100.0;
|
||||
st.h_scale *= p.h_scale / 100.0;
|
||||
st.v_scale *= p.v_scale / 100.0;
|
||||
if class == CharClass::Kana && p.scale_at_center && p.v_scale != 100.0 {
|
||||
if class == CompositeClass::Fixed(CharClass::Kana) && p.scale_at_center && p.v_scale != 100.0 {
|
||||
let centre = self.style_face(&st).map_or(0.38, |f| f.ideographic_centre());
|
||||
st.baseline_shift += st.size * centre * (1.0 - p.v_scale / 100.0);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ impl FontDb {
|
||||
stamp: Mutex::new(None),
|
||||
instances: Mutex::new(Instances::default()),
|
||||
composites: RwLock::new(HashMap::new()),
|
||||
custom_classes: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,51 @@ fn composite_runs_split_by_class() {
|
||||
assert!(db.composite_split(&TextObject::point(Point::ZERO, "Ab", style("Inter", 12.0))).is_none());
|
||||
}
|
||||
|
||||
/// A custom character set takes the characters it lists ahead of their class, the later of two
|
||||
/// sets wins a character both list, and the set's part sizes, raises and scales them. The fonts
|
||||
/// used for the text, how it is covered and the face of one character follow the sets too.
|
||||
#[test]
|
||||
fn custom_character_sets_take_their_characters_first() {
|
||||
use vectorcraft_doc::{CompositePart, CustomCharSet};
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
let mut f = mixed("Custom");
|
||||
let mut part = CompositePart::new("Inter", "Regular");
|
||||
part.size = 150.0;
|
||||
part.baseline = -10.0;
|
||||
part.h_scale = 90.0;
|
||||
let mut first = CustomCharSet::new("Letters", CompositePart::new("Source Sans 3", "Regular"));
|
||||
first.characters = "Bか".into();
|
||||
let mut second = CustomCharSet::new("B only", part);
|
||||
second.characters = "B".into();
|
||||
f.custom = vec![first, second];
|
||||
db.set_composite_font(&f);
|
||||
let t = TextObject::point(Point::ZERO, "ABか1", style("Custom", 20.0));
|
||||
let (split, from) = db.composite_split(&t).expect("split");
|
||||
let pieces: Vec<(&str, &str)> = split.runs.iter().map(|r| (r.text.as_str(), r.style.font_family.as_str())).collect();
|
||||
assert_eq!(pieces, [("A", "JetBrains Mono"), ("B", "Inter"), ("か", "Source Sans 3"), ("1", "Inter")]);
|
||||
assert_eq!(from, [0, 0, 0, 0]);
|
||||
let b = &split.runs[1].style;
|
||||
assert_eq!((b.size, b.baseline_shift, b.h_scale, b.v_scale), (30.0, -2.0, 90.0, 100.0));
|
||||
// か is in a set, so the Kana part's Scale at Center does not apply to it.
|
||||
let ka = &split.runs[2].style;
|
||||
assert_eq!((ka.v_scale, ka.baseline_shift), (100.0, 0.0));
|
||||
assert_eq!(db.composite_face("Custom", 'B').map(|(_, id)| id), Some(face_id(&db, "Inter")));
|
||||
let fonts = db.composite_fonts_for("Custom", "BA").unwrap();
|
||||
assert_eq!(fonts, [("Inter".to_string(), "Regular".to_string()), ("JetBrains Mono".to_string(), "Regular".to_string())]);
|
||||
let l = layout(&db, &t);
|
||||
let ids: Vec<u32> = l.glyphs.iter().map(|g| g.font_id).collect();
|
||||
assert_eq!(ids[1], face_id(&db, "Inter"));
|
||||
// A changed set is a changed definition: lending it lays the type out again.
|
||||
let mut changed = f.clone();
|
||||
changed.custom[1].characters = "A".into();
|
||||
let g0 = db.generation();
|
||||
let lent = db.lend_composite_fonts(&[changed]);
|
||||
assert_ne!(db.generation(), g0);
|
||||
let (split, _) = db.composite_split(&t).expect("split");
|
||||
assert_eq!(split.runs[0].style.font_family, "Inter");
|
||||
drop(lent);
|
||||
}
|
||||
|
||||
/// Laid out, each character is drawn in its class's font, sized by its part, and the glyphs still
|
||||
/// name the run of the text they belong to.
|
||||
#[test]
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
//! Type › Composite Fonts… (with Show East Asian Options): the active document's composite fonts,
|
||||
//! the one picked in Font shown as a table with a row for each class of character (Kanji, Kana,
|
||||
//! Punctuation, Symbols, Roman, Numerals) and its font, style, size, baseline and scales. New adds
|
||||
//! a font based on the one picked, Delete Font takes it out, Import… adds the fonts of a file and
|
||||
//! Export… writes the one picked to a file. Nothing changes in the document until OK, which runs
|
||||
//! `type.compositeFonts.set` with every font as one undo step; Cancel leaves them as they were.
|
||||
//! Punctuation, Symbols, Roman, Numerals), then a row for each custom character set, with its font,
|
||||
//! style, size, baseline and scales. New adds a font based on the one picked, Delete Font takes it
|
||||
//! out, Import… adds the fonts of a file and Export… writes the one picked to a file. Custom shows
|
||||
//! the custom character sets of the font picked: New adds a set based on the one picked, Delete Set
|
||||
//! takes it out, and Add puts the characters typed (Direct Entry) or the character of a Unicode code
|
||||
//! into it; Delete takes the character picked out. Nothing changes in the document until OK, which
|
||||
//! runs `type.compositeFonts.set` with every font as one undo step; Cancel leaves them as they were.
|
||||
//!
|
||||
//! Fields: `fonts` (the composite fonts as `type.compositeFonts.set` takes them), `selected` (the
|
||||
//! index of the one picked) and `name` (its name as typed).
|
||||
//! index of the one picked) and `name` (its name as typed); `custom` (whether the custom character
|
||||
//! sets show), `set` (the index of the set picked), `setName` (its name as typed), `entryMode`
|
||||
//! (`direct` or `unicode`), `entry` (the characters or code typed) and `char` (the index of the
|
||||
//! character picked in the set).
|
||||
//!
|
||||
//! Behavior reference (public documentation):
|
||||
//! <https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=347>.
|
||||
//! <https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=347>,
|
||||
//! custom character sets:
|
||||
//! <https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=348>.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::text::{COMPOSITE_BASELINE, COMPOSITE_SCALE, COMPOSITE_SIZE};
|
||||
use vectorcraft_doc::{CharClass, CompositeFont, CompositePart};
|
||||
use vectorcraft_doc::text::{COMPOSITE_BASELINE, COMPOSITE_SCALE, COMPOSITE_SIZE, CUSTOM_CHARS_MAX, CUSTOM_SETS_MAX};
|
||||
use vectorcraft_doc::{CharClass, CompositeFont, CompositePart, CustomCharSet};
|
||||
use vectorcraft_engine::cmd::compositefonts::EXTS;
|
||||
|
||||
use super::{DialogSpec, form};
|
||||
@@ -27,12 +35,19 @@ pub const KIND: &str = "compositeFonts";
|
||||
/// The command OK runs, and the menu item's.
|
||||
pub const COMMAND: &str = "type.compositeFonts.set";
|
||||
|
||||
/// How many of a custom set's characters each row of its list shows.
|
||||
const CHARS_PER_ROW: usize = 20;
|
||||
|
||||
/// The tallest the table of parts grows before it scrolls, in points: the header, the six classes
|
||||
/// and a few custom sets.
|
||||
const TABLE_HEIGHT: f32 = 340.0;
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Composite Fonts").into(), body, confirm, min_width: 760.0, ..DialogSpec::FORM };
|
||||
|
||||
/// Open Composite Fonts on the active document's composite fonts, the first one picked.
|
||||
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
let fonts = app.session.active().ok_or("no document open")?.doc.composite_fonts.to_vec();
|
||||
let mut d = Dialog::new(KIND, json!({ "fonts": fonts, "selected": 0 }));
|
||||
let mut d = Dialog::new(KIND, json!({ "fonts": fonts, "selected": 0, "entryMode": "direct" }));
|
||||
show_selected(&mut d);
|
||||
app.ui.dialog = Some(d);
|
||||
Ok(Value::Null)
|
||||
@@ -47,12 +62,17 @@ fn put_fonts(d: &mut Dialog, fonts: &[CompositeFont]) {
|
||||
d.fields.insert("fonts".into(), json!(fonts));
|
||||
}
|
||||
|
||||
/// The index of the font picked, within the fonts (0 when there are none).
|
||||
fn selected(d: &Dialog, count: usize) -> usize {
|
||||
d.fields.get("selected").and_then(Value::as_u64).map_or(0, |i| i as usize).min(count.saturating_sub(1))
|
||||
/// The index field `key` holds, within `count` items (0 when there are none).
|
||||
fn index(d: &Dialog, key: &str, count: usize) -> usize {
|
||||
d.fields.get(key).and_then(Value::as_u64).map_or(0, |i| i as usize).min(count.saturating_sub(1))
|
||||
}
|
||||
|
||||
/// Pick font `i`: its name goes into Name.
|
||||
/// The index of the font picked, within the fonts (0 when there are none).
|
||||
fn selected(d: &Dialog, count: usize) -> usize {
|
||||
index(d, "selected", count)
|
||||
}
|
||||
|
||||
/// Pick font `i`: its name goes into Name, and its first custom set is picked.
|
||||
fn select(d: &mut Dialog, i: usize) {
|
||||
d.fields.insert("selected".into(), json!(i));
|
||||
show_selected(d);
|
||||
@@ -62,6 +82,17 @@ fn show_selected(d: &mut Dialog) {
|
||||
let fonts = fonts(d);
|
||||
let name = fonts.get(selected(d, fonts.len())).map(|f| f.name.clone()).unwrap_or_default();
|
||||
d.fields.insert("name".into(), json!(name));
|
||||
select_set(d, 0);
|
||||
}
|
||||
|
||||
/// Pick custom set `i` of the font picked: its name goes into the set's Name, and no character is
|
||||
/// picked.
|
||||
fn select_set(d: &mut Dialog, i: usize) {
|
||||
d.fields.insert("set".into(), json!(i));
|
||||
d.fields.remove("char");
|
||||
let fonts = fonts(d);
|
||||
let name = fonts.get(selected(d, fonts.len())).and_then(|f| f.custom.get(i)).map(|s| s.name.clone()).unwrap_or_default();
|
||||
d.fields.insert("setName".into(), json!(name));
|
||||
}
|
||||
|
||||
/// The name of a class's row.
|
||||
@@ -85,12 +116,25 @@ fn new_name(fonts: &[CompositeFont]) -> String {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// "Character Set N", the first N no set in `sets` is named.
|
||||
fn new_set_name(sets: &[CustomCharSet]) -> String {
|
||||
(1..).map(|n| format!("Character Set {n}")).find(|n| !sets.iter().any(|s| CompositeFont::same_name(&s.name, n))).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// What a button asked for, done once the table is drawn.
|
||||
enum Action {
|
||||
New,
|
||||
Delete,
|
||||
Import,
|
||||
Export,
|
||||
/// Custom: show or hide the custom character sets.
|
||||
Custom,
|
||||
NewSet,
|
||||
DeleteSet,
|
||||
/// Add the characters typed, or the character of the code typed, to the set picked.
|
||||
AddChars,
|
||||
/// Take the character picked out of the set picked.
|
||||
DeleteChar,
|
||||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
@@ -122,6 +166,9 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
if ui.add_enabled_ui(!list.is_empty(), |ui| widgets::flat_button(ui, tl!("Export…"), 64.0)).inner.clicked() {
|
||||
act = Some(Action::Export);
|
||||
}
|
||||
if ui.add_enabled_ui(!list.is_empty(), |ui| widgets::flat_button(ui, tl!("Custom"), 64.0)).inner.clicked() {
|
||||
act = Some(Action::Custom);
|
||||
}
|
||||
});
|
||||
if let Some(k) = picked {
|
||||
select(d, k);
|
||||
@@ -136,6 +183,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
f.name = d.str("name");
|
||||
ui.add_space(8.0);
|
||||
parts_table(app, ui, f);
|
||||
if d.bool("custom") {
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
if let Some(a) = custom_sets(ui, d, f) {
|
||||
act = Some(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
put_fonts(d, &list);
|
||||
if let Some(a) = act {
|
||||
@@ -151,6 +205,80 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The custom character sets of `f`: Character Set with New and Delete Set, the set's Name, the
|
||||
/// Character row that adds characters, and the set's characters with Delete. The button pressed.
|
||||
fn custom_sets(ui: &mut egui::Ui, d: &mut Dialog, f: &mut CompositeFont) -> Option<Action> {
|
||||
let mut act = None;
|
||||
let k = index(d, "set", f.custom.len());
|
||||
let names: Vec<&str> = f.custom.iter().map(|s| s.name.as_str()).collect();
|
||||
let mut picked = None;
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Character Set:"));
|
||||
if names.is_empty() {
|
||||
widgets::dim_label(ui, tl!("None"));
|
||||
} else if let Some(n) = widgets::dropdown_names(ui, "composite-fonts-set", names.get(k).copied().unwrap_or_default(), &names, 220.0) {
|
||||
picked = Some(n);
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
if ui.add_enabled_ui(f.custom.len() < CUSTOM_SETS_MAX, |ui| widgets::flat_button(ui, tl!("New"), 48.0)).inner.clicked() {
|
||||
act = Some(Action::NewSet);
|
||||
}
|
||||
if ui.add_enabled_ui(!f.custom.is_empty(), |ui| widgets::flat_button(ui, tl!("Delete Set"), 84.0)).inner.clicked() {
|
||||
act = Some(Action::DeleteSet);
|
||||
}
|
||||
});
|
||||
if let Some(n) = picked {
|
||||
select_set(d, n);
|
||||
return None;
|
||||
}
|
||||
let Some(set) = f.custom.get_mut(k) else { return act };
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Name:"));
|
||||
form::text(ui, d, "setName", 260.0);
|
||||
});
|
||||
set.name = d.str("setName");
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Character:"));
|
||||
let modes = [tl!("Direct Entry"), tl!("Unicode")];
|
||||
let unicode = d.str("entryMode") == "unicode";
|
||||
if let Some(m) = widgets::dropdown(ui, "composite-fonts-entry-mode", modes[usize::from(unicode)], &modes, 120.0) {
|
||||
d.fields.insert("entryMode".into(), json!(if m == 1 { "unicode" } else { "direct" }));
|
||||
}
|
||||
form::text(ui, d, "entry", 160.0);
|
||||
if widgets::flat_button(ui, tl!("Add"), 48.0).clicked() {
|
||||
act = Some(Action::AddChars);
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let chosen = d.fields.get("char").and_then(Value::as_u64).map(|c| c as usize);
|
||||
let mut pick_char = None;
|
||||
// Only the rows in view are built, so a set of thousands of characters stays quick to draw.
|
||||
let chars: Vec<char> = set.characters.chars().collect();
|
||||
let rows = chars.len().div_ceil(CHARS_PER_ROW);
|
||||
egui::ScrollArea::vertical().id_salt("composite-fonts-chars").max_height(120.0).show_rows(ui, 24.0, rows, |ui, range| {
|
||||
for row in range {
|
||||
ui.horizontal(|ui| {
|
||||
for (n, c) in chars.iter().enumerate().skip(row * CHARS_PER_ROW).take(CHARS_PER_ROW) {
|
||||
let r = ui.add(egui::Button::selectable(chosen == Some(n), c.to_string()).min_size(egui::vec2(24.0, 24.0)));
|
||||
if r.on_hover_text(format!("U+{:04X}", *c as u32)).clicked() {
|
||||
pick_char = Some(n);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if let Some(n) = pick_char {
|
||||
d.fields.insert("char".into(), json!(n));
|
||||
}
|
||||
let can_delete = chosen.is_some_and(|n| n < chars.len());
|
||||
if ui.add_enabled_ui(can_delete, |ui| widgets::flat_button(ui, tl!("Delete"), 64.0)).inner.clicked() {
|
||||
act = Some(Action::DeleteChar);
|
||||
}
|
||||
act
|
||||
}
|
||||
|
||||
/// Do what button `act` asks for on the dialog's fonts.
|
||||
fn run(app: &mut VectorcraftApp, d: &mut Dialog, act: Action) -> Result<(), String> {
|
||||
let mut list = fonts(d);
|
||||
@@ -178,61 +306,151 @@ fn run(app: &mut VectorcraftApp, d: &mut Dialog, act: Action) -> Result<(), Stri
|
||||
let f = list.get(i).ok_or("no composite font")?;
|
||||
crate::io::save_command_output(app, "type.compositeFonts.export", EXTS[0], json!({ "fonts": [f] }))?;
|
||||
}
|
||||
Action::Custom => {
|
||||
let open = !d.bool("custom");
|
||||
d.fields.insert("custom".into(), json!(open));
|
||||
}
|
||||
Action::NewSet | Action::DeleteSet | Action::AddChars | Action::DeleteChar => {
|
||||
let f = list.get_mut(i).ok_or("no composite font")?;
|
||||
let at = edit_sets(d, f, act)?;
|
||||
put_fonts(d, &list);
|
||||
if let Some(k) = at {
|
||||
select_set(d, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The table of `f`'s parts: a row for each class with its font, style, size, baseline, scales and
|
||||
/// (Kana) Scale at Center; the values a class can't take are not shown.
|
||||
/// Do custom set action `act` on font `f`. The set to pick afterwards, when the sets changed.
|
||||
fn edit_sets(d: &mut Dialog, f: &mut CompositeFont, act: Action) -> Result<Option<usize>, String> {
|
||||
let k = index(d, "set", f.custom.len());
|
||||
match act {
|
||||
Action::NewSet => {
|
||||
if f.custom.len() >= CUSTOM_SETS_MAX {
|
||||
return Err(format!("a composite font keeps at most {CUSTOM_SETS_MAX} character sets"));
|
||||
}
|
||||
// Based on the set picked: its characters, font and values.
|
||||
let mut s = f.custom.get(k).cloned().unwrap_or_else(|| CustomCharSet::new("", f.parts.kanji.clone()));
|
||||
s.name = new_set_name(&f.custom);
|
||||
f.custom.push(s);
|
||||
Ok(Some(f.custom.len() - 1))
|
||||
}
|
||||
Action::DeleteSet => {
|
||||
if k < f.custom.len() {
|
||||
f.custom.remove(k);
|
||||
}
|
||||
Ok(Some(k.min(f.custom.len().saturating_sub(1))))
|
||||
}
|
||||
Action::AddChars => {
|
||||
let set = f.custom.get_mut(k).ok_or("no character set")?;
|
||||
let entry = d.str("entry");
|
||||
let add: Vec<char> = if d.str("entryMode") == "unicode" {
|
||||
entry
|
||||
.split(|c: char| c.is_whitespace() || c == ',')
|
||||
.filter(|code| !code.is_empty())
|
||||
.map(|code| CustomCharSet::char_from_code(code).ok_or_else(|| format!("`{code}` is not a code from U+0000 to U+FFFF")))
|
||||
.collect::<Result<_, _>>()?
|
||||
} else {
|
||||
entry.chars().collect()
|
||||
};
|
||||
if let Some(c) = add.iter().find(|&&c| !CustomCharSet::takes(c)) {
|
||||
return Err(format!("`{}`: U+{:04X} is a control character or past U+FFFF", set.name, *c as u32));
|
||||
}
|
||||
let mut count = set.characters.chars().count();
|
||||
for c in add {
|
||||
if set.characters.contains(c) {
|
||||
continue;
|
||||
}
|
||||
if count == CUSTOM_CHARS_MAX {
|
||||
return Err(format!("`{}`: a character set keeps at most {CUSTOM_CHARS_MAX} characters", set.name));
|
||||
}
|
||||
set.characters.push(c);
|
||||
count += 1;
|
||||
}
|
||||
d.fields.insert("entry".into(), json!(""));
|
||||
Ok(None)
|
||||
}
|
||||
Action::DeleteChar => {
|
||||
let set = f.custom.get_mut(k).ok_or("no character set")?;
|
||||
let n = d.fields.get("char").and_then(Value::as_u64).map(|c| c as usize);
|
||||
if let Some(n) = n {
|
||||
set.characters = set.characters.chars().enumerate().filter(|(j, _)| *j != n).map(|(_, c)| c).collect();
|
||||
}
|
||||
d.fields.remove("char");
|
||||
Ok(None)
|
||||
}
|
||||
Action::New | Action::Delete | Action::Import | Action::Export | Action::Custom => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The table of `f`'s parts: a row for each class, then for each custom character set, with its
|
||||
/// font, style, size, baseline, scales and (Kana) Scale at Center; the values a row can't take are
|
||||
/// not shown. Past [`TABLE_HEIGHT`] the table scrolls, so the controls under it stay on screen.
|
||||
fn parts_table(app: &VectorcraftApp, ui: &mut egui::Ui, f: &mut CompositeFont) {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
egui::ScrollArea::vertical().id_salt("composite-fonts-table").max_height(TABLE_HEIGHT).show(ui, |ui| parts_grid(app, ui, f));
|
||||
}
|
||||
|
||||
/// The rows of [`parts_table`].
|
||||
fn parts_grid(app: &VectorcraftApp, ui: &mut egui::Ui, f: &mut CompositeFont) {
|
||||
egui::Grid::new("composite-fonts-parts").num_columns(8).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
for h in ["", tl!("Font"), tl!("Style"), tl!("Size"), tl!("Baseline"), tl!("Horizontal Scale"), tl!("Vertical Scale"), ""] {
|
||||
widgets::dim_label(ui, h);
|
||||
}
|
||||
ui.end_row();
|
||||
for class in CharClass::ALL {
|
||||
let p: &mut CompositePart = f.parts.get_mut(class);
|
||||
ui.label(class_label(class));
|
||||
let id = ("composite-font", class.key());
|
||||
if let Some(crate::font_menu::FontPick::Chosen(family, style)) =
|
||||
crate::font_menu::font_menu(ui, id, &p.font, 190.0, None, crate::font_menu::MenuLook::of(app))
|
||||
&& db.composite_font(&family).is_none()
|
||||
{
|
||||
p.style = style.unwrap_or_else(|| db.styles(&family).first().cloned().unwrap_or_else(|| "Regular".into()));
|
||||
p.font = family;
|
||||
}
|
||||
let styles = db.styles(&p.font);
|
||||
let style_names: Vec<&str> = styles.iter().map(String::as_str).collect();
|
||||
if let Some(k) = widgets::dropdown_names(ui, ("composite-font-style", class.key()), &p.style, &style_names, 120.0)
|
||||
&& let Some(s) = styles.get(k)
|
||||
{
|
||||
p.style.clone_from(s);
|
||||
}
|
||||
let percent = |ui: &mut egui::Ui, key: &str, v: &mut f64, range: &std::ops::RangeInclusive<f64>| {
|
||||
if let Some(n) = widgets::range_field(ui, ("composite-font", class.key(), key), *v, range.clone(), "%", 1, 64.0) {
|
||||
*v = n;
|
||||
}
|
||||
};
|
||||
percent(ui, "size", &mut p.size, &COMPOSITE_SIZE);
|
||||
percent(ui, "baseline", &mut p.baseline, &COMPOSITE_BASELINE);
|
||||
if class.scales() {
|
||||
percent(ui, "hScale", &mut p.h_scale, &COMPOSITE_SCALE);
|
||||
percent(ui, "vScale", &mut p.v_scale, &COMPOSITE_SCALE);
|
||||
} else {
|
||||
ui.label("");
|
||||
ui.label("");
|
||||
}
|
||||
if class == CharClass::Kana {
|
||||
ui.checkbox(&mut p.scale_at_center, tl!("Scale at Center"));
|
||||
} else {
|
||||
ui.label("");
|
||||
}
|
||||
ui.end_row();
|
||||
part_row(app, ui, class.key(), f.parts.get_mut(class), class.scales(), class == CharClass::Kana);
|
||||
}
|
||||
for (n, s) in f.custom.iter_mut().enumerate() {
|
||||
// A set's name is the user's own, shown as it is.
|
||||
ui.label(s.name.as_str());
|
||||
part_row(app, ui, &format!("custom-{n}"), &mut s.part, true, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The cells of part `p`'s row (keyed `key`) after its label: font, style, size, baseline, the
|
||||
/// scales when `scales`, and Scale at Center when `center`.
|
||||
fn part_row(app: &VectorcraftApp, ui: &mut egui::Ui, key: &str, p: &mut CompositePart, scales: bool, center: bool) {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let id = ("composite-font", key);
|
||||
if let Some(crate::font_menu::FontPick::Chosen(family, style)) =
|
||||
crate::font_menu::font_menu(ui, id, &p.font, 190.0, None, crate::font_menu::MenuLook::of(app))
|
||||
&& db.composite_font(&family).is_none()
|
||||
{
|
||||
p.style = style.unwrap_or_else(|| db.styles(&family).first().cloned().unwrap_or_else(|| "Regular".into()));
|
||||
p.font = family;
|
||||
}
|
||||
let styles = db.styles(&p.font);
|
||||
let style_names: Vec<&str> = styles.iter().map(String::as_str).collect();
|
||||
if let Some(k) = widgets::dropdown_names(ui, ("composite-font-style", key), &p.style, &style_names, 120.0)
|
||||
&& let Some(s) = styles.get(k)
|
||||
{
|
||||
p.style.clone_from(s);
|
||||
}
|
||||
let percent = |ui: &mut egui::Ui, field: &str, v: &mut f64, range: &std::ops::RangeInclusive<f64>| {
|
||||
if let Some(n) = widgets::range_field(ui, ("composite-font", key, field), *v, range.clone(), "%", 1, 64.0) {
|
||||
*v = n;
|
||||
}
|
||||
};
|
||||
percent(ui, "size", &mut p.size, &COMPOSITE_SIZE);
|
||||
percent(ui, "baseline", &mut p.baseline, &COMPOSITE_BASELINE);
|
||||
if scales {
|
||||
percent(ui, "hScale", &mut p.h_scale, &COMPOSITE_SCALE);
|
||||
percent(ui, "vScale", &mut p.v_scale, &COMPOSITE_SCALE);
|
||||
} else {
|
||||
ui.label("");
|
||||
ui.label("");
|
||||
}
|
||||
if center {
|
||||
ui.checkbox(&mut p.scale_at_center, tl!("Scale at Center"));
|
||||
} else {
|
||||
ui.label("");
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
/// Import…: the fonts of a picked file join those in the dialog, a font of the same name replaced,
|
||||
/// and the first of them is picked.
|
||||
fn import(app: &mut VectorcraftApp, d: &mut Dialog) -> Result<(), String> {
|
||||
|
||||
@@ -172,3 +172,90 @@ fn the_dialog_draws_with_and_without_fonts() {
|
||||
frame(&mut app);
|
||||
assert_eq!(fonts(dialog(&mut app)).len(), 1);
|
||||
}
|
||||
|
||||
fn try_press(app: &mut VectorcraftApp, act: Action) -> Result<(), String> {
|
||||
let mut d = app.ui.dialog.clone().unwrap();
|
||||
let r = run(app, &mut d, act);
|
||||
app.ui.dialog = Some(d);
|
||||
r
|
||||
}
|
||||
|
||||
fn sets(app: &mut VectorcraftApp) -> Vec<CustomCharSet> {
|
||||
fonts(dialog(app)).first().map(|f| f.custom.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Custom shows the font's custom character sets: New adds a set (based on the one picked), Add
|
||||
/// puts typed characters or the characters of Unicode codes into it once each, Delete takes the
|
||||
/// picked character out, Delete Set takes the set out; OK sets them with the font as one step.
|
||||
#[test]
|
||||
fn custom_character_sets_are_edited_in_the_dialog() {
|
||||
let mut app = app();
|
||||
open(&mut app).unwrap();
|
||||
press(&mut app, Action::New);
|
||||
press(&mut app, Action::Custom);
|
||||
assert!(dialog(&mut app).bool("custom"));
|
||||
frame(&mut app);
|
||||
press(&mut app, Action::NewSet);
|
||||
frame(&mut app);
|
||||
assert_eq!(dialog(&mut app).str("setName"), "Character Set 1");
|
||||
let kanji = fonts(dialog(&mut app))[0].parts.kanji.clone();
|
||||
assert_eq!(sets(&mut app)[0].part, kanji);
|
||||
// Direct Entry adds each character once.
|
||||
dialog(&mut app).fields.insert("entry".into(), json!("※A※"));
|
||||
press(&mut app, Action::AddChars);
|
||||
assert_eq!(sets(&mut app)[0].characters, "※A");
|
||||
assert_eq!(dialog(&mut app).str("entry"), "");
|
||||
// Unicode takes codes, with or without U+, and skips characters already in the set.
|
||||
dialog(&mut app).fields.insert("entryMode".into(), json!("unicode"));
|
||||
dialog(&mut app).fields.insert("entry".into(), json!("U+3042, 41"));
|
||||
press(&mut app, Action::AddChars);
|
||||
assert_eq!(sets(&mut app)[0].characters, "※Aあ");
|
||||
for bad in ["zz", "1F600", "0009"] {
|
||||
dialog(&mut app).fields.insert("entry".into(), json!(bad));
|
||||
assert!(try_press(&mut app, Action::AddChars).is_err(), "{bad}");
|
||||
assert_eq!(sets(&mut app)[0].characters, "※Aあ");
|
||||
}
|
||||
dialog(&mut app).fields.insert("entryMode".into(), json!("direct"));
|
||||
dialog(&mut app).fields.insert("entry".into(), json!("B😀"));
|
||||
assert!(try_press(&mut app, Action::AddChars).is_err());
|
||||
assert_eq!(sets(&mut app)[0].characters, "※Aあ");
|
||||
frame(&mut app);
|
||||
// Delete takes the picked character out.
|
||||
dialog(&mut app).fields.insert("char".into(), json!(0));
|
||||
press(&mut app, Action::DeleteChar);
|
||||
assert_eq!(sets(&mut app)[0].characters, "Aあ");
|
||||
// The set's name is edited in Name; New is based on the set picked.
|
||||
dialog(&mut app).fields.insert("setName".into(), json!("Letters"));
|
||||
frame(&mut app);
|
||||
press(&mut app, Action::NewSet);
|
||||
let list = sets(&mut app);
|
||||
assert_eq!(list.iter().map(|s| (s.name.as_str(), s.characters.as_str())).collect::<Vec<_>>(), [("Letters", "Aあ"), ("Character Set 1", "Aあ")]);
|
||||
assert_eq!(dialog(&mut app).fields.get("set"), Some(&json!(1)));
|
||||
press(&mut app, Action::DeleteSet);
|
||||
assert_eq!(sets(&mut app).len(), 1);
|
||||
assert_eq!(dialog(&mut app).str("setName"), "Letters");
|
||||
frame(&mut app);
|
||||
dialog(&mut app).fields.insert("name".into(), json!("UI Composite Sets"));
|
||||
frame(&mut app);
|
||||
let before = steps(&app);
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!(steps(&app), before + 1);
|
||||
let f = &app.session.active().unwrap().doc.composite_fonts[0];
|
||||
assert_eq!((f.custom.len(), f.custom[0].name.as_str(), f.custom[0].characters.as_str()), (1, "Letters", "Aあ"));
|
||||
}
|
||||
|
||||
/// Two sets of the same name keep the dialog open and change nothing.
|
||||
#[test]
|
||||
fn custom_sets_sharing_a_name_keep_the_dialog_open() {
|
||||
let mut app = app();
|
||||
open(&mut app).unwrap();
|
||||
press(&mut app, Action::New);
|
||||
press(&mut app, Action::NewSet);
|
||||
press(&mut app, Action::NewSet);
|
||||
dialog(&mut app).fields.insert("setName".into(), json!("character set 1"));
|
||||
dialog(&mut app).fields.insert("custom".into(), json!(true));
|
||||
frame(&mut app);
|
||||
assert!(super::super::confirm(&mut app).is_err());
|
||||
assert!(app.ui.dialog.is_some());
|
||||
assert!(names(&app).is_empty());
|
||||
}
|
||||
|
||||
@@ -3614,6 +3614,8 @@ digits Malayalam Малаялам
|
||||
Kana Кана
|
||||
Numerals Цифри
|
||||
Scale at Center Мащабиране от центъра
|
||||
Delete Set Изтриване на набор
|
||||
Direct Entry Директно въвеждане
|
||||
Up/Down: Нагоре/надолу:
|
||||
Left/Right: Наляво/надясно:
|
||||
Tate-chu-yoko Up/Down Тате-чу-йоко нагоре/надолу
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1640,6 +1640,8 @@ digits Malayalam Malajalam
|
||||
Kana Kana
|
||||
Numerals Brojevi
|
||||
Scale at Center Skaliraj od centra
|
||||
Delete Set Izbriši skup
|
||||
Direct Entry Direktan unos
|
||||
Up/Down: Gore/dolje:
|
||||
Left/Right: Lijevo/desno:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko gore/dolje
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4685,6 +4685,8 @@ digits Malayalam Malaiàlam
|
||||
Kana Kana
|
||||
Numerals Xifres
|
||||
Scale at Center Escala des del centre
|
||||
Delete Set Suprimeix el joc
|
||||
Direct Entry Entrada directa
|
||||
Up/Down: Amunt/avall:
|
||||
Left/Right: Esquerra/dreta:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko amunt/avall
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1669,6 +1669,8 @@ digits Malayalam Malajálamské
|
||||
Kana Kana
|
||||
Numerals Číslice
|
||||
Scale at Center Měřítko od středu
|
||||
Delete Set Odstranit sadu
|
||||
Direct Entry Přímé zadání
|
||||
Up/Down: Nahoru/dolů:
|
||||
Left/Right: Vlevo/vpravo:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko nahoru/dolů
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2184,6 +2184,8 @@ digits Malayalam Malayalam
|
||||
Kana Kana
|
||||
Numerals Tal
|
||||
Scale at Center Skalér fra midten
|
||||
Delete Set Slet sæt
|
||||
Direct Entry Direkte indtastning
|
||||
Up/Down: Op/ned:
|
||||
Left/Right: Venstre/højre:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko op/ned
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3657,6 +3657,8 @@ effect Points Punkte
|
||||
Kana Kana
|
||||
Numerals Ziffern
|
||||
Scale at Center Von der Mitte aus skalieren
|
||||
Delete Set Satz löschen
|
||||
Direct Entry Direkteingabe
|
||||
Up/Down: Oben/Unten:
|
||||
Left/Right: Links/Rechts:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko oben/unten
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4191,6 +4191,8 @@ digits Malayalam Μαλαγιάλαμ
|
||||
Kana Κάνα
|
||||
Numerals Αριθμοί
|
||||
Scale at Center Κλιμάκωση από το κέντρο
|
||||
Delete Set Διαγραφή συνόλου
|
||||
Direct Entry Άμεση καταχώριση
|
||||
Up/Down: Πάνω/Κάτω:
|
||||
Left/Right: Αριστερά/Δεξιά:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko πάνω/κάτω
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3705,6 +3705,8 @@ digits Malayalam Malayálam
|
||||
Kana Kana
|
||||
Numerals Números
|
||||
Scale at Center Escalar desde el centro
|
||||
Delete Set Eliminar juego
|
||||
Direct Entry Entrada directa
|
||||
Up/Down: Arriba/abajo:
|
||||
Left/Right: Izquierda/derecha:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko arriba/abajo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1642,6 +1642,8 @@ digits Malayalam Malajalam
|
||||
Kana Kana
|
||||
Numerals Numerot
|
||||
Scale at Center Skaalaa keskeltä
|
||||
Delete Set Poista merkistö
|
||||
Direct Entry Suora syöttö
|
||||
Up/Down: Ylös/alas:
|
||||
Left/Right: Vasen/oikea:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko ylös/alas
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3697,6 +3697,8 @@ digits Malayalam Malayalam
|
||||
Kana Kana
|
||||
Numerals Chiffres
|
||||
Scale at Center Mettre à l’échelle depuis le centre
|
||||
Delete Set Supprimer le jeu
|
||||
Direct Entry Saisie directe
|
||||
Up/Down: Haut/bas :
|
||||
Left/Right: Gauche/droite :
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko haut/bas
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3257,6 +3257,8 @@
|
||||
Scale Text Attributes ટેક્સ્ટ ગુણધર્મો સ્કેલ કરો
|
||||
Scale Tool સ્કેલ ટૂલ
|
||||
Scale at Center કેન્દ્રથી સ્કેલ કરો
|
||||
Delete Set સેટ કાઢી નાખો
|
||||
Direct Entry સીધી એન્ટ્રી
|
||||
Scale in Perspective પર્સ્પેક્ટિવમાં સ્કેલ કરો
|
||||
Scale to Fit Artboard આર્ટબોર્ડમાં બંધબેસતું સ્કેલ કરો
|
||||
Scale: સ્કેલ:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 72 and column 5.
|
@@ -5070,6 +5070,8 @@ antiAlias Sharp शार्प
|
||||
Kana काना
|
||||
Numerals अंक
|
||||
Scale at Center केंद्र से आकार बदलें
|
||||
Delete Set सेट हटाएँ
|
||||
Direct Entry सीधी प्रविष्टि
|
||||
Technical Drawing तकनीकी ड्राइंग
|
||||
Telugu तेलुगु
|
||||
Temperature तापमान
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4373,6 +4373,8 @@ mark-style Japanese Japán
|
||||
Kana Kana
|
||||
Numerals Számjegyek
|
||||
Scale at Center Méretezés középről
|
||||
Delete Set Készlet törlése
|
||||
Direct Entry Közvetlen bevitel
|
||||
Up/Down: Fel/le:
|
||||
Left/Right: Balra/jobbra:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko fel/le
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1575,6 +1575,8 @@ digits Malayalam Malayalam
|
||||
Kana Kana
|
||||
Numerals Angka
|
||||
Scale at Center Skalakan dari tengah
|
||||
Delete Set Hapus set
|
||||
Direct Entry Entri langsung
|
||||
Up/Down: Atas/bawah:
|
||||
Left/Right: Kiri/kanan:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko atas/bawah
|
||||
|
||||
|
@@ -3751,6 +3751,8 @@ digits Malayalam Malayalam
|
||||
Kana Kana
|
||||
Numerals Numeri
|
||||
Scale at Center Ridimensiona dal centro
|
||||
Delete Set Elimina set
|
||||
Direct Entry Immissione diretta
|
||||
Up/Down: Su/giù:
|
||||
Left/Right: Sinistra/destra:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko su/giù
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4079,6 +4079,8 @@ file-filter Templates テンプレート
|
||||
Kana かな
|
||||
Numerals 数字
|
||||
Scale at Center 中心から拡大・縮小
|
||||
Delete Set セットを削除
|
||||
Direct Entry 直接入力
|
||||
Up/Down: 上下:
|
||||
Left/Right: 左右:
|
||||
Tate-chu-yoko Up/Down 縦中横の上下
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1513,6 +1513,8 @@ digits Malayalam მალაიალამური
|
||||
Kana კანა
|
||||
Numerals ციფრები
|
||||
Scale at Center ცენტრიდან მასშტაბირება
|
||||
Delete Set ნაკრების წაშლა
|
||||
Direct Entry პირდაპირი შეყვანა
|
||||
Up/Down: ზემოთ/ქვემოთ:
|
||||
Left/Right: მარცხნივ/მარჯვნივ:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko ზემოთ/ქვემოთ
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4067,6 +4067,8 @@ file-filter Templates 템플릿
|
||||
Kana 가나
|
||||
Numerals 숫자
|
||||
Scale at Center 중심에서 크기 조정
|
||||
Delete Set 집합 삭제
|
||||
Direct Entry 직접 입력
|
||||
Up/Down: 위/아래:
|
||||
Left/Right: 왼쪽/오른쪽:
|
||||
Tate-chu-yoko Up/Down 다테추요코 위/아래
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2093,6 +2093,8 @@ digits Malayalam Malayalam
|
||||
Kana Kana
|
||||
Numerals Cijfers
|
||||
Scale at Center Schalen vanuit het midden
|
||||
Delete Set Set verwijderen
|
||||
Direct Entry Directe invoer
|
||||
Up/Down: Omhoog/omlaag:
|
||||
Left/Right: Links/rechts:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko omhoog/omlaag
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1581,6 +1581,8 @@ digits Malayalam Malajalam
|
||||
Kana Kana
|
||||
Numerals Cyfry
|
||||
Scale at Center Skaluj od środka
|
||||
Delete Set Usuń zestaw
|
||||
Direct Entry Wprowadzanie bezpośrednie
|
||||
Up/Down: W górę/w dół:
|
||||
Left/Right: W lewo/w prawo:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko w górę/w dół
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5267,6 +5267,8 @@ axis Both Ambos
|
||||
Kana Kana
|
||||
Numerals Números
|
||||
Scale at Center Dimensionar a partir do centro
|
||||
Delete Set Excluir conjunto
|
||||
Direct Entry Entrada direta
|
||||
Up/Down: Para Cima/Para Baixo:
|
||||
Left/Right: Esquerda/Direita:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko Para Cima/Para Baixo
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3737,6 +3737,8 @@ digits Malayalam Малаялам
|
||||
Kana Кана
|
||||
Numerals Цифры
|
||||
Scale at Center Масштабировать от центра
|
||||
Delete Set Удалить набор
|
||||
Direct Entry Прямой ввод
|
||||
Up/Down: Вверх/вниз:
|
||||
Left/Right: Влево/вправо:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko вверх/вниз
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3627,6 +3627,8 @@ digits Malayalam Malayalam
|
||||
Kana Kana
|
||||
Numerals Siffror
|
||||
Scale at Center Skala från mitten
|
||||
Delete Set Ta bort uppsättning
|
||||
Direct Entry Direktinmatning
|
||||
Up/Down: Upp/ned:
|
||||
Left/Right: Vänster/höger:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko upp/ned
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3491,6 +3491,8 @@ digits Malayalam Malayalam
|
||||
Kana Kana
|
||||
Numerals Rakamlar
|
||||
Scale at Center Merkezden Ölçekle
|
||||
Delete Set Seti Sil
|
||||
Direct Entry Doğrudan Giriş
|
||||
Up/Down: Yukarı/Aşağı:
|
||||
Left/Right: Sol/Sağ:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko Yukarı/Aşağı
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3744,6 +3744,8 @@ effect Points Точки
|
||||
Kana Кана
|
||||
Numerals Цифри
|
||||
Scale at Center Масштабувати від центру
|
||||
Delete Set Видалити набір
|
||||
Direct Entry Пряме введення
|
||||
Up/Down: Вгору/вниз:
|
||||
Left/Right: Ліворуч/праворуч:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko вгору/вниз
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4090,6 +4090,8 @@ mark-style Japanese Kiểu Nhật
|
||||
Kana Kana
|
||||
Numerals Chữ số
|
||||
Scale at Center Co giãn từ tâm
|
||||
Delete Set Xóa bộ
|
||||
Direct Entry Nhập trực tiếp
|
||||
Up/Down: Lên/Xuống:
|
||||
Left/Right: Trái/Phải:
|
||||
Tate-chu-yoko Up/Down Tate-chu-yoko lên/xuống
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -3565,6 +3565,8 @@ digits Malayalam 马拉雅拉姆数字
|
||||
Kana 假名
|
||||
Numerals 数字
|
||||
Scale at Center 从中心缩放
|
||||
Delete Set 删除集
|
||||
Direct Entry 直接输入
|
||||
Up/Down: 上/下:
|
||||
Left/Right: 左/右:
|
||||
Tate-chu-yoko Up/Down 纵中横上/下
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4824,6 +4824,8 @@ digits Malayalam 馬來亞拉姆數字
|
||||
Kana 假名
|
||||
Numerals 數字
|
||||
Scale at Center 從中心縮放
|
||||
Delete Set 刪除集
|
||||
Direct Entry 直接輸入
|
||||
Up/Down: 上/下:
|
||||
Left/Right: 左/右:
|
||||
Tate-chu-yoko Up/Down 縱中橫上/下
|
||||
|
||||
|
Can't render this file because it is too large.
|
+22
-5
@@ -9295,8 +9295,19 @@ value out of range, a scale on another class, an unknown class or key, a name th
|
||||
characters, shared by two fonts (in any case) or the name of an installed font, and a part set in a composite font are
|
||||
each refused with an error, and nothing changes. A document keeps at most 256.
|
||||
|
||||
`custom` lists a font's custom character sets
|
||||
([Customize characters in a composite font](https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=348)),
|
||||
each `{name, characters?, font?, style?, size?, baseline?, hScale?, vScale?}`. A set sets the characters it lists in
|
||||
its own font, style, size, baseline and scales, ahead of their class; a value left out is the `kanji` part's. When two
|
||||
sets list a character, the later set sets it. A set's name is trimmed and unique in its font in any case. Its
|
||||
characters are each listed once, none a control character or past U+FFFF. A set may list no characters, and then sets
|
||||
none. A font keeps at most 64 sets and a set at most 8,192 characters, VectorCraft's own limits. A set name that is
|
||||
empty or shared, a character listed twice or not allowed, `scaleAtCenter` on a set, or an unknown key is refused with
|
||||
an error, and nothing changes.
|
||||
|
||||
Type set in a composite font names it as its family (`text.setStyle {font: "My Composite"}`), and the font menus list
|
||||
composite fonts first. The layout sets each character in its class's font, a letter and its combining marks together.
|
||||
composite fonts first. The layout sets each character in its custom set's or its class's font, a letter and its
|
||||
combining marks together.
|
||||
A composite font taken out of the document leaves its type naming it, and the type shows as missing. Composite fonts
|
||||
are saved with the document. SVG and legacy `.ai` write type in one of the document's composite fonts as outlines, and
|
||||
SVG, PDF, EPS and legacy `.ai` export draw it in the font's parts also when the document was read without an open app.
|
||||
@@ -9309,18 +9320,24 @@ parts its characters are drawn in, and `missingGlyphs` counts the characters tha
|
||||
characters are drawn in. `text.glyphAlternates` lists the alternates of a character from its part's font.
|
||||
|
||||
`type.compositeFonts.export {path?, names?, fonts?}` writes the document's composite fonts (all, or those `names`
|
||||
lists), or the `fonts` given, to a `.vccomposite` file; without `path` it returns the file's text as `data`.
|
||||
lists), or the `fonts` given, to a `.vccomposite` file; without `path` it returns the file's text as `data`. Fonts
|
||||
whose file would be larger than 4 MB, the most Import reads, are refused with an error.
|
||||
`type.compositeFonts.import {path | data | dataBase64, apply?}` reads a file of at most 4 MB. With `apply` (the
|
||||
default) its fonts join the document's, replacing a font of the same name, as one undo step; with `apply: false` it
|
||||
only returns them. Opening a `.vccomposite` file imports it.
|
||||
|
||||
Type › Composite Fonts… (shown with Show East Asian Options) opens the Composite Fonts dialog on the document's fonts:
|
||||
Font picks one, and a table sets each class's font, style, size, baseline and scales. New adds a font based on the one
|
||||
picked, Delete Font takes it out, Import… adds the fonts of a file and Export… writes the one picked. OK runs
|
||||
`type.compositeFonts.set` with every font as one undo step; a refused font keeps the dialog open.
|
||||
Font picks one, and a table sets each class's and each custom set's font, style, size, baseline and scales. New adds a
|
||||
font based on the one picked, Delete Font takes it out, Import… adds the fonts of a file and Export… writes the one
|
||||
picked. Custom shows the font's custom character sets: Character Set picks one, New adds a set based on the one picked,
|
||||
Delete Set takes it out, and Name renames it. Character › Direct Entry adds the characters typed, and Character ›
|
||||
Unicode adds the characters of hexadecimal codes (`3042`, `U+3042`, several separated by spaces or commas); Add skips
|
||||
characters the set already lists. Delete takes the character picked out of the set. OK runs `type.compositeFonts.set`
|
||||
with every font as one undo step; a refused font keeps the dialog open.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"type.compositeFonts.set","params":{"fonts":[{"name":"My Composite","parts":{"kanji":{"font":"Source Han Sans"},"roman":{"font":"Source Sans 3","size":95,"baseline":-2},"numerals":{"font":"Source Sans 3"}}}]}}}
|
||||
{"name":"run_command","arguments":{"command":"type.compositeFonts.set","params":{"fonts":[{"name":"My Composite","parts":{"kanji":{"font":"Source Han Sans"}},"custom":[{"name":"Marks","characters":"※〒","font":"Source Sans 3","size":90}]}]}}}
|
||||
```
|
||||
|
||||
## Optical Margin Alignment
|
||||
|
||||
@@ -693,6 +693,7 @@ since 2026-10-01.
|
||||
- **Create Outlines keeps the characters' gradients across the type:** Type › Create Outlines and Object › Expand give every glyph, ruby glyph and underline or strikethrough bar the gradient its characters drew with: a gradient with no vector of its own stays fitted to the whole layout rather than to each glyph, and one with a vector keeps its place, both moved, scaled and rotated with the type; the characters' stroke keeps the weight it was drawn at on scaled type. `object.expand.info` measures gradient fills on the same paths. Outlined characters keep their stroke and fill (the help page, <https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=337>); the gradient fitted across the whole layout is VectorCraft's own rule. Measured by `tests::objexpand::create_outlines_keeps_a_character_gradient_across_the_type` and `tests::objexpand::expand_keeps_a_character_gradient_across_the_type`. Scores unchanged (estimated).
|
||||
- **Composite fonts that two documents define differently:** when two open documents each define a composite font (Type › Composite Fonts…) by the same name with different parts, each document draws and exports its type in its own definition: in a tiled or floating window drawn while the front document draws, when it is placed in a document that defines the name differently, and in an export made while another document is in front. Evidence: `tests::composite::nested_loans_of_one_name_use_each_documents_definition` and `threads_lending_one_name_at_once_use_their_own_definitions` (`crates/text`), `tests::fontchange::a_placed_document_draws_its_own_definition_of_a_name_the_page_defines` (`crates/render`). Scores unchanged (estimated).
|
||||
- **Composite fonts only another document defines:** a document draws, measures and exports its type only in the composite fonts it defines itself. Type naming a composite font that only another open document, or the document a placed document sits in, defines is set as in a missing font, also when the document defines no composite font: in a window, a thumbnail or an export of a document in the back, in a file being opened and in a placed document. A temporary document drawn while a document draws keeps that document's composite fonts. Evidence: `tests::composite::a_lent_document_does_not_use_another_documents_composite_font` and `a_document_without_composite_fonts_uses_none_and_a_preview_is_unchanged` (`crates/text`), `tests::fontchange::a_placed_document_draws_its_own_definition_of_a_name_the_page_defines` and `a_document_draws_type_in_another_documents_composite_font_as_missing` (`crates/render`), `a_document_replaced_in_the_back_measures_a_font_it_lacks_as_missing` (`crates/engine`). Scores unchanged (estimated).
|
||||
- **Composite fonts' custom character sets:** Type › Composite Fonts… › Custom adds named character sets to a composite font, each setting the characters it lists in its own font, style, size, baseline and scales ahead of their class; the later of two sets that list a character sets it ([the help page](https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=348)). Characters are added by Direct Entry or by Unicode code and deleted one at a time; the sets are saved with the document and in `.vccomposite` files (`custom` in `type.compositeFonts.set`). Evidence: `tests::composite::custom_character_sets_take_their_characters_first` (`crates/text`), `compositefonts::tests::custom_character_sets_set_their_characters` (`crates/engine`), `dialogs::composite_fonts::tests::custom_character_sets_are_edited_in_the_dialog` (`crates/ui-egui`). Scores unchanged (estimated).
|
||||
- **Character-style Auto leading:** capture and explicit style attributes retain Auto as a reset, distinct from omitted leading. Redefine and later numeric updates preserve local leading overrides; the Character Styles marker recognizes Auto correctly. Regression assertions cover partial definitions, Undo/Redo and native save/load serialization. Public references: [Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html) and [Leading](https://helpx.adobe.com/illustrator/using/line-character-spacing.html).
|
||||
- **Character-style numeric bounds:** `charStyle.new`, `charStyle.edit`, and `charStyle.setAttrs` accept size from 0.1 to 1296 pt, explicit leading from 0.1 to 5000 pt, horizontal and vertical scale from 1% to 10,000%, and baseline shift and Tate-chu-yoko offsets from -1296 to 1296 pt. Out-of-range input leaves the style and Undo history unchanged; Auto leading remains available. These limits match the Character panel controls.
|
||||
- **Character Style Options, General and Basic Character Formats:** the dialog edits a style's name and stored character formats together. Empty fields remain unset, checkboxes show an indeterminate unset state, Basic's Reset Panel clears its explicit settings, and Preview can be canceled without changing history. OK saves one undo step; New Character Style offers the same fields from captured text or the interface language's defaults without a selection. Public reference: [Character and paragraph styles](https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| CJK: Standard Vertical Roman Alignment | done | Character panel menu › Standard Vertical Roman Alignment (`text.setFormat` / `text.setRangeStyle {standardVerticalRoman}`), with Show East Asian Options, off by default: in vertical type the marked half-width characters (Latin letters, digits and other characters that would lie on their side) stand upright one below the other, each in its upright cell; spaces keep their width and tate-chu-yoko blocks stay across the column; horizontal type keeps the mark and does not change. Character Style Options has its own checkbox for it. Evidence: `tests_vertical::standard_vertical_roman_alignment_stands_half_width_characters_upright`, `typecmd::standard_vertical_roman_is_a_character_attribute_saved_only_when_on` |
|
||||
| CJK: East Asian glyph forms | done | Glyphs panel menu › Traditional Forms, Expert Forms, JIS 04, JIS 90, JIS 78 and JIS 83 Forms, Monospaced Full-Width, Half-Width, Third-Width and Quarter-Width Forms, and Revert To Default Forms, with Show East Asian Options ([Format East Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)): the selected characters are drawn with the font's OpenType feature for that form (`trad`, `expt`, `jp04`, `jp90`, `jp78`, `jp83`, `fwid`, `hwid`, `twid`, `qwid`; [feature list](https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist)), one form at a time (`text.setFormat` / `text.setRangeStyle {glyphForm}`); a form none of the faces that draw the selected text (fallback faces included) has a feature for is disabled, and Revert To Default Forms draws the default glyphs. Saved natively only when set, carried by character styles, drawn on the canvas and in PDF, and written to SVG as `font-feature-settings`. Character Style Options has no field for it. Evidence: `tests::typo::glyph_forms_draw_the_fonts_east_asian_forms`, `typecmd::area_tests::glyph_form_is_one_character_attribute_saved_only_when_set`, `panels::glyphs::tests::east_asian_glyph_forms_in_the_panel_menu_set_the_selected_characters_form` |
|
||||
| CJK: warichu | done | Character panel menu › Warichu and Warichu Settings… (`text.setFormat` / `text.setRangeStyle {warichu, warichuLines, warichuGap, warichuScale, warichuAlign}`, `ui.warichuSettings`), with Show East Asian Options ([Format East Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)): marked characters set at a percentage of their size and stacked on 1 to 6 lines inside their line, one above the other in horizontal type and side by side in vertical type, with a line gap and eight alignments; a block stays whole on one line. Saved natively, carried by character styles, drawn on the canvas and in every export (SVG, PDF and `.ai` write such type as outlines). Type on a path sets the characters at their own size. Evidence: `tests::warichu`, `dialogs::warichu::tests`, `typecmd::area_tests::warichu_is_a_character_attribute_set_saved_and_exported` |
|
||||
| CJK: composite fonts | partial | Type › Composite Fonts… (`type.compositeFonts.set`), with Show East Asian Options: a font made of a font per class of character (Kanji, Kana, Punctuation, Symbols, Roman, Numerals), each with its own style, size and baseline in percent of the type's size, Kana, Roman and Numerals with horizontal and vertical scale, and Kana with Scale at Center; New, Delete Font, Import… and Export… (`.vccomposite`, `type.compositeFonts.import` and `.export`) ([Creating composite fonts](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=347)). Composite fonts are listed first in the font menus, saved with the document, and their type is laid out per class; SVG and legacy `.ai` write it as outlines. Not yet: custom character sets, the Units menu, Show Sample. Evidence: `text::composite::tests::characters_fall_in_their_classes`, `tests::composite::composite_type_draws_each_class_in_its_font`, `compositefonts::tests`, `dialogs::composite_fonts::tests` |
|
||||
| CJK: composite fonts | partial | Type › Composite Fonts… (`type.compositeFonts.set`), with Show East Asian Options: a font made of a font per class of character (Kanji, Kana, Punctuation, Symbols, Roman, Numerals), each with its own style, size and baseline in percent of the type's size, Kana, Roman and Numerals with horizontal and vertical scale, and Kana with Scale at Center; New, Delete Font, Import… and Export… (`.vccomposite`, `type.compositeFonts.import` and `.export`) ([Creating composite fonts](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=347)); custom character sets (Custom: New, Delete Set, characters added by Direct Entry or Unicode code and deleted), each with its own font, style, size, baseline and scales, ahead of the classes, the later set winning a character two list (`custom` in `type.compositeFonts.set`) ([Customize characters in a composite font](https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=348)). Composite fonts are listed first in the font menus, saved with the document, and their type is laid out per set and class; SVG and legacy `.ai` write it as outlines. Not yet: the Units menu, Show Sample. Evidence: `text::composite::tests::characters_fall_in_their_classes`, `text::composite::tests::custom_sets_take_their_characters_before_the_classes`, `tests::composite::composite_type_draws_each_class_in_its_font`, `tests::composite::custom_character_sets_take_their_characters_first`, `compositefonts::tests`, `dialogs::composite_fonts::tests` |
|
||||
| CJK: ruby | partial | ruby (ふりがな) for live text (`type.ruby`): a reading over a byte range, above (right of the column in vertical type) or below, start/centre/end alignment, size, overhang or squeeze; saved natively, laid out with its line growing, drawn on the canvas, in SVG and PDF (as outlines where ruby is present), in EPS, EMF/WMF and legacy `.ai` (legacy `.ai` writes type with ruby as outlines), and kept by Create Outlines, Expand Appearance and flattening, each glyph where the layout puts it (`typecmd::ruby_tests::create_outlines_keeps_the_ruby_where_it_is_laid_out`) | #633 |
|
||||
| Language per character (Character panel › Language, character styles), text shaped in it | done | `text.setFormat` / `text.setRangeStyle {language}` by name or BCP 47 tag, one of Document Setup's languages (Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu added, #4943; Assamese and Sanskrit added, #4947; Serbian: Cyrillic, Serbian: Latin and Croatian); the shaper gets the tag, so the font's language forms apply (OpenType `locl`: Turkish keeps `fi` apart and the dot on its i, #4940); new type takes the document's language; saved as the tag only when set, carried by character styles (#4946). English and eleven Indian languages hyphenate by run language; Sanskrit automatic hyphenation awaits allowlisted patterns (#4947). Not yet: `.ai`/PDF import of a text's language, paragraph styles. Evidence: `features::tests::language_picks_the_fonts_forms`, `features::tests::serbian_gets_the_fonts_serbian_forms`, `typecmd::area_tests::language_is_a_character_attribute_saved_only_when_set`, `tests::typo::indic_language_runs_hyphenate_with_both_composers`, `hyphen::tests::sanskrit_without_allowlisted_patterns_uses_only_exceptions`; spelling is checked in each run's Language (`text.spelling.check`, #4936) |
|
||||
| Language per character (Character panel › Language, character styles), text shaped in it | done | `text.setFormat` / `text.setRangeStyle {language}` by name or BCP 47 tag, one of Document Setup's languages (Hindi, Marathi, Gujarati, Bengali, Punjabi, Tamil, Telugu, Kannada, Malayalam, Odia and Urdu added, #4943; Serbian: Cyrillic, Serbian: Latin and Croatian; Bulgarian, #5945); the shaper gets the tag, so the font's language forms apply (OpenType `locl`: Turkish keeps `fi` apart and the dot on its i, #4940); new type takes the document's language; saved as the tag only when set, carried by character styles (#4946). Not yet: hyphenation by it in languages other than English and the eleven Indian languages above, `.ai`/PDF import of a text's language, paragraph styles. Evidence: `features::tests::language_picks_the_fonts_forms`, `features::tests::serbian_gets_the_fonts_serbian_forms`, `typecmd::area_tests::language_is_a_character_attribute_saved_only_when_set`; spelling is checked in each run's Language (`text.spelling.check`, #4936) |
|
||||
|
||||
Reference in New Issue
Block a user