diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index 16e827685..9129e4f9f 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -48,24 +48,34 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT // drawing) renders its REAL stroke glyphs — pass the absolute // path through so `Face::resolve` picks the SHX face. Fonts fetched // from the community repository live in the per-user fonts folder, - // so that is searched last, after the drawing folder. - let shx_path = file - .to_ascii_lowercase() - .ends_with(".shx") - .then(|| { - let base = document - .source_path - .as_deref() - .map(std::path::Path::new) - .and_then(|p| p.parent()); - crate::io::resolve_image_file(file, base).or_else(|| { - crate::io::font_repo::local_font_file(file) - .map(|path| path.to_string_lossy().into_owned()) - }) + // so that is searched last, after the drawing folder. A bare name + // (`txt`) is looked up as its .shx file first, before any + // installed face (measured: `Txt` draws txt.shx, `swiss` swiss.ttf). + let explicit_shx = lff::is_shx_name(file); + let shx_file = if explicit_shx { + Some(file.to_string()) + } else if std::path::Path::new(file).extension().is_none() { + Some(format!("{file}.shx")) + } else { + None + }; + let shx_path = shx_file.and_then(|shx_file| { + let base = document + .source_path + .as_deref() + .map(std::path::Path::new) + .and_then(|p| p.parent()); + crate::io::resolve_image_file(&shx_file, base).or_else(|| { + crate::io::font_repo::local_font_file(&shx_file) + .map(|path| path.to_string_lossy().into_owned()) }) - .flatten(); + }); if let Some(p) = shx_path { p + } else if explicit_shx { + // A missing .shx file draws with the stroke substitute of its + // name, never with an installed TrueType face (measured). + file.rsplit(['/', '\\']).next().unwrap_or(file).trim().to_string() } else if let Some(family) = resolve_font_reference(file) { // The file is installed, so its family is the face to draw with. // Substituting a stroke font instead drops every letter that @@ -93,7 +103,7 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT style_name.trim().to_string() }; - if !lff::is_builtin(&font_name) { + if !lff::is_builtin(&font_name) && !lff::is_shx_name(&font_name) { if let Some(canonical) = crate::scene::text::sysfont::canonical_family_name(&font_name) { font_name = canonical; } diff --git a/src/modules/annotate/txt2mtxt.rs b/src/modules/annotate/txt2mtxt.rs index c0970a8aa..808419ff5 100644 --- a/src/modules/annotate/txt2mtxt.rs +++ b/src/modules/annotate/txt2mtxt.rs @@ -125,14 +125,17 @@ fn piece<'a>(handle: Handle, entity: &'a EntityType, doc: &CadDocument) -> Optio match entity { EntityType::Text(t) => { let p = crate::entities::text::text_run_placement_at_scale(t, doc, 1.0); - let advance = text_local_bounds( + // The text runs to its last inked stroke, not to the pen's end + // (measured: "Second line" in txt at 2.5 spans 23.75, the + // advance less the last glyph's gap). + let right = text_local_bounds( &p.font, &resolve_dxf_special_chars(&t.value), p.height, p.width_factor, p.oblique_angle, ) - .map_or(0.0, |b| b.advance.abs() as f64); + .map_or(0.0, |b| b.ink_max[0] as f64); Some(Piece { handle, entity, @@ -140,7 +143,7 @@ fn piece<'a>(handle: Handle, entity: &'a EntityType, doc: &CadDocument) -> Optio height: p.height as f64, origin: p.origin, rotation: p.rotation as f64, - rect: [0.0, advance, 0.0, p.height as f64], + rect: [0.0, right, 0.0, p.height as f64], }) } EntityType::MText(m) => { diff --git a/src/modules/insert/pdf_shx_text.rs b/src/modules/insert/pdf_shx_text.rs index 9fb63284b..60db49a9f 100644 --- a/src/modules/insert/pdf_shx_text.rs +++ b/src/modules/insert/pdf_shx_text.rs @@ -120,23 +120,27 @@ pub fn set_settings(mut settings: ShxTextSettings) { /// What `Face::resolve` loads for a listed font: an added file as it is; a /// font name's .shx next to the drawing (`drawing` is its path) or in the -/// fonts folder; else the name, which the built-in stroke fonts answer. -pub fn resolve_font(name: &str, drawing: Option<&str>) -> String { +/// fonts folder; else the embedded stroke font of that very name. Nothing +/// otherwise: no alias stand-in and no TrueType face — measured, a listed +/// font without its .shx file is not compared at all ("- 0%"). +pub fn resolve_font(name: &str, drawing: Option<&str>) -> Option { if name.to_ascii_lowercase().ends_with(".shx") && std::path::Path::new(name).is_file() { - return name.to_string(); + return Some(name.to_string()); } - let file = format!("{}.shx", font_label(name)); + let label = font_label(name); + let file = format!("{label}.shx"); let base = drawing.map(std::path::Path::new).and_then(|p| p.parent()); crate::io::resolve_image_file(&file, base) .or_else(|| { crate::io::font_repo::local_font_file(&file).map(|p| p.to_string_lossy().into_owned()) }) - .unwrap_or_else(|| font_label(name)) + .or_else(|| crate::scene::text::lff::is_embedded_font(&label).then_some(label)) } // ── Recognition ──────────────────────────────────────────────────────────── /// One curve of the selection, flattened to world XY. +#[derive(Clone)] pub struct Stroke { pub handle: Handle, pub layer: String, @@ -680,6 +684,10 @@ struct LineMatch { /// Recognised glyph per character (None: not recognised). glyphs: Vec>, height: f64, + /// The created text's height: the mean of the characters' sizes, a + /// round letter's overshoot included (measured: nine letters 4.996 and + /// an O 5.138 high read 5.0102). + text_height: f64, width_factor: f64, } @@ -750,12 +758,12 @@ fn match_line(line: &Line, font: &FontShapes) -> LineMatch { } } } - let height = median( - support - .iter() - .map(|(ci, gi)| line.chars[*ci].bbox.h() / font.glyphs[*gi].bbox.h()) - .collect(), - ); + let sizes: Vec = support + .iter() + .map(|(ci, gi)| line.chars[*ci].bbox.h() / font.glyphs[*gi].bbox.h()) + .collect(); + let text_height = sizes.iter().sum::() / sizes.len().max(1) as f64; + let height = median(sizes); let Some(height) = height.filter(|h| *h > 0.0) else { // Nothing with a height: the reference's flat-line height, flat // glyphs (a dash) taken as they are. Anything else here (a frame @@ -777,6 +785,7 @@ fn match_line(line: &Line, font: &FontShapes) -> LineMatch { return LineMatch { glyphs, height: FLAT_LINE_HEIGHT, + text_height: FLAT_LINE_HEIGHT, width_factor: 1.0, }; }; @@ -846,6 +855,7 @@ fn match_line(line: &Line, font: &FontShapes) -> LineMatch { LineMatch { glyphs, height, + text_height, width_factor, } } @@ -897,6 +907,26 @@ fn line_text(line: &Line, m: &LineMatch, font: &FontShapes) -> Option<(String, B Some((value, ink)) } +/// The strokes of `strokes` but those of an object (a frame, a border line) +/// whose extents' diagonal is more than five times the mean diagonal of the +/// selected objects: such an object is no character and joins no group. +/// Measured: with a 200 × 100 frame among 66 text curves a line 39 long +/// still becomes "-" and one 39.2 long is left; twice the drawing, 78 and +/// 78.4; one text line alone (the line counted in), five times the mean. +fn without_oversized(strokes: &[Stroke]) -> Vec { + let mut boxes: std::collections::HashMap = Default::default(); + for s in strokes { + boxes.entry(s.handle).or_insert_with(BBox::empty).union(&BBox::of(&s.points)); + } + let diagonal = |b: &BBox| b.w().hypot(b.h()); + let mean = boxes.values().map(diagonal).sum::() / boxes.len().max(1) as f64; + strokes + .iter() + .filter(|s| diagonal(&boxes[&s.handle]) <= 5.0 * mean) + .cloned() + .collect() +} + fn percent(recognised: usize, total: usize) -> u32 { if total == 0 { 0 @@ -906,25 +936,25 @@ fn percent(recognised: usize, total: usize) -> u32 { } /// Recognise the text in `strokes` with the fonts of `settings`; `resolve` -/// turns a listed font into what `Face::resolve` loads. +/// turns a listed font into what `Face::resolve` loads (`None`: the font +/// is not available and is not compared). pub fn recognize( strokes: &[Stroke], settings: &ShxTextSettings, - resolve: impl Fn(&str) -> String, + resolve: impl Fn(&str) -> Option, ) -> Recognition { - let mut pieces = pieces_of(strokes); - // A shape many times the size of the selection's typical piece (a - // frame, a border line) is no character: it joins no group. - if let Some(typical) = median(pieces.iter().map(|p| p.bbox.size()).collect()) { - pieces.retain(|p| p.bbox.size() <= 10.0 * typical); - } + let strokes = &without_oversized(strokes); + let pieces = pieces_of(strokes); let lines = lines_of(strokes, pieces); let groups = groups_of(&lines); let fonts: Vec<(String, FontShapes)> = settings .fonts .iter() .filter(|f| f.enabled) - .filter_map(|f| font_shapes(&resolve(&f.name)).map(|shapes| (f.name.clone(), shapes))) + .filter_map(|f| { + let spec = resolve(&f.name)?; + font_shapes(&spec).map(|shapes| (f.name.clone(), shapes)) + }) .collect(); let mut out = Recognition { groups: Vec::new(), @@ -998,7 +1028,7 @@ pub fn recognize( out.lines.push(TextLine { value, insertion: ink.min, - height: m.height, + height: m.text_height, font: font_label(spec), font_spec: spec.clone(), layer: strokes[line.first].layer.clone(), diff --git a/src/scene/text/font_face.rs b/src/scene/text/font_face.rs index 5838f4ef0..716adb298 100644 --- a/src/scene/text/font_face.rs +++ b/src/scene/text/font_face.rs @@ -96,7 +96,9 @@ impl Face { } } let is_builtin = lff::is_builtin(font_name); - let has_sys = sysfont::has_family(font_name); + // A missing `.shx` file draws with a stroke substitute, never with a + // TrueType face of a similar name (measured). + let has_sys = !lff::is_shx_name(font_name) && sysfont::has_family(font_name); if !is_builtin && has_sys { let canonical = sysfont::canonical_family_name(font_name).unwrap_or_else(|| font_name.to_string()); diff --git a/src/scene/text/lff.rs b/src/scene/text/lff.rs index d942cfd52..b3d77ebdd 100644 --- a/src/scene/text/lff.rs +++ b/src/scene/text/lff.rs @@ -524,15 +524,32 @@ pub fn is_builtin(name: &str) -> bool { /// An alias names a real font (ISOCPEUR is a TrueType face): an installed /// copy of exactly that font draws it, Cyrillic included, where the LFF -/// stand-in has no such glyphs (#1764). +/// stand-in has no such glyphs (#1764). A name written as an `.shx` file +/// never means the TrueType face: a missing `isocpeur.shx` or `swiss.shx` +/// draws with the stroke substitute, not the installed `.ttf` (measured). fn installed_alias(stem: &str, name: &str) -> bool { use crate::scene::text::sysfont; - ALIASES.iter().any(|(alias, _)| *alias == stem) + !is_shx_name(name) + && ALIASES.iter().any(|(alias, _)| *alias == stem) && (sysfont::family_for_file(name).is_some() || sysfont::canonical_family_name(stem) .is_some_and(|family| family.eq_ignore_ascii_case(stem))) } +/// Whether `name` is written as an `.shx` file name. +pub fn is_shx_name(name: &str) -> bool { + name.trim().to_ascii_lowercase().ends_with(".shx") +} + +/// Whether an embedded font is that very font (its own stem, as `simplex` +/// or `romans.shx`), not a stand-in reached through an alias (`txt` draws +/// with `standard`). +pub fn is_embedded_font(name: &str) -> bool { + let key = name.trim().to_ascii_lowercase(); + let stem = key.strip_suffix(".shx").unwrap_or(&key); + FONTS_SRC.iter().any(|(s, _)| *s == stem) +} + pub fn get_font(name: &str) -> &'static Font { let map = fonts_map(); let key = name.trim().to_ascii_uppercase(); diff --git a/src/ui/window/pdf_shx_dialog.rs b/src/ui/window/pdf_shx_dialog.rs index 18f19ce9d..7456cb56f 100644 --- a/src/ui/window/pdf_shx_dialog.rs +++ b/src/ui/window/pdf_shx_dialog.rs @@ -75,10 +75,13 @@ impl ShxSettingsState { let Some(font) = self.settings.fonts.get(self.selected) else { return; }; - let spec = crate::modules::insert::pdf_shx_text::resolve_font( + // A font that is not available has nothing to show. + let Some(spec) = crate::modules::insert::pdf_shx_text::resolve_font( &font.name, self.drawing.as_deref(), - ); + ) else { + return; + }; let (strokes, _) = crate::scene::text::lff::tessellate_text_ex( [0.0, 0.0], 9.0,