mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
File › Package and Document Info › Font Details looked fonts up with FontDb::face and compared the face's family with the name the document stores. A font a document named by another of its names (a PostScript or localized name, as SVG and PDF files do) was reported as not available and left out of the package, although the canvas drew it and exports embedded it. - Package and Font Details resolve fonts with FontDb::resolve, as text.fonts and the renderer do. A style the family lacks is still copied as its closest style; the report now names the style it copies, and Font Details lists the font as substituted, with that style's file and permission. - FontFace::embeddable reads fsType as exports do (Embedding), so fonts that allow bitmap embedding only (bit 9), or that set the restricted bit together with reserved bit 0 (0x0003), are no longer copied. Document Info's permission labels follow the same rule: when a font sets more than one usage bit, the label names the most permissive one. - The Package dialog and its status line name the fonts it didn't copy.
130 lines
6.1 KiB
Rust
130 lines
6.1 KiB
Rust
//! Embedded fonts: licence flags and subsets a viewer can use.
|
|
|
|
use skrifa::MetadataProvider;
|
|
use skrifa::instance::{LocationRef, Size};
|
|
use skrifa::raw::types::Tag;
|
|
use skrifa::raw::{FontData, FontRead, TableProvider};
|
|
|
|
use super::embed::Embedding;
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn fs_type_flags_decide_the_embedding() {
|
|
assert_eq!(Embedding::from_fs_type(0), Embedding::Subset, "installable");
|
|
assert_eq!(Embedding::from_fs_type(0x0004), Embedding::Subset, "preview & print");
|
|
assert_eq!(Embedding::from_fs_type(0x0008), Embedding::Subset, "editable");
|
|
assert_eq!(Embedding::from_fs_type(0x0002), Embedding::Forbidden, "restricted");
|
|
assert_eq!(Embedding::from_fs_type(0x0006), Embedding::Subset, "the most permissive bit wins");
|
|
assert_eq!(Embedding::from_fs_type(0x0200), Embedding::Forbidden, "bitmaps only");
|
|
assert_eq!(Embedding::from_fs_type(0x0108), Embedding::Whole, "no subsetting");
|
|
}
|
|
|
|
/// The bundled Source Sans 3 Regular renamed `family` (13 characters, as long as the original
|
|
/// name) with OS/2 `fsType` set to `fs_type`.
|
|
fn licensed_font(family: &str, fs_type: u16) -> Vec<u8> {
|
|
let mut data = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf").to_vec();
|
|
let table = |data: &[u8], tag: &[u8; 4]| {
|
|
let f = skrifa::FontRef::new(data).unwrap();
|
|
let r = f.table_directory.table_records().iter().find(|r| r.tag().to_be_bytes() == *tag).unwrap();
|
|
(r.offset() as usize, r.length() as usize)
|
|
};
|
|
let (os2, _) = table(&data, b"OS/2");
|
|
data[os2 + 8..os2 + 10].copy_from_slice(&fs_type.to_be_bytes());
|
|
let (name, len) = table(&data, b"name");
|
|
assert_eq!(family.len(), "Source Sans 3".len());
|
|
let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::<Vec<u8>>();
|
|
for (from, to) in [(utf16("Source Sans 3"), utf16(family)), (b"Source Sans 3".to_vec(), family.as_bytes().to_vec())] {
|
|
let mut i = name;
|
|
while let Some(at) = data[i..name + len].windows(from.len()).position(|w| w == from) {
|
|
data[i + at..i + at + to.len()].copy_from_slice(&to);
|
|
i += at + to.len();
|
|
}
|
|
}
|
|
data
|
|
}
|
|
|
|
/// A font file goes with a document (File → Package) only when exports may embed it: not when
|
|
/// its license is restricted or allows bitmaps only.
|
|
#[test]
|
|
fn fonts_exports_may_not_embed_are_not_embeddable() {
|
|
let db = FontDb::with_font_dirs(vec![]);
|
|
for (family, fs_type, embeddable) in [("Bitmap Sans 3", 0x0200, false), ("Lowbit Sans 3", 0x0003, false), ("Wholed Sans 3", 0x0100, true)] {
|
|
assert!(db.add_font(licensed_font(family, fs_type)) > 0, "{family}");
|
|
let face = db.face(family, "Regular").unwrap();
|
|
assert_eq!((face.family.as_str(), face.embeddable()), (family, embeddable), "fsType {fs_type:#06x}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_subset_holds_only_the_glyphs_used_and_maps_their_characters() {
|
|
let face = FontDb::global().face("Source Sans 3", "Regular").unwrap();
|
|
assert_eq!(face.embedding(), Embedding::Subset);
|
|
let e = face.embed(&['H', 'e', 'l', 'l', 'o', ' ']).unwrap();
|
|
assert!(e.subset && !e.cff);
|
|
assert!(e.data.len() * 20 < face.data().len(), "{} of {} bytes", e.data.len(), face.data().len());
|
|
let f = skrifa::FontRef::new(&e.data).unwrap();
|
|
// .notdef and the five characters' glyphs.
|
|
assert_eq!(f.maxp().unwrap().num_glyphs(), 6);
|
|
let map = f.charmap();
|
|
let metrics = f.glyph_metrics(Size::unscaled(), LocationRef::default());
|
|
for c in ['H', 'e', 'l', 'o', ' '] {
|
|
let g = map.map(c).unwrap_or_else(|| panic!("{c} mapped"));
|
|
assert_eq!(metrics.advance_width(g).unwrap() as f64, face.advance(face.glyph_for(c)), "{c}: same advance");
|
|
}
|
|
assert!(map.map('x').is_none(), "unused characters are left out");
|
|
let h = f.outline_glyphs().get(map.map('H').unwrap()).unwrap();
|
|
let mut pen = Commands::default();
|
|
h.draw(skrifa::outline::DrawSettings::unhinted(Size::unscaled(), LocationRef::default()), &mut pen).unwrap();
|
|
assert!(pen.0 > 4, "H keeps its outline");
|
|
// The tables a web viewer needs, the licence flags kept and the file checksum right.
|
|
for t in [b"cmap", b"OS/2", b"head", b"hhea", b"hmtx", b"maxp", b"name", b"post", b"glyf", b"loca"] {
|
|
assert!(f.table_data(Tag::new(t)).is_some(), "{}", String::from_utf8_lossy(t));
|
|
}
|
|
let os2 = f.os2().unwrap();
|
|
assert_eq!(os2.fs_type(), face.skrifa().unwrap().os2().unwrap().fs_type());
|
|
assert_eq!((os2.us_first_char_index(), os2.us_last_char_index()), (' ' as u16, 'o' as u16));
|
|
let (words, rest) = e.data.as_chunks::<4>();
|
|
assert!(rest.is_empty());
|
|
assert_eq!(words.iter().fold(0u32, |s, w| s.wrapping_add(u32::from_be_bytes(*w))), 0xb1b0_afba);
|
|
}
|
|
|
|
#[test]
|
|
fn cmaps_map_runs_and_characters_beyond_the_basic_plane() {
|
|
let face = FontDb::global().face("Inter", "Regular").unwrap();
|
|
let e = face.embed(&['A', 'B', 'z']).unwrap();
|
|
let f = skrifa::FontRef::new(&e.data).unwrap();
|
|
let cmap = f.cmap().unwrap();
|
|
let (a, b, z) = (cmap.map_codepoint('A').unwrap(), cmap.map_codepoint('B').unwrap(), cmap.map_codepoint('z').unwrap());
|
|
assert!(a != b && b != z && a.to_u32() != 0);
|
|
assert_eq!(cmap.map_codepoint('C'), None);
|
|
// No bundled face maps characters beyond the BMP: the table is checked on its own.
|
|
let raw = embed::cmap(&[(0x41, 1), (0x42, 2), (0x1f600, 3)]).unwrap();
|
|
let cmap = skrifa::raw::tables::cmap::Cmap::read(FontData::new(&raw)).unwrap();
|
|
assert_eq!(cmap.encoding_records().len(), 2, "format 4 and format 12");
|
|
for (c, g) in [('A', 1), ('B', 2), ('\u{1f600}', 3)] {
|
|
assert_eq!(cmap.map_codepoint(c).map(|g| g.to_u32()), Some(g), "{c}");
|
|
}
|
|
assert_eq!(cmap.map_codepoint('C'), None);
|
|
assert!(embed::cmap(&[]).is_none());
|
|
}
|
|
|
|
/// Counts outline commands.
|
|
#[derive(Default)]
|
|
struct Commands(usize);
|
|
|
|
impl skrifa::outline::OutlinePen for Commands {
|
|
fn move_to(&mut self, _: f32, _: f32) {
|
|
self.0 += 1;
|
|
}
|
|
fn line_to(&mut self, _: f32, _: f32) {
|
|
self.0 += 1;
|
|
}
|
|
fn quad_to(&mut self, _: f32, _: f32, _: f32, _: f32) {
|
|
self.0 += 1;
|
|
}
|
|
fn curve_to(&mut self, _: f32, _: f32, _: f32, _: f32, _: f32, _: f32) {
|
|
self.0 += 1;
|
|
}
|
|
fn close(&mut self) {}
|
|
}
|