mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-15 10:23:43 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
305f452283 | ||
|
|
78bcdce794 | ||
|
|
fd11b47915 |
Generated
+7
-8
@@ -714,6 +714,7 @@ version = "0.21.0"
|
||||
dependencies = [
|
||||
"cadcraft-geom",
|
||||
"craft-dxf",
|
||||
"craft-shx",
|
||||
"encoding_rs",
|
||||
"harfrust 0.14.0",
|
||||
"icu_segmenter",
|
||||
@@ -1131,11 +1132,6 @@ 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"
|
||||
@@ -1153,11 +1149,9 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-dxf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=b1d4c67196ee7c6b668428c8d211e7d5171dff88#b1d4c67196ee7c6b668428c8d211e7d5171dff88"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=006e4df225fca57e95ead0fb1f91920900798a99#006e4df225fca57e95ead0fb1f91920900798a99"
|
||||
dependencies = [
|
||||
"craft-codepage",
|
||||
"encoding_rs",
|
||||
"kurbo",
|
||||
"log",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
@@ -1200,6 +1194,11 @@ 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"
|
||||
|
||||
+2
-1
@@ -14,8 +14,9 @@ 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 = "b1d4c67196ee7c6b668428c8d211e7d5171dff88" }
|
||||
craft-dxf = { git = "https://github.com/storytold/craft-libs", rev = "006e4df225fca57e95ead0fb1f91920900798a99" }
|
||||
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" }
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 }
|
||||
|
||||
+294
-86
@@ -12,22 +12,79 @@ 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());
|
||||
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(' ');
|
||||
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,
|
||||
}
|
||||
out.push_str(stack.trim_end_matches('/'));
|
||||
}
|
||||
Token::Char('\r') => {}
|
||||
Token::Char(c) => out.push(c),
|
||||
_ => {}
|
||||
'{' | '}' => i += 1,
|
||||
'\r' => i += 1,
|
||||
_ => {
|
||||
out.push(c);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -168,8 +225,11 @@ const MAX_ATOMS: usize = 200_000;
|
||||
/// Scale of stacked text relative to the surrounding text.
|
||||
const STACK_SCALE: f64 = 0.7;
|
||||
|
||||
// Shared DXF escape handling and parameterized-command classification.
|
||||
pub(crate) use craft_dxf::mtext::unescape_codes;
|
||||
/// 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')
|
||||
}
|
||||
|
||||
/// The line-spacing factor to lay out with: AutoCAD's 0.25..=4 range, 1 for a missing or
|
||||
/// non-finite value.
|
||||
@@ -177,6 +237,90 @@ 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())
|
||||
}
|
||||
@@ -214,7 +358,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>) {
|
||||
use craft_dxf::mtext_tokens::{StackMode, Token, Tokens, split_stack};
|
||||
let chars: Vec<char> = contents.chars().collect();
|
||||
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];
|
||||
@@ -254,94 +398,158 @@ fn parse(contents: &str, base: Fmt) -> (Vec<Atom>, Vec<Fmt>) {
|
||||
*cur = fmts.len() - 1;
|
||||
}
|
||||
};
|
||||
for token in Tokens::new(contents, StackMode::Structured) {
|
||||
let mut i = 0;
|
||||
while let Some(&c) = chars.get(i) {
|
||||
if atoms.len() > MAX_ATOMS {
|
||||
break;
|
||||
}
|
||||
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());
|
||||
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.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
Token::Char('\r') => {}
|
||||
Token::Char('\n') => {
|
||||
'\r' => i += 1,
|
||||
'\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);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
use cadcraft_geom::{Bounds2, Vec2};
|
||||
|
||||
use super::{Atom, Fmt, MTextLayout, MTextParams, MTextPiece, Seg, num, parse, seg_plain, seg_shape, seg_width, split_thai};
|
||||
use super::{Atom, Fmt, MTextLayout, MTextParams, MTextPiece, Seg, arg, 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,28 +265,58 @@ pub struct Paragraphs {
|
||||
impl Paragraphs {
|
||||
pub fn scan(contents: &str) -> Paragraphs {
|
||||
let contents = super::unescape_codes(contents);
|
||||
use craft_dxf::mtext_tokens::{StackMode, Token, Tokens};
|
||||
let chars: Vec<char> = contents.chars().collect();
|
||||
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);
|
||||
};
|
||||
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;
|
||||
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.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
'\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
|
||||
|
||||
@@ -1,313 +0,0 @@
|
||||
//! 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)
|
||||
}
|
||||
@@ -26,7 +26,6 @@
|
||||
//! above, below, modes, encoding, embedding, 0) and then each shape as (number, length,
|
||||
//! record).
|
||||
|
||||
mod compile;
|
||||
mod parse;
|
||||
mod program;
|
||||
mod table;
|
||||
@@ -39,7 +38,8 @@ use std::sync::{Arc, PoisonError, RwLock};
|
||||
|
||||
use cadcraft_geom::Vec2;
|
||||
|
||||
pub use compile::compile;
|
||||
use craft_shx::Record;
|
||||
pub use craft_shx::{ShxError, ShxKind, 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,46 +47,6 @@ 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 {
|
||||
@@ -96,13 +56,6 @@ 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>>>;
|
||||
|
||||
|
||||
+21
-220
@@ -1,225 +1,26 @@
|
||||
//! 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.
|
||||
//! Bridge the shared bounded container reader into CADCraft's font and glyph cache.
|
||||
|
||||
use std::collections::HashMap;
|
||||
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::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> {
|
||||
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())
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user