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) <[email protected]>
This commit is contained in:
1 parent
0b522ceb86
commit
ff4981ffe6
6 files changed
+351
-35
No files matched your search
@@ -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 {
|
||||
font_name,
|
||||
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)
|
||||
.and_then(|p| p.parent());
|
||||
for style in doc.text_styles.iter() {
|
||||
let file = style.font_file.trim();
|
||||
if file.is_empty() || !file.to_ascii_lowercase().ends_with(".shx") {
|
||||
continue;
|
||||
}
|
||||
let resolved = crate::io::resolve_image_file(file, base).is_some()
|
||||
|| local_font_file(file).is_some();
|
||||
if resolved {
|
||||
continue;
|
||||
}
|
||||
let name = file.rsplit(['/', '\\']).next().unwrap_or(file).to_string();
|
||||
if !missing.iter().any(|m| m.eq_ignore_ascii_case(&name)) {
|
||||
missing.push(name);
|
||||
// The primary font and the big font (Asian double-byte glyphs) are
|
||||
// both plain .shx files the style depends on.
|
||||
for file in [style.font_file.trim(), style.big_font_file.trim()] {
|
||||
if file.is_empty() || !file.to_ascii_lowercase().ends_with(".shx") {
|
||||
continue;
|
||||
}
|
||||
let resolved = crate::io::resolve_image_file(file, base).is_some()
|
||||
|| local_font_file(file).is_some();
|
||||
if resolved {
|
||||
continue;
|
||||
}
|
||||
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());
|
||||
|
||||
@@ -47,12 +47,37 @@ pub enum Face {
|
||||
/// Space advance (9-unit).
|
||||
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 {
|
||||
/// Resolve a style's font name to a concrete face. Embedded stroke fonts
|
||||
/// take priority; only otherwise-unknown names try the system fonts.
|
||||
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
|
||||
// LFF substitutes only cover names we can't load.
|
||||
if font_name.to_ascii_lowercase().ends_with(".shx")
|
||||
@@ -87,6 +112,13 @@ impl Face {
|
||||
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
|
||||
/// 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
|
||||
@@ -103,6 +135,14 @@ impl Face {
|
||||
Face::Shx { path, .. } => shx::font_glyph(path, ch as u16)
|
||||
.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,
|
||||
// SHX advances already include the trailing gap by design.
|
||||
Face::Shx { .. } => 0.0,
|
||||
Face::WithBig { primary, .. } => primary.letter_spacing(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +163,7 @@ impl Face {
|
||||
match self {
|
||||
Face::Lff(f) => f.word_spacing,
|
||||
Face::Ttf { word, .. } | Face::Shx { word, .. } => *word,
|
||||
Face::WithBig { primary, .. } => primary.word_spacing(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +174,7 @@ impl Face {
|
||||
match self {
|
||||
Face::Ttf { family, .. } => Some(family),
|
||||
Face::Lff(_) | Face::Shx { .. } => None,
|
||||
Face::WithBig { primary, .. } => primary.ttf_family(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +185,7 @@ impl Face {
|
||||
Face::Lff(f) => f.line_spacing,
|
||||
Face::Ttf { .. } => 1.0,
|
||||
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.
|
||||
eprintln!("Font {} resolved to Lff (probably not installed)", test_name);
|
||||
}
|
||||
Face::Shx { .. } => {
|
||||
// A system TTF name never resolves to an SHX shape font.
|
||||
Face::Shx { .. } | Face::WithBig { .. } => {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[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],
|
||||
];
|
||||
|
||||
/// 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 {
|
||||
kind: ShxKind,
|
||||
/// shape number → (name, spec bytes)
|
||||
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> {
|
||||
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 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;
|
||||
}
|
||||
};
|
||||
let mut p = head_end + 1;
|
||||
let rd_u16 = |p: &mut usize| -> Option<u16> {
|
||||
let v = u16::from_le_bytes([*bytes.get(*p)?, *bytes.get(*p + 1)?]);
|
||||
*p += 2;
|
||||
Some(v)
|
||||
};
|
||||
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));
|
||||
}
|
||||
let mut shapes = HashMap::new();
|
||||
for (num, len) in dir {
|
||||
let end = p.checked_add(len)?.min(bytes.len());
|
||||
let blob = &bytes[p..end];
|
||||
p = end;
|
||||
// Name = leading NUL-terminated ASCII, spec follows.
|
||||
let rd_u32 = |p: &mut usize| -> Option<u32> {
|
||||
let v = u32::from_le_bytes([
|
||||
*bytes.get(*p)?,
|
||||
*bytes.get(*p + 1)?,
|
||||
*bytes.get(*p + 2)?,
|
||||
*bytes.get(*p + 3)?,
|
||||
]);
|
||||
*p += 4;
|
||||
Some(v)
|
||||
};
|
||||
// Glyph data = leading NUL-terminated ASCII name, spec follows.
|
||||
let split_named = |blob: &[u8]| -> (String, Vec<u8>) {
|
||||
let name_end = blob.iter().position(|&b| b == 0).unwrap_or(0);
|
||||
let name = String::from_utf8_lossy(&blob[..name_end]).into_owned();
|
||||
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
|
||||
@@ -410,8 +542,16 @@ fn interpret(
|
||||
}
|
||||
}
|
||||
7 => {
|
||||
let sub = *b.get(i).unwrap_or(&0) as u16;
|
||||
let mut sub = *b.get(i).unwrap_or(&0) as u16;
|
||||
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 {
|
||||
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