vectorcraft/crates/text/src/embed.rs

239 lines
9.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Fonts carried by exported documents (SVG `@font-face`): what a font's licence allows (its
//! OS/2 `fsType` flags) and font files holding only the characters a document uses.
use std::borrow::Cow;
use skrifa::MetadataProvider;
use skrifa::raw::TableProvider;
use skrifa::raw::types::Tag;
use crate::FontFace;
/// What a font's licence lets a document do with it (OS/2 `fsType`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Embedding {
/// The font may be embedded, as a subset.
Subset,
/// The font may be embedded whole only (no subsetting).
Whole,
/// Restricted licence, or bitmap embedding only: the outlines may not be embedded.
Forbidden,
}
impl Embedding {
/// The permission an OS/2 `fsType` value grants. Bits 0–3 are the usage permissions (none:
/// installable; 2: restricted; 4: preview and print; 8: editable), the most permissive one
/// winning when several are set; bit 8 forbids subsetting and bit 9 allows bitmaps only.
pub fn from_fs_type(fs: u16) -> Self {
let restricted = fs & 0x0002 != 0 && fs & 0x000c == 0;
if restricted || fs & 0x0200 != 0 {
Self::Forbidden
} else if fs & 0x0100 != 0 {
Self::Whole
} else {
Self::Subset
}
}
}
/// A font file a document carries.
#[derive(Clone, Debug)]
pub struct EmbeddedFont {
/// An OpenType file of the one face (`cmap`, `OS/2` and the other tables a viewer needs).
pub data: Vec<u8>,
/// CFF outlines (`font/otf`) rather than TrueType ones (`font/ttf`).
pub cff: bool,
/// Only the glyphs of the characters asked for; false: the whole face, as its licence asks
/// (or when it can't be subset).
pub subset: bool,
}
const CMAP: Tag = Tag::new(b"cmap");
const OS2: Tag = Tag::new(b"OS/2");
const HEAD: Tag = Tag::new(b"head");
impl FontFace {
/// What the face's licence allows (a face without an `OS/2` table states no restriction).
pub fn embedding(&self) -> Embedding {
self.skrifa().and_then(|f| f.os2().ok()).map_or(Embedding::Subset, |os2| Embedding::from_fs_type(os2.fs_type()))
}
/// The face as a font file to embed: the glyphs of `chars` alone when its licence allows
/// subsetting, else the whole face. Ligatures, kerning and other layout features are left out
/// of subsets (viewers draw each character's own glyph). `None` when the licence forbids
/// embedding or the font can't be read.
pub fn embed(&self, chars: &[char]) -> Option<EmbeddedFont> {
let font = self.skrifa()?;
let cff = [b"CFF ", b"CFF2"].iter().any(|t| font.table_data(Tag::new(t)).is_some());
let whole = || Some(EmbeddedFont { data: sfnt(font.table_directory.sfnt_version(), tables(&font))?, cff, subset: false });
match self.embedding() {
Embedding::Forbidden => None,
Embedding::Whole => whole(),
Embedding::Subset => match subset(self, &font, chars) {
Some(data) => Some(EmbeddedFont { data, cff, subset: true }),
// Outlines the subsetter can't read (CFF2): what the licence allows anyway.
None => whole(),
},
}
}
}
/// Every table of `font`, by tag.
fn tables<'a>(font: &skrifa::FontRef<'a>) -> Vec<(Tag, Cow<'a, [u8]>)> {
font.table_directory.table_records().iter().filter_map(|r| font.table_data(r.tag()).map(|d| (r.tag(), Cow::Borrowed(d.as_bytes())))).collect()
}
/// `face` reduced to the glyphs of `chars`, with a `cmap` mapping them and the face's `OS/2`.
fn subset(face: &FontFace, font: &skrifa::FontRef<'_>, chars: &[char]) -> Option<Vec<u8>> {
let charmap = font.charmap();
let mut remapper = subsetter::GlyphRemapper::new();
let mut map: Vec<(u32, u16)> = chars
.iter()
.filter_map(|c| {
let gid = u16::try_from(charmap.map(*c)?.to_u32()).ok().filter(|g| *g != 0)?;
Some((*c as u32, remapper.remap(gid)))
})
.collect();
map.sort_unstable();
map.dedup_by_key(|m| m.0);
let data = subsetter::subset(face.data(), face.index(), &remapper).ok()?;
let sub = skrifa::FontRef::new(&data).ok()?;
let mut t: Vec<(Tag, Cow<'_, [u8]>)> = tables(&sub).into_iter().filter(|(tag, _)| *tag != CMAP && *tag != OS2).collect();
t.push((CMAP, Cow::Owned(cmap(&map)?)));
if let Some(os2) = font.table_data(OS2) {
t.push((OS2, Cow::Owned(os2_for(os2.as_bytes(), &map))));
}
sfnt(sub.table_directory.sfnt_version(), t)
}
/// The face's `OS/2` table with its first and last character fitted to the subset's.
fn os2_for(os2: &[u8], map: &[(u32, u16)]) -> Vec<u8> {
let mut out = os2.to_vec();
let bmp = |c: u32| u16::try_from(c).unwrap_or(u16::MAX).to_be_bytes();
if let (Some(first), Some(last), Some(slot)) = (map.first(), map.last(), out.get_mut(64..68)) {
slot[..2].copy_from_slice(&bmp(first.0));
slot[2..].copy_from_slice(&bmp(last.0));
}
out
}
/// Runs of consecutive characters mapped to consecutive glyphs: (first char, last char, first
/// glyph).
fn runs(map: &[(u32, u16)]) -> Vec<(u32, u32, u16)> {
let mut out: Vec<(u32, u32, u16)> = Vec::new();
for &(c, g) in map {
match out.last_mut() {
Some((start, end, g0)) if c == *end + 1 && u32::from(g) == u32::from(*g0) + (c - *start) => *end = c,
_ => out.push((c, c, g)),
}
}
out
}
/// A `cmap` table mapping `map`'s characters (sorted, unique) to their glyphs: a format 4
/// subtable for the Basic Multilingual Plane and, when characters lie beyond it (or it would be
/// too long), a format 12 one for all of them. `None` when there is nothing to map.
pub(crate) fn cmap(map: &[(u32, u16)]) -> Option<Vec<u8>> {
let all = runs(map);
let last = all.last()?.1;
let mut bmp: Vec<(u32, u32, u16)> = runs(&map.iter().copied().filter(|(c, _)| *c < 0xffff).collect::<Vec<_>>());
// The closing segment every format 4 subtable ends with.
bmp.push((0xffff, 0xffff, 0));
let f4_len = 16 + 8 * bmp.len();
let f4 = f4_len <= usize::from(u16::MAX);
let f12 = !f4 || last >= 0xffff;
let mut subtables: Vec<(u16, Vec<u8>)> = Vec::new();
if f4 {
let seg_x2 = u16::try_from(bmp.len() * 2).ok()?;
let sel = bmp.len().ilog2() as u16;
let range = 2u16.checked_pow(u32::from(sel))?.checked_mul(2)?;
let mut s = Vec::with_capacity(f4_len);
for v in [4, u16::try_from(f4_len).ok()?, 0, seg_x2, range, sel, seg_x2 - range] {
s.extend(v.to_be_bytes());
}
let u = |c: u32| u16::try_from(c).unwrap_or(u16::MAX);
s.extend(bmp.iter().flat_map(|(_, end, _)| u(*end).to_be_bytes()));
s.extend(0u16.to_be_bytes());
s.extend(bmp.iter().flat_map(|(start, _, _)| u(*start).to_be_bytes()));
// The closing segment maps to glyph 0: 0xffff + 1.
s.extend(bmp.iter().flat_map(|(start, end, g)| if *end == 0xffff { 1u16 } else { g.wrapping_sub(u(*start)) }.to_be_bytes()));
s.extend(bmp.iter().flat_map(|_| 0u16.to_be_bytes()));
subtables.push((1, s));
}
if f12 {
let n = u32::try_from(all.len()).ok()?;
let mut s = Vec::with_capacity(16 + 12 * all.len());
s.extend(12u16.to_be_bytes());
s.extend(0u16.to_be_bytes());
for v in [16 + 12 * n, 0, n] {
s.extend(v.to_be_bytes());
}
for (start, end, g) in &all {
for v in [*start, *end, u32::from(*g)] {
s.extend(v.to_be_bytes());
}
}
subtables.push((10, s));
}
// Header and encoding records (Windows platform: 1 = BMP, 10 = full repertoire).
let mut out = Vec::new();
out.extend(0u16.to_be_bytes());
out.extend(u16::try_from(subtables.len()).ok()?.to_be_bytes());
let mut offset = 4 + 8 * subtables.len();
for (encoding, s) in &subtables {
out.extend(3u16.to_be_bytes());
out.extend(encoding.to_be_bytes());
out.extend(u32::try_from(offset).ok()?.to_be_bytes());
offset += s.len();
}
for (_, s) in subtables {
out.extend(s);
}
Some(out)
}
/// The sum of `data` as big-endian 32-bit words (zero-padded), as table directories record it.
fn checksum(data: &[u8]) -> u32 {
let (words, rest) = data.as_chunks::<4>();
let mut last = [0u8; 4];
last[..rest.len()].copy_from_slice(rest);
words.iter().chain([&last]).fold(0u32, |s, w| s.wrapping_add(u32::from_be_bytes(*w)))
}
/// An OpenType file of `tables` (unique tags) with checksums and the `head` checksum adjustment.
fn sfnt(version: u32, mut tables: Vec<(Tag, Cow<'_, [u8]>)>) -> Option<Vec<u8>> {
tables.sort_by_key(|t| t.0);
tables.dedup_by_key(|t| t.0);
let n = u16::try_from(tables.len()).ok().filter(|n| *n > 0)?;
let sel = n.ilog2() as u16;
let range = 2u16.checked_pow(u32::from(sel))?.checked_mul(16)?;
let mut out = Vec::with_capacity(12 + 16 * tables.len() + tables.iter().map(|t| t.1.len() + 3).sum::<usize>());
out.extend(version.to_be_bytes());
for v in [n, range, sel, n.checked_mul(16)? - range] {
out.extend(v.to_be_bytes());
}
let mut offset = 12 + 16 * tables.len();
let mut head_at = None;
for (tag, data) in &mut tables {
if *tag == HEAD {
// The adjustment is summed as zero; it is set once the whole file is written.
data.to_mut().get_mut(8..12)?.fill(0);
head_at = Some(offset + 8);
}
out.extend(tag.to_be_bytes());
out.extend(checksum(data).to_be_bytes());
out.extend(u32::try_from(offset).ok()?.to_be_bytes());
out.extend(u32::try_from(data.len()).ok()?.to_be_bytes());
offset += data.len().next_multiple_of(4);
}
for (_, data) in &tables {
out.extend_from_slice(data);
out.resize(out.len().next_multiple_of(4), 0);
}
if let Some(at) = head_at {
let adjust = 0xb1b0_afba_u32.wrapping_sub(checksum(&out));
out.get_mut(at..at + 4)?.copy_from_slice(&adjust.to_be_bytes());
}
Some(out)
}