mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-02 07:16:53 -06:00
SAVEAS portableFonts: Windows fonts for Linux text style fonts (#2576)
* SAVEAS portableFonts: Windows fonts for Linux text style fonts
A text style using a Linux TrueType font (Liberation, DejaVu, Open Sans)
shows in AutoCAD on Windows with a substitute it picks. A save now names
those styles on the command line (and as `nonPortableFonts` in the
result), and SAVEAS {"portableFonts": true} writes the Windows equivalent
in the file (Liberation -> Arial, Times New Roman, Courier New; DejaVu
Sans -> Verdana; Open Sans -> Arial) while the drawing keeps its own
fonts. Narrow and condensed families have no Windows stand-in (Arial
Narrow comes with Office) and are kept and named. QSAVE takes no SAVEAS
options.
Tests: stand_in_tests in cmd/file.rs, tests/save_portable_fonts.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Portable fonts kept in the file, applied by the writer
Reworked after review. SAVEAS `portableFonts` writes a copy of the drawing
with `Drawing::portable_fonts` set, and the DXF/DWG writer turns each
style's Linux font into its Windows font, keeping the style's own font in
the CADCraft FONT xdata already used for the built-in font. Reading the
file gives the Linux fonts back (unless the font was changed since, in
AutoCAD); the drawing itself is never changed.
It applies to that save: a later QSAVE writes the drawing's fonts and
says so, naming the styles and SAVEAS portableFonts. The family table
moves to `cadcraft_fonts::windows`, used by the writer and the note.
Styles from an xref aren't saved and aren't named. The parity docs are
left to the maintainers (CLAUDE.md).
Tests: crates/io/tests/it/portable_fonts.rs (DXF, DWG, a font changed in
AutoCAD), crates/engine/tests/save_portable_fonts.rs, windows.rs tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Portable fonts: match the Windows font by file name
A STYLE whose Windows font carries a path (C:\Windows\Fonts\…) still
gets its own font back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Portable fonts: say the font list again only when it changes
A save still returns nonPortableFonts, but the command line note isn't
repeated on every QSAVE of the same drawing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4a9371ad2c
commit
c72c0eebfb
@@ -132,6 +132,10 @@ pub struct Drawing {
|
||||
/// Records of the file this drawing was read from that the reader could not place (a damaged
|
||||
/// file), counted by the section they came from. Never written to a file.
|
||||
pub skipped_records: BTreeMap<String, usize>,
|
||||
/// A save option, set on the copy SAVEAS `portableFonts` writes: the file gets Windows fonts
|
||||
/// for the Linux fonts of text styles, and each style keeps its own in CADCraft xdata, so the
|
||||
/// drawing reopens with it. Never set on an open drawing.
|
||||
pub portable_fonts: bool,
|
||||
}
|
||||
|
||||
impl Default for Drawing {
|
||||
@@ -191,6 +195,7 @@ impl Drawing {
|
||||
read_notes: Vec::new(),
|
||||
refedit: None,
|
||||
skipped_records: BTreeMap::new(),
|
||||
portable_fonts: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,12 +85,13 @@ pub fn deliver() -> Option<DeliverHook> {
|
||||
DELIVER.get().copied()
|
||||
}
|
||||
|
||||
/// How a drawing file is written: its version (`None`: the drawing's own, `$ACADVER`) and
|
||||
/// binary instead of ASCII DXF.
|
||||
/// How a drawing file is written: its version (`None`: the drawing's own, `$ACADVER`), binary
|
||||
/// instead of ASCII DXF, and Windows fonts for Linux ones ([`cadcraft_doc::Drawing::portable_fonts`]).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct SaveAs {
|
||||
pub version: Option<Version>,
|
||||
pub binary: bool,
|
||||
pub portable_fonts: bool,
|
||||
}
|
||||
|
||||
/// Write a drawing with [`SaveAs`] options (installed by hosts whose writer supports them).
|
||||
@@ -131,7 +132,12 @@ fn save_options(cmd: &str, p: &Value) -> Result<SaveAs> {
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(_) => return Err(bad(cmd, "`binary` must be true or false")),
|
||||
};
|
||||
Ok(SaveAs { version, binary })
|
||||
let portable_fonts = match p.get("portableFonts") {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::Bool(b)) => *b,
|
||||
Some(_) => return Err(bad(cmd, "`portableFonts` must be true or false")),
|
||||
};
|
||||
Ok(SaveAs { version, binary, portable_fonts })
|
||||
}
|
||||
|
||||
/// Whether `path` names a DXF file (or has no extension, which saves as DXF).
|
||||
@@ -148,7 +154,9 @@ fn write_bytes(s: &Session, cmd: &str, path: &str, opts: &SaveAs) -> Result<Vec<
|
||||
}
|
||||
// A drawing file is the drawing outside the Block Editor; an image is what is shown.
|
||||
let d: &cadcraft_doc::Drawing = if is_drawing_file(path) { s.state()?.drawing() } else { s.doc()? };
|
||||
let r = if *opts == SaveAs::default() {
|
||||
let copy = (opts.portable_fonts && is_drawing_file(path)).then(|| Drawing { portable_fonts: true, ..d.clone() });
|
||||
let d = copy.as_ref().unwrap_or(d);
|
||||
let r = if opts.version.is_none() && !opts.binary {
|
||||
(hooks.write)(d, path)
|
||||
} else {
|
||||
let w = write_as().ok_or_else(|| bad(cmd, "saving at a chosen version or as binary DXF is not available in this build"))?;
|
||||
@@ -178,11 +186,13 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
CommandSpec::new("saveas", "Save As...", run_saveas_reporting)
|
||||
.menu(&["File", "Save As..."])
|
||||
.key("Cmd+Shift+S")
|
||||
.params("{path, version?: 2000|2004|2007|2010|2013|2018, binary?: bool}")
|
||||
.params(
|
||||
"{path, version?: 2000|2004|2007|2010|2013|2018, binary?: bool, portableFonts?: bool (Windows fonts for Linux ones, in this file)}",
|
||||
)
|
||||
.noundo(),
|
||||
CommandSpec::new("document.switch", "Switch Drawing", run_switch).params("{index}").enabled(always).noundo(),
|
||||
CommandSpec::new("document.bytes", "Drawing as bytes", run_bytes)
|
||||
.params("{format?: dxf, version?: 2000…2018, binary?: bool} → {data: base64}")
|
||||
.params("{format?: dxf, version?: 2000…2018, binary?: bool, portableFonts?: bool} → {data: base64}")
|
||||
.noundo(),
|
||||
CommandSpec::new("script", "Run Script...", run_script)
|
||||
.menu(&["Tools", "Run Script..."])
|
||||
@@ -395,7 +405,7 @@ fn run_closeall(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
pub(crate) fn save_to(s: &mut Session, path: &str) -> Result<usize> {
|
||||
let opts = SaveAs { version: None, binary: s.state()?.binary_dxf && is_dxf(path) };
|
||||
let opts = SaveAs { binary: s.state()?.binary_dxf && is_dxf(path), ..SaveAs::default() };
|
||||
save_as(s, "save", path, &opts)
|
||||
}
|
||||
|
||||
@@ -677,6 +687,7 @@ fn run_saveas(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if is_drawing_file(&path) {
|
||||
r["version"] = json!(opts.version.unwrap_or_else(|| drawing_version(s, &path)).year());
|
||||
r["binary"] = json!(opts.binary);
|
||||
r["portableFonts"] = json!(opts.portable_fonts);
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
@@ -799,9 +810,43 @@ fn save_reporting(s: &mut Session, p: &Value, run: fn(&mut Session, &Value) -> R
|
||||
if r.get("path").and_then(Value::as_str).is_some_and(drops_unknown) {
|
||||
report_not_saved(s, &mut r, &lost);
|
||||
}
|
||||
if r.get("path").and_then(Value::as_str).is_some_and(is_drawing_file) {
|
||||
report_fonts(s, &mut r);
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Name the text styles whose font in the saved file AutoCAD on Windows doesn't have: in the
|
||||
/// result, and at the command line when the list changed since the last save. Styles from an xref
|
||||
/// (`XREF|name`) aren't saved: AutoCAD takes them from the xref.
|
||||
fn report_fonts(s: &mut Session, r: &mut Value) {
|
||||
use cadcraft_fonts::windows::{missing_on_windows, windows_font, written_font};
|
||||
let portable = r.get("portableFonts").and_then(Value::as_bool).unwrap_or(false);
|
||||
let Ok(d) = s.state().map(|st| st.drawing()) else { return };
|
||||
let fonts: Vec<(String, String)> = d
|
||||
.text_styles
|
||||
.iter()
|
||||
.filter(|t| !t.name.contains('|') && missing_on_windows(written_font(&t.font, portable)))
|
||||
.map(|t| (t.name.clone(), t.font.clone()))
|
||||
.collect();
|
||||
let list = fonts.iter().map(|(style, font)| format!("{style} ({font})")).collect::<Vec<_>>().join(", ");
|
||||
let changed = s.state_mut().map(|st| std::mem::replace(&mut st.fonts_noted, list.clone()) != list).unwrap_or(true);
|
||||
if fonts.is_empty() {
|
||||
return;
|
||||
}
|
||||
if changed {
|
||||
let hint = if fonts.iter().any(|(_, font)| windows_font(font).is_some()) {
|
||||
" SAVEAS with portableFonts writes Windows fonts for those that have one."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
s.echo(format!("Fonts AutoCAD on Windows doesn't have: {list}.{hint}"));
|
||||
}
|
||||
if let Some(o) = r.as_object_mut() {
|
||||
o.insert("nonPortableFonts".into(), json!(fonts.iter().map(|(style, font)| json!({ "style": style, "font": font })).collect::<Vec<_>>()));
|
||||
}
|
||||
}
|
||||
|
||||
fn run_qsave_reporting(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
save_reporting(s, p, run_save)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ pub struct DocState {
|
||||
/// The Block Editor (BEDIT), when it is open: `doc` is then the editor's drawing, and the
|
||||
/// drawing itself waits in it ([`DocState::drawing`]).
|
||||
pub block_edit: Option<blockedit::BlockEdit>,
|
||||
/// The fonts missing on Windows that a save last named: the next save names them again only
|
||||
/// when they change.
|
||||
pub(crate) fonts_noted: String,
|
||||
}
|
||||
|
||||
static NEXT_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
@@ -61,6 +64,7 @@ impl DocState {
|
||||
uid: NEXT_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
binary_dxf: false,
|
||||
block_edit: None,
|
||||
fonts_noted: String::new(),
|
||||
}
|
||||
}
|
||||
/// The drawing has changes not saved to its file, or the Block Editor has changes not yet
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//! Text styles with Linux fonts AutoCAD on Windows doesn't have: a save names them, and SAVEAS
|
||||
//! with portableFonts writes Windows fonts in that file while the drawing keeps its own.
|
||||
|
||||
use cadcraft_doc::{Drawing, TextStyle};
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::cmd::file::{IoHooks, set_io};
|
||||
use serde_json::json;
|
||||
|
||||
fn dxf(d: &Drawing, _: &str) -> Result<Vec<u8>, String> {
|
||||
Ok(cadcraft_io::write_dxf(d).into_bytes())
|
||||
}
|
||||
|
||||
fn session() -> Session {
|
||||
set_io(IoHooks { read: |_, _| Ok(Drawing::new_metric()), write: dxf, plot: None });
|
||||
let mut d = Drawing::new_metric();
|
||||
for (name, font) in [("Notes", "DejaVuSans.ttf"), ("Plain", "arial.ttf"), ("Tight", "Liberation Sans Narrow")] {
|
||||
d.text_styles.push(TextStyle { name: name.into(), font: font.into(), ..TextStyle::default() });
|
||||
}
|
||||
let mut s = Session::empty();
|
||||
s.open_drawing(d, "fonts.dxf", None);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portable_fonts_writes_windows_fonts_in_that_file() {
|
||||
let mut s = session();
|
||||
let dir = std::env::temp_dir().join(format!("cadcraft-portable-fonts-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = |name: &str| dir.join(name).to_string_lossy().to_string();
|
||||
let file = |name: &str| std::fs::read_to_string(dir.join(name)).unwrap();
|
||||
let both = json!([{ "style": "Notes", "font": "DejaVuSans.ttf" }, { "style": "Tight", "font": "Liberation Sans Narrow" }]);
|
||||
|
||||
// portableFonts writes Verdana for DejaVu Sans; the narrow style has no Windows font.
|
||||
let r = s.execute("saveas", &json!({ "path": path("portable.dxf"), "portableFonts": true })).unwrap();
|
||||
assert_eq!(r["nonPortableFonts"], json!([{ "style": "Tight", "font": "Liberation Sans Narrow" }]));
|
||||
assert_eq!(s.log.last().map(String::as_str), Some("Fonts AutoCAD on Windows doesn't have: Tight (Liberation Sans Narrow)."));
|
||||
assert!(file("portable.dxf").contains("verdana.ttf"));
|
||||
assert!(s.state().unwrap().drawing().text_styles.iter().any(|t| t.font == "DejaVuSans.ttf"), "the drawing keeps its fonts");
|
||||
|
||||
// A later save without it writes the drawing's fonts again, and says so.
|
||||
let r = s.execute("qsave", &json!({})).unwrap();
|
||||
assert_eq!(r["nonPortableFonts"], both);
|
||||
assert!(s.log.last().is_some_and(|l| l.ends_with("SAVEAS with portableFonts writes Windows fonts for those that have one.")), "{:?}", s.log);
|
||||
assert!(!file("portable.dxf").contains("verdana.ttf"));
|
||||
|
||||
// The same list isn't said again on every save.
|
||||
let lines = s.log.len();
|
||||
let r = s.execute("qsave", &json!({})).unwrap();
|
||||
assert_eq!(r["nonPortableFonts"], both);
|
||||
assert!(s.log.iter().skip(lines).all(|l| !l.starts_with("Fonts AutoCAD")), "{:?}", s.log);
|
||||
|
||||
assert!(s.execute("saveas", &json!({ "path": path("bad.dxf"), "portableFonts": "yes" })).is_err());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ mod mtext;
|
||||
mod stroke;
|
||||
pub mod subset;
|
||||
pub mod ttf;
|
||||
pub mod windows;
|
||||
|
||||
use cadcraft_geom::{Bounds2, Vec2};
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Linux font families that AutoCAD on Windows doesn't have, and the Windows fonts a drawing
|
||||
//! saved with portable fonts gets instead.
|
||||
|
||||
/// Family key (lower case, without spaces, hyphens or underscores; a longer key before a shorter
|
||||
/// one it starts with) and the Windows fonts for its regular, bold, italic and bold italic styles.
|
||||
const FAMILIES: [(&str, [&str; 4]); 7] = [
|
||||
("liberationsans", ["arial.ttf", "arialbd.ttf", "ariali.ttf", "arialbi.ttf"]),
|
||||
("liberationserif", ["times.ttf", "timesbd.ttf", "timesi.ttf", "timesbi.ttf"]),
|
||||
("liberationmono", ["cour.ttf", "courbd.ttf", "couri.ttf", "courbi.ttf"]),
|
||||
("opensans", ["arial.ttf", "arialbd.ttf", "ariali.ttf", "arialbi.ttf"]),
|
||||
("dejavusansmono", ["cour.ttf", "courbd.ttf", "couri.ttf", "courbi.ttf"]),
|
||||
("dejavusans", ["verdana.ttf", "verdanab.ttf", "verdanai.ttf", "verdanaz.ttf"]),
|
||||
("dejavuserif", ["times.ttf", "timesbd.ttf", "timesi.ttf", "timesbi.ttf"]),
|
||||
];
|
||||
|
||||
/// The family of a TrueType `font` (file or family name) from [`FAMILIES`], with the rest of its
|
||||
/// key (the style: `bold`, `italic`, …).
|
||||
fn family(font: &str) -> Option<(&'static [&'static str; 4], String)> {
|
||||
let name = font.trim().rsplit(['/', '\\']).next().unwrap_or_default().to_lowercase();
|
||||
let stem = name.strip_suffix(".ttf").or_else(|| name.strip_suffix(".otf")).unwrap_or(&name);
|
||||
if stem.contains('.') {
|
||||
return None; // another kind of file, such as an SHX font
|
||||
}
|
||||
let key: String = stem.chars().filter(|c| !matches!(c, ' ' | '-' | '_')).collect();
|
||||
let (family, fonts) = FAMILIES.iter().find(|(f, _)| key.starts_with(f))?;
|
||||
Some((fonts, key.get(family.len()..).unwrap_or_default().to_string()))
|
||||
}
|
||||
|
||||
/// Whether `font` is a Linux font AutoCAD on Windows doesn't have.
|
||||
pub fn missing_on_windows(font: &str) -> bool {
|
||||
family(font).is_some()
|
||||
}
|
||||
|
||||
/// The Windows font for a Linux `font`. Narrow and condensed styles have none (Arial Narrow comes
|
||||
/// with Office, not with Windows).
|
||||
pub fn windows_font(font: &str) -> Option<&'static str> {
|
||||
let (fonts, style) = family(font)?;
|
||||
if style.contains("narrow") || style.contains("condensed") {
|
||||
return None;
|
||||
}
|
||||
let bold = style.contains("bold");
|
||||
let italic = style.contains("italic") || style.contains("oblique");
|
||||
fonts.get(usize::from(bold) + 2 * usize::from(italic)).copied()
|
||||
}
|
||||
|
||||
/// The font a drawing file gets for a text style's `font`: with `portable`, the Windows font for
|
||||
/// a Linux one.
|
||||
pub fn written_font(font: &str, portable: bool) -> &str {
|
||||
if portable { windows_font(font).unwrap_or(font) } else { font }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn windows_fonts_by_file_or_family_name() {
|
||||
assert_eq!(windows_font("DejaVuSans.ttf"), Some("verdana.ttf"));
|
||||
assert_eq!(windows_font("/usr/share/fonts/truetype/LiberationSerif-Bold.ttf"), Some("timesbd.ttf"));
|
||||
assert_eq!(windows_font("Liberation Mono"), Some("cour.ttf"));
|
||||
assert_eq!(windows_font("LiberationMono-BoldItalic.ttf"), Some("courbi.ttf"));
|
||||
assert_eq!(windows_font("OpenSans-SemiBold.ttf"), Some("arialbd.ttf"));
|
||||
assert_eq!(windows_font("Open Sans"), Some("arial.ttf"));
|
||||
assert_eq!(windows_font("DejaVuSansMono-BoldOblique.ttf"), Some("courbi.ttf"));
|
||||
assert_eq!(windows_font("DejaVuSerif-Italic.ttf"), Some("timesi.ttf"));
|
||||
// Narrow and condensed styles are Linux fonts with no Windows one.
|
||||
for font in ["Liberation Sans Narrow", "LiberationSansNarrow-Bold.ttf", "OpenSans-CondensedBold.ttf", "DejaVuSansCondensed.ttf"] {
|
||||
assert!(missing_on_windows(font) && windows_font(font).is_none(), "{font}");
|
||||
}
|
||||
// Fonts AutoCAD has, other kinds of file, or names that aren't fonts.
|
||||
for font in ["arial.ttf", "txt", "dejavusans.shx", "", "/", "Liberation"] {
|
||||
assert!(!missing_on_windows(font) && windows_font(font).is_none(), "{font}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn written_font_only_changes_with_portable_fonts() {
|
||||
assert_eq!(written_font("DejaVuSans.ttf", true), "verdana.ttf");
|
||||
assert_eq!(written_font("DejaVuSans.ttf", false), "DejaVuSans.ttf");
|
||||
assert_eq!(written_font("Liberation Sans Narrow", true), "Liberation Sans Narrow");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use super::{
|
||||
Color, DashElement, DimStyle, Drawing, HVal, HashMap, Layer, Linetype, Lineweight, NamedView, Rgb, Rx, Tag, TextStyle, Ucs, Vec2, Vec3,
|
||||
VportConfig, VportTile, records,
|
||||
};
|
||||
use cadcraft_fonts::windows::windows_font;
|
||||
|
||||
pub(super) fn header(tags: &[Tag], d: &mut Drawing) {
|
||||
let mut i = 0;
|
||||
@@ -66,9 +67,9 @@ fn annotative(tags: &[Tag]) -> bool {
|
||||
}
|
||||
|
||||
/// A text style's font: CADCraft's own font name from `CADCRAFT` xdata (`1000 FONT`, name)
|
||||
/// while the font file is still the `txt` written for other readers; the TrueType family of the
|
||||
/// style's `ACAD` xdata (`1000` typeface) when it has no font file (how AutoCAD stores TrueType
|
||||
/// styles); otherwise the file.
|
||||
/// while the font file is still the `txt` or Windows font written for other readers; the TrueType
|
||||
/// family of the style's `ACAD` xdata (`1000` typeface) when it has no font file (how AutoCAD
|
||||
/// stores TrueType styles); otherwise the file.
|
||||
fn style_font(tags: &[Tag], file: String) -> String {
|
||||
if file.trim().is_empty()
|
||||
&& let Some(face) =
|
||||
@@ -82,8 +83,9 @@ fn style_font(tags: &[Tag], file: String) -> String {
|
||||
_ => None,
|
||||
});
|
||||
let stem = file.trim().to_ascii_lowercase();
|
||||
let base = stem.rsplit(['/', '\\']).next().unwrap_or_default();
|
||||
match own {
|
||||
Some(name) if !name.trim().is_empty() && (stem == "txt" || stem == "txt.shx") => name,
|
||||
Some(name) if !name.trim().is_empty() && (stem == "txt" || stem == "txt.shx" || windows_font(&name) == Some(base)) => name,
|
||||
_ => file,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +90,10 @@ fn write_tables(w: &mut W, plan: TableWritePlan<'_>) -> HashMap<String, String>
|
||||
w.f(50, s.oblique.to_degrees());
|
||||
w.i(71, if s.backwards { 2 } else { 0 } | if s.upside_down { 4 } else { 0 });
|
||||
w.f(42, 0.2);
|
||||
// The built-in font goes out as `txt`, which every reader has; CADCraft's own name
|
||||
// travels in `CADCRAFT` xdata (`1000 FONT`, name) so the style reopens with it.
|
||||
let file = if s.font == cadcraft_fonts_name() { "txt" } else { s.font.as_str() };
|
||||
// The built-in font goes out as `txt`, which every reader has, and a Linux font as its
|
||||
// Windows one with portable fonts; CADCraft's own name travels in `CADCRAFT` xdata
|
||||
// (`1000 FONT`, name) so the style reopens with it.
|
||||
let file = if s.font == cadcraft_fonts_name() { "txt" } else { cadcraft_fonts::windows::written_font(&s.font, d.portable_fonts) };
|
||||
w.s(3, file);
|
||||
w.s(4, &s.big_font);
|
||||
if s.annotative {
|
||||
|
||||
@@ -54,6 +54,7 @@ mod pdf_read;
|
||||
mod plot_layout_scale;
|
||||
mod plot_model_png_scale;
|
||||
mod plot_styles;
|
||||
mod portable_fonts;
|
||||
mod region_roundtrip;
|
||||
mod roundtrip;
|
||||
mod roundtrip_audit;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Portable fonts: a drawing saved with them gets the Windows font for each Linux font in its
|
||||
//! STYLE table, keeps its own in CADCraft xdata, and reopens with it, from DXF and DWG.
|
||||
|
||||
use cadcraft_doc::{Drawing, TextStyle};
|
||||
use cadcraft_io::{read, read_dxf, write, write_dxf};
|
||||
|
||||
fn drawing(portable_fonts: bool) -> Drawing {
|
||||
let mut d = Drawing { portable_fonts, ..Drawing::new_metric() };
|
||||
// A Linux font, and one with no Windows font.
|
||||
for (name, font) in [("Notes", "DejaVuSans-Bold.ttf"), ("Tight", "Liberation Sans Narrow")] {
|
||||
d.text_styles.push(TextStyle { name: name.into(), font: font.into(), ..TextStyle::default() });
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// (style, font) of the drawing's text styles.
|
||||
fn fonts(d: &Drawing) -> Vec<(String, String)> {
|
||||
d.text_styles.iter().map(|t| (t.name.clone(), t.font.clone())).collect()
|
||||
}
|
||||
|
||||
/// (style, font file) of the STYLE records of DXF text.
|
||||
fn style_files(dxf: &str) -> Vec<(String, String)> {
|
||||
let mut out: Vec<(String, String)> = Vec::new();
|
||||
let mut style = false;
|
||||
for t in cadcraft_dxf::parse(dxf.as_bytes()).unwrap() {
|
||||
match t.code {
|
||||
0 => style = t.str() == "STYLE",
|
||||
2 if style => out.push((t.str(), String::new())),
|
||||
3 if style => out.last_mut().unwrap().1 = t.str(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles_get_windows_fonts_and_reopen_with_their_own() {
|
||||
let d = drawing(true);
|
||||
let dxf = write_dxf(&d);
|
||||
let files = style_files(&dxf);
|
||||
for (style, file) in [("Notes", "verdanab.ttf"), ("Tight", "Liberation Sans Narrow")] {
|
||||
assert!(files.contains(&(style.into(), file.into())), "{style}: {files:?}");
|
||||
}
|
||||
for back in [read_dxf(dxf.as_bytes()).unwrap(), read(&write(&d, "t.dwg").unwrap(), "t.dwg").unwrap()] {
|
||||
assert_eq!(fonts(&back), fonts(&drawing(false)));
|
||||
}
|
||||
// The Windows font with a path is still the one written for the style's own.
|
||||
let path = read_dxf(dxf.replace("verdanab.ttf", "C:\\Windows\\Fonts\\verdanab.ttf").as_bytes()).unwrap();
|
||||
assert_eq!(fonts(&path), fonts(&drawing(false)));
|
||||
// A font changed in AutoCAD since (the xdata left as it was) is the font read.
|
||||
let edited = read_dxf(dxf.replace("verdanab.ttf", "times.ttf").as_bytes()).unwrap();
|
||||
assert!(fonts(&edited).contains(&("Notes".into(), "times.ttf".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_portable_fonts_the_file_has_the_drawings_fonts() {
|
||||
let files = style_files(&write_dxf(&drawing(false)));
|
||||
assert!(files.contains(&("Notes".into(), "DejaVuSans-Bold.ttf".into())), "{files:?}");
|
||||
}
|
||||
Reference in New Issue
Block a user