forked from 1b-agent/opencadstudio-upstream
feat(text): read unifont and big-font .shx files; pair a style's big font
`shx.rs` only accepted the classic `AutoCAD-86 shapes 1.x` container, so every modern Autodesk font (`txt.shx`, `simplex.shx`, `romans.shx`, … are `unifont 1.0`) and every Asian big font (`chineset.shx`, `hztxt.shx`, `bigfont.shx` — `bigfont 1.0`) was rejected and text fell back to the LFF substitutes and TrueType fallback glyphs. Drawings authored with SHX big fonts (the norm for Chinese / Japanese / Korean sheets) never showed their real glyphs. - Parse all three containers. Unifont glyphs are keyed by Unicode; big-font glyphs by their double-byte code from a sparse hash table of (code, length, offset) entries. Big fonts reference subshapes with a two-byte `7, 0, hi, lo`. - `bigfont_glyph` encodes a character with encoding_rs (Big5 / GBK / Shift_JIS / EUC-KR, most likely first for the file name) and takes the first code the font has. - `Face::WithBig` pairs a style's big font with its primary face: full-width characters come from the big font, everything else from the primary — how a STYLE with a "Big Font" behaves in AutoCAD. `resolve_text_style` resolves `big_font_file` like the primary (next to the drawing, then the user fonts folder) and rides it along in the font name. - The missing-fonts dialog lists unresolvable big fonts too. Unit tests cover a synthetic big font (Big5 lookup, cap-height scaling, no aliasing of unknown codes), a synthetic unifont, and the pairing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0b522ceb86
commit
ff4981ffe6
6 files changed
+351
-35
No files matched your search
Generated
+1
@@ -18,6 +18,7 @@ dependencies = [
|
|||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"cosmic-text 0.15.0",
|
"cosmic-text 0.15.0",
|
||||||
"dwg-thumbnailer",
|
"dwg-thumbnailer",
|
||||||
|
"encoding_rs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"flate2",
|
"flate2",
|
||||||
"fluent-syntax",
|
"fluent-syntax",
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ ttf-parser = "0.25"
|
|||||||
cosmic-text = "0.15"
|
cosmic-text = "0.15"
|
||||||
unicode-bidi = "0.3"
|
unicode-bidi = "0.3"
|
||||||
lyon_tessellation = "1.0.20"
|
lyon_tessellation = "1.0.20"
|
||||||
|
# Big-font (.shx) lookups: encode a char to the font's Big5 / GBK / Shift_JIS
|
||||||
|
# / EUC-KR double-byte code. Already in the tree through other deps.
|
||||||
|
encoding_rs = "0.8"
|
||||||
pollster = "0.4"
|
pollster = "0.4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -77,6 +77,27 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A style's "Big Font" (`chineset.shx`, `hztxt.shx`, …) supplies the
|
||||||
|
// double-byte characters. Resolve it like the primary font (next to the
|
||||||
|
// drawing, then the per-user fonts folder) and pair it with the face; an
|
||||||
|
// unresolvable big font just leaves those characters to the fallback.
|
||||||
|
if let Some(big) = style
|
||||||
|
.map(|s| s.big_font_file.trim())
|
||||||
|
.filter(|big| !big.is_empty() && big.to_ascii_lowercase().ends_with(".shx"))
|
||||||
|
{
|
||||||
|
let base = document
|
||||||
|
.source_path
|
||||||
|
.as_deref()
|
||||||
|
.map(std::path::Path::new)
|
||||||
|
.and_then(|p| p.parent());
|
||||||
|
let big_path = crate::io::resolve_image_file(big, base).or_else(|| {
|
||||||
|
crate::io::font_repo::local_font_file(big).map(|p| p.to_string_lossy().into_owned())
|
||||||
|
});
|
||||||
|
if let Some(big_path) = big_path {
|
||||||
|
font_name = Face::with_big_font(&font_name, &big_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ResolvedTextStyle {
|
ResolvedTextStyle {
|
||||||
font_name,
|
font_name,
|
||||||
width_factor: style.map(|s| s.width_factor as f32).unwrap_or(1.0),
|
width_factor: style.map(|s| s.width_factor as f32).unwrap_or(1.0),
|
||||||
|
|||||||
+15
-12
@@ -70,18 +70,21 @@ pub fn missing_shx_fonts(doc: &CadDocument) -> Vec<String> {
|
|||||||
.map(Path::new)
|
.map(Path::new)
|
||||||
.and_then(|p| p.parent());
|
.and_then(|p| p.parent());
|
||||||
for style in doc.text_styles.iter() {
|
for style in doc.text_styles.iter() {
|
||||||
let file = style.font_file.trim();
|
// The primary font and the big font (Asian double-byte glyphs) are
|
||||||
if file.is_empty() || !file.to_ascii_lowercase().ends_with(".shx") {
|
// both plain .shx files the style depends on.
|
||||||
continue;
|
for file in [style.font_file.trim(), style.big_font_file.trim()] {
|
||||||
}
|
if file.is_empty() || !file.to_ascii_lowercase().ends_with(".shx") {
|
||||||
let resolved = crate::io::resolve_image_file(file, base).is_some()
|
continue;
|
||||||
|| local_font_file(file).is_some();
|
}
|
||||||
if resolved {
|
let resolved = crate::io::resolve_image_file(file, base).is_some()
|
||||||
continue;
|
|| local_font_file(file).is_some();
|
||||||
}
|
if resolved {
|
||||||
let name = file.rsplit(['/', '\\']).next().unwrap_or(file).to_string();
|
continue;
|
||||||
if !missing.iter().any(|m| m.eq_ignore_ascii_case(&name)) {
|
}
|
||||||
missing.push(name);
|
let name = file.rsplit(['/', '\\']).next().unwrap_or(file).to_string();
|
||||||
|
if !missing.iter().any(|m| m.eq_ignore_ascii_case(&name)) {
|
||||||
|
missing.push(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
missing.sort_by_key(|name| name.to_ascii_lowercase());
|
missing.sort_by_key(|name| name.to_ascii_lowercase());
|
||||||
|
|||||||
@@ -47,12 +47,37 @@ pub enum Face {
|
|||||||
/// Space advance (9-unit).
|
/// Space advance (9-unit).
|
||||||
word: f32,
|
word: f32,
|
||||||
},
|
},
|
||||||
|
/// A face paired with an Asian big font (`chineset.shx`, `hztxt.shx`, …):
|
||||||
|
/// double-byte characters come from the big font, everything else from
|
||||||
|
/// `primary` — exactly how a STYLE with a "Big Font" behaves in AutoCAD.
|
||||||
|
WithBig {
|
||||||
|
primary: Box<Face>,
|
||||||
|
/// Absolute path of the `AutoCAD-86 bigfont 1.0` file.
|
||||||
|
big: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Separator between a style's primary font name and its big-font path in
|
||||||
|
/// the single `font_name` string the text pipeline passes around (see
|
||||||
|
/// [`Face::with_big_font`]); a control character no font name contains.
|
||||||
|
pub const BIG_FONT_SEP: char = '\u{1}';
|
||||||
|
|
||||||
impl Face {
|
impl Face {
|
||||||
/// Resolve a style's font name to a concrete face. Embedded stroke fonts
|
/// Resolve a style's font name to a concrete face. Embedded stroke fonts
|
||||||
/// take priority; only otherwise-unknown names try the system fonts.
|
/// take priority; only otherwise-unknown names try the system fonts.
|
||||||
pub fn resolve(font_name: &str) -> Face {
|
pub fn resolve(font_name: &str) -> Face {
|
||||||
|
// `primary<SEP>big` pairs a big font with the primary face.
|
||||||
|
if let Some((primary, big)) = font_name.split_once(BIG_FONT_SEP) {
|
||||||
|
let face = Face::resolve(primary);
|
||||||
|
return if !big.is_empty() && shx::is_bigfont(big) {
|
||||||
|
Face::WithBig {
|
||||||
|
primary: Box::new(face),
|
||||||
|
big: big.to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
face
|
||||||
|
};
|
||||||
|
}
|
||||||
// A resolvable on-disk .SHX font renders its real stroke glyphs; the
|
// A resolvable on-disk .SHX font renders its real stroke glyphs; the
|
||||||
// LFF substitutes only cover names we can't load.
|
// LFF substitutes only cover names we can't load.
|
||||||
if font_name.to_ascii_lowercase().ends_with(".shx")
|
if font_name.to_ascii_lowercase().ends_with(".shx")
|
||||||
@@ -87,6 +112,13 @@ impl Face {
|
|||||||
Face::Lff(lff::get_font(font_name))
|
Face::Lff(lff::get_font(font_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `font_name` string that pairs `primary` with the big font at
|
||||||
|
/// `big_path` for [`Face::resolve`]. Every text entry point takes a font
|
||||||
|
/// name, so the pairing rides along in that string.
|
||||||
|
pub fn with_big_font(primary: &str, big_path: &str) -> String {
|
||||||
|
format!("{primary}{BIG_FONT_SEP}{big_path}")
|
||||||
|
}
|
||||||
|
|
||||||
/// Look up a glyph. A stroke (LFF) font uses its own glyphs; anything it
|
/// Look up a glyph. A stroke (LFF) font uses its own glyphs; anything it
|
||||||
/// lacks falls back to a system TrueType font chosen by cosmic-text (filled
|
/// lacks falls back to a system TrueType font chosen by cosmic-text (filled
|
||||||
/// outline, so covers scripts no stroke font provides). The TTF path mirrors
|
/// outline, so covers scripts no stroke font provides). The TTF path mirrors
|
||||||
@@ -103,6 +135,14 @@ impl Face {
|
|||||||
Face::Shx { path, .. } => shx::font_glyph(path, ch as u16)
|
Face::Shx { path, .. } => shx::font_glyph(path, ch as u16)
|
||||||
.map(GlyphRef::Owned)
|
.map(GlyphRef::Owned)
|
||||||
.or_else(|| ttf_glyph::fallback_glyph(ch).map(GlyphRef::Owned)),
|
.or_else(|| ttf_glyph::fallback_glyph(ch).map(GlyphRef::Owned)),
|
||||||
|
Face::WithBig { primary, big } => {
|
||||||
|
if ttf_glyph::is_full_width(ch) {
|
||||||
|
if let Some(g) = shx::bigfont_glyph(big, ch) {
|
||||||
|
return Some(GlyphRef::Owned(g));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
primary.glyph(ch)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +154,7 @@ impl Face {
|
|||||||
Face::Ttf { .. } => 0.0,
|
Face::Ttf { .. } => 0.0,
|
||||||
// SHX advances already include the trailing gap by design.
|
// SHX advances already include the trailing gap by design.
|
||||||
Face::Shx { .. } => 0.0,
|
Face::Shx { .. } => 0.0,
|
||||||
|
Face::WithBig { primary, .. } => primary.letter_spacing(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +163,7 @@ impl Face {
|
|||||||
match self {
|
match self {
|
||||||
Face::Lff(f) => f.word_spacing,
|
Face::Lff(f) => f.word_spacing,
|
||||||
Face::Ttf { word, .. } | Face::Shx { word, .. } => *word,
|
Face::Ttf { word, .. } | Face::Shx { word, .. } => *word,
|
||||||
|
Face::WithBig { primary, .. } => primary.word_spacing(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +174,7 @@ impl Face {
|
|||||||
match self {
|
match self {
|
||||||
Face::Ttf { family, .. } => Some(family),
|
Face::Ttf { family, .. } => Some(family),
|
||||||
Face::Lff(_) | Face::Shx { .. } => None,
|
Face::Lff(_) | Face::Shx { .. } => None,
|
||||||
|
Face::WithBig { primary, .. } => primary.ttf_family(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +185,7 @@ impl Face {
|
|||||||
Face::Lff(f) => f.line_spacing,
|
Face::Lff(f) => f.line_spacing,
|
||||||
Face::Ttf { .. } => 1.0,
|
Face::Ttf { .. } => 1.0,
|
||||||
Face::Shx { line, .. } => *line,
|
Face::Shx { line, .. } => *line,
|
||||||
|
Face::WithBig { primary, .. } => primary.line_spacing(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,11 +280,27 @@ mod tests {
|
|||||||
// they should resolve to Ttf. If they aren't installed, Lff fallback is accepted.
|
// they should resolve to Ttf. If they aren't installed, Lff fallback is accepted.
|
||||||
eprintln!("Font {} resolved to Lff (probably not installed)", test_name);
|
eprintln!("Font {} resolved to Lff (probably not installed)", test_name);
|
||||||
}
|
}
|
||||||
Face::Shx { .. } => {
|
Face::Shx { .. } | Face::WithBig { .. } => {
|
||||||
// A system TTF name never resolves to an SHX shape font.
|
// A system TTF name never resolves to an SHX shape font
|
||||||
|
// (nor gains a big font without the pairing separator).
|
||||||
eprintln!("Font {} resolved to Shx (unexpected)", test_name);
|
eprintln!("Font {} resolved to Shx (unexpected)", test_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn big_font_pairing_routes_ideographs_and_leaves_latin_alone() {
|
||||||
|
// Without a real big-font file the pairing is dropped: the primary
|
||||||
|
// face is used as-is.
|
||||||
|
assert!(matches!(
|
||||||
|
Face::resolve(&Face::with_big_font("txt", "C:/nowhere/chineset.shx")),
|
||||||
|
Face::Lff(_)
|
||||||
|
));
|
||||||
|
// A stroke face still renders Latin from its own glyphs when paired.
|
||||||
|
let face = Face::resolve("txt");
|
||||||
|
assert!(face.glyph('A').is_some());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+249
-21
@@ -50,7 +50,20 @@ const DIRS: [[f64; 2]; 16] = [
|
|||||||
[1.0, -0.5],
|
[1.0, -0.5],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Which `AutoCAD-86` container a file is; decides how glyphs are keyed and
|
||||||
|
/// how a `7` (subshape) opcode is encoded.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
enum ShxKind {
|
||||||
|
/// `shapes 1.0/1.1`: keyed by 8-bit code (or shape number).
|
||||||
|
Shapes,
|
||||||
|
/// `unifont 1.0`: keyed by Unicode code point.
|
||||||
|
Unifont,
|
||||||
|
/// `bigfont 1.0`: keyed by a double-byte legacy code (Big5, GBK, …).
|
||||||
|
Bigfont,
|
||||||
|
}
|
||||||
|
|
||||||
struct ShxFile {
|
struct ShxFile {
|
||||||
|
kind: ShxKind,
|
||||||
/// shape number → (name, spec bytes)
|
/// shape number → (name, spec bytes)
|
||||||
shapes: HashMap<u16, (String, Vec<u8>)>,
|
shapes: HashMap<u16, (String, Vec<u8>)>,
|
||||||
}
|
}
|
||||||
@@ -81,39 +94,158 @@ fn load_file(path: &str) -> Option<Arc<ShxFile>> {
|
|||||||
|
|
||||||
fn parse_file(path: &str) -> Option<ShxFile> {
|
fn parse_file(path: &str) -> Option<ShxFile> {
|
||||||
let bytes = std::fs::read(path).ok()?;
|
let bytes = std::fs::read(path).ok()?;
|
||||||
// Header line: `AutoCAD-86 shapes 1.0`/`1.1` + CR LF SUB. Reject fonts.
|
// Header line: `AutoCAD-86 <kind> 1.x` + CR LF SUB.
|
||||||
let head_end = bytes.iter().position(|&b| b == 0x1A)?;
|
let head_end = bytes.iter().position(|&b| b == 0x1A)?;
|
||||||
let header = String::from_utf8_lossy(&bytes[..head_end]);
|
let header = String::from_utf8_lossy(&bytes[..head_end]);
|
||||||
if !header.contains("shapes") {
|
let kind = if header.contains("shapes") {
|
||||||
|
ShxKind::Shapes
|
||||||
|
} else if header.contains("unifont") {
|
||||||
|
ShxKind::Unifont
|
||||||
|
} else if header.contains("bigfont") {
|
||||||
|
ShxKind::Bigfont
|
||||||
|
} else {
|
||||||
return None;
|
return None;
|
||||||
}
|
};
|
||||||
let mut p = head_end + 1;
|
let mut p = head_end + 1;
|
||||||
let rd_u16 = |p: &mut usize| -> Option<u16> {
|
let rd_u16 = |p: &mut usize| -> Option<u16> {
|
||||||
let v = u16::from_le_bytes([*bytes.get(*p)?, *bytes.get(*p + 1)?]);
|
let v = u16::from_le_bytes([*bytes.get(*p)?, *bytes.get(*p + 1)?]);
|
||||||
*p += 2;
|
*p += 2;
|
||||||
Some(v)
|
Some(v)
|
||||||
};
|
};
|
||||||
let _first = rd_u16(&mut p)?;
|
let rd_u32 = |p: &mut usize| -> Option<u32> {
|
||||||
let _last = rd_u16(&mut p)?;
|
let v = u32::from_le_bytes([
|
||||||
let count = rd_u16(&mut p)? as usize;
|
*bytes.get(*p)?,
|
||||||
let mut dir: Vec<(u16, usize)> = Vec::with_capacity(count);
|
*bytes.get(*p + 1)?,
|
||||||
for _ in 0..count {
|
*bytes.get(*p + 2)?,
|
||||||
let num = rd_u16(&mut p)?;
|
*bytes.get(*p + 3)?,
|
||||||
let len = rd_u16(&mut p)? as usize;
|
]);
|
||||||
dir.push((num, len));
|
*p += 4;
|
||||||
}
|
Some(v)
|
||||||
let mut shapes = HashMap::new();
|
};
|
||||||
for (num, len) in dir {
|
// Glyph data = leading NUL-terminated ASCII name, spec follows.
|
||||||
let end = p.checked_add(len)?.min(bytes.len());
|
let split_named = |blob: &[u8]| -> (String, Vec<u8>) {
|
||||||
let blob = &bytes[p..end];
|
|
||||||
p = end;
|
|
||||||
// Name = leading NUL-terminated ASCII, spec follows.
|
|
||||||
let name_end = blob.iter().position(|&b| b == 0).unwrap_or(0);
|
let name_end = blob.iter().position(|&b| b == 0).unwrap_or(0);
|
||||||
let name = String::from_utf8_lossy(&blob[..name_end]).into_owned();
|
let name = String::from_utf8_lossy(&blob[..name_end]).into_owned();
|
||||||
let spec = blob.get(name_end + 1..).unwrap_or(&[]).to_vec();
|
let spec = blob.get(name_end + 1..).unwrap_or(&[]).to_vec();
|
||||||
shapes.insert(num, (name, spec));
|
(name, spec)
|
||||||
|
};
|
||||||
|
let mut shapes = HashMap::new();
|
||||||
|
match kind {
|
||||||
|
ShxKind::Shapes => {
|
||||||
|
let _first = rd_u16(&mut p)?;
|
||||||
|
let _last = rd_u16(&mut p)?;
|
||||||
|
let count = rd_u16(&mut p)? as usize;
|
||||||
|
let mut dir: Vec<(u16, usize)> = Vec::with_capacity(count);
|
||||||
|
for _ in 0..count {
|
||||||
|
let num = rd_u16(&mut p)?;
|
||||||
|
let len = rd_u16(&mut p)? as usize;
|
||||||
|
dir.push((num, len));
|
||||||
|
}
|
||||||
|
for (num, len) in dir {
|
||||||
|
let end = p.checked_add(len)?.min(bytes.len());
|
||||||
|
let blob = &bytes[p..end];
|
||||||
|
p = end;
|
||||||
|
shapes.insert(num, split_named(blob));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ShxKind::Unifont => {
|
||||||
|
// u32 glyph count (including the font header), then the header
|
||||||
|
// entry (u16 length + name NUL above below modes encoding
|
||||||
|
// embedded 0), then `count - 1` × (u16 code, u16 length, data).
|
||||||
|
let count = rd_u32(&mut p)? as usize;
|
||||||
|
let head_len = rd_u16(&mut p)? as usize;
|
||||||
|
let end = p.checked_add(head_len)?.min(bytes.len());
|
||||||
|
shapes.insert(0, split_named(&bytes[p..end]));
|
||||||
|
p = end;
|
||||||
|
for _ in 1..count {
|
||||||
|
let Some(code) = rd_u16(&mut p) else { break };
|
||||||
|
let Some(len) = rd_u16(&mut p) else { break };
|
||||||
|
let end = p.checked_add(len as usize)?.min(bytes.len());
|
||||||
|
shapes.insert(code, split_named(&bytes[p..end]));
|
||||||
|
p = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ShxKind::Bigfont => {
|
||||||
|
// u16 bytes per table entry (8), u16 table entries, u16 range
|
||||||
|
// count, ranges × (u16 lo, u16 hi) of lead bytes, then the entry
|
||||||
|
// table — a sparse hash table of (u16 code, u16 length, u32 file
|
||||||
|
// offset); empty slots are all zero. Shape 0 (name NUL above
|
||||||
|
// below modes) is the font header.
|
||||||
|
let entry_size = rd_u16(&mut p)? as usize;
|
||||||
|
let entries = rd_u16(&mut p)? as usize;
|
||||||
|
let ranges = rd_u16(&mut p)? as usize;
|
||||||
|
p = p.checked_add(ranges * 4)?;
|
||||||
|
if entry_size < 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for i in 0..entries {
|
||||||
|
let mut q = p.checked_add(i * entry_size)?;
|
||||||
|
let Some(code) = rd_u16(&mut q) else { break };
|
||||||
|
let Some(len) = rd_u16(&mut q) else { break };
|
||||||
|
let Some(off) = rd_u32(&mut q) else { break };
|
||||||
|
if off == 0 && len == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let start = off as usize;
|
||||||
|
let end = start.saturating_add(len as usize).min(bytes.len());
|
||||||
|
if start >= end {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
shapes.insert(code, split_named(&bytes[start..end]));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(ShxFile { shapes })
|
Some(ShxFile { kind, shapes })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when the file is an Asian big font (`AutoCAD-86 bigfont 1.0`).
|
||||||
|
pub fn is_bigfont(path: &str) -> bool {
|
||||||
|
load_file(path).is_some_and(|f| f.kind == ShxKind::Bigfont)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The double-byte legacy encodings a big font may be keyed by, most likely
|
||||||
|
/// first for this file name. The font itself does not say; try them in turn
|
||||||
|
/// and take the first code the font actually has.
|
||||||
|
fn bigfont_encodings(path: &str) -> [&'static encoding_rs::Encoding; 4] {
|
||||||
|
use encoding_rs::{BIG5, EUC_KR, GBK, SHIFT_JIS};
|
||||||
|
let name = path
|
||||||
|
.rsplit(['/', '\\'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or(path)
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
if name.contains("gb") || name.contains("hz") || name.contains("china") {
|
||||||
|
[GBK, BIG5, SHIFT_JIS, EUC_KR]
|
||||||
|
} else if name.contains("bigfont") || name.contains("extfont") || name.contains("jp") {
|
||||||
|
[SHIFT_JIS, BIG5, GBK, EUC_KR]
|
||||||
|
} else if name.contains("whg") || name.contains("kor") || name.contains("han") {
|
||||||
|
[EUC_KR, BIG5, GBK, SHIFT_JIS]
|
||||||
|
} else {
|
||||||
|
// `chineset.shx` and most Taiwanese fonts.
|
||||||
|
[BIG5, GBK, SHIFT_JIS, EUC_KR]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up `ch` in a big font: encode it to the font's double-byte code and
|
||||||
|
/// fetch that shape, normalised like [`font_glyph`]. `None` when the file is
|
||||||
|
/// not a big font or has no glyph for the character.
|
||||||
|
pub fn bigfont_glyph(path: &str, ch: char) -> Option<Arc<crate::scene::text::lff::Glyph>> {
|
||||||
|
let file = load_file(path)?;
|
||||||
|
if file.kind != ShxKind::Bigfont {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut buf = [0u8; 4];
|
||||||
|
let s = ch.encode_utf8(&mut buf);
|
||||||
|
for enc in bigfont_encodings(path) {
|
||||||
|
let (bytes, _, unmappable) = enc.encode(s);
|
||||||
|
if unmappable || bytes.len() != 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let code = u16::from_be_bytes([bytes[0], bytes[1]]);
|
||||||
|
if file.shapes.contains_key(&code) {
|
||||||
|
return font_glyph(path, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tessellate the shape named `name` (case-insensitive) from the SHX at
|
/// Tessellate the shape named `name` (case-insensitive) from the SHX at
|
||||||
@@ -410,8 +542,16 @@ fn interpret(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
7 => {
|
7 => {
|
||||||
let sub = *b.get(i).unwrap_or(&0) as u16;
|
let mut sub = *b.get(i).unwrap_or(&0) as u16;
|
||||||
i += 1;
|
i += 1;
|
||||||
|
// A big font references subshapes by a two-byte code:
|
||||||
|
// `7, 0, hi, lo`.
|
||||||
|
if file.kind == ShxKind::Bigfont && sub == 0 {
|
||||||
|
let hi = *b.get(i).unwrap_or(&0) as u16;
|
||||||
|
let lo = *b.get(i + 1).unwrap_or(&0) as u16;
|
||||||
|
i += 2;
|
||||||
|
sub = (hi << 8) | lo;
|
||||||
|
}
|
||||||
if !skipping && sub != 0 && sub != shape_number {
|
if !skipping && sub != 0 && sub != shape_number {
|
||||||
interpret(file, sub, st, cur, out, depth + 1);
|
interpret(file, sub, st, cur, out, depth + 1);
|
||||||
}
|
}
|
||||||
@@ -510,3 +650,91 @@ fn interpret(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Shape bytecode for a glyph: a horizontal 6-unit stroke, then end.
|
||||||
|
/// (`0x60` = length 6, direction 0.)
|
||||||
|
const BAR: [u8; 2] = [0x60, 0x00];
|
||||||
|
|
||||||
|
fn tmp(name: &str, bytes: &[u8]) -> String {
|
||||||
|
let dir = std::env::temp_dir().join(format!("ocs-shx-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join(name);
|
||||||
|
std::fs::write(&path, bytes).unwrap();
|
||||||
|
path.to_string_lossy().into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A one-glyph `bigfont 1.0` keyed by the Big5 code of `中` (0xA4A4),
|
||||||
|
/// laid out like `chineset.shx`: header, 8-byte entries in a sparse
|
||||||
|
/// table, font header shape 0 (`name NUL above below modes`).
|
||||||
|
fn synthetic_bigfont() -> Vec<u8> {
|
||||||
|
let mut b = b"AutoCAD-86 bigfont 1.0\r\n\x1a".to_vec();
|
||||||
|
let entries: u16 = 4;
|
||||||
|
b.extend_from_slice(&8u16.to_le_bytes());
|
||||||
|
b.extend_from_slice(&entries.to_le_bytes());
|
||||||
|
b.extend_from_slice(&1u16.to_le_bytes()); // one lead-byte range
|
||||||
|
b.extend_from_slice(&0x80u16.to_le_bytes());
|
||||||
|
b.extend_from_slice(&0xFFu16.to_le_bytes());
|
||||||
|
let table_at = b.len();
|
||||||
|
b.resize(table_at + 8 * entries as usize, 0);
|
||||||
|
let head_data: Vec<u8> = [b"BIG\x00".as_slice(), &[48u8, 0, 2]].concat();
|
||||||
|
let glyph_data: Vec<u8> = [b"\x00".as_slice(), &BAR].concat();
|
||||||
|
let head_off = b.len() as u32;
|
||||||
|
b.extend_from_slice(&head_data);
|
||||||
|
let glyph_off = b.len() as u32;
|
||||||
|
b.extend_from_slice(&glyph_data);
|
||||||
|
// Slot 0 = shape 0; slot 2 = the glyph (slots 1 and 3 stay empty).
|
||||||
|
let mut entry = |slot: usize, code: u16, len: u16, off: u32| {
|
||||||
|
let at = table_at + slot * 8;
|
||||||
|
b[at..at + 2].copy_from_slice(&code.to_le_bytes());
|
||||||
|
b[at + 2..at + 4].copy_from_slice(&len.to_le_bytes());
|
||||||
|
b[at + 4..at + 8].copy_from_slice(&off.to_le_bytes());
|
||||||
|
};
|
||||||
|
entry(0, 0, head_data.len() as u16, head_off);
|
||||||
|
entry(2, 0xA4A4, glyph_data.len() as u16, glyph_off);
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `unifont 1.0` with the font header and one glyph for `A` (U+0041).
|
||||||
|
fn synthetic_unifont() -> Vec<u8> {
|
||||||
|
let mut b = b"AutoCAD-86 unifont 1.0\r\n\x1a".to_vec();
|
||||||
|
b.extend_from_slice(&2u32.to_le_bytes()); // header + 1 glyph
|
||||||
|
let head: Vec<u8> = [b"UNI\x00".as_slice(), &[6u8, 2, 2, 0, 0, 0]].concat();
|
||||||
|
b.extend_from_slice(&(head.len() as u16).to_le_bytes());
|
||||||
|
b.extend_from_slice(&head);
|
||||||
|
let glyph: Vec<u8> = [b"\x00".as_slice(), &BAR].concat();
|
||||||
|
b.extend_from_slice(&0x41u16.to_le_bytes());
|
||||||
|
b.extend_from_slice(&(glyph.len() as u16).to_le_bytes());
|
||||||
|
b.extend_from_slice(&glyph);
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bigfont_glyph_is_found_by_big5_code_and_scaled_to_cap_height() {
|
||||||
|
let path = tmp("cjk.shx", &synthetic_bigfont());
|
||||||
|
assert!(is_bigfont(&path));
|
||||||
|
let (above, below) = font_metrics(&path).expect("font header");
|
||||||
|
assert_eq!((above, below), (48.0, 0.0));
|
||||||
|
// `中` → Big5 A4A4 → the glyph; `A` is not in a big font.
|
||||||
|
let g = bigfont_glyph(&path, '中').expect("glyph for 中");
|
||||||
|
// 6 font units at above = 48 → 6 * 9 / 48 = 1.125 text units.
|
||||||
|
assert!((g.advance - 1.125).abs() < 1e-4, "advance {}", g.advance);
|
||||||
|
assert_eq!(g.strokes.len(), 1);
|
||||||
|
assert!(bigfont_glyph(&path, 'A').is_none());
|
||||||
|
assert!(bigfont_glyph(&path, '國').is_none(), "unknown code must miss, not alias");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unifont_glyph_is_keyed_by_unicode() {
|
||||||
|
let path = tmp("uni.shx", &synthetic_unifont());
|
||||||
|
assert!(!is_bigfont(&path));
|
||||||
|
assert_eq!(font_metrics(&path), Some((6.0, 2.0)));
|
||||||
|
let g = font_glyph(&path, 'A' as u16).expect("glyph for A");
|
||||||
|
assert!((g.advance - 9.0).abs() < 1e-4, "6 units at above 6 → 9: {}", g.advance);
|
||||||
|
assert!(font_glyph(&path, 'B' as u16).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user