CJK 1.3: documents bring their own fonts
Opening a .designcraft or .idml file from disk (file.open, file.openIdml, book chapters, recovered documents) loads the fonts in the "Document Fonts" folder beside it. Their faces come before installed faces of the same family and style. The files are untrusted: at most 200 are read, 256 MB each (full CJK collections fit) and 512 MB in all; files over a limit by their listed length are skipped unread, and damaged, oversized and surplus files are listed in the open result's warnings. Document fonts stay loaded for the session (the font database is process-wide). Faces record their source (bundled, installed file, document file, memory); font.list reports it. File > Package copies the font files the document uses into the package's Document Fonts folder unless OS/2 fsType restricts the licence, and the report says which fonts weren't copied and why.
This commit is contained in:
parent
2da99b70e2
commit
832939691c
|
|
@ -146,7 +146,8 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- UI scaling (Preferences › Interface, 50–200%; `window.uiScale`); the window title steps aside when the bar is crowded.
|
||||
- Underline / Strikethrough Options: weight, offset, colour and tint (Character panel; screen, PDF and IDML).
|
||||
- Relink to Folder and Relink File Extension (`links.relinkFolder`; missing links found by name).
|
||||
- File › Package (⌥⇧⌘P): the document relinked to a Links folder, the placed files, an IDML copy, an optional PDF and a report (fonts, links, preflight, instructions); Copy Links To (`links.copyTo`).
|
||||
- File › Package (⌥⇧⌘P): the document relinked to a Links folder, the placed files, the font files it uses in a Document Fonts folder (except fonts whose licence restricts it, OS/2 `fsType`), an IDML copy, an optional PDF and a report (fonts, links, preflight, instructions); Copy Links To (`links.copyTo`).
|
||||
- Document fonts: opening a `.designcraft` or `.idml` file loads the fonts in the `Document Fonts` folder beside it (as Package writes it), ahead of installed fonts with the same name, so a packaged document opens with its fonts on another machine. Damaged, oversized (over 256 MB per file, 512 MB in all) and surplus (over 200) font files are skipped and listed in `file.open`'s `warnings`; `font.list` gives each font's `source`. Document fonts stay loaded until DesignCraft quits, available to every open document; on the web there is no folder to read.
|
||||
- Load Swatches / Save Swatches for Exchange (.ase: CMYK, RGB, Lab, Gray; spot or process) from the Swatches panel menu; `swatch.load` / `swatch.save`.
|
||||
- Pathfinder (Object › Pathfinder and a Pathfinder panel): Add, Subtract, Intersect, Exclude Overlap, Minus Back; curves stay curves (flo_curves); `object.pathfinder`.
|
||||
- Make / Release Compound Path (⌘8; nested paths become holes), Create Outlines (⇧⌘O; one path per text colour), Scissors tool (C) with `path.split`.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ regex = { workspace = true }
|
|||
quick-xml = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
designcraft-fonts = { workspace = true, features = ["testing"] }
|
||||
zip = { workspace = true }
|
||||
hayro-syntax = "0.7.2"
|
||||
hayro-interpret = { version = "0.7.0", default-features = false, features = ["embed-fonts"] }
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ fn load(path: &str) -> Result<designcraft_doc::Document> {
|
|||
let _ = path;
|
||||
vec![]
|
||||
};
|
||||
// Skipped font files are logged; a chapter opens without them.
|
||||
let _ = super::file::load_document_fonts(path);
|
||||
if path.to_lowercase().ends_with(".idml") {
|
||||
return designcraft_idml::import_idml(&bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
always, file_new),
|
||||
cmd!(noundo "file.newSample", "Sample Document", ["Help"], None, "{} — a multi-page magazine sample", always, file_sample),
|
||||
cmd!(query "file.presets", "Document Presets", [], None, "{}", always, |_, _| Ok(serde_json::to_value(PRESETS).unwrap_or_default())),
|
||||
cmd!(noundo "file.open", "Open…", ["File"], Some("Cmd+O"), "{path} — .designcraft or .idml", always, file_open),
|
||||
cmd!(noundo "file.open", "Open…", ["File"], Some("Cmd+O"),
|
||||
"{path} — .designcraft or .idml; the fonts in a `Document Fonts` folder beside it load first → {index, documentFonts: faces loaded, warnings: font files skipped}",
|
||||
always, file_open),
|
||||
cmd!(noundo "file.openBytes", "Open Bytes", [], None, "{name, base64} — DesignCraft JSON or an IDML package", always, file_open_bytes),
|
||||
cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?}", has_doc, file_save),
|
||||
cmd!(noundo "file.saveAs", "Save As…", ["File"], Some("Cmd+Shift+S"), "{path}", has_doc, file_save),
|
||||
|
|
@ -159,8 +161,9 @@ fn file_open(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
let bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let d = from_bytes(&bytes)?;
|
||||
let (faces, warnings) = load_document_fonts(path);
|
||||
let i = s.add_document(DocState::new(d, Some(path.to_string())));
|
||||
Ok(json!({"index": i}))
|
||||
Ok(json!({"index": i, "documentFonts": faces, "warnings": warnings}))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
|
|
@ -169,6 +172,23 @@ fn file_open(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Load the fonts in the `Document Fonts` folder beside the document file at `path` → (faces
|
||||
/// loaded, a warning for each font file skipped). Nothing on the web (no folders).
|
||||
pub(crate) fn load_document_fonts(path: &str) -> (usize, Vec<String>) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use designcraft_fonts::DOCUMENT_FONTS_FOLDER;
|
||||
let Some(folder) = std::path::Path::new(path).parent().map(|d| d.join(DOCUMENT_FONTS_FOLDER)) else { return (0, Vec::new()) };
|
||||
let r = designcraft_fonts::FontDb::global().load_document_fonts(&folder);
|
||||
(r.faces, r.skipped.into_iter().map(|s| format!("{DOCUMENT_FONTS_FOLDER}: {s}")).collect())
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = path;
|
||||
(0, Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn file_open_bytes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let b = base64_decode(str_param(p, "base64").unwrap_or(""));
|
||||
// IDML packages are recognised by their stored `mimetype` first entry.
|
||||
|
|
@ -656,3 +676,87 @@ mod layout_place_tests {
|
|||
assert!(b.execute("file.place", &json!({"base64": b64, "name": "src.idml", "layoutPage": 2})).is_err(), "page 2 is empty");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod document_fonts_tests {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use designcraft_fonts::testing::font_with;
|
||||
use designcraft_fonts::{DOCUMENT_FONTS_FOLDER, FontDb};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("dc-open-docfonts-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(dir.join(DOCUMENT_FONTS_FOLDER)).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
/// A document whose text is set in `family`, written to `path` (`.idml`: as IDML).
|
||||
fn write_document(path: &Path, family: &str) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "Hello"})).unwrap();
|
||||
s.execute("text.select", &json!({"story": r["story"], "anchor": 0, "focus": 5})).unwrap();
|
||||
s.execute("type.char", &json!({"fontFamily": family, "fontStyle": "Regular"})).unwrap();
|
||||
if path.extension().is_some_and(|e| e == "idml") {
|
||||
std::fs::write(path, designcraft_idml::export_idml(&s.doc().unwrap().doc)).unwrap();
|
||||
} else {
|
||||
s.execute("file.save", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn font_entry(s: &mut Session, family: &str) -> Value {
|
||||
let l = s.execute("font.list", &json!({})).unwrap();
|
||||
l.as_array().unwrap().iter().find(|f| f["family"] == family).cloned().unwrap_or_else(|| panic!("{family} in {l}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_a_document_loads_its_document_fonts() {
|
||||
const FAMILY: &str = "DocFont Open Test";
|
||||
let dir = temp_dir("designcraft");
|
||||
let fonts = dir.join(DOCUMENT_FONTS_FOLDER);
|
||||
std::fs::write(fonts.join("open.ttf"), font_with(FAMILY, &['H', 'e', 'l', 'o']).unwrap()).unwrap();
|
||||
std::fs::write(fonts.join("broken.otf"), b"not a font").unwrap();
|
||||
let path = dir.join("Brochure.designcraft");
|
||||
write_document(&path, FAMILY);
|
||||
assert!(!FontDb::global().has_family(FAMILY));
|
||||
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("file.open", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
assert_eq!(r["documentFonts"], 1, "{r}");
|
||||
assert!(r["warnings"].to_string().contains("broken.otf"), "a damaged font is reported, not fatal: {r}");
|
||||
let f = font_entry(&mut s, FAMILY);
|
||||
assert_eq!((f["missing"].clone(), f["source"].clone()), (json!(false), json!("document")), "{f}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_an_idml_file_loads_its_document_fonts() {
|
||||
const FAMILY: &str = "DocFont IDML Test";
|
||||
let dir = temp_dir("idml");
|
||||
std::fs::write(dir.join(DOCUMENT_FONTS_FOLDER).join("idml.ttf"), font_with(FAMILY, &['H']).unwrap()).unwrap();
|
||||
let path = dir.join("Brochure.idml");
|
||||
write_document(&path, FAMILY);
|
||||
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("file.open", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
assert_eq!(r["documentFonts"], 1, "{r}");
|
||||
assert_eq!(font_entry(&mut s, FAMILY)["missing"], false);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_without_the_folder_opens_as_before() {
|
||||
let dir = std::env::temp_dir().join(format!("dc-open-docfonts-none-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("Plain.designcraft");
|
||||
write_document(&path, designcraft_fonts::DEFAULT_FAMILY);
|
||||
let r = Session::new().execute("file.open", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
assert_eq!((r["documentFonts"].clone(), r["warnings"].clone()), (json!(0), json!([])), "{r}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::collections::BTreeMap;
|
|||
use std::sync::Arc;
|
||||
|
||||
use designcraft_doc::{CharAttrs, Document, Story};
|
||||
use designcraft_fonts::FontSource;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, cmd, has_doc, str_param};
|
||||
|
|
@ -12,7 +13,7 @@ use crate::{Result, Session};
|
|||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(query "font.list", "Fonts in Document", [], None, "{} → [{family, style, characters, missing, styleMissing}] (missing first)", has_doc, |s, _| {
|
||||
cmd!(query "font.list", "Fonts in Document", [], None, "{} → [{family, style, characters, missing, styleMissing, source: bundled|installed|document|added (null when missing)}] (missing first)", has_doc, |s, _| {
|
||||
Ok(Value::Array(list(&s.doc()?.doc)))
|
||||
}),
|
||||
cmd!(
|
||||
|
|
@ -73,7 +74,16 @@ fn list(d: &Document) -> Vec<Value> {
|
|||
.map(|((family, style), n)| {
|
||||
let missing = !db.has_family(&family);
|
||||
let style_missing = !missing && !db.styles(&family).iter().any(|s| s.eq_ignore_ascii_case(&style));
|
||||
(missing || style_missing, json!({"family": family, "style": style, "characters": n, "missing": missing, "styleMissing": style_missing}))
|
||||
let source = (!missing).then(|| match db.face(&family, &style).source {
|
||||
FontSource::Bundled => "bundled",
|
||||
FontSource::Installed(_) => "installed",
|
||||
FontSource::Document(_) => "document",
|
||||
FontSource::Memory => "added",
|
||||
});
|
||||
(
|
||||
missing || style_missing,
|
||||
json!({"family": family, "style": style, "characters": n, "missing": missing, "styleMissing": style_missing, "source": source}),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
out.sort_by_key(|(m, _)| !*m);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"{path?, embedImages?: true} — writes an IDML package to `path`, or returns {base64} without a path",
|
||||
has_doc, export_idml),
|
||||
cmd!(noundo "file.openIdml", "Open IDML", [], None,
|
||||
"{path | base64, name?} — opens an IDML package as a new document (linked images are read next to the file or from its Links/ folder)",
|
||||
"{path | base64, name?} — opens an IDML package as a new document (linked images are read next to the file or from its Links/ folder; the fonts in a `Document Fonts` folder beside it load first) → {index, documentFonts, warnings}",
|
||||
always, open_idml),
|
||||
]
|
||||
}
|
||||
|
|
@ -71,7 +71,11 @@ pub(crate) fn open_idml(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
if let Some(n) = name {
|
||||
d.title = n;
|
||||
}
|
||||
let (faces, warnings) = match str_param(p, "path").filter(|_| str_param(p, "base64").is_none()) {
|
||||
Some(path) => super::file::load_document_fonts(path),
|
||||
None => (0, Vec::new()),
|
||||
};
|
||||
// Never save over the .idml with the native format: the document starts unsaved.
|
||||
let i = s.add_document(DocState::new(d, None));
|
||||
Ok(json!({"index": i}))
|
||||
Ok(json!({"index": i, "documentFonts": faces, "warnings": warnings}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,5 +297,6 @@ pub fn file_bytes(d: &designcraft_doc::Document) -> Vec<u8> {
|
|||
pub fn file_from(b: &[u8]) -> Result<designcraft_doc::Document> {
|
||||
file::from_bytes(b)
|
||||
}
|
||||
pub(crate) use file::load_document_fonts;
|
||||
pub use file::{base64_decode, base64_encode, from_bytes, to_bytes};
|
||||
pub(crate) use place_text::autoflow as place_text_autoflow;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
|
||||
use designcraft_doc::{AssetId, Document};
|
||||
use designcraft_fonts::{FontFace, FontSource};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, cmd, has_doc, str_param};
|
||||
|
|
@ -16,7 +17,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Package…",
|
||||
["File"],
|
||||
None,
|
||||
"{dir: folder to create, idml?: true, pdf?: false, instructions?: text} — the document (its placed files relinked to Links/), Links/, an IDML copy, an optional PDF and a report → {dir, files, report}",
|
||||
"{dir: folder to create, idml?: true, pdf?: false, instructions?: text} — the document (its placed files relinked to Links/), Links/, the font files it uses in Document Fonts/ (unless their licence restricts it), an IDML copy, an optional PDF and a report → {dir, files, report}",
|
||||
has_doc,
|
||||
package
|
||||
),
|
||||
|
|
@ -36,6 +37,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
fn link_name(name: &str, id: AssetId, taken: &mut Vec<String>) -> String {
|
||||
let base =
|
||||
Path::new(name).file_name().map(|n| n.to_string_lossy().to_string()).filter(|n| !n.is_empty()).unwrap_or_else(|| format!("asset-{}", id.0));
|
||||
unique_name(base, taken)
|
||||
}
|
||||
|
||||
/// `base`, or `base` numbered ("logo 2.png"), unique among `taken` (case-insensitively).
|
||||
fn unique_name(base: String, taken: &mut Vec<String>) -> String {
|
||||
let (stem, ext) = match base.rsplit_once('.') {
|
||||
Some((s, e)) => (s.to_string(), format!(".{e}")),
|
||||
None => (base.clone(), String::new()),
|
||||
|
|
@ -74,6 +80,36 @@ fn write_links(d: &Document, dir: &Path, only: Option<&[AssetId]>) -> Result<(Do
|
|||
Ok((out, written))
|
||||
}
|
||||
|
||||
/// Copy the font file of `face` into `dir` (created when first needed) unless its licence
|
||||
/// restricts it or it is already there (`copied`: the source files copied) → what the package
|
||||
/// report adds after the font's name (nothing when copied).
|
||||
fn copy_font(face: &FontFace, dir: &Path, copied: &mut Vec<PathBuf>, taken: &mut Vec<String>, files: &mut Vec<PathBuf>) -> String {
|
||||
let from = match &face.source {
|
||||
FontSource::Bundled => return " — included with DesignCraft".into(),
|
||||
FontSource::Memory => return " — not copied: no font file".into(),
|
||||
FontSource::Installed(p) | FontSource::Document(p) => p,
|
||||
};
|
||||
if face.restricted_licence() {
|
||||
return " — not copied: its licence doesn't allow it".into();
|
||||
}
|
||||
if copied.contains(from) {
|
||||
return String::new();
|
||||
}
|
||||
let name = from.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "font".into());
|
||||
let to = dir.join(unique_name(name, taken));
|
||||
// Packaging into the folder the font is in: it is there already (and copying a file onto
|
||||
// itself empties it on some systems).
|
||||
let same = std::fs::canonicalize(&to).is_ok_and(|t| std::fs::canonicalize(from).is_ok_and(|f| f == t));
|
||||
match std::fs::create_dir_all(dir).and_then(|_| if same { Ok(0) } else { std::fs::copy(from, &to) }) {
|
||||
Ok(_) => {
|
||||
copied.push(from.clone());
|
||||
files.push(to);
|
||||
String::new()
|
||||
}
|
||||
Err(e) => format!(" — not copied: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dir = PathBuf::from(str_param(p, "dir").ok_or_else(|| bad("file.package", "missing `dir`"))?);
|
||||
let st = s.doc()?;
|
||||
|
|
@ -86,6 +122,17 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let doc_path = dir.join(format!("{title}.designcraft"));
|
||||
std::fs::write(&doc_path, super::to_bytes(&packed)).map_err(|e| EngineError::Other(format!("{}: {e}", doc_path.display())))?;
|
||||
files.insert(0, doc_path);
|
||||
// The font files, and what the report says about each font.
|
||||
let db = designcraft_fonts::FontDb::global();
|
||||
let (fonts_dir, mut copied, mut taken) = (dir.join(designcraft_fonts::DOCUMENT_FONTS_FOLDER), Vec::new(), Vec::new());
|
||||
let mut font_lines = Vec::new();
|
||||
for f in fonts.as_array().into_iter().flatten() {
|
||||
let (family, style) = (f["family"].as_str().unwrap_or(""), f["style"].as_str().unwrap_or(""));
|
||||
let missing = f["missing"].as_bool().unwrap_or(false) || f["styleMissing"].as_bool().unwrap_or(false);
|
||||
let note =
|
||||
if missing { " — MISSING".to_string() } else { copy_font(&db.face(family, style), &fonts_dir, &mut copied, &mut taken, &mut files) };
|
||||
font_lines.push(format!(" {family} {style}{note}\n"));
|
||||
}
|
||||
if p.get("idml").and_then(Value::as_bool).unwrap_or(true) {
|
||||
let path = dir.join(format!("{title}.idml"));
|
||||
std::fs::write(&path, designcraft_idml::export_idml(&packed)).map_err(|e| EngineError::Other(format!("{}: {e}", path.display())))?;
|
||||
|
|
@ -103,10 +150,7 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
r += &format!("Instructions\n{t}\n\n");
|
||||
}
|
||||
r += "Fonts\n";
|
||||
for f in fonts.as_array().into_iter().flatten() {
|
||||
let missing = f["missing"].as_bool().unwrap_or(false) || f["styleMissing"].as_bool().unwrap_or(false);
|
||||
r += &format!(" {} {}{}\n", f["family"].as_str().unwrap_or(""), f["style"].as_str().unwrap_or(""), if missing { " — MISSING" } else { "" });
|
||||
}
|
||||
r += &font_lines.concat();
|
||||
r += "\nLinks\n";
|
||||
for l in links.as_array().into_iter().flatten() {
|
||||
r += &format!(" {} ({})\n", l["name"].as_str().unwrap_or(""), l["status"].as_str().unwrap_or(""));
|
||||
|
|
@ -171,4 +215,49 @@ mod tests {
|
|||
assert!(s.doc().unwrap().doc.assets.values().next().unwrap().link.as_deref().unwrap().contains("Copies"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_copies_the_fonts_their_licences_allow() {
|
||||
use designcraft_fonts::DOCUMENT_FONTS_FOLDER;
|
||||
use designcraft_fonts::testing::{font_with, with_fs_type};
|
||||
const FREE: &str = "DocFont Package Free";
|
||||
const RESTRICTED: &str = "DocFont Package Restricted";
|
||||
let dir = std::env::temp_dir().join(format!("dc-package-fonts-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let fonts = dir.join("src").join(DOCUMENT_FONTS_FOLDER);
|
||||
std::fs::create_dir_all(&fonts).unwrap();
|
||||
std::fs::write(fonts.join("free.ttf"), font_with(FREE, &['F']).unwrap()).unwrap();
|
||||
std::fs::write(fonts.join("restricted.otf"), with_fs_type(font_with(RESTRICTED, &['R']).unwrap(), 0x0002).unwrap()).unwrap();
|
||||
// Text in both fonts, and in the bundled default font.
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "Free Restricted Default"})).unwrap();
|
||||
for (a, b, family) in [(0, 4, FREE), (5, 15, RESTRICTED)] {
|
||||
s.execute("text.select", &json!({"story": r["story"], "anchor": a, "focus": b})).unwrap();
|
||||
s.execute("type.char", &json!({"fontFamily": family, "fontStyle": "Regular"})).unwrap();
|
||||
}
|
||||
let src = dir.join("src").join("Fonts.designcraft");
|
||||
s.execute("file.save", &json!({"path": src.to_string_lossy()})).unwrap();
|
||||
let mut s = Session::new();
|
||||
assert_eq!(s.execute("file.open", &json!({"path": src.to_string_lossy()})).unwrap()["documentFonts"], 2);
|
||||
|
||||
let out = dir.join("Pkg");
|
||||
let r = s.execute("file.package", &json!({"dir": out.to_string_lossy(), "idml": false})).unwrap();
|
||||
let packed_fonts = out.join(DOCUMENT_FONTS_FOLDER);
|
||||
assert!(packed_fonts.join("free.ttf").exists());
|
||||
assert!(!packed_fonts.join("restricted.otf").exists(), "a restricted licence keeps the font out");
|
||||
assert!(r["files"].to_string().contains("free.ttf"), "{}", r["files"]);
|
||||
let report = r["report"].as_str().unwrap();
|
||||
assert!(report.contains(&format!("{RESTRICTED} Regular — not copied: its licence doesn't allow it")), "{report}");
|
||||
assert!(report.contains(&format!("{FREE} Regular\n")), "{report}");
|
||||
assert!(report.contains(&format!("{} Regular — included with DesignCraft", designcraft_fonts::DEFAULT_FAMILY)), "{report}");
|
||||
// The packaged document opens with its fonts.
|
||||
let doc = r["files"][0].as_str().unwrap().to_string();
|
||||
assert_eq!(Session::new().execute("file.open", &json!({"path": doc})).unwrap()["documentFonts"], 1);
|
||||
// Packaging into the folder the fonts come from leaves them whole.
|
||||
let len = std::fs::metadata(fonts.join("free.ttf")).unwrap().len();
|
||||
s.execute("file.package", &json!({"dir": dir.join("src").to_string_lossy(), "idml": false})).unwrap();
|
||||
assert_eq!(std::fs::metadata(fonts.join("free.ttf")).unwrap().len(), len);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ pub fn open(s: &mut Session, dir: &Path) -> Result<Vec<usize>> {
|
|||
Err(_) => continue,
|
||||
};
|
||||
let path = meta.get("path").and_then(Value::as_str).map(str::to_string);
|
||||
if let Some(p) = &path {
|
||||
// Skipped font files are logged; the document opens without them.
|
||||
let _ = crate::cmd::load_document_fonts(p);
|
||||
}
|
||||
let mut st = DocState::new(d, path);
|
||||
// Unsaved: the copy on disk (if any) is older than what was recovered.
|
||||
st.saved_doc = std::sync::Arc::new((*st.doc).clone());
|
||||
|
|
|
|||
|
|
@ -43,6 +43,45 @@ enum FontBytes {
|
|||
Owned(Arc<Vec<u8>>),
|
||||
}
|
||||
|
||||
/// Where a face's font came from.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FontSource {
|
||||
/// Compiled into DesignCraft.
|
||||
Bundled,
|
||||
/// Font data handed to [`FontDb::add_font`], with no file behind it.
|
||||
Memory,
|
||||
/// An installed font file (the system's or the user's font folders).
|
||||
Installed(std::path::PathBuf),
|
||||
/// A font file in a document's `Document Fonts` folder ([`FontDb::load_document_fonts`]).
|
||||
Document(std::path::PathBuf),
|
||||
}
|
||||
|
||||
impl FontSource {
|
||||
/// The font file, if the face was loaded from one.
|
||||
pub fn path(&self) -> Option<&std::path::Path> {
|
||||
match self {
|
||||
FontSource::Installed(p) | FontSource::Document(p) => Some(p),
|
||||
FontSource::Bundled | FontSource::Memory => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The folder beside a document whose fonts the document brings with it (as File › Package
|
||||
/// writes it).
|
||||
pub const DOCUMENT_FONTS_FOLDER: &str = "Document Fonts";
|
||||
/// Caps on what a `Document Fonts` folder (untrusted files) can make [`FontDb::load_document_fonts`]
|
||||
/// read: font files read, bytes per file and bytes in all.
|
||||
pub const MAX_DOCUMENT_FONT_FILES: usize = 200;
|
||||
pub const MAX_DOCUMENT_FONT_BYTES: u64 = 256 << 20;
|
||||
pub const MAX_DOCUMENT_FONTS_TOTAL: u64 = 512 << 20;
|
||||
|
||||
/// What [`FontDb::load_document_fonts`] did: faces added, and a note for each file it skipped.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DocumentFonts {
|
||||
pub faces: usize,
|
||||
pub skipped: Vec<String>,
|
||||
}
|
||||
|
||||
/// One loaded font face.
|
||||
pub struct FontFace {
|
||||
id: u32,
|
||||
|
|
@ -53,6 +92,8 @@ pub struct FontFace {
|
|||
/// usWeightClass-style weight (400 = regular).
|
||||
pub weight: f32,
|
||||
pub italic: bool,
|
||||
/// Where the font came from (a file to copy when packaging, a document's own font).
|
||||
pub source: FontSource,
|
||||
bytes: FontBytes,
|
||||
index: u32,
|
||||
pub upem: f64,
|
||||
|
|
@ -213,6 +254,15 @@ impl FontFace {
|
|||
.and_then(|gdef| gdef.glyph_class_def()?.ok())
|
||||
.is_some_and(|classes| classes.get(GlyphId::new(gid)) == 3)
|
||||
}
|
||||
|
||||
/// Does the font's licence forbid passing it on? OS/2 `fsType` Restricted License embedding
|
||||
/// (bit 1) without a less restrictive bit (Preview & Print, Editable). Fonts without an OS/2
|
||||
/// table aren't restricted.
|
||||
pub fn restricted_licence(&self) -> bool {
|
||||
use skrifa::raw::TableProvider;
|
||||
let fs_type = self.skrifa().and_then(|f| f.os2().ok()).map_or(0, |t| t.fs_type());
|
||||
fs_type & 0x0002 != 0 && fs_type & 0x000C == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
@ -492,9 +542,11 @@ type Found = (u32, String, String, Vec<([u8; 4], f32)>);
|
|||
/// Parse every face in `data` (a font file or collection); a variable font yields one face per
|
||||
/// named instance (InDesign lists them as styles).
|
||||
fn enumerate_faces(data: &[u8]) -> Vec<Found> {
|
||||
/// A collection's count is input: what a damaged one can make the loop try.
|
||||
const MAX_FACES: u32 = 1024;
|
||||
let count = match FileRef::new(data) {
|
||||
Ok(FileRef::Font(_)) => 1,
|
||||
Ok(FileRef::Collection(c)) => c.len(),
|
||||
Ok(FileRef::Collection(c)) => c.len().min(MAX_FACES),
|
||||
Err(_) => 0,
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
|
|
@ -527,14 +579,14 @@ pub(crate) fn last_resort_face() -> Arc<FontFace> {
|
|||
// The font is compiled in (`include_bytes!`), so parsing it can't depend on input; the
|
||||
// `last_resort_face_parses` test proves it on every run.
|
||||
#[allow(clippy::expect_used)]
|
||||
let face = make_face(FontBytes::Static(BUNDLED[0]), 0, FALLBACK_FAMILY.into(), "Regular".into(), Vec::new())
|
||||
let face = make_face(FontBytes::Static(BUNDLED[0]), FontSource::Bundled, 0, FALLBACK_FAMILY.into(), "Regular".into(), Vec::new())
|
||||
.expect("the compiled-in Source Sans 3 Regular parses");
|
||||
Arc::new(face)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn make_face(bytes: FontBytes, index: u32, family: String, style: String, coords: Vec<([u8; 4], f32)>) -> Option<FontFace> {
|
||||
fn make_face(bytes: FontBytes, source: FontSource, index: u32, family: String, style: String, coords: Vec<([u8; 4], f32)>) -> Option<FontFace> {
|
||||
let data: &[u8] = match &bytes {
|
||||
FontBytes::Static(b) => b,
|
||||
FontBytes::Owned(v) => v.as_slice(),
|
||||
|
|
@ -569,6 +621,7 @@ fn make_face(bytes: FontBytes, index: u32, family: String, style: String, coords
|
|||
coords,
|
||||
location,
|
||||
instance,
|
||||
source,
|
||||
bytes,
|
||||
index,
|
||||
bmp: std::sync::OnceLock::new(),
|
||||
|
|
@ -619,7 +672,7 @@ impl FontDb {
|
|||
let craft = crate::japanese_document_fonts().into_iter().map(|f| f.bytes);
|
||||
for data in BUNDLED.iter().copied().chain(craft) {
|
||||
for (i, family, style, coords) in enumerate_faces(data) {
|
||||
if let Some(f) = make_face(FontBytes::Static(data), i, family, style, coords) {
|
||||
if let Some(f) = make_face(FontBytes::Static(data), FontSource::Bundled, i, family, style, coords) {
|
||||
faces.push(Arc::new(f));
|
||||
}
|
||||
}
|
||||
|
|
@ -699,13 +752,30 @@ impl FontDb {
|
|||
/// Add a user font (TTF/OTF/TTC bytes). Returns the number of faces added (0 if unparseable or
|
||||
/// every face was already present).
|
||||
pub fn add_font(&self, bytes: Vec<u8>) -> usize {
|
||||
self.add_faces(bytes, FontSource::Memory)
|
||||
}
|
||||
|
||||
/// Add the faces of a font file from `source`. A document's font is skipped only when the same
|
||||
/// folder already gave that family and style (it stands in for other fonts of the name); any
|
||||
/// other is skipped when a face of that family and style is loaded.
|
||||
fn add_faces(&self, bytes: Vec<u8>, source: FontSource) -> usize {
|
||||
let data = Arc::new(bytes);
|
||||
let folder = |s: &FontSource| match s {
|
||||
FontSource::Document(p) => p.parent().map(std::path::Path::to_path_buf),
|
||||
_ => None,
|
||||
};
|
||||
let doc_folder = folder(&source);
|
||||
let mut added = 0;
|
||||
for (i, family, style, coords) in enumerate_faces(&data) {
|
||||
if self.read_faces().iter().any(|f| f.family.eq_ignore_ascii_case(&family) && f.style.eq_ignore_ascii_case(&style)) {
|
||||
let same = |f: &&Arc<FontFace>| {
|
||||
f.family.eq_ignore_ascii_case(&family)
|
||||
&& f.style.eq_ignore_ascii_case(&style)
|
||||
&& (doc_folder.is_none() || folder(&f.source) == doc_folder)
|
||||
};
|
||||
if self.read_faces().iter().any(|f| same(&f)) {
|
||||
continue;
|
||||
}
|
||||
if let Some(f) = make_face(FontBytes::Owned(data.clone()), i, family, style, coords) {
|
||||
if let Some(f) = make_face(FontBytes::Owned(data.clone()), source.clone(), i, family, style, coords) {
|
||||
self.faces.write().unwrap_or_else(|e| e.into_inner()).push(Arc::new(f));
|
||||
added += 1;
|
||||
}
|
||||
|
|
@ -789,12 +859,72 @@ impl FontDb {
|
|||
let mut any = false;
|
||||
for p in paths {
|
||||
if let Ok(data) = std::fs::read(&p) {
|
||||
any |= self.add_font(data) > 0;
|
||||
any |= self.add_faces(data, FontSource::Installed(p)) > 0;
|
||||
}
|
||||
}
|
||||
any
|
||||
}
|
||||
|
||||
/// Load the font files (.ttf, .otf, .ttc, .otc; not in subfolders) of a document's `Document
|
||||
/// Fonts` folder. Their faces come before other faces of the same family and style when a
|
||||
/// family is looked up, and stay loaded. The files are untrusted: past
|
||||
/// [`MAX_DOCUMENT_FONT_FILES`] (in name order), files over [`MAX_DOCUMENT_FONT_BYTES`] or past
|
||||
/// [`MAX_DOCUMENT_FONTS_TOTAL`] in all, and files that aren't fonts are skipped with a note. A
|
||||
/// missing folder loads nothing.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn load_document_fonts(&self, folder: &std::path::Path) -> DocumentFonts {
|
||||
use std::io::Read;
|
||||
let mut out = DocumentFonts::default();
|
||||
let Ok(entries) = std::fs::read_dir(folder) else { return out };
|
||||
let mut files: Vec<std::path::PathBuf> = entries
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| {
|
||||
let ext = p.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase());
|
||||
matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) && p.is_file()
|
||||
})
|
||||
.collect();
|
||||
files.sort();
|
||||
let mut total = 0u64;
|
||||
for (n, path) in files.iter().enumerate() {
|
||||
if n == MAX_DOCUMENT_FONT_FILES {
|
||||
out.skipped.push(format!("{} more font files not loaded (at most {MAX_DOCUMENT_FONT_FILES} are)", files.len() - n));
|
||||
break;
|
||||
}
|
||||
let name = path.file_name().map_or_else(|| path.display().to_string(), |n| n.to_string_lossy().into_owned());
|
||||
let mut data = Vec::new();
|
||||
let too_large = format!("larger than {} MB", MAX_DOCUMENT_FONT_BYTES >> 20);
|
||||
let over_total = format!("over the {} MB the folder's fonts may take in all", MAX_DOCUMENT_FONTS_TOTAL >> 20);
|
||||
// The listed length skips oversized files unread; one byte past the limit while reading
|
||||
// catches a file that grew since.
|
||||
let listed = std::fs::metadata(path).map_or(0, |m| m.len());
|
||||
let read = if listed > MAX_DOCUMENT_FONT_BYTES || total.saturating_add(listed) > MAX_DOCUMENT_FONTS_TOTAL {
|
||||
Ok(0)
|
||||
} else {
|
||||
std::fs::File::open(path).and_then(|f| f.take(MAX_DOCUMENT_FONT_BYTES + 1).read_to_end(&mut data))
|
||||
};
|
||||
let reason = match read {
|
||||
Err(e) => Some(format!("can't be read ({e})")),
|
||||
Ok(_) if listed > MAX_DOCUMENT_FONT_BYTES => Some(too_large),
|
||||
Ok(len) if len as u64 > MAX_DOCUMENT_FONT_BYTES => Some(too_large),
|
||||
Ok(len) if total.saturating_add(listed.max(len as u64)) > MAX_DOCUMENT_FONTS_TOTAL => Some(over_total),
|
||||
Ok(_) if enumerate_faces(&data).is_empty() => Some("not a font DesignCraft can read".into()),
|
||||
Ok(len) => {
|
||||
total = total.saturating_add(len as u64);
|
||||
None
|
||||
}
|
||||
};
|
||||
match reason {
|
||||
Some(r) => out.skipped.push(format!("{name}: {r}")),
|
||||
None => out.faces += self.add_faces(data, FontSource::Document(path.clone())),
|
||||
}
|
||||
}
|
||||
for s in &out.skipped {
|
||||
log::warn!("{}: {s}", folder.display());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Resolve a family + style to a face, falling back to the closest style of the family, then to
|
||||
/// Source Sans 3 Regular. Installed system fonts are found by name whatever ran before.
|
||||
pub fn face(&self, family: &str, style: &str) -> Arc<FontFace> {
|
||||
|
|
@ -864,7 +994,7 @@ impl FontDb {
|
|||
None => coords.push((tag, v)),
|
||||
}
|
||||
}
|
||||
let f = make_face(base.bytes.clone(), base.index, base.family.clone(), style.to_string(), coords)?;
|
||||
let f = make_face(base.bytes.clone(), base.source.clone(), base.index, base.family.clone(), style.to_string(), coords)?;
|
||||
let f = Arc::new(f);
|
||||
self.faces.write().unwrap_or_else(|e| e.into_inner()).push(f.clone());
|
||||
Some(f)
|
||||
|
|
@ -881,7 +1011,12 @@ impl FontDb {
|
|||
return self.instance(family, style);
|
||||
}
|
||||
let faces = self.read_faces();
|
||||
let cands: Vec<&Arc<FontFace>> = faces.iter().filter(|f| f.family.eq_ignore_ascii_case(family)).collect();
|
||||
// Document fonts first, the most recently loaded first: they stand in for other fonts of
|
||||
// the same name.
|
||||
let (mut cands, others): (Vec<&Arc<FontFace>>, Vec<&Arc<FontFace>>) =
|
||||
faces.iter().filter(|f| f.family.eq_ignore_ascii_case(family)).partition(|f| matches!(f.source, FontSource::Document(_)));
|
||||
cands.reverse();
|
||||
cands.extend(others);
|
||||
if cands.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -977,7 +1112,7 @@ impl FontDb {
|
|||
let Ok(data) = std::fs::read(&p) else { continue };
|
||||
let hit =
|
||||
enumerate_faces(&data).iter().any(|(i, _, _, _)| skrifa::FontRef::from_index(&data, *i).is_ok_and(|f| f.charmap().map(c).is_some()));
|
||||
if hit && self.add_font(data) > 0 && covered(self) {
|
||||
if hit && self.add_faces(data, FontSource::Installed(p)) > 0 && covered(self) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1036,3 +1171,8 @@ mod tests_sysfonts;
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[path = "tests_fallback.rs"]
|
||||
mod tests_fallback;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[path = "tests_docfonts.rs"]
|
||||
mod tests_docfonts;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ mod fontdb;
|
|||
#[cfg(any(test, feature = "testing"))]
|
||||
pub mod testing;
|
||||
|
||||
pub use fontdb::{FALLBACK_FAMILY, FaceRef, FontDb, FontFace, base_style, bundled, system_font_dirs};
|
||||
pub use fontdb::{
|
||||
DOCUMENT_FONTS_FOLDER, DocumentFonts, FALLBACK_FAMILY, FaceRef, FontDb, FontFace, FontSource, MAX_DOCUMENT_FONT_BYTES, MAX_DOCUMENT_FONT_FILES,
|
||||
MAX_DOCUMENT_FONTS_TOTAL, base_style, bundled, system_font_dirs,
|
||||
};
|
||||
pub use harfrust::Feature;
|
||||
use harfrust::{Direction, Language, ShapeOptions, Tag, UnicodeBuffer};
|
||||
pub use kurbo::BezPath;
|
||||
|
|
|
|||
|
|
@ -82,3 +82,13 @@ pub fn font_with(family: &str, chars: &[char]) -> Option<Vec<u8>> {
|
|||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// `font` with its OS/2 `fsType` (embedding licence bits) set to `fs_type`. `None` if the font has
|
||||
/// no OS/2 table.
|
||||
pub fn with_fs_type(mut font: Vec<u8>, fs_type: u16) -> Option<Vec<u8>> {
|
||||
let tables = usize::from(u16::from_be_bytes(font.get(4..6)?.try_into().ok()?));
|
||||
let rec = (0..tables).map(|r| 12 + r * 16).find(|rec| font.get(*rec..rec + 4) == Some(b"OS/2"))?;
|
||||
let offset = u32::from_be_bytes(font.get(rec + 8..rec + 12)?.try_into().ok()?) as usize;
|
||||
font.get_mut(offset + 8..offset + 10)?.copy_from_slice(&fs_type.to_be_bytes());
|
||||
Some(font)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
//! Document fonts: the fonts in a `Document Fonts` folder beside a document, loaded ahead of the
|
||||
//! installed fonts of the same name; damaged, oversized and surplus files are skipped.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::*;
|
||||
use crate::testing::{font_with, with_fs_type};
|
||||
|
||||
/// An empty temporary folder unique to `name` and this process.
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("dc-docfonts-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
fn write_font(dir: &Path, file: &str, family: &str, chars: &[char]) -> PathBuf {
|
||||
let path = dir.join(file);
|
||||
std::fs::write(&path, font_with(family, chars).unwrap()).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_fonts_load_ahead_of_installed_ones() {
|
||||
let root = temp_dir("precedence");
|
||||
let system = root.join("system");
|
||||
let folder = root.join(DOCUMENT_FONTS_FOLDER);
|
||||
std::fs::create_dir_all(&system).unwrap();
|
||||
std::fs::create_dir_all(&folder).unwrap();
|
||||
write_font(&system, "installed.ttf", "DocFont Precedence", &['a']);
|
||||
let file = write_font(&folder, "doc.ttf", "DocFont Precedence", &['b']);
|
||||
|
||||
let db = FontDb::with_font_dirs(vec![system]);
|
||||
// The installed face is loaded first (the document was composed before its folder appeared).
|
||||
let installed = db.face("DocFont Precedence", "Regular");
|
||||
assert!(installed.covers('a') && matches!(installed.source, FontSource::Installed(_)), "{:?}", installed.source);
|
||||
|
||||
let r = db.load_document_fonts(&folder);
|
||||
assert_eq!((r.faces, r.skipped.len()), (1, 0), "{:?}", r.skipped);
|
||||
let face = db.face("DocFont Precedence", "Regular");
|
||||
assert!(face.covers('b'), "the document's face wins");
|
||||
assert_eq!(face.source, FontSource::Document(file.clone()));
|
||||
assert_eq!(face.source.path(), Some(file.as_path()));
|
||||
|
||||
// Loading the folder again adds nothing.
|
||||
let again = db.load_document_fonts(&folder);
|
||||
assert_eq!((again.faces, again.skipped.len()), (0, 0));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_oversized_and_other_files_are_skipped() {
|
||||
let folder = temp_dir("skipped");
|
||||
write_font(&folder, "good.otf", "DocFont Good", &['g']);
|
||||
std::fs::write(folder.join("broken.ttf"), b"not a font at all").unwrap();
|
||||
// Sparse: takes no disk space, but its length is over the limit.
|
||||
std::fs::File::create(folder.join("huge.ttc")).unwrap().set_len(MAX_DOCUMENT_FONT_BYTES + 1).unwrap();
|
||||
std::fs::write(folder.join("readme.txt"), b"fonts for the brochure").unwrap();
|
||||
std::fs::create_dir_all(folder.join("nested.ttf")).unwrap();
|
||||
write_font(&folder.join("nested.ttf"), "inner.ttf", "DocFont Nested", &['n']);
|
||||
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
let r = db.load_document_fonts(&folder);
|
||||
assert_eq!(r.faces, 1);
|
||||
assert!(db.has_family("DocFont Good"));
|
||||
assert!(!db.has_family("DocFont Nested"), "subfolders aren't read");
|
||||
assert_eq!(r.skipped.len(), 2, "{:?}", r.skipped);
|
||||
assert!(r.skipped.iter().any(|s| s.contains("broken.ttf")), "{:?}", r.skipped);
|
||||
assert!(r.skipped.iter().any(|s| s.contains("huge.ttc") && s.contains(&format!("{} MB", MAX_DOCUMENT_FONT_BYTES >> 20))), "{:?}", r.skipped);
|
||||
let _ = std::fs::remove_dir_all(&folder);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_number_of_files_read_is_capped() {
|
||||
let folder = temp_dir("cap");
|
||||
for i in 0..MAX_DOCUMENT_FONT_FILES + 5 {
|
||||
std::fs::write(folder.join(format!("{i:04}.ttf")), b"junk").unwrap();
|
||||
}
|
||||
// Sorts after every junk file, so it is past the cap.
|
||||
write_font(&folder, "zz-real.ttf", "DocFont Past Cap", &['z']);
|
||||
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
let r = db.load_document_fonts(&folder);
|
||||
assert_eq!(r.faces, 0);
|
||||
assert!(!db.has_family("DocFont Past Cap"));
|
||||
assert_eq!(r.skipped.len(), MAX_DOCUMENT_FONT_FILES + 1, "one note for each file read, one for the rest");
|
||||
assert!(r.skipped.last().is_some_and(|s| s.contains("6 more") && s.contains("200")), "{:?}", r.skipped.last());
|
||||
let _ = std::fs::remove_dir_all(&folder);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_cjk_collections_are_within_the_limit() {
|
||||
// macOS's Songti collection is 67 MB; full Noto/Source Han CJK collections are larger.
|
||||
let folder = temp_dir("cjk-size");
|
||||
std::fs::File::create(folder.join("cjk.ttc")).unwrap().set_len(200 << 20).unwrap();
|
||||
let r = FontDb::with_font_dirs(vec![]).load_document_fonts(&folder);
|
||||
assert_eq!(r.skipped.len(), 1, "{:?}", r.skipped);
|
||||
assert!(!r.skipped[0].contains("larger than"), "skipped for not being a font, not for its size: {:?}", r.skipped);
|
||||
let _ = std::fs::remove_dir_all(&folder);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_folder_loads_nothing() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
let r = db.load_document_fonts(&std::env::temp_dir().join("dc-docfonts-no-such-folder").join(DOCUMENT_FONTS_FOLDER));
|
||||
assert_eq!((r.faces, r.skipped.len()), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_licence_reads_the_embedding_bits() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
for (fs_type, restricted) in [(0x0000, false), (0x0002, true), (0x0006, false), (0x000A, false), (0x0008, false), (0x0302, true)] {
|
||||
let family = format!("DocFont Licence {fs_type:04x}");
|
||||
assert_eq!(db.add_font(with_fs_type(font_with(&family, &['l']).unwrap(), fs_type).unwrap()), 1);
|
||||
assert_eq!(db.face(&family, "Regular").restricted_licence(), restricted, "fsType {fs_type:#06x}");
|
||||
}
|
||||
assert!(!db.face(FALLBACK_FAMILY, "Regular").restricted_licence(), "the bundled fonts are OFL");
|
||||
assert_eq!(db.face(FALLBACK_FAMILY, "Regular").source, FontSource::Bundled);
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ presets or tables are copied. Our default mojikumi and kinsoku tables are built
|
|||
| Ruby | group ruby only, fixed 50 % size, no options, doesn't affect leading |
|
||||
| Kenten | the IDML kinds and a custom character, drawn as characters; no position, size, alignment, colour or font |
|
||||
| Composite fonts | done: model, `style.compositeFont.*`, shaping, IDML, font list, replace and Preflight; no dialog |
|
||||
| CJK fonts | Japanese faces (Shippori Mincho first) from the optional craft-fonts build input; fallback by language (Japanese: the craft-fonts faces, then system fonts; Simplified and Traditional Chinese, Korean: system fonts); no per-document fonts |
|
||||
| CJK fonts | Japanese faces (Shippori Mincho first) from the optional craft-fonts build input; fallback by language (Japanese: the craft-fonts faces, then system fonts; Simplified and Traditional Chinese, Korean: system fonts); document fonts: a `Document Fonts` folder beside an opened file loads ahead of installed fonts, and Package copies the fonts used there (licence permitting). They stay loaded for the session (shared by every open document, not unloaded on close) and aren't read on the web |
|
||||
| Everything else below | missing |
|
||||
|
||||
Known bugs to fix first:
|
||||
|
|
|
|||
Loading…
Reference in New Issue