Merge pull request #7631 from dotCooCoo/fix/composite-font-lent-document-only

Type: a document draws only its own composite font definitions
This commit is contained in:
BFlat
2026-11-15 03:20:55 +07:00
committed by GitHub
8 changed files with 200 additions and 53 deletions
+14 -5
View File
@@ -671,10 +671,11 @@ fn a_document_replaced_in_the_back_is_measured_before_it_comes_to_the_front() {
}
/// Type a document replaced in the back sets in a composite font that only the front document
/// defines is measured in that font while the front one is registered, and again, as missing, when
/// the replaced document comes to the front.
/// defines is measured as missing, though the front one is registered, also when the replaced
/// document defines no composite font, and still as missing when it comes to the front.
#[test]
fn a_document_replaced_in_the_back_measures_a_font_it_lacks_again_in_front() {
fn a_document_replaced_in_the_back_measures_a_font_it_lacks_as_missing() {
let db = vectorcraft_text::FontDb::global();
let mut s = session();
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Ab12", "size": 20, "font": "CF Lacked"})).unwrap()["id"].as_u64().unwrap();
let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc);
@@ -682,8 +683,16 @@ fn a_document_replaced_in_the_back_measures_a_font_it_lacks_again_in_front() {
s.execute("type.compositeFonts.set", &json!({"fonts": [font("CF Lacked")]})).unwrap();
assert!(s.replace_document(0, vectorcraft_format::load(&bytes).unwrap()));
let back = text_in(&s, 0, id);
let expected = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &back).bounds;
assert_eq!(back.cached_bounds, Some(expected));
assert!(s.documents()[0].doc.composite_fonts.is_empty());
let in_front = vectorcraft_text::layout(db, &back).bounds;
// The same type set in a family nothing defines.
let mut nowhere = back.clone();
for r in &mut nowhere.runs {
r.style.font_family = "No Such Family CF Lacked".into();
}
let missing = vectorcraft_text::layout(db, &nowhere).bounds;
assert!((in_front.width() - missing.width()).abs() > 1e-6, "the front document's font draws otherwise");
assert_eq!(back.cached_bounds, Some(missing));
assert!(s.set_active(0));
assert!(vectorcraft_text::FontDb::global().composite_font("CF Lacked").is_none());
let actual = text(&s, id);
+3 -2
View File
@@ -550,9 +550,10 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
// Optical Margin Alignment setting and writing direction. When that changes the direction of a
// frame of a restored document, the story flows through that frame's thread again. Threaded
// frames keep their size: Auto Size is off for them. The story is laid out in the document's
// own composite fonts.
// own composite fonts, and type naming one the document does not define flows as in a missing
// font, also when the document defines none.
let fonts = if doc.text_threads.is_empty() { Default::default() } else { doc.composite_fonts.clone() };
let lent = vectorcraft_text::FontDb::global().lend_composite_fonts(&fonts);
let lent = vectorcraft_text::FontDb::global().lend_document_composite_fonts(&fonts);
if !doc.text_threads.is_empty() && !restored {
crate::cmd::threads::reflow(&Document::new(1.0, 1.0), &mut doc);
} else if !doc.text_threads.is_empty() {
+6 -4
View File
@@ -415,14 +415,16 @@ impl Session {
/// Measure the type not measured yet in open document `index`, which is in the back, in its own
/// composite fonts, lent for the measuring, so its type has the bounds it will have when the
/// document comes to the front. The front document stays the one whose composite fonts are
/// registered. The names of the document's composite fonts its type was measured in are kept,
/// so its coming to the front does not measure that type again.
/// document comes to the front: type naming a composite font the document does not define is
/// measured as in a missing font, also when the front document defines one of that name, and
/// also when the document defines none. The front document stays the one whose composite fonts
/// are registered. The names of the document's composite fonts its type was measured in are
/// kept, so its coming to the front does not measure that type again.
fn measure_back_document(&mut self, index: usize) {
let Some(st) = self.docs.get(index) else { return };
let fonts = st.doc.composite_fonts.clone();
let db = vectorcraft_text::FontDb::global();
let lent = db.lend_composite_fonts(&fonts);
let lent = db.lend_document_composite_fonts(&fonts);
// A name another loan holds with a different definition is not lent: type set in it is
// measured again when the document comes to the front.
let own: HashSet<String> =
+3 -2
View File
@@ -425,8 +425,9 @@ impl Renderer {
if !k.is_finite() || k <= 1e-12 {
return;
}
// The art in its own coordinates, with its own resources (its composite fonts included).
let _lent = vectorcraft_text::FontDb::global().lend_composite_fonts(&e.doc.composite_fonts);
// The art in its own coordinates, with its own resources (its composite fonts included, and
// none of the host document's).
let _lent = vectorcraft_text::FontDb::global().lend_document_composite_fonts(&e.doc.composite_fonts);
let symbols = e.doc.symbol_boxes().quick();
let visible = m.inverse().transform_rect_bbox(f.visible);
let inner = Frame { doc: &e.doc, symbols: &symbols, view: f.view * m, visible, px: f.px / k, output: f.output * m, ..*f };
+50 -1
View File
@@ -209,7 +209,7 @@ fn a_documents_own_composite_font_draws_without_being_registered() {
}
/// A placed document draws its type in its own definition of a composite font that the document
/// placing it defines differently.
/// placing it defines differently, and as missing in one only the document placing it defines.
#[test]
fn a_placed_document_draws_its_own_definition_of_a_name_the_page_defines() {
use vectorcraft_doc::{CompositeFont, CompositePart, CompositeParts, ImageBlob, LinkInfo, PlacedDocument};
@@ -260,6 +260,55 @@ fn a_placed_document_draws_its_own_definition_of_a_name_the_page_defines() {
assert!(own == page(&plain, plain_id, "fontchange-placed-twice-plain", vec![]), "drawn in the placed document's definition");
assert!(own != page(&inter, inter_id, "fontchange-placed-twice-inter", vec![]), "not in the page's");
assert!(vectorcraft_text::FontDb::global().composite_font(NAME).is_none());
// A placed document that defines no composite font draws type naming the page's as missing.
let (undefined, undefined_id) = typed(NAME);
let (missing, missing_id) = typed("CF Placed Nowhere");
let borrowed = page(&undefined, undefined_id, "fontchange-placed-undefined", vec![definition("Inter")]);
assert!(borrowed == page(&missing, missing_id, "fontchange-placed-missing", vec![]), "drawn as in a missing font");
assert!(borrowed != page(&inter, inter_id, "fontchange-placed-undefined-inter", vec![]), "not in the page's definition");
}
/// A document that defines no composite font, drawn while another document's composite font of
/// the name its type is set in is registered (that document in front), draws that type as in a
/// missing font: in a render, an ink render, a thumbnail and a node render.
#[test]
fn a_document_draws_type_in_another_documents_composite_font_as_missing() {
use vectorcraft_doc::{CompositeFont, CompositePart, CompositeParts};
let _adding = adding_fonts();
const NAME: &str = "CF Front Only";
let db = vectorcraft_text::FontDb::global();
let typed = |family: &str| {
let mut d = Document::new(300.0, 150.0);
let style = CharStyle { size: 60.0, font_family: family.into(), ..Default::default() };
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 100.0), "Hamburg", style))));
let l = d.layers[0].id;
let id = d.insert(Some(l), 0, n).unwrap();
(d, id)
};
let (back, id) = typed(NAME);
let (missing, missing_id) = typed("CF Front Nowhere");
let (plain, plain_id) = typed("JetBrains Mono");
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let region = Rect::new(0.0, 0.0, 300.0, 150.0);
let draw = |d: &Document, id: NodeId| {
let n = d.node(id).unwrap().clone();
(
Renderer::new().render(d, 300, 150, Affine::IDENTITY, &opts).pixels,
Renderer::new().render_region_inks(d, region, 1.0, &opts),
Renderer::new().render_thumbnail(d, id, 64).map(|r| r.pixels),
Renderer::new().render_node(d, &std::sync::Arc::new(n), 300, 150, Affine::IDENTITY).map(|r| r.pixels),
)
};
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 });
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");
assert!(drawn.0 == as_missing.0, "rendered");
assert!(drawn.1 == as_missing.1, "rendered as ink amounts");
assert!(drawn.2 == as_missing.2, "thumbnail");
assert!(drawn.3 == as_missing.3, "node");
}
/// Type at 80% opacity, in Multiply and with a drop shadow redraws in a font that becomes
+57 -39
View File
@@ -19,7 +19,7 @@ type Resolved = Option<(Arc<FontFace>, FontMatch)>;
thread_local! {
/// The composite fonts the guards of this thread lend ([`FontDb::lend_composite_fonts`]), the
/// latest loan last.
/// latest loan last. Only the latest loan of a database is looked in.
static LENT: RefCell<Vec<Loan>> = const { RefCell::new(Vec::new()) };
}
@@ -34,8 +34,8 @@ struct Loan {
number: u64,
/// The fonts, by lowercased name.
fonts: HashMap<String, Arc<CompositeFont>>,
/// The database's generation when they were lent, when each was then the font the database
/// holds by its name.
/// The database's generation when they were lent, when they were then exactly the fonts the
/// database holds: each the font it holds by its name, and none it holds left out.
same_at: Option<u64>,
/// A number for the definitions lent: loans of the same definitions have the same one.
print: u64,
@@ -67,8 +67,8 @@ pub struct CompositeCoverage {
impl FontDb {
/// Make `font` available by its name, in place of a composite font of that name (any case).
/// The font menus and anything else that lists fonts change only when the definition does.
/// Whether it did. A font of that name a guard lends ([`FontDb::lend_composite_fonts`]) stays
/// in use on the guard's thread.
/// Whether it did. While a guard lends composite fonts ([`FontDb::lend_composite_fonts`]), the
/// guard's thread uses the fonts it lends and not this one.
pub fn set_composite_font(&self, font: &CompositeFont) -> bool {
let key = font.name.to_lowercase();
{
@@ -91,15 +91,13 @@ impl FontDb {
removed
}
/// The composite font named `name` (any case): the one the latest guard of this thread that
/// lends that name lends ([`FontDb::lend_composite_fonts`]), else the one the database holds.
/// The composite font named `name` (any case). While a guard of this thread lends a document's
/// composite fonts ([`FontDb::lend_composite_fonts`]), the one the latest such guard lends, and
/// `None` when that document does not define the name, also when the database or an earlier
/// guard holds one of that name. Otherwise the one the database holds.
pub fn composite_font(&self, name: &str) -> Option<Arc<CompositeFont>> {
if self.lends() {
let key = name.to_lowercase();
if let Some(f) = self.lent_font(&key) {
return Some(f);
}
return self.composites.read().unwrap_or_else(|e| e.into_inner()).get(&key).cloned();
return self.lent_font(&name.to_lowercase());
}
let map = self.composites.read().unwrap_or_else(|e| e.into_inner());
if map.is_empty() {
@@ -113,30 +111,27 @@ impl FontDb {
LENT.try_with(|l| l.borrow().iter().any(|loan| loan.db == self.id)).unwrap_or(false)
}
/// The composite font named `key` (lowercased) that the latest guard of this thread lending that
/// name lends.
/// The composite font named `key` (lowercased) that the latest guard of this thread lending
/// composite fonts of this database lends.
fn lent_font(&self, key: &str) -> Option<Arc<CompositeFont>> {
LENT.try_with(|l| l.borrow().iter().rev().filter(|loan| loan.db == self.id).find_map(|loan| loan.fonts.get(key).cloned())).ok().flatten()
LENT.try_with(|l| l.borrow().iter().rev().find(|loan| loan.db == self.id).and_then(|loan| loan.fonts.get(key).cloned())).ok().flatten()
}
/// [`FontDb::generation`] on this thread, from the database's own generation `g`: `g` while each
/// font the guards of this thread lend is the one the database held by its name when it was lent
/// and the database has not changed since, else a number made from `g` and the definitions
/// lent, so what was laid out in one document's definitions is not taken for another's.
/// [`FontDb::generation`] on this thread, from the database's own generation `g`: `g` while no
/// guard of this thread lends composite fonts of this database, or while the latest one lends
/// exactly the fonts the database held when it was lent and the database has not changed since,
/// else a number made from `g` and the definitions lent, so what was laid out in one document's
/// definitions is not taken for another's.
pub(super) fn lent_generation(&self, g: u64) -> u64 {
LENT.try_with(|l| {
let l = l.borrow();
let loans = l.iter().filter(|loan| loan.db == self.id);
if loans.clone().all(|loan| loan.same_at == Some(g)) {
return g;
}
let mut h = DefaultHasher::new();
g.hash(&mut h);
for loan in loans {
LENT.try_with(|l| match l.borrow().iter().rev().find(|loan| loan.db == self.id) {
Some(loan) if loan.same_at != Some(g) => {
let mut h = DefaultHasher::new();
g.hash(&mut h);
loan.print.hash(&mut h);
// The database's generations count up from 0 and never reach the top bit.
h.finish() | 1 << 63
}
// The database's generations count up from 0 and never reach the top bit.
h.finish() | 1 << 63
_ => g,
})
.unwrap_or(g)
}
@@ -245,20 +240,41 @@ impl FontDb {
}
}
/// Make each of the composite fonts `fonts` (a document's) available on this thread while the
/// returned guard lives, in place of a composite font of the same name that the database holds
/// or that an earlier guard of this thread lends. An app's session registers the composite fonts
/// of its front document; with the guard, a document exported without a session, one that is not
/// in front, or one placed in another document draws its type in its own, also while another
/// thread draws another document that defines the same name differently, or the session
/// registers another document's. Of fonts that share a name (in any case), the first is lent.
/// Make the composite fonts `fonts` (a document's) the only ones available on this thread while
/// the returned guard lives: type naming a composite font they do not define is set as in a
/// missing font, also when the database or an earlier guard of this thread holds one of that
/// name. An app's session registers the composite fonts of its front document; with the guard, a
/// document exported without a session, one that is not in front, or one placed in another
/// document draws its type in its own, also while another thread draws another document that
/// defines the same name differently, or the session registers another document's. Of fonts
/// that share a name (in any case), the first is lent. When none of `fonts` is usable and no
/// guard of this thread lends composite fonts of the database, the guard lends none: type naming
/// any composite font is set as in a missing font while it lives. When none is usable inside
/// another guard (a temporary document drawn while a document draws), nothing is lent and that
/// guard's fonts stay in use; [`FontDb::lend_document_composite_fonts`] lends none there too.
/// Other threads, and the fonts the database holds and lists, are left as they are. The guard
/// stays on its thread and ends its loan when it drops, also when the thread unwinds.
/// stays on its thread and ends its loan when it drops, also when the thread unwinds. With no
/// guard (type laid out on its own), the fonts the database holds are used.
pub fn lend_composite_fonts(&self, fonts: &[CompositeFont]) -> LentCompositeFonts<'_> {
self.lend(fonts, false)
}
/// [`FontDb::lend_composite_fonts`] for a document that is drawn or measured as itself, also
/// inside another document's guard (a document placed in another): when it defines no usable
/// composite font, type naming any composite font is set as in a missing font while the guard
/// lives.
pub fn lend_document_composite_fonts(&self, fonts: &[CompositeFont]) -> LentCompositeFonts<'_> {
self.lend(fonts, true)
}
/// Lend the usable fonts of `fonts`. With none, lend an empty set when `always` or when no
/// guard of this thread lends fonts of this database (and the database holds some), else
/// nothing.
fn lend(&self, fonts: &[CompositeFont], always: bool) -> LentCompositeFonts<'_> {
// A font a document can't use (its name an installed family's, or a value out of range)
// is not lent: an app leaves it out when it opens the document.
let usable: Vec<&CompositeFont> = fonts.iter().filter(|f| f.check().is_ok() && !self.has_installed_family(&f.name)).collect();
if usable.is_empty() {
if usable.is_empty() && !always && (self.lends() || self.composites.read().unwrap_or_else(|e| e.into_inner()).is_empty()) {
return LentCompositeFonts { number: None, thread: PhantomData };
}
let mut lent: HashMap<String, Arc<CompositeFont>> = HashMap::with_capacity(usable.len());
@@ -281,6 +297,8 @@ impl FontDb {
};
lent.insert(key, font);
}
// Each lent font is the database's; with as many, none it holds is left out.
same &= lent.len() == map.len();
self.generation.load(Ordering::Relaxed)
};
let number = NEXT_LOAN.fetch_add(1, Ordering::Relaxed);
+66
View File
@@ -192,6 +192,72 @@ fn nested_loans_of_one_name_use_each_documents_definition() {
assert_eq!(db.generation(), plain);
}
/// While one document's composite fonts are lent, a name only another document defines (the one
/// the database holds, or one an outer guard lends) is no composite font: type set in it lays out
/// as in a missing font, also after it was laid out in that definition with nothing lent.
#[test]
fn a_lent_document_does_not_use_another_documents_composite_font() {
let db = FontDb::with_font_dirs(vec![]);
let (a, b) = (roman_at("CF Only A", 50.0), roman_at("CF Only B", 50.0));
// The session registered B's font, and A's (the same definition) as well.
db.set_composite_font(&a);
db.set_composite_font(&b);
let half = advance(&db, "JetBrains Mono", 10.0);
assert!((advance(&db, "CF Only B", 20.0) - half).abs() < 1e-9);
let missing = advance(&db, "CF Nowhere", 20.0);
assert!((missing - half).abs() > 1e-6, "the missing font draws otherwise");
// Lending looks the name up among the installed families, which catalogs them the first time.
assert!(!db.has_installed_family("CF Only A"));
let plain = db.generation();
{
let _a = db.lend_composite_fonts(std::slice::from_ref(&a));
assert!(db.composite_font("CF Only A").is_some());
assert!(db.composite_font("cf only b").is_none());
assert!(db.composite_fonts_for("CF Only B", "A").is_none());
assert_ne!(db.generation(), plain, "type laid out with B's font is not reused");
assert!((advance(&db, "CF Only B", 20.0) - missing).abs() < 1e-9);
assert!((advance(&db, "CF Only A", 20.0) - half).abs() < 1e-9);
// A document placed in A that defines B's font sees only its own.
let _b = db.lend_composite_fonts(std::slice::from_ref(&b));
assert!(db.composite_font("CF Only A").is_none());
assert!((advance(&db, "CF Only B", 20.0) - half).abs() < 1e-9);
}
assert_eq!(db.generation(), plain);
assert!((advance(&db, "CF Only B", 20.0) - half).abs() < 1e-9);
}
/// A document that defines no composite font, drawn or lent as a document, uses none of the
/// database's. A temporary document that defines none, drawn inside another document's guard,
/// keeps that guard's fonts; with no guard at all, the database's stay in use.
#[test]
fn a_document_without_composite_fonts_uses_none_and_a_preview_is_unchanged() {
let db = FontDb::with_font_dirs(vec![]);
let (a, b) = (roman_at("CF Held A", 50.0), roman_at("CF Held B", 50.0));
db.set_composite_font(&b);
let half = advance(&db, "JetBrains Mono", 10.0);
let missing = advance(&db, "CF Nowhere Else", 20.0);
let plain = db.generation();
for lend in [FontDb::lend_composite_fonts, FontDb::lend_document_composite_fonts] {
let _none = lend(&db, &[]);
assert!(db.composite_font("CF Held B").is_none());
assert_ne!(db.generation(), plain);
assert!((advance(&db, "CF Held B", 20.0) - missing).abs() < 1e-9);
}
{
let _a = db.lend_composite_fonts(std::slice::from_ref(&a));
// A temporary document drawn inside it lends nothing and keeps A's fonts.
let _preview = db.lend_composite_fonts(&[]);
assert!(db.composite_font("CF Held A").is_some());
assert!(db.composite_font("CF Held B").is_none());
// A document placed in it that defines none uses none of A's.
let _placed = db.lend_document_composite_fonts(&[]);
assert!(db.composite_font("CF Held A").is_none());
}
assert_eq!(db.generation(), plain);
assert!(db.composite_font("CF Held B").is_some());
assert!((advance(&db, "CF Held B", 20.0) - half).abs() < 1e-9);
}
/// Two threads that lend different definitions of one name at once each lay out in their own.
#[test]
fn threads_lending_one_name_at_once_use_their_own_definitions() {
+1
View File
@@ -682,6 +682,7 @@ since 2026-10-01.
- **OpenType SVG colour fonts:** each glyph's SVG document is read by the SVG importer (registered by the engine) into vector art in the run's colours, then drawn, exported and outlined as COLR glyphs are; PDF real text writes them as text that draws nothing and draws their art where they sit. Measured by `tests_svg_glyphs`, `svg/tests/it/svg_glyphs.rs`, `tests_colourfonts::create_outlines_turns_svg_glyphs_into_groups`, `tests_colourfonts::pdf_real_text_draws_svg_glyphs_as_art_and_keeps_their_characters`. Scores unchanged (estimated).
- **Create Outlines keeps the type object's own appearance:** Type › Create Outlines and Object › Expand put the type object's own fills and strokes (the Appearance panel's items around its Characters row) on paths of the whole outline, below or above the glyph paths as that row places them, with one gradient across every glyph, so the outlines draw what the type drew; `object.expand.info` reports those strokes, pattern fills and gradient fills. 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>); a type object's appearance is separate from its characters' (the help page, <https://helpx.adobe.com/illustrator/using/appearance-attributes.html>); where the object's items go is VectorCraft's own rule. Measured by `tests::pathops::create_outlines_keeps_the_objects_own_fills_and_strokes`, `tests::objexpand::create_outlines_draws_the_type_objects_own_paint`, `tests::objexpand::type_objects_own_stroke_pattern_and_gradient_are_reported`, `tests::objexpand::type_without_an_outline_reports_no_own_stroke_or_fill`, `tests::objexpand::a_stroke_under_an_unpainted_stroke_is_reported_and_outlined` and `tests::objexpand::expand_keeps_the_type_objects_own_paint`. 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).
- **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).