Files
OneCAD-Draft/src/scene/text/shx.rs
T
Hakan SevenandClaude Opus 5.5 c70a217e3f SHX: fractional arcs span their octants (#1638)
The end angle subtracted a whole octant less the end offset, so the arc
in the ISOCP 9 stopped about 45 degrees short and the glyph shrank to
0.68 of the cap height; clockwise arcs and n = 0 (a full circle) drew
nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:30:05 +03:00

792 lines
29 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Compiled AutoCAD shape-file (.SHX) reader + shape tessellator.
//
// Parses the classic `AutoCAD-86 shapes 1.0/1.1` container (the format SHAPE
// entities and complex-linetype shapes reference) and interprets a shape's
// bytecode into unit-space polylines. Fonts (`AutoCAD-86 unifont` etc.) are
// out of scope here — text keeps going through the LFF/TTF substitutes.
//
// Bytecode reference (each item, pen starts DOWN at the origin):
// 0 end of shape
// 1 / 2 pen down / pen up
// 3 n / 4 n divide / multiply the vector scale by n
// 5 / 6 push / pop the current position (4-deep stack)
// 7 n draw subshape n
// 8 dx dy signed byte displacement
// 9 … repeated signed (dx,dy) pairs until (0,0)
// 0xA r f octant arc: radius, flags (sign|start-octant|octant-count)
// 0xB s e hr r f fractional arc
// 0xC dx dy b bulge segment; 0xD … repeated until (0,0)
// 0xE vertical-text-only flag: skip the NEXT item horizontally
// else vector: high nibble = length, low nibble = one of 16
// predefined directions
//
// Results are cached per (file, shape-number); parse tables per file.
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
/// One shape's tessellation in unit space: polylines of XY points.
pub type ShapePolylines = Arc<Vec<Vec<[f64; 2]>>>;
/// The 16 SHX vector directions. Diagonal-ish entries deliberately use the
/// classic half-steps (not normalized) — lengths scale per direction exactly
/// as AutoCAD draws them.
const DIRS: [[f64; 2]; 16] = [
[1.0, 0.0],
[1.0, 0.5],
[1.0, 1.0],
[0.5, 1.0],
[0.0, 1.0],
[-0.5, 1.0],
[-1.0, 1.0],
[-1.0, 0.5],
[-1.0, 0.0],
[-1.0, -0.5],
[-1.0, -1.0],
[-0.5, -1.0],
[0.0, -1.0],
[0.5, -1.0],
[1.0, -1.0],
[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>)>,
}
fn file_cache() -> &'static Mutex<HashMap<String, Option<Arc<ShxFile>>>> {
static C: OnceLock<Mutex<HashMap<String, Option<Arc<ShxFile>>>>> = OnceLock::new();
C.get_or_init(|| Mutex::new(HashMap::new()))
}
fn shape_cache() -> &'static Mutex<HashMap<(String, u16), Option<(ShapePolylines, f64)>>> {
static C: OnceLock<Mutex<HashMap<(String, u16), Option<(ShapePolylines, f64)>>>> =
OnceLock::new();
C.get_or_init(|| Mutex::new(HashMap::new()))
}
fn load_file(path: &str) -> Option<Arc<ShxFile>> {
if let Ok(c) = file_cache().lock() {
if let Some(hit) = c.get(path) {
return hit.clone();
}
}
let parsed = parse_file(path).map(Arc::new);
if let Ok(mut c) = file_cache().lock() {
c.insert(path.to_string(), parsed.clone());
}
parsed
}
fn parse_file(path: &str) -> Option<ShxFile> {
let bytes = std::fs::read(path).ok()?;
// 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]);
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 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();
(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 { 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 code pages a big font's double-byte codes may be in, most likely
/// first, guessed from the file name. The codec maps a name to its encoding
/// (`ansi_950` Big5, `ansi_936` GBK, `ansi_932` Shift_JIS, `ansi_949` EUC-KR).
fn bigfont_code_pages(path: &str) -> [&'static str; 4] {
let name = path
.rsplit(['/', '\\'])
.next()
.unwrap_or(path)
.to_ascii_lowercase();
if name.contains("gb") || name.contains("hz") || name.contains("china") {
["ansi_936", "ansi_950", "ansi_932", "ansi_949"]
} else if name.contains("bigfont") || name.contains("extfont") || name.contains("jp") {
["ansi_932", "ansi_950", "ansi_936", "ansi_949"]
} else if name.contains("whg") || name.contains("kor") || name.contains("han") {
["ansi_949", "ansi_950", "ansi_936", "ansi_932"]
} else {
// `chineset.shx` and most Taiwanese fonts.
["ansi_950", "ansi_936", "ansi_932", "ansi_949"]
}
}
/// 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 page in bigfont_code_pages(path) {
let Some(enc) = codec::io::dxf::code_page::encoding_from_code_page(page) else {
continue;
};
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
/// `path` — the `.lin` catalog references linetype shapes by name.
pub fn shape_polylines_by_name(path: &str, name: &str) -> Option<ShapePolylines> {
let file = load_file(path)?;
let num = file
.shapes
.iter()
.find(|(_, (n, _))| n.eq_ignore_ascii_case(name))
.map(|(&num, _)| num)?;
shape_polylines(path, num)
}
/// Tessellate `shape_number` from the SHX at `path` into unit-space
/// polylines. `None` when the file/shape is missing or not a shapes file.
pub fn shape_polylines(path: &str, shape_number: u16) -> Option<ShapePolylines> {
shape_with_advance(path, shape_number).map(|(p, _)| p)
}
/// Like [`shape_polylines`], plus the pen's final X — a font glyph's advance.
pub fn shape_with_advance(path: &str, shape_number: u16) -> Option<(ShapePolylines, f64)> {
let key = (path.to_string(), shape_number);
if let Ok(c) = shape_cache().lock() {
if let Some(hit) = c.get(&key) {
return hit.clone();
}
}
let built = build_shape(path, shape_number);
if let Ok(mut c) = shape_cache().lock() {
c.insert(key, built.clone());
}
built
}
fn build_shape(path: &str, shape_number: u16) -> Option<(ShapePolylines, f64)> {
let file = load_file(path)?;
let mut out: Vec<Vec<[f64; 2]>> = Vec::new();
let mut cur: Vec<[f64; 2]> = vec![[0.0, 0.0]];
let mut st = InterpState {
x: 0.0,
y: 0.0,
scale: 1.0,
pen_down: true,
stack: Vec::new(),
};
interpret(&file, shape_number, &mut st, &mut cur, &mut out, 0);
if cur.len() > 1 {
out.push(cur);
}
let advance = st.x;
if out.is_empty() {
// A pen-up-only glyph (space) still carries its advance.
if advance.abs() < 1e-12 {
return None;
}
return Some((Arc::new(Vec::new()), advance));
}
Some((Arc::new(out), advance))
}
// ── SHX text fonts ────────────────────────────────────────────────────────────
//
// A classic SHX *font* is the same container with glyphs keyed by character
// code and shape #0 holding the font header: `above, below, modes, 0` (the
// cap height above the baseline and the descender depth).
/// Font metrics: (above, below). `None` when the file has no shape #0 header.
pub fn font_metrics(path: &str) -> Option<(f64, f64)> {
let file = load_file(path)?;
let (_, spec) = file.shapes.get(&0)?;
let above = *spec.first()? as f64;
let below = *spec.get(1)? as f64;
if above <= 0.0 {
return None;
}
Some((above, below))
}
fn glyph_cache(
) -> &'static Mutex<HashMap<(String, u16), Option<Arc<crate::scene::text::lff::Glyph>>>> {
static C: OnceLock<Mutex<HashMap<(String, u16), Option<Arc<crate::scene::text::lff::Glyph>>>>> =
OnceLock::new();
C.get_or_init(|| Mutex::new(HashMap::new()))
}
/// Look up character `code` as a font glyph, normalised so the font's
/// `above` (cap height) maps to 9 units — the scale every stroke-font
/// consumer in the text pipeline expects.
pub fn font_glyph(path: &str, code: u16) -> Option<Arc<crate::scene::text::lff::Glyph>> {
let key = (path.to_string(), code);
if let Ok(c) = glyph_cache().lock() {
if let Some(hit) = c.get(&key) {
return hit.clone();
}
}
let built = (|| {
let (above, _) = font_metrics(path)?;
let (polys, advance) = shape_with_advance(path, code)?;
let k = 9.0 / above;
let strokes: Vec<Vec<[f32; 2]>> = polys
.iter()
.filter(|p| p.len() > 1)
.map(|p| {
p.iter()
.map(|&[x, y]| [(x * k) as f32, (y * k) as f32])
.collect()
})
.collect();
Some(Arc::new(crate::scene::text::lff::Glyph {
strokes,
advance: (advance * k) as f32,
fill_tris: Vec::new(),
}))
})();
if let Ok(mut c) = glyph_cache().lock() {
c.insert(key, built.clone());
}
built
}
struct InterpState {
x: f64,
y: f64,
scale: f64,
pen_down: bool,
stack: Vec<(f64, f64)>,
}
/// Append a straight segment / move in interpreter space.
fn advance(
st: &mut InterpState,
cur: &mut Vec<[f64; 2]>,
out: &mut Vec<Vec<[f64; 2]>>,
dx: f64,
dy: f64,
) {
st.x += dx * st.scale;
st.y += dy * st.scale;
if st.pen_down {
cur.push([st.x, st.y]);
} else {
if cur.len() > 1 {
out.push(std::mem::take(cur));
}
cur.clear();
cur.push([st.x, st.y]);
}
}
/// Arc from the current point: `start`/`sweep` in radians over `r` (already
/// scale-applied). Sampled into short segments.
fn emit_arc(
st: &mut InterpState,
cur: &mut Vec<[f64; 2]>,
out: &mut Vec<Vec<[f64; 2]>>,
r: f64,
start: f64,
sweep: f64,
) {
if r <= 0.0 || sweep == 0.0 {
return;
}
let cx = st.x - r * start.cos();
let cy = st.y - r * start.sin();
let steps = ((sweep.abs() / std::f64::consts::TAU * 32.0).ceil() as usize).max(2);
for i in 1..=steps {
let a = start + sweep * i as f64 / steps as f64;
let nx = cx + r * a.cos();
let ny = cy + r * a.sin();
if st.pen_down {
cur.push([nx, ny]);
} else if i == steps {
if cur.len() > 1 {
out.push(std::mem::take(cur));
}
cur.clear();
cur.push([nx, ny]);
}
st.x = nx;
st.y = ny;
}
}
/// Bulge segment (like LWPOLYLINE bulge, in ±127 units).
fn emit_bulge(
st: &mut InterpState,
cur: &mut Vec<[f64; 2]>,
out: &mut Vec<Vec<[f64; 2]>>,
dx: f64,
dy: f64,
bulge: f64,
) {
let (sx, sy) = (st.x, st.y);
let (ex, ey) = (sx + dx * st.scale, sy + dy * st.scale);
let b = bulge / 127.0;
if b.abs() < 1e-6 || !st.pen_down {
advance(st, cur, out, dx, dy);
return;
}
// Bulge = tan(theta/4); sample the arc through the chord.
let theta = 4.0 * b.atan();
let chord = ((ex - sx).powi(2) + (ey - sy).powi(2)).sqrt();
if chord < 1e-12 {
return;
}
let r = chord / (2.0 * (theta / 2.0).sin().abs());
let mid_x = (sx + ex) * 0.5;
let mid_y = (sy + ey) * 0.5;
let d = (r * r - chord * chord / 4.0).max(0.0).sqrt() * if theta > 0.0 { 1.0 } else { -1.0 };
let (nx, ny) = (-(ey - sy) / chord, (ex - sx) / chord);
let (cx, cy) = (mid_x + nx * d, mid_y + ny * d);
let a0 = (sy - cy).atan2(sx - cx);
let steps = ((theta.abs() / std::f64::consts::TAU * 32.0).ceil() as usize).max(2);
for i in 1..=steps {
let a = a0 + theta * i as f64 / steps as f64;
let px = cx + r * a.cos();
let py = cy + r * a.sin();
cur.push([px, py]);
st.x = px;
st.y = py;
}
st.x = ex;
st.y = ey;
if let Some(last) = cur.last_mut() {
*last = [ex, ey];
}
}
fn interpret(
file: &ShxFile,
shape_number: u16,
st: &mut InterpState,
cur: &mut Vec<[f64; 2]>,
out: &mut Vec<Vec<[f64; 2]>>,
depth: usize,
) {
if depth > 4 {
return;
}
let Some((_, spec)) = file.shapes.get(&shape_number) else {
return;
};
let b = spec.as_slice();
let mut i = 0usize;
let mut skip_next = false;
while i < b.len() {
let code = b[i];
i += 1;
// 0xE: the next item applies to vertical text only — decode it to
// know its length, but discard its effect.
let skipping = skip_next;
skip_next = false;
match code {
0 => return,
1 => {
if !skipping {
st.pen_down = true;
}
}
2 => {
if !skipping {
st.pen_down = false;
}
}
3 | 4 => {
let n = *b.get(i).unwrap_or(&1) as f64;
i += 1;
if !skipping && n > 0.0 {
if code == 3 {
st.scale /= n;
} else {
st.scale *= n;
}
}
}
5 => {
if !skipping && st.stack.len() < 4 {
st.stack.push((st.x, st.y));
}
}
6 => {
if !skipping {
if let Some((px, py)) = st.stack.pop() {
st.x = px;
st.y = py;
if cur.len() > 1 {
out.push(std::mem::take(cur));
}
cur.clear();
cur.push([px, py]);
}
}
}
7 => {
let two_bytes = |i: usize| {
((*b.get(i).unwrap_or(&0) as u16) << 8) | *b.get(i + 1).unwrap_or(&0) as u16
};
// A unifont names its subshape by a two-byte code point
// (`7, hi, lo`); read as one byte, the low byte ran on as an
// opcode and the glyph came out mangled (ISOCP's `9`, #1556).
// A big font uses `7, 0, hi, lo`; a shape file one byte.
let sub = if file.kind == ShxKind::Unifont {
i += 2;
two_bytes(i - 2)
} else {
let sub = *b.get(i).unwrap_or(&0) as u16;
i += 1;
if file.kind == ShxKind::Bigfont && sub == 0 {
i += 2;
two_bytes(i - 2)
} else {
sub
}
};
if !skipping && sub != 0 && sub != shape_number {
interpret(file, sub, st, cur, out, depth + 1);
}
}
8 => {
let dx = *b.get(i).unwrap_or(&0) as i8 as f64;
let dy = *b.get(i + 1).unwrap_or(&0) as i8 as f64;
i += 2;
if !skipping {
advance(st, cur, out, dx, dy);
}
}
9 => loop {
let dx = *b.get(i).unwrap_or(&0) as i8 as f64;
let dy = *b.get(i + 1).unwrap_or(&0) as i8 as f64;
i += 2;
if dx == 0.0 && dy == 0.0 {
break;
}
if !skipping {
advance(st, cur, out, dx, dy);
}
},
0xA => {
let r = *b.get(i).unwrap_or(&0) as f64;
let f = *b.get(i + 1).unwrap_or(&0);
i += 2;
if !skipping {
let cw = f & 0x80 != 0;
let start_oct = ((f >> 4) & 7) as f64;
let mut n_oct = (f & 7) as f64;
if n_oct == 0.0 {
n_oct = 8.0;
}
let start = start_oct * std::f64::consts::FRAC_PI_4;
let sweep = n_oct * std::f64::consts::FRAC_PI_4 * if cw { -1.0 } else { 1.0 };
emit_arc(st, cur, out, r * st.scale, start, sweep);
}
}
0xB => {
let so = *b.get(i).unwrap_or(&0) as f64;
let eo = *b.get(i + 1).unwrap_or(&0) as f64;
let hr = *b.get(i + 2).unwrap_or(&0) as f64;
let r = *b.get(i + 3).unwrap_or(&0) as f64;
let f = *b.get(i + 4).unwrap_or(&0);
i += 5;
if !skipping {
let radius = (hr * 256.0 + r) * st.scale;
let cw = f & 0x80 != 0;
let start_oct = ((f >> 4) & 7) as f64;
let mut n_oct = (f & 7) as f64;
if n_oct == 0.0 {
n_oct = 8.0;
}
// The arc spans `n_oct` octants from `start_oct`, starting
// `so`/256 of an octant into the first one and ending `eo`/256
// into the last (0 = on its far boundary) (#1638).
let q = std::f64::consts::FRAC_PI_4;
let dir = if cw { -1.0 } else { 1.0 };
let start = (start_oct + dir * so / 256.0) * q;
let end = if eo == 0.0 {
(start_oct + dir * n_oct) * q
} else {
(start_oct + dir * (n_oct - 1.0 + eo / 256.0)) * q
};
let sweep = end - start;
if sweep * dir > 0.0 {
emit_arc(st, cur, out, radius, start, sweep);
}
}
}
0xC => {
let dx = *b.get(i).unwrap_or(&0) as i8 as f64;
let dy = *b.get(i + 1).unwrap_or(&0) as i8 as f64;
let bu = *b.get(i + 2).unwrap_or(&0) as i8 as f64;
i += 3;
if !skipping {
emit_bulge(st, cur, out, dx, dy, bu);
}
}
0xD => loop {
let dx = *b.get(i).unwrap_or(&0) as i8 as f64;
let dy = *b.get(i + 1).unwrap_or(&0) as i8 as f64;
i += 2;
if dx == 0.0 && dy == 0.0 {
break;
}
let bu = *b.get(i).unwrap_or(&0) as i8 as f64;
i += 1;
if !skipping {
emit_bulge(st, cur, out, dx, dy, bu);
}
},
0xE => {
skip_next = true;
}
v => {
let len = (v >> 4) as f64;
let dir = DIRS[(v & 0xF) as usize];
if !skipping {
advance(st, cur, out, dir[0] * len, dir[1] * len);
}
}
}
}
}
#[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());
}
/// The `9` program of the ISOCP fonts: a fractional arc into the stem,
/// then octant arcs. Its ink spans 36 font units, the cap height; the
/// fractional arc used to end short and drop the glyph to ~0.68 (#1638).
#[test]
fn fractional_arc_spans_its_octants() {
const NINE: [u8; 25] = [
0x02, 0x08, 0x0a, 0x02, 0x01, 0x0b, 0x6c, 0x00, 0x00, 0x12, 0x62, 0xb4, 0x0a, 0x08,
0x04, 0x4c, 0x0a, 0x08, 0x42, 0x80, 0x02, 0x08, 0x06, 0xee, 0x00,
];
let mut b = b"AutoCAD-86 unifont 1.0\r\n\x1a".to_vec();
b.extend_from_slice(&2u32.to_le_bytes());
let head: Vec<u8> = [b"UNI\x00".as_slice(), &[36u8, 8, 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(), &NINE].concat();
b.extend_from_slice(&('9' as u16).to_le_bytes());
b.extend_from_slice(&(glyph.len() as u16).to_le_bytes());
b.extend_from_slice(&glyph);
let path = tmp("nine.shx", &b);
let g = font_glyph(&path, '9' as u16).expect("glyph for 9");
let ys = g.strokes.iter().flatten().map(|p| p[1]);
let (lo, hi) = ys.fold((f32::MAX, f32::MIN), |(lo, hi), y| (lo.min(y), hi.max(y)));
// 36 font units at above = 36 → the 9-unit cap height.
assert!(((hi - lo) - 9.0).abs() < 0.05, "ink height {}", hi - lo);
}
}