Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor db70717001 Use shared MTEXT lexical tokens 2026-11-15 16:06:13 +00:00
Dylan M. Taylor 173cdf6e8a Use shared MTEXT escape decoding 2026-11-15 16:00:46 +00:00
8 changed files with 692 additions and 372 deletions
Generated
+8 -7
View File
@@ -714,7 +714,6 @@ version = "0.21.0"
dependencies = [
"cadcraft-geom",
"craft-dxf",
"craft-shx",
"encoding_rs",
"harfrust 0.14.0",
"icu_segmenter",
@@ -1132,6 +1131,11 @@ dependencies = [
"libc",
]
[[package]]
name = "craft-codepage"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b1d4c67196ee7c6b668428c8d211e7d5171dff88#b1d4c67196ee7c6b668428c8d211e7d5171dff88"
[[package]]
name = "craft-docpath"
version = "0.1.0"
@@ -1149,9 +1153,11 @@ dependencies = [
[[package]]
name = "craft-dxf"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=006e4df225fca57e95ead0fb1f91920900798a99#006e4df225fca57e95ead0fb1f91920900798a99"
source = "git+https://github.com/storytold/craft-libs?rev=b1d4c67196ee7c6b668428c8d211e7d5171dff88#b1d4c67196ee7c6b668428c8d211e7d5171dff88"
dependencies = [
"craft-codepage",
"encoding_rs",
"kurbo",
"log",
"thiserror 2.0.21",
]
@@ -1194,11 +1200,6 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "craft-shx"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=3c5007339754def577b3c5274f523d96384a35c4#3c5007339754def577b3c5274f523d96384a35c4"
[[package]]
name = "craft-ui"
version = "0.1.0"
+1 -2
View File
@@ -14,9 +14,8 @@ repository = "https://github.com/storytold/cadcraft"
# internal
cadcraft-geom = { path = "crates/geom" }
# DXF tags: the shared reader and writer (moved from crates/dxf; craft-libs#791).
craft-dxf = { git = "https://github.com/storytold/craft-libs", rev = "006e4df225fca57e95ead0fb1f91920900798a99" }
craft-dxf = { git = "https://github.com/storytold/craft-libs", rev = "b1d4c67196ee7c6b668428c8d211e7d5171dff88" }
craft-dwg = { git = "https://github.com/storytold/craft-libs", rev = "7bb6c90076a0177d01cc4c482e7fd8dc9f31a8bf" }
craft-shx = { git = "https://github.com/storytold/craft-libs", rev = "3c5007339754def577b3c5274f523d96384a35c4" }
cadcraft-color = { path = "crates/color" }
cadcraft-doc = { path = "crates/doc" }
cadcraft-constraints = { path = "crates/constraints" }
-1
View File
@@ -11,7 +11,6 @@ workspace = true
[dependencies]
cadcraft-geom = { workspace = true }
craft-dxf = { workspace = true }
craft-shx = { workspace = true }
skrifa = { workspace = true }
harfrust = { workspace = true }
unicode-bidi = { workspace = true }
+86 -294
View File
@@ -12,79 +12,22 @@ pub fn plain_mtext(s: &str) -> String {
let s = unescape_codes(s);
let s = s.as_ref();
let mut out = String::with_capacity(s.len());
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while let Some(&c) = chars.get(i) {
match c {
'\\' => {
let code = chars.get(i + 1).copied().unwrap_or(' ');
match code {
'P' => {
out.push('\n');
i += 2;
}
'N' => {
out.push('\n');
i += 2;
}
'~' => {
out.push(' ');
i += 2;
}
'\\' | '{' | '}' => {
out.push(code);
i += 2;
}
'S' => {
// \Snum^den; \Snum/den; \Snum#den;
let mut j = i + 2;
let mut stack = String::new();
while let Some(&ch) = chars.get(j) {
if ch == ';' {
break;
}
stack.push(match ch {
'^' | '#' => '/',
c => c,
});
j += 1;
}
// "6\S1/2;" reads as "6 1/2".
if out.chars().last().is_some_and(|c| c.is_ascii_digit()) && stack.starts_with(|c: char| c.is_ascii_digit()) {
out.push(' ');
}
out.push_str(stack.trim_end_matches('/'));
i = j + 1;
}
'L' | 'l' | 'O' | 'o' | 'K' | 'k' => i += 2,
// `\U+XXXX`: a character by its Unicode code, as the layout reads it.
'U' if crate::unicode_escape(&chars, i).is_some() => {
if let Some(u) = crate::unicode_escape(&chars, i) {
out.push(u);
}
i += 7;
}
// Codes with an argument terminated by ';' (\f, \F, \H, \W, \Q, \T, \A, \C, \c, \p).
_ if takes_arg(code) => {
let mut j = i + 2;
while let Some(&ch) = chars.get(j) {
if ch == ';' {
break;
}
j += 1;
}
i = j + 1;
}
// An unknown code has no argument: drop the backslash and the code only.
_ => i += 2,
use craft_dxf::mtext_tokens::{StackMode, Token, Tokens};
for token in Tokens::new(s, StackMode::Plain) {
match token {
Token::Control('P' | 'N') => out.push('\n'),
Token::Control('~') => out.push(' '),
Token::Control(c @ ('\\' | '{' | '}')) => out.push(c),
Token::Stack(raw) => {
let stack: String = raw.chars().map(|c| if matches!(c, '^' | '#') { '/' } else { c }).collect();
if out.chars().last().is_some_and(|c| c.is_ascii_digit()) && stack.starts_with(|c: char| c.is_ascii_digit()) {
out.push(' ');
}
out.push_str(stack.trim_end_matches('/'));
}
'{' | '}' => i += 1,
'\r' => i += 1,
_ => {
out.push(c);
i += 1;
}
Token::Char('\r') => {}
Token::Char(c) => out.push(c),
_ => {}
}
}
out
@@ -225,11 +168,8 @@ const MAX_ATOMS: usize = 200_000;
/// Scale of stacked text relative to the surrounding text.
const STACK_SCALE: f64 = 0.7;
/// Codes that take an argument terminated by `;` (besides `\S`, which has its own scan). Any
/// other code stands alone, so a missing `;` must not swallow the text after it.
pub(crate) fn takes_arg(code: char) -> bool {
matches!(code, 'f' | 'F' | 'H' | 'W' | 'Q' | 'T' | 'A' | 'C' | 'c' | 'p')
}
// Shared DXF escape handling and parameterized-command classification.
pub(crate) use craft_dxf::mtext::unescape_codes;
/// The line-spacing factor to lay out with: AutoCAD's 0.25..=4 range, 1 for a missing or
/// non-finite value.
@@ -237,90 +177,6 @@ pub(crate) fn sane_line_spacing(v: f64) -> f64 {
if v.is_finite() && v > 0.0 { v.clamp(0.25, 4.0) } else { 1.0 }
}
/// Four hex digits at `chars[at..at + 4]`.
fn hex4(chars: &[char], at: usize) -> Option<u32> {
let digits = chars.get(at..at.checked_add(4)?)?;
digits.iter().try_fold(0u32, |acc, c| Some(acc * 16 + c.to_digit(16)?))
}
/// Push a decoded character so that the tokenizers read it as text, never as markup.
fn push_literal(out: &mut String, c: char) {
if matches!(c, '\\' | '{' | '}') {
out.push('\\');
}
out.push(c);
}
/// Decode `\U+hhhh` (one UTF-16 unit; a surrogate pair gives one character, a lone surrogate
/// U+FFFD) and `\M+nhhhh` (double-byte East Asian characters) to the characters they stand
/// for. Malformed escapes are left for the tokenizers, which drop the backslash and the code.
/// An escaped backslash (`\\U+0041`) is not an escape.
pub(crate) fn unescape_codes(s: &str) -> std::borrow::Cow<'_, str> {
if !s.contains("\\U+") && !s.contains("\\M+") {
return std::borrow::Cow::Borrowed(s);
}
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while let Some(&c) = chars.get(i) {
if c != '\\' {
out.push(c);
i += 1;
continue;
}
let (n, plus) = (chars.get(i + 1).copied(), chars.get(i + 2).copied());
if n == Some('U') && plus == Some('+') {
if let Some(unit) = hex4(&chars, i + 3) {
i += 7;
let low = |at: usize| {
(chars.get(at) == Some(&'\\') && chars.get(at + 1) == Some(&'U') && chars.get(at + 2) == Some(&'+'))
.then(|| hex4(&chars, at + 3))
.flatten()
.filter(|u| (0xDC00..0xE000).contains(u))
};
let ch = if (0xD800..0xDC00).contains(&unit) {
match low(i) {
Some(lo) => {
i += 7;
char::from_u32(0x10000 + ((unit - 0xD800) << 10) + (lo - 0xDC00))
}
None => None,
}
} else {
char::from_u32(unit)
};
push_literal(&mut out, ch.unwrap_or('\u{fffd}'));
continue;
}
} else if n == Some('M') && plus == Some('+') && chars.get(i + 3).is_some_and(char::is_ascii_digit) && hex4(&chars, i + 4).is_some() {
push_literal(&mut out, craft_dxf::mif_escape_at(&chars, i).unwrap_or('\u{fffd}'));
i += 8;
continue;
}
// Any other backslash pair stays as it is (and an escaped backslash stays one).
out.push('\\');
i += 1;
if let Some(&next) = chars.get(i) {
out.push(next);
i += 1;
}
}
std::borrow::Cow::Owned(out)
}
fn arg(chars: &[char], from: usize) -> (String, usize) {
let mut j = from;
let mut a = String::new();
while let Some(&ch) = chars.get(j) {
if ch == ';' {
return (a, j + 1);
}
a.push(ch);
j += 1;
}
(a, j)
}
fn num(a: &str) -> Option<f64> {
a.trim().parse::<f64>().ok().filter(|v| v.is_finite())
}
@@ -358,7 +214,7 @@ fn font_switch(a: &str) -> Option<TextFont> {
/// Parse MTEXT contents into words, spaces and paragraph breaks with their formats.
fn parse(contents: &str, base: Fmt) -> (Vec<Atom>, Vec<Fmt>) {
let chars: Vec<char> = contents.chars().collect();
use craft_dxf::mtext_tokens::{StackMode, Token, Tokens, split_stack};
let h_min = base.height * 1e-3;
let h_max = if (base.height * 1e3).is_finite() { base.height * 1e3 } else { f64::MAX };
let mut fmts = vec![base];
@@ -398,158 +254,94 @@ fn parse(contents: &str, base: Fmt) -> (Vec<Atom>, Vec<Fmt>) {
*cur = fmts.len() - 1;
}
};
let mut i = 0;
while let Some(&c) = chars.get(i) {
for token in Tokens::new(contents, StackMode::Structured) {
if atoms.len() > MAX_ATOMS {
break;
}
match c {
'\\' => {
let code = chars.get(i + 1).copied().unwrap_or(' ');
i += 2;
match code {
'P' | 'N' => {
end_word(&mut atoms, &mut word);
atoms.push(Atom::Para(cur));
}
'~' => push_char(&mut word, '\u{a0}', cur),
'\\' | '{' | '}' => push_char(&mut word, code, cur),
'L' => derive(&mut fmts, &mut cur, &|f| f.under = true),
'l' => derive(&mut fmts, &mut cur, &|f| f.under = false),
'O' => derive(&mut fmts, &mut cur, &|f| f.over = true),
'o' => derive(&mut fmts, &mut cur, &|f| f.over = false),
'K' | 'k' => {}
// `\U+XXXX`: a character by its Unicode code.
'U' if i >= 2 && crate::unicode_escape(&chars, i - 2).is_some() => {
if let Some(u) = crate::unicode_escape(&chars, i - 2) {
push_char(&mut word, u, cur);
}
i += 5;
}
'S' => {
let mut j = i;
let (mut top, mut bottom, mut sep) = (String::new(), String::new(), None);
while let Some(&ch) = chars.get(j) {
if ch == ';' {
break;
}
if ch == '\\' {
if let Some(&n) = chars.get(j + 1) {
if sep.is_none() { top.push(n) } else { bottom.push(n) }
}
j += 2;
continue;
}
if sep.is_none() && matches!(ch, '/' | '#' | '^') {
sep = Some(ch);
} else if sep.is_none() {
top.push(ch);
} else {
bottom.push(ch);
}
j += 1;
}
i = j + 1;
word.push((Seg::Stack(top, bottom, sep.unwrap_or('/')), cur));
}
'f' | 'F' => {
let (a, n) = arg(&chars, i);
i = n;
if let Some(font) = font_switch(&a) {
derive(&mut fmts, &mut cur, &|f| f.font = font.clone());
}
}
'H' => {
let (a, n) = arg(&chars, i);
i = n;
let (rel, v) = match a.trim().strip_suffix(['x', 'X']) {
Some(r) => (true, num(r)),
None => (false, num(&a)),
};
if let Some(v) = v.filter(|v| *v > 0.0 && *v < 1e6) {
// Relative heights compound; keep the result within 1e-3..1e3 times the
// MTEXT's own height, never infinite.
derive(&mut fmts, &mut cur, &|f| f.height = if rel { (f.height * v).min(h_max).max(h_min) } else { v });
}
}
'W' => {
let (a, n) = arg(&chars, i);
i = n;
// `\W2x;` multiplies the current factor (like `\H2x;`); `\W2;` sets it.
let (rel, v) = match a.trim().strip_suffix(['x', 'X']) {
Some(r) => (true, num(r)),
None => (false, num(&a)),
};
if let Some(v) = v.filter(|v| *v > 0.01 && *v < 100.0) {
// Relative factors compound; keep the result within the absolute range.
derive(&mut fmts, &mut cur, &|f| f.wf = if rel { (f.wf * v).clamp(0.01, 100.0) } else { v });
}
}
'Q' => {
let (a, n) = arg(&chars, i);
i = n;
if let Some(v) = num(&a).filter(|v| v.abs() < 85.0) {
derive(&mut fmts, &mut cur, &|f| f.oblique = v.to_radians());
}
}
'C' => {
let (a, n) = arg(&chars, i);
i = n;
if let Ok(v) = a.trim().parse::<u16>() {
let col = if v == 0 || v >= 256 { None } else { Some(MTextColor::Aci(v)) };
derive(&mut fmts, &mut cur, &|f| f.color = col);
}
}
'c' => {
let (a, n) = arg(&chars, i);
i = n;
if let Ok(v) = a.trim().parse::<u32>() {
derive(&mut fmts, &mut cur, &|f| f.color = Some(MTextColor::Rgb(v & 0xFF_FFFF)));
}
}
'A' => {
let (a, n) = arg(&chars, i);
i = n;
if let Ok(v @ 0..=2) = a.trim().parse::<u8>() {
derive(&mut fmts, &mut cur, &|f| f.valign = v);
}
}
_ if takes_arg(code) => {
// \T and \p: argument up to ';'.
let (_, n) = arg(&chars, i);
i = n;
}
// An unknown code has no argument: only the backslash and the code are dropped.
_ => {}
match token {
Token::Control('P' | 'N') => {
end_word(&mut atoms, &mut word);
atoms.push(Atom::Para(cur));
}
Token::Control('~') => push_char(&mut word, '\u{a0}', cur),
Token::Control(c @ ('\\' | '{' | '}')) => push_char(&mut word, c, cur),
Token::Control('L') => derive(&mut fmts, &mut cur, &|f| f.under = true),
Token::Control('l') => derive(&mut fmts, &mut cur, &|f| f.under = false),
Token::Control('O') => derive(&mut fmts, &mut cur, &|f| f.over = true),
Token::Control('o') => derive(&mut fmts, &mut cur, &|f| f.over = false),
Token::Stack(raw) => {
let (top, bottom, sep) = split_stack(raw);
word.push((Seg::Stack(top, bottom, sep.unwrap_or('/')), cur));
}
Token::Argument('f' | 'F', a) => {
if let Some(font) = font_switch(a) {
derive(&mut fmts, &mut cur, &|f| f.font = font.clone());
}
}
'{' => {
Token::Argument('H', a) => {
let (rel, v) = match a.trim().strip_suffix(['x', 'X']) {
Some(r) => (true, num(r)),
None => (false, num(a)),
};
if let Some(v) = v.filter(|v| *v > 0.0 && *v < 1e6) {
// Relative heights compound; keep the result within 1e-3..1e3 times the
// MTEXT's own height, never infinite.
derive(&mut fmts, &mut cur, &|f| f.height = if rel { (f.height * v).min(h_max).max(h_min) } else { v });
}
}
Token::Argument('W', a) => {
// `\W2x;` multiplies the current factor (like `\H2x;`); `\W2;` sets it.
let (rel, v) = match a.trim().strip_suffix(['x', 'X']) {
Some(r) => (true, num(r)),
None => (false, num(a)),
};
if let Some(v) = v.filter(|v| *v > 0.01 && *v < 100.0) {
// Relative factors compound; keep the result within the absolute range.
derive(&mut fmts, &mut cur, &|f| f.wf = if rel { (f.wf * v).clamp(0.01, 100.0) } else { v });
}
}
Token::Argument('Q', a) => {
if let Some(v) = num(a).filter(|v| v.abs() < 85.0) {
derive(&mut fmts, &mut cur, &|f| f.oblique = v.to_radians());
}
}
Token::Argument('C', a) => {
if let Ok(v) = a.trim().parse::<u16>() {
let col = if v == 0 || v >= 256 { None } else { Some(MTextColor::Aci(v)) };
derive(&mut fmts, &mut cur, &|f| f.color = col);
}
}
Token::Argument('c', a) => {
if let Ok(v) = a.trim().parse::<u32>() {
derive(&mut fmts, &mut cur, &|f| f.color = Some(MTextColor::Rgb(v & 0xFF_FFFF)));
}
}
Token::Argument('A', a) => {
if let Ok(v @ 0..=2) = a.trim().parse::<u8>() {
derive(&mut fmts, &mut cur, &|f| f.valign = v);
}
}
Token::OpenGroup => {
if stack.len() < 256 {
stack.push(cur);
}
i += 1;
}
'}' => {
Token::CloseGroup => {
if let Some(f) = stack.pop() {
cur = f;
}
i += 1;
}
'\r' => i += 1,
'\n' => {
Token::Char('\r') => {}
Token::Char('\n') => {
end_word(&mut atoms, &mut word);
atoms.push(Atom::Para(cur));
i += 1;
}
' ' => {
Token::Char(' ') => {
end_word(&mut atoms, &mut word);
atoms.push(Atom::Space(cur));
i += 1;
}
_ => {
push_char(&mut word, c, cur);
i += 1;
}
Token::Char(c) => push_char(&mut word, c, cur),
_ => {}
}
}
end_word(&mut atoms, &mut word);
+15 -45
View File
@@ -7,7 +7,7 @@
use cadcraft_geom::{Bounds2, Vec2};
use super::{Atom, Fmt, MTextLayout, MTextParams, MTextPiece, Seg, arg, num, parse, seg_plain, seg_shape, seg_width, split_thai};
use super::{Atom, Fmt, MTextLayout, MTextParams, MTextPiece, Seg, num, parse, seg_plain, seg_shape, seg_width, split_thai};
/// Columns an MTEXT's text flows through, each as wide as the MTEXT's width.
#[derive(Clone, Debug, Default, PartialEq)]
@@ -265,58 +265,28 @@ pub struct Paragraphs {
impl Paragraphs {
pub fn scan(contents: &str) -> Paragraphs {
let contents = super::unescape_codes(contents);
let chars: Vec<char> = contents.chars().collect();
use craft_dxf::mtext_tokens::{StackMode, Token, Tokens};
let mut out = Paragraphs { states: vec![ParaProps::default()], ..Default::default() };
let mut cur = 0usize;
let mut i = 0;
let brk = |out: &mut Paragraphs, cur: usize, column: bool| {
out.para.push(cur);
out.column_break.push(column);
};
while let Some(&c) = chars.get(i) {
match c {
'\\' => {
let code = chars.get(i + 1).copied().unwrap_or(' ');
i += 2;
match code {
'P' => brk(&mut out, cur, false),
'N' => brk(&mut out, cur, true),
'~' | '\\' | '{' | '}' | 'L' | 'l' | 'O' | 'o' | 'K' | 'k' => {}
'S' => {
let mut j = i;
while let Some(&ch) = chars.get(j) {
if ch == ';' {
break;
}
j += if ch == '\\' { 2 } else { 1 };
}
i = j + 1;
}
'p' => {
let (a, n) = arg(&chars, i);
i = n;
if out.states.len() < MAX_STATES
&& let Some(st) = out.states.get(cur)
{
let mut st = st.clone();
st.apply(&a);
out.states.push(st);
cur = out.states.len() - 1;
}
}
_ if super::takes_arg(code) => {
let (_, n) = arg(&chars, i);
i = n;
}
// An unknown code has no argument: only the backslash and the code go.
_ => {}
for token in Tokens::new(&contents, StackMode::Structured) {
match token {
Token::Control('P') | Token::Char('\n') => brk(&mut out, cur, false),
Token::Control('N') => brk(&mut out, cur, true),
Token::Argument('p', a) => {
if out.states.len() < MAX_STATES
&& let Some(st) = out.states.get(cur)
{
let mut st = st.clone();
st.apply(a);
out.states.push(st);
cur = out.states.len() - 1;
}
}
'\n' => {
brk(&mut out, cur, false);
i += 1;
}
_ => i += 1,
_ => {}
}
}
// A `\p` anywhere in a paragraph applies to all of it: the break pushed the state at
+313
View File
@@ -0,0 +1,313 @@
//! Shape source (`.shp`) to a compiled SHX file, as COMPILE does.
//!
//! A source is a list of definitions: a header line `*number,defbytes,NAME` followed by its
//! specification bytes (on as many lines as needed, ending with 0). Numbers are decimal, or
//! hexadecimal after a leading `0` (`012`, `08cfb`); parentheses only group; `;` starts a
//! comment. A big font starts with `*BIGFONT nchars,nranges,first,last,…` and a Unicode font
//! with `*UNIFONT,6,name` followed by `above,below,modes,encoding,type,0`. Shape 0 is a text
//! font's record (`above,below,modes,0`).
//!
//! Operands are encoded by what they are: a subshape number takes two bytes in a Unicode font
//! (and after `7,0` in a big font), displacements and bulges are two's complement, and an arc's
//! direction byte (`-043`) keeps its sign in the high bit. `defbytes` is not checked: the
//! program's own length is written.
use super::ShxError;
use super::parse::{BIGFONT, SHAPES_10, UNIFONT};
/// Largest source compiled.
const MAX_SOURCE: usize = 8 << 20;
#[derive(Clone, Copy, PartialEq)]
enum Kind {
Shapes,
Big,
Uni,
}
struct Shape {
number: u16,
name: String,
program: Vec<u8>,
}
/// A number token: value (sign included).
fn number(tok: &str, line: usize) -> Result<i64, ShxError> {
let t: String = tok.chars().filter(|c| !matches!(c, '(' | ')') && !c.is_whitespace()).collect();
let (neg, digits) = match t.strip_prefix('-') {
Some(d) => (true, d),
None => (false, t.strip_prefix('+').unwrap_or(&t)),
};
let err = || ShxError::Source(line, format!("not a number: {tok:?}"));
let v = if digits.len() > 1 && digits.starts_with('0') {
i64::from_str_radix(digits.get(1..).unwrap_or_default(), 16).map_err(|_| err())?
} else {
digits.parse::<i64>().map_err(|_| err())?
};
if v > 0xFFFF {
return Err(err());
}
Ok(if neg { -v } else { v })
}
/// The comma-separated numbers of a line (comment removed, blank fields skipped).
fn numbers(text: &str, line: usize) -> Result<Vec<i64>, ShxError> {
text.split(',').map(str::trim).filter(|t| !t.is_empty() && *t != "(" && *t != ")").map(|t| number(t, line)).collect()
}
/// The spec-byte encoder: walks the tokens as the reader walks the bytes.
struct Enc<'a> {
toks: &'a [i64],
i: usize,
out: Vec<u8>,
kind: Kind,
line: usize,
}
impl Enc<'_> {
fn next(&mut self) -> Result<i64, ShxError> {
let v = self.toks.get(self.i).copied().ok_or_else(|| ShxError::Source(self.line, "the shape ends inside a command".into()))?;
self.i += 1;
Ok(v)
}
fn byte(&mut self) -> Result<(), ShxError> {
let v = self.next()?;
let b = u8::try_from(v).map_err(|_| ShxError::Source(self.line, format!("{v} is not a byte")))?;
self.out.push(b);
Ok(())
}
/// Two's complement −128..127.
fn signed(&mut self) -> Result<i64, ShxError> {
let v = self.next()?;
let b = i8::try_from(v).map_err(|_| ShxError::Source(self.line, format!("{v} is not −128..127")))?;
self.out.extend(b.to_le_bytes());
Ok(v)
}
/// Sign in the high bit (an arc's direction byte).
fn sign_magnitude(&mut self) -> Result<(), ShxError> {
let v = self.next()?;
let m = u8::try_from(v.unsigned_abs())
.ok()
.filter(|m| *m < 0x80)
.ok_or_else(|| ShxError::Source(self.line, format!("{v} is not an octant byte")))?;
self.out.push(if v < 0 { m | 0x80 } else { m });
Ok(())
}
fn word(&mut self) -> Result<(), ShxError> {
let v = self.next()?;
let w = u16::try_from(v).map_err(|_| ShxError::Source(self.line, format!("{v} is not a shape number")))?;
self.out.extend(w.to_be_bytes());
Ok(())
}
fn program(mut self) -> Result<Vec<u8>, ShxError> {
while self.i < self.toks.len() {
let code = self.next()?;
let c = u8::try_from(code).map_err(|_| ShxError::Source(self.line, format!("{code} is not a code")))?;
self.out.push(c);
match c {
0 => break,
3 | 4 => self.byte()?,
7 => match self.kind {
Kind::Uni => self.word()?,
Kind::Big => {
let first = self.toks.get(self.i).copied().unwrap_or(0);
if first == 0 {
self.byte()?;
self.word()?;
for _ in 0..4 {
self.byte()?;
}
} else {
self.byte()?;
}
}
Kind::Shapes => self.byte()?,
},
8 => {
self.signed()?;
self.signed()?;
}
9 => loop {
let (x, y) = (self.signed()?, self.signed()?);
if x == 0 && y == 0 {
break;
}
},
10 => {
self.byte()?;
self.sign_magnitude()?;
}
11 => {
for _ in 0..4 {
self.byte()?;
}
self.sign_magnitude()?;
}
12 => {
for _ in 0..3 {
self.signed()?;
}
}
13 => loop {
let (x, y) = (self.signed()?, self.signed()?);
if x == 0 && y == 0 {
break;
}
self.signed()?;
},
_ => {}
}
}
if self.out.last() != Some(&0) {
self.out.push(0);
}
Ok(self.out)
}
}
/// Compile shape source into an SHX file (a shape file or text font, a big font, or a Unicode
/// font, as its first definition says).
pub fn compile(source: &str) -> Result<Vec<u8>, ShxError> {
if source.len() > MAX_SOURCE {
return Err(ShxError::TooLarge);
}
let mut kind = Kind::Shapes;
let mut ranges: Vec<(u16, u16)> = Vec::new();
let mut uni_info: Option<(String, Vec<u8>)> = None;
// Definitions: header (number, name, line) and the numbers that follow it.
let mut defs: Vec<(i64, String, usize, Vec<i64>)> = Vec::new();
let mut uni_header_pending = false;
for (no, raw) in source.lines().enumerate() {
let line = no + 1;
let text = raw.split(';').next().unwrap_or_default().trim();
if text.is_empty() {
continue;
}
if let Some(head) = text.strip_prefix('*') {
let upper = head.trim_start().to_ascii_uppercase();
if let Some(rest) = upper.strip_prefix("BIGFONT") {
kind = Kind::Big;
let v = numbers(rest, line)?;
let n = usize::try_from(v.get(1).copied().unwrap_or(0)).unwrap_or(0);
for k in 0..n {
let lo = v.get(2 + 2 * k).copied().unwrap_or(0);
let hi = v.get(3 + 2 * k).copied().unwrap_or(lo);
let to = |x: i64| u16::try_from(x).map_err(|_| ShxError::Source(line, "bad escape range".into()));
ranges.push((to(lo)?, to(hi)?));
}
continue;
}
if upper.starts_with("UNIFONT") {
kind = Kind::Uni;
let name = head.splitn(3, ',').nth(2).unwrap_or_default().trim().to_string();
uni_info = Some((name, Vec::new()));
uni_header_pending = true;
continue;
}
uni_header_pending = false;
let mut parts = head.splitn(3, ',');
let num = number(parts.next().unwrap_or_default().trim(), line)?;
let _defbytes = parts.next();
let name = parts.next().unwrap_or_default().trim().to_string();
defs.push((num, name, line, Vec::new()));
continue;
}
let v = numbers(text, line)?;
if uni_header_pending {
if let Some((_, info)) = uni_info.as_mut() {
for x in v {
info.push(u8::try_from(x).map_err(|_| ShxError::Source(line, "bad font record".into()))?);
}
}
continue;
}
match defs.last_mut() {
Some(d) => d.3.extend(v),
None => return Err(ShxError::Source(line, "specification bytes before any *number,defbytes,NAME line".into())),
}
}
let mut shapes: Vec<Shape> = Vec::new();
for (num, name, line, toks) in defs {
let number = u16::try_from(num).map_err(|_| ShxError::Source(line, format!("shape number {num} out of range")))?;
let program = if number == 0 {
// The font record: plain bytes.
toks.iter().map(|x| u8::try_from(*x).map_err(|_| ShxError::Source(line, "bad font record".into()))).collect::<Result<Vec<u8>, _>>()?
} else {
Enc { toks: &toks, i: 0, out: Vec::new(), kind, line }.program()?
};
if shapes.iter().any(|s| s.number == number) {
return Err(ShxError::Source(line, format!("shape {number} is defined twice")));
}
shapes.push(Shape { number, name, program });
}
if shapes.is_empty() {
return Err(ShxError::Empty);
}
shapes.sort_by_key(|s| s.number);
let record = |s: &Shape| -> Result<Vec<u8>, ShxError> {
let mut r: Vec<u8> = s.name.bytes().filter(|b| *b != 0).collect();
r.push(0);
r.extend(&s.program);
if r.len() > usize::from(u16::MAX) { Err(ShxError::TooLarge) } else { Ok(r) }
};
let count = u16::try_from(shapes.len()).map_err(|_| ShxError::TooLarge)?;
let len16 = |r: &[u8]| u16::try_from(r.len()).unwrap_or(u16::MAX);
let mut out = Vec::new();
match kind {
Kind::Shapes => {
out.extend(SHAPES_10);
out.extend(shapes.first().map_or(0, |s| s.number).to_le_bytes());
out.extend(shapes.last().map_or(0, |s| s.number).to_le_bytes());
out.extend(count.to_le_bytes());
let recs: Vec<Vec<u8>> = shapes.iter().map(record).collect::<Result<_, _>>()?;
for (s, r) in shapes.iter().zip(&recs) {
out.extend(s.number.to_le_bytes());
out.extend(len16(r).to_le_bytes());
}
for r in &recs {
out.extend(r);
}
out.extend(b"EOF");
}
Kind::Big => {
out.extend(BIGFONT);
let nranges = u16::try_from(ranges.len()).map_err(|_| ShxError::TooLarge)?;
out.extend(count.to_le_bytes());
out.extend(count.to_le_bytes());
out.extend(nranges.to_le_bytes());
for (lo, hi) in &ranges {
out.extend(lo.to_le_bytes());
out.extend(hi.to_le_bytes());
}
let recs: Vec<Vec<u8>> = shapes.iter().map(record).collect::<Result<_, _>>()?;
let mut off = out.len() + 8 * recs.len();
for (s, r) in shapes.iter().zip(&recs) {
out.extend(s.number.to_le_bytes());
out.extend(len16(r).to_le_bytes());
out.extend(u32::try_from(off).map_err(|_| ShxError::TooLarge)?.to_le_bytes());
off += r.len();
}
for r in &recs {
out.extend(r);
}
}
Kind::Uni => {
out.extend(UNIFONT);
let (name, info) = uni_info.unwrap_or_default();
out.extend((u32::from(count) + 1).to_le_bytes());
let mut rec: Vec<u8> = name.bytes().filter(|b| *b != 0).collect();
rec.push(0);
rec.extend(&info);
out.extend(len16(&rec).to_le_bytes());
out.extend(&rec);
for s in &shapes {
let r = record(s)?;
out.extend(s.number.to_le_bytes());
out.extend(len16(&r).to_le_bytes());
out.extend(r);
}
}
}
Ok(out)
}
+49 -2
View File
@@ -26,6 +26,7 @@
//! above, below, modes, encoding, embedding, 0) and then each shape as (number, length,
//! record).
mod compile;
mod parse;
mod program;
mod table;
@@ -38,8 +39,7 @@ use std::sync::{Arc, PoisonError, RwLock};
use cadcraft_geom::Vec2;
use craft_shx::Record;
pub use craft_shx::{ShxError, ShxKind, compile};
pub use compile::compile;
pub use table::{
FontProvider, add_drawing_dir, clear_registered, drawing_dirs, find, font_map, generation, installed, register, search_path, set_font_dirs,
set_font_map, set_provider, set_search_path,
@@ -47,6 +47,46 @@ pub use table::{
pub use text::ShxText;
pub(crate) use text::{shape, width};
/// Why a font file can't be read or a shape source can't be compiled.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ShxError {
/// Not an SHX file (unknown signature).
NotShx,
/// The file ends inside its header or index.
Truncated,
/// The file is larger, or has more of something, than CADCraft reads.
TooLarge,
/// The file is well-formed but holds no shape.
Empty,
/// Shape source error: line and message.
Source(usize, String),
}
impl std::fmt::Display for ShxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ShxError::NotShx => f.write_str("not an SHX shape or font file"),
ShxError::Truncated => f.write_str("the SHX file is truncated"),
ShxError::TooLarge => f.write_str("the SHX file is too large"),
ShxError::Empty => f.write_str("the SHX file has no shapes"),
ShxError::Source(line, msg) => write!(f, "line {line}: {msg}"),
}
}
}
impl std::error::Error for ShxError {}
/// The kind of SHX file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShxKind {
/// A shape file or a text font with single-byte character codes (`shapes 1.0/1.1`).
Shapes,
/// A big font: double-byte character codes after an escape (lead) byte.
BigFont,
/// A Unicode font: shape numbers are Unicode code points.
Unifont,
}
/// A shape's program run into polylines, in font units (pen starts at the origin).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ShxGlyph {
@@ -56,6 +96,13 @@ pub struct ShxGlyph {
pub advance: Vec2,
}
/// One shape record: where its name and program are in [`ShxFont::data`].
#[derive(Clone, Copy, Debug)]
pub(crate) struct Record {
name: (usize, usize),
program: (usize, usize),
}
/// Glyphs cached per font, keyed by (shape, vertical).
type GlyphCache = HashMap<(u16, bool), Option<Arc<ShxGlyph>>>;
+220 -21
View File
@@ -1,26 +1,225 @@
//! Bridge the shared bounded container reader into CADCraft's font and glyph cache.
//! Reading the compiled SHX containers (layouts in the module documentation). Every count and
//! offset comes from the file, so each is checked against its length; records that run past
//! the end are dropped and the rest kept.
use super::{ShxError, ShxFont};
pub(super) use craft_shx::parse::MAX_FILE_BYTES;
#[cfg(test)]
pub(super) use craft_shx::parse::{BIGFONT, SHAPES_10, UNIFONT};
use std::collections::HashMap;
use std::sync::RwLock;
use super::{Record, ShxError, ShxFont, ShxKind};
pub(super) const SHAPES_10: &[u8] = b"AutoCAD-86 shapes 1.0\r\n\x1a";
pub(super) const SHAPES_11: &[u8] = b"AutoCAD-86 shapes 1.1\r\n\x1a";
pub(super) const BIGFONT: &[u8] = b"AutoCAD-86 bigfont 1.0\r\n\x1a";
pub(super) const UNIFONT: &[u8] = b"AutoCAD-86 unifont 1.0\r\n\x1a";
/// Largest file read (the largest big fonts are a few megabytes).
pub(super) const MAX_FILE_BYTES: usize = 32 << 20;
/// Most escape ranges in a big font (lead bytes: at most 256).
const MAX_RANGES: usize = 256;
/// A checked little-endian reader.
struct Rd<'a> {
b: &'a [u8],
pos: usize,
}
impl<'a> Rd<'a> {
fn take(&mut self, n: usize) -> Result<&'a [u8], ShxError> {
let end = self.pos.checked_add(n).ok_or(ShxError::Truncated)?;
let s = self.b.get(self.pos..end).ok_or(ShxError::Truncated)?;
self.pos = end;
Ok(s)
}
fn u16(&mut self) -> Result<u16, ShxError> {
match self.take(2)? {
[a, b] => Ok(u16::from_le_bytes([*a, *b])),
_ => Err(ShxError::Truncated),
}
}
fn u32(&mut self) -> Result<u32, ShxError> {
match self.take(4)? {
[a, b, c, d] => Ok(u32::from_le_bytes([*a, *b, *c, *d])),
_ => Err(ShxError::Truncated),
}
}
}
/// A font being read.
struct Font {
kind: ShxKind,
name: String,
header: bool,
above: f64,
below: f64,
dual: bool,
encoding: u8,
char_width: f64,
ranges: Vec<(u16, u16)>,
records: HashMap<u16, Record>,
}
impl Font {
fn new(kind: ShxKind) -> Font {
Font {
kind,
name: String::new(),
header: false,
above: 0.0,
below: 0.0,
dual: false,
encoding: 0,
char_width: 0.0,
ranges: Vec::new(),
records: HashMap::new(),
}
}
/// Add the record of shape `number` at `start..end` of `data`: a NUL-terminated name, then
/// the program. Shape 0 is the font record. A record without a name terminator is dropped;
/// the first record of a number wins.
fn record(&mut self, data: &[u8], number: u16, start: usize, end: usize) {
let Some(bytes) = data.get(start..end) else { return };
let Some(nul) = bytes.iter().position(|b| *b == 0) else { return };
let name = (start, start + nul);
let program = (start + nul + 1, end);
if number == 0 {
if !self.header {
self.font_record(data.get(program.0..program.1).unwrap_or_default(), data.get(name.0..name.1).unwrap_or_default());
}
return;
}
self.records.entry(number).or_insert(Record { name, program });
}
/// The font record: above, below, modes (and for a unifont encoding and embedding); an
/// extended big font's is character height, 0, modes, character width.
fn font_record(&mut self, info: &[u8], name: &[u8]) {
self.header = true;
self.name = name.iter().take(255).map(|c| char::from(*c)).collect();
let byte = |i: usize| info.get(i).copied().unwrap_or(0);
self.above = f64::from(byte(0));
self.below = f64::from(byte(1));
self.dual = byte(2) == 2;
if self.kind == ShxKind::Unifont {
self.encoding = byte(3);
}
if self.kind == ShxKind::BigFont && info.len() >= 5 && byte(1) == 0 {
// Extended: height, 0, modes, width, 0.
self.below = 0.0;
self.char_width = f64::from(byte(3));
}
}
fn finish(self, data: Vec<u8>) -> Result<ShxFont, ShxError> {
if self.records.is_empty() {
return Err(ShxError::Empty);
}
Ok(ShxFont {
kind: self.kind,
name: self.name,
header: self.header,
above: self.above,
below: self.below,
dual: self.dual,
encoding: self.encoding,
char_width: if self.char_width > 0.0 { self.char_width } else { self.above },
ranges: self.ranges,
data,
records: self.records,
cache: RwLock::default(),
hebrew: std::sync::OnceLock::new(),
})
}
}
pub(super) fn parse(bytes: &[u8]) -> Result<ShxFont, ShxError> {
let font = craft_shx::parse::parse(bytes)?;
Ok(ShxFont {
kind: font.kind,
name: font.name,
header: font.header,
above: font.above,
below: font.below,
dual: font.dual,
encoding: font.encoding,
char_width: font.char_width,
ranges: font.ranges,
data: font.data,
records: font.records,
cache: RwLock::default(),
hebrew: std::sync::OnceLock::new(),
})
if bytes.len() > MAX_FILE_BYTES {
return Err(ShxError::TooLarge);
}
if bytes.starts_with(SHAPES_10) || bytes.starts_with(SHAPES_11) {
shapes(bytes)
} else if bytes.starts_with(BIGFONT) {
bigfont(bytes)
} else if bytes.starts_with(UNIFONT) {
unifont(bytes)
} else {
Err(ShxError::NotShx)
}
}
fn shapes(bytes: &[u8]) -> Result<ShxFont, ShxError> {
let mut r = Rd { b: bytes, pos: SHAPES_10.len() };
let _first = r.u16()?;
let _last = r.u16()?;
let count = usize::from(r.u16()?);
// The whole index must be there before anything is read from it.
let index = r.take(count.checked_mul(4).ok_or(ShxError::TooLarge)?)?;
let mut f = Font::new(ShxKind::Shapes);
let mut pos = r.pos;
for [a, b, c, d] in index.as_chunks::<4>().0 {
let (number, len) = (u16::from_le_bytes([*a, *b]), usize::from(u16::from_le_bytes([*c, *d])));
let Some(end) = pos.checked_add(len).filter(|e| *e <= bytes.len()) else { break };
f.record(bytes, number, pos, end);
pos = end;
}
f.finish(bytes.to_vec())
}
fn bigfont(bytes: &[u8]) -> Result<ShxFont, ShxError> {
let mut r = Rd { b: bytes, pos: BIGFONT.len() };
let index_size = usize::from(r.u16()?);
let count = usize::from(r.u16()?);
let nranges = usize::from(r.u16()?);
if nranges > MAX_RANGES {
return Err(ShxError::TooLarge);
}
let mut f = Font::new(ShxKind::BigFont);
for _ in 0..nranges {
let (lo, hi) = (r.u16()?, r.u16()?);
f.ranges.push((lo.min(hi), lo.max(hi)));
}
// The index has `count` entries of 8 bytes; a file whose count doesn't fit but whose index
// size does is read by that.
let fits = |n: usize| n.checked_mul(8).and_then(|l| r.pos.checked_add(l)).is_some_and(|e| e <= bytes.len());
let n = if fits(count) {
count
} else if fits(index_size) {
index_size
} else {
return Err(ShxError::Truncated);
};
let index = r.take(n.checked_mul(8).ok_or(ShxError::TooLarge)?)?;
for [a, b, c, d, o0, o1, o2, o3] in index.as_chunks::<8>().0 {
let number = u16::from_le_bytes([*a, *b]);
let len = usize::from(u16::from_le_bytes([*c, *d]));
let Ok(off) = usize::try_from(u32::from_le_bytes([*o0, *o1, *o2, *o3])) else { continue };
if len == 0 {
continue;
}
let Some(end) = off.checked_add(len).filter(|e| *e <= bytes.len()) else { continue };
f.record(bytes, number, off, end);
}
f.finish(bytes.to_vec())
}
fn unifont(bytes: &[u8]) -> Result<ShxFont, ShxError> {
let mut r = Rd { b: bytes, pos: UNIFONT.len() };
let count = r.u32()?;
let info_len = usize::from(r.u16()?);
let info = r.take(info_len)?;
let mut f = Font::new(ShxKind::Unifont);
let nul = info.iter().position(|b| *b == 0).unwrap_or(info.len());
f.font_record(info.get(nul.saturating_add(1)..).unwrap_or_default(), info.get(..nul).unwrap_or_default());
// `count` includes the font record; never trusted beyond the data that is there.
let mut left = count.saturating_sub(1);
while left > 0 {
left -= 1;
let Ok(number) = r.u16() else { break };
let Ok(len) = r.u16().map(usize::from) else { break };
let start = r.pos;
let Some(end) = start.checked_add(len).filter(|e| *e <= bytes.len()) else { break };
r.pos = end;
f.record(bytes, number, start, end);
}
f.finish(bytes.to_vec())
}