forked from 1b-agent/opencadstudio-upstream
fix(pdfshxtext): .shx-only font resolution, oversized-object rule, mean text height
A listed font is compared only from its .shx file or the embedded stroke font of that name; no TrueType or alias stand-in. Objects with a diagonal over five times the selection's mean diagonal join no group. The created MTEXT height is the mean of the character sizes. Text styles look a bare font name up as .shx first, and a missing .shx name never draws with a TrueType face. TXT2MTXT measures TEXT to its last inked stroke. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2eb60f333f
commit
416ec43c38
6 files changed
+110
-45
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<String> {
|
||||
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<Option<usize>>,
|
||||
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<f64> = support
|
||||
.iter()
|
||||
.map(|(ci, gi)| line.chars[*ci].bbox.h() / font.glyphs[*gi].bbox.h())
|
||||
.collect();
|
||||
let text_height = sizes.iter().sum::<f64>() / 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<Stroke> {
|
||||
let mut boxes: std::collections::HashMap<Handle, BBox> = 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::<f64>() / 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<String>,
|
||||
) -> 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(),
|
||||
|
||||
@@ -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());
|
||||
|
||||
+19
-2
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user