mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-15 06:46:45 -04:00
Squash of PR #571 by @vangel76. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
70b0cce997
commit
7a13ca7d05
10 files changed
+1126
-19
No files matched your search
Generated
+1
@@ -655,6 +655,7 @@ dependencies = [
|
||||
"cadcraft-doc",
|
||||
"cadcraft-dwg",
|
||||
"cadcraft-dxf",
|
||||
"cadcraft-fonts",
|
||||
"cadcraft-geom",
|
||||
"cadcraft-render",
|
||||
"miniz_oxide 0.8.9",
|
||||
|
||||
+42
-2
@@ -17,6 +17,7 @@ pub mod bidi;
|
||||
mod complex;
|
||||
mod mtext;
|
||||
mod stroke;
|
||||
pub mod subset;
|
||||
pub mod ttf;
|
||||
|
||||
use cadcraft_geom::{Bounds2, Vec2};
|
||||
@@ -131,7 +132,7 @@ pub fn shape_stroke(s: &str, height: f64, width_factor: f64, oblique: f64) -> Sh
|
||||
Some(g) => {
|
||||
let contours = ttf::place_glyph(&g, x, h, wf, shear);
|
||||
if !contours.is_empty() {
|
||||
out.glyphs.push(contours);
|
||||
out.push_glyph(contours, ttf::text_glyph(&g, c, x, h, wf, shear));
|
||||
}
|
||||
adv = g.advance * h / g.cap * wf;
|
||||
}
|
||||
@@ -242,16 +243,53 @@ impl TextFont {
|
||||
|
||||
/// Shaped text in local or world coordinates: open strokes (stroke-font glyphs, underlines,
|
||||
/// fraction bars) and closed glyph outlines grouped per glyph (fill each group even-odd).
|
||||
/// Outline glyphs also come as [`TextGlyph`]s, for output that keeps text as text (PDF).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Shaped {
|
||||
pub strokes: Vec<Vec<Vec2>>,
|
||||
pub glyphs: Vec<Vec<Vec<Vec2>>>,
|
||||
pub width: f64,
|
||||
/// The outline glyphs as font glyphs: which face and glyph each of [`Shaped::glyphs`] is,
|
||||
/// and where it sits.
|
||||
pub text: Vec<TextGlyph>,
|
||||
}
|
||||
|
||||
/// A glyph of an installed outline font, placed: its outline is `Shaped::glyphs[outline]`.
|
||||
/// Point `(u, v)` of the glyph in em units (1000 font units per em, as PDF glyph space) lands
|
||||
/// at `origin + x * u / 1000 + y * v / 1000`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TextGlyph {
|
||||
pub face: ttf::Face,
|
||||
/// Glyph id in the face.
|
||||
pub gid: u16,
|
||||
/// The character it shows.
|
||||
pub ch: char,
|
||||
/// Index of its outline in [`Shaped::glyphs`].
|
||||
pub outline: usize,
|
||||
/// The glyph origin (on the baseline).
|
||||
pub origin: Vec2,
|
||||
/// One em along the baseline (width factor applied).
|
||||
pub x: Vec2,
|
||||
/// One em up (obliquing applied).
|
||||
pub y: Vec2,
|
||||
}
|
||||
|
||||
impl Shaped {
|
||||
/// Apply a point transform to every vertex.
|
||||
/// Add a glyph outline and, when it is a font glyph, its [`TextGlyph`].
|
||||
pub(crate) fn push_glyph(&mut self, contours: Vec<Vec<Vec2>>, text: Option<TextGlyph>) {
|
||||
if let Some(t) = text {
|
||||
self.text.push(TextGlyph { outline: self.glyphs.len(), ..t });
|
||||
}
|
||||
self.glyphs.push(contours);
|
||||
}
|
||||
/// Apply a point transform to every vertex (an affine map: text glyphs keep their axes).
|
||||
pub fn map(&mut self, f: impl Fn(Vec2) -> Vec2) {
|
||||
for t in &mut self.text {
|
||||
let o = f(t.origin);
|
||||
t.x = f(t.origin + t.x) - o;
|
||||
t.y = f(t.origin + t.y) - o;
|
||||
t.origin = o;
|
||||
}
|
||||
for s in &mut self.strokes {
|
||||
for p in s.iter_mut() {
|
||||
*p = f(*p);
|
||||
@@ -267,6 +305,8 @@ impl Shaped {
|
||||
}
|
||||
/// Append another shaped piece (already in the same coordinates).
|
||||
pub fn extend(&mut self, other: Shaped) {
|
||||
let base = self.glyphs.len();
|
||||
self.text.extend(other.text.into_iter().map(|t| TextGlyph { outline: t.outline + base, ..t }));
|
||||
self.strokes.extend(other.strokes);
|
||||
self.glyphs.extend(other.glyphs);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
//! TrueType subsets for embedding fonts in exported files (PDF): only the glyphs a document
|
||||
//! uses, plus the composite-glyph components they need. Glyph ids are kept (unused glyphs
|
||||
//! become empty), so text written with the original ids shows the same glyphs.
|
||||
//!
|
||||
//! Embedding follows the font's licence bits (OS/2 `fsType`): fonts marked "restricted
|
||||
//! licence", "no subsetting" or "bitmap only" are never embedded; callers draw their text as
|
||||
//! outlines instead. Fonts without TrueType outlines (CFF-flavoured OpenType) aren't subset
|
||||
//! here either.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use skrifa::instance::{LocationRef, Size};
|
||||
use skrifa::raw::types::Tag;
|
||||
use skrifa::{FontRef, MetadataProvider};
|
||||
|
||||
use crate::ttf::Face;
|
||||
|
||||
/// Largest subset written (bytes).
|
||||
pub const MAX_SUBSET_BYTES: usize = 32 << 20;
|
||||
/// Most components followed in one composite glyph, and most glyphs a subset may reach.
|
||||
const MAX_COMPONENTS: usize = 256;
|
||||
|
||||
/// Whether a face may be embedded as a subset.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Embedding {
|
||||
/// Installable, editable or preview & print embedding (or no OS/2 table).
|
||||
Allowed,
|
||||
/// `fsType` restricted licence: no embedding.
|
||||
Restricted,
|
||||
/// `fsType` bit 8: the font may only be embedded whole.
|
||||
NoSubsetting,
|
||||
/// `fsType` bit 9: only bitmaps may be embedded.
|
||||
BitmapOnly,
|
||||
/// No TrueType outlines (`glyf` / `loca`), or the font doesn't parse.
|
||||
NotTrueType,
|
||||
}
|
||||
|
||||
/// Why a subset couldn't be made.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SubsetError {
|
||||
/// The font may not be embedded as a subset.
|
||||
NotAllowed(Embedding),
|
||||
/// A table is missing or malformed.
|
||||
Malformed(&'static str),
|
||||
/// The subset would be larger than [`MAX_SUBSET_BYTES`].
|
||||
TooLarge,
|
||||
}
|
||||
|
||||
fn font(face: &Face) -> Option<FontRef<'_>> {
|
||||
FontRef::from_index(&face.bytes, face.index).ok()
|
||||
}
|
||||
|
||||
fn tag(t: &[u8; 4]) -> Tag {
|
||||
Tag::new(t)
|
||||
}
|
||||
|
||||
fn table<'a>(f: &FontRef<'a>, t: &[u8; 4]) -> Option<&'a [u8]> {
|
||||
f.table_data(tag(t)).map(|d| d.as_bytes())
|
||||
}
|
||||
|
||||
fn u16_at(b: &[u8], i: usize) -> Option<u16> {
|
||||
Some(u16::from_be_bytes([*b.get(i)?, *b.get(i + 1)?]))
|
||||
}
|
||||
|
||||
fn u32_at(b: &[u8], i: usize) -> Option<u32> {
|
||||
Some(u32::from_be_bytes([*b.get(i)?, *b.get(i + 1)?, *b.get(i + 2)?, *b.get(i + 3)?]))
|
||||
}
|
||||
|
||||
/// The embedding permission of a face.
|
||||
pub fn embedding(face: &Face) -> Embedding {
|
||||
let Some(f) = font(face) else { return Embedding::NotTrueType };
|
||||
if table(&f, b"glyf").is_none() || table(&f, b"loca").is_none() || table(&f, b"head").is_none() {
|
||||
return Embedding::NotTrueType;
|
||||
}
|
||||
let Some(fs_type) = table(&f, b"OS/2").and_then(|o| u16_at(o, 8)) else { return Embedding::Allowed };
|
||||
if fs_type & 0x0200 != 0 {
|
||||
Embedding::BitmapOnly
|
||||
} else if fs_type & 0x000F == 0x0002 {
|
||||
Embedding::Restricted
|
||||
} else if fs_type & 0x0100 != 0 {
|
||||
Embedding::NoSubsetting
|
||||
} else {
|
||||
Embedding::Allowed
|
||||
}
|
||||
}
|
||||
|
||||
/// Font-wide metrics for a font descriptor, in units of 1/1000 em.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FontInfo {
|
||||
/// PostScript name: letters, digits and hyphens only (never empty).
|
||||
pub postscript_name: String,
|
||||
/// Glyph bounding box: x min, y min, x max, y max.
|
||||
pub bbox: [f64; 4],
|
||||
pub ascent: f64,
|
||||
pub descent: f64,
|
||||
pub cap_height: f64,
|
||||
/// Degrees, counter-clockwise from vertical.
|
||||
pub italic_angle: f64,
|
||||
}
|
||||
|
||||
/// A finite value or 0.
|
||||
fn fin(v: f32) -> f64 {
|
||||
let v = f64::from(v);
|
||||
if v.is_finite() { v } else { 0.0 }
|
||||
}
|
||||
|
||||
/// The descriptor metrics of a face.
|
||||
pub fn info(face: &Face) -> Option<FontInfo> {
|
||||
let f = font(face)?;
|
||||
let m = f.metrics(Size::new(1000.0), LocationRef::default());
|
||||
let bbox = m.bounds.map(|b| [fin(b.x_min), fin(b.y_min), fin(b.x_max), fin(b.y_max)]).unwrap_or([0.0, -250.0, 1000.0, 1000.0]);
|
||||
let name: String = f
|
||||
.localized_strings(skrifa::string::StringId::POSTSCRIPT_NAME)
|
||||
.english_or_first()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default()
|
||||
.chars()
|
||||
.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
|
||||
.take(63)
|
||||
.collect();
|
||||
Some(FontInfo {
|
||||
postscript_name: if name.is_empty() { "Font".into() } else { name },
|
||||
bbox,
|
||||
ascent: fin(m.ascent),
|
||||
descent: fin(m.descent),
|
||||
cap_height: m.cap_height.map(fin).unwrap_or(fin(m.ascent) * 0.72),
|
||||
italic_angle: fin(m.italic_angle),
|
||||
})
|
||||
}
|
||||
|
||||
/// The advance width of glyph `gid` in units of 1/1000 em.
|
||||
pub fn advance(face: &Face, gid: u16) -> f64 {
|
||||
font(face)
|
||||
.and_then(|f| f.glyph_metrics(Size::new(1000.0), LocationRef::default()).advance_width(skrifa::GlyphId::new(u32::from(gid))))
|
||||
.map(fin)
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Glyph data offsets from `loca`: `n + 1` entries, checked against the `glyf` length.
|
||||
fn loca_offsets(loca: &[u8], long: bool, n: usize, glyf_len: usize) -> Option<Vec<usize>> {
|
||||
let mut v = Vec::with_capacity(n + 1);
|
||||
for i in 0..=n {
|
||||
let o = if long { u32_at(loca, i * 4)? as usize } else { usize::from(u16_at(loca, i * 2)?) * 2 };
|
||||
v.push(o.min(glyf_len));
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
|
||||
/// The data of glyph `g` (empty when its range is bad).
|
||||
fn glyph_data<'a>(glyf: &'a [u8], offsets: &[usize], g: usize) -> &'a [u8] {
|
||||
match (offsets.get(g), offsets.get(g + 1)) {
|
||||
(Some(a), Some(b)) if a < b => glyf.get(*a..*b).unwrap_or(&[]),
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// The glyph ids a composite glyph is built from.
|
||||
fn components(data: &[u8]) -> Vec<u16> {
|
||||
let mut out = Vec::new();
|
||||
// numberOfContours < 0: a composite.
|
||||
if u16_at(data, 0).is_none_or(|n| (n as i16) >= 0) {
|
||||
return out;
|
||||
}
|
||||
let mut pos = 10;
|
||||
while out.len() < MAX_COMPONENTS {
|
||||
let (Some(flags), Some(gid)) = (u16_at(data, pos), u16_at(data, pos + 2)) else { break };
|
||||
out.push(gid);
|
||||
pos += 4 + if flags & 0x0001 != 0 { 4 } else { 2 };
|
||||
pos += if flags & 0x0008 != 0 {
|
||||
2
|
||||
} else if flags & 0x0040 != 0 {
|
||||
4
|
||||
} else if flags & 0x0080 != 0 {
|
||||
8
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if flags & 0x0020 == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The sum of a table's big-endian 32-bit words (zero-padded).
|
||||
fn checksum(data: &[u8]) -> u32 {
|
||||
data.chunks(4).fold(0u32, |sum, c| {
|
||||
let mut w = [0u8; 4];
|
||||
for (d, s) in w.iter_mut().zip(c) {
|
||||
*d = *s;
|
||||
}
|
||||
sum.wrapping_add(u32::from_be_bytes(w))
|
||||
})
|
||||
}
|
||||
|
||||
/// A subset of `face` with the glyphs `gids` (and `.notdef` and the components they use). The
|
||||
/// other glyphs keep their ids but have no outline.
|
||||
pub fn subset(face: &Face, gids: &BTreeSet<u16>) -> Result<Vec<u8>, SubsetError> {
|
||||
let allowed = embedding(face);
|
||||
if allowed != Embedding::Allowed {
|
||||
return Err(SubsetError::NotAllowed(allowed));
|
||||
}
|
||||
let f = font(face).ok_or(SubsetError::Malformed("font"))?;
|
||||
let head = table(&f, b"head").filter(|h| h.len() >= 54).ok_or(SubsetError::Malformed("head"))?;
|
||||
let maxp = table(&f, b"maxp").ok_or(SubsetError::Malformed("maxp"))?;
|
||||
let glyf = table(&f, b"glyf").ok_or(SubsetError::Malformed("glyf"))?;
|
||||
let loca = table(&f, b"loca").ok_or(SubsetError::Malformed("loca"))?;
|
||||
let n = usize::from(u16_at(maxp, 4).ok_or(SubsetError::Malformed("maxp"))?);
|
||||
let long = u16_at(head, 50).ok_or(SubsetError::Malformed("head"))? != 0;
|
||||
let offsets = loca_offsets(loca, long, n, glyf.len()).ok_or(SubsetError::Malformed("loca"))?;
|
||||
// The glyphs to keep: asked for, .notdef, and composite components (transitively).
|
||||
let mut keep: BTreeSet<u16> = BTreeSet::new();
|
||||
let mut todo: Vec<u16> = std::iter::once(0).chain(gids.iter().copied()).filter(|g| usize::from(*g) < n).collect();
|
||||
while let Some(g) = todo.pop() {
|
||||
if !keep.insert(g) {
|
||||
continue;
|
||||
}
|
||||
for c in components(glyph_data(glyf, &offsets, usize::from(g))) {
|
||||
if usize::from(c) < n && !keep.contains(&c) {
|
||||
todo.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut new_glyf: Vec<u8> = Vec::new();
|
||||
let mut new_loca: Vec<u8> = Vec::with_capacity((n + 1) * 4);
|
||||
for g in 0..n {
|
||||
new_loca.extend_from_slice(&u32::try_from(new_glyf.len()).map_err(|_| SubsetError::TooLarge)?.to_be_bytes());
|
||||
if u16::try_from(g).is_ok_and(|g| keep.contains(&g)) {
|
||||
new_glyf.extend_from_slice(glyph_data(glyf, &offsets, g));
|
||||
while !new_glyf.len().is_multiple_of(4) {
|
||||
new_glyf.push(0);
|
||||
}
|
||||
}
|
||||
if new_glyf.len() > MAX_SUBSET_BYTES {
|
||||
return Err(SubsetError::TooLarge);
|
||||
}
|
||||
}
|
||||
new_loca.extend_from_slice(&u32::try_from(new_glyf.len()).map_err(|_| SubsetError::TooLarge)?.to_be_bytes());
|
||||
let mut new_head = head.to_vec();
|
||||
// checkSumAdjustment is set once the file is complete; offsets are long now.
|
||||
new_head.get_mut(8..12).ok_or(SubsetError::Malformed("head"))?.copy_from_slice(&[0; 4]);
|
||||
new_head.get_mut(50..52).ok_or(SubsetError::Malformed("head"))?.copy_from_slice(&1u16.to_be_bytes());
|
||||
let mut tables: Vec<([u8; 4], Vec<u8>)> = vec![(*b"glyf", new_glyf), (*b"head", new_head), (*b"loca", new_loca), (*b"maxp", maxp.to_vec())];
|
||||
for t in [b"cvt ", b"fpgm", b"hhea", b"hmtx", b"prep"] {
|
||||
if let Some(d) = table(&f, t) {
|
||||
tables.push((*t, d.to_vec()));
|
||||
}
|
||||
}
|
||||
if !tables.iter().any(|(t, _)| t == b"hhea") || !tables.iter().any(|(t, _)| t == b"hmtx") {
|
||||
return Err(SubsetError::Malformed("hhea/hmtx"));
|
||||
}
|
||||
tables.sort_by_key(|t| t.0);
|
||||
let out = sfnt(&tables);
|
||||
if out.len() > MAX_SUBSET_BYTES { Err(SubsetError::TooLarge) } else { Ok(out) }
|
||||
}
|
||||
|
||||
/// Write a TrueType file from `(tag, data)` tables sorted by tag, with checksums and the
|
||||
/// `head` checksum adjustment.
|
||||
fn sfnt(tables: &[([u8; 4], Vec<u8>)]) -> Vec<u8> {
|
||||
let count = tables.len() as u16;
|
||||
let mut pow = 1u16;
|
||||
let mut sel = 0u16;
|
||||
while pow.saturating_mul(2) <= count {
|
||||
pow = pow.saturating_mul(2);
|
||||
sel += 1;
|
||||
}
|
||||
let range = pow.saturating_mul(16);
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(&0x0001_0000u32.to_be_bytes());
|
||||
for v in [count, range, sel, count.saturating_mul(16).saturating_sub(range)] {
|
||||
out.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
let mut offset = 12 + 16 * tables.len();
|
||||
let mut head_at = None;
|
||||
for (t, d) in tables {
|
||||
out.extend_from_slice(t);
|
||||
out.extend_from_slice(&checksum(d).to_be_bytes());
|
||||
out.extend_from_slice(&(offset as u32).to_be_bytes());
|
||||
out.extend_from_slice(&(d.len() as u32).to_be_bytes());
|
||||
offset += d.len().div_ceil(4) * 4;
|
||||
}
|
||||
for (t, d) in tables {
|
||||
if t == b"head" {
|
||||
head_at = Some(out.len());
|
||||
}
|
||||
out.extend_from_slice(d);
|
||||
while !out.len().is_multiple_of(4) {
|
||||
out.push(0);
|
||||
}
|
||||
}
|
||||
if let Some(h) = head_at {
|
||||
let adj = 0xB1B0_AFBAu32.wrapping_sub(checksum(&out));
|
||||
if let Some(slot) = out.get_mut(h + 8..h + 12) {
|
||||
slot.copy_from_slice(&adj.to_be_bytes());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn composite_components_are_followed_and_bounded() {
|
||||
// numberOfContours = -1, then two components (word args, MORE_COMPONENTS on the first).
|
||||
let mut g = vec![0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
g.extend_from_slice(&[0x00, 0x21, 0x00, 0x05, 0, 0, 0, 0]);
|
||||
g.extend_from_slice(&[0x00, 0x00, 0x00, 0x07, 0, 0]);
|
||||
assert_eq!(components(&g), [5, 7]);
|
||||
// Truncated and endless (always MORE_COMPONENTS) data stay bounded.
|
||||
assert_eq!(components(&g[..14]), [5]);
|
||||
let mut endless = vec![0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
for _ in 0..2000 {
|
||||
endless.extend_from_slice(&[0x00, 0x20, 0x00, 0x01, 0, 0]);
|
||||
}
|
||||
assert_eq!(components(&endless).len(), MAX_COMPONENTS);
|
||||
assert!(components(&[0, 1, 0]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn junk_fonts_are_not_embedded() {
|
||||
let junk = Face { bytes: std::sync::Arc::new(b"not a font".to_vec()), index: 0 };
|
||||
assert_eq!(embedding(&junk), Embedding::NotTrueType);
|
||||
assert!(subset(&junk, &BTreeSet::from([1])).is_err());
|
||||
assert!(info(&junk).is_none());
|
||||
assert_eq!(advance(&junk, 3), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sfnt_directory_and_checksums() {
|
||||
let mut head = vec![0u8; 54];
|
||||
head[12..16].copy_from_slice(&0x5F0F_3CF5u32.to_be_bytes());
|
||||
let out = sfnt(&[(*b"head", head), (*b"maxp", vec![0, 0, 0x50, 0, 0, 1])]);
|
||||
assert_eq!(u16_at(&out, 4), Some(2));
|
||||
assert_eq!(out.len() % 4, 0);
|
||||
// The whole file sums to the magic number once the adjustment is in.
|
||||
assert_eq!(checksum(&out), 0xB1B0_AFBA);
|
||||
}
|
||||
}
|
||||
+31
-2
@@ -35,6 +35,13 @@ pub struct Face {
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
/// The same face of the same loaded file.
|
||||
impl PartialEq for Face {
|
||||
fn eq(&self, other: &Face) -> bool {
|
||||
Arc::ptr_eq(&self.bytes, &other.bytes) && self.index == other.index
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Face {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Face").field("len", &self.bytes.len()).field("index", &self.index).finish()
|
||||
@@ -578,6 +585,9 @@ pub(crate) struct GlyphOutline {
|
||||
pub(crate) cap: f64,
|
||||
/// The font maps the character (false: the missing-glyph shape).
|
||||
mapped: bool,
|
||||
/// Its glyph id and face, for output that keeps text as text ([`crate::TextGlyph`]).
|
||||
gid: u32,
|
||||
face: Option<Face>,
|
||||
}
|
||||
|
||||
/// A glyph by character (through the face's character map) or by glyph id (shaper output).
|
||||
@@ -698,7 +708,8 @@ fn outline(face: &Face, k: GlyphKey) -> Arc<GlyphOutline> {
|
||||
pen.flush();
|
||||
}
|
||||
let advance = f64::from(f.glyph_metrics(Size::new(UPEM), LocationRef::default()).advance_width(gid).unwrap_or(UPEM * 0.5));
|
||||
let g = Arc::new(GlyphOutline { contours: pen.contours, advance, cap: cap_height(&f), mapped: mapped.is_some() });
|
||||
let (cap, mapped, gid) = (cap_height(&f), mapped.is_some(), gid.to_u32());
|
||||
let g = Arc::new(GlyphOutline { contours: pen.contours, advance, cap, mapped, gid, face: Some(face.clone()) });
|
||||
let mut cache = glyph_cache().lock().unwrap_or_else(PoisonError::into_inner);
|
||||
if cache.len() > MAX_CACHED_GLYPHS {
|
||||
cache.clear();
|
||||
@@ -722,6 +733,24 @@ fn glyph_or_fallback(face: &Face, c: char) -> Arc<GlyphOutline> {
|
||||
fallback_glyph(c).unwrap_or(g)
|
||||
}
|
||||
|
||||
/// The [`crate::TextGlyph`] of a glyph placed like [`place_glyph`] does (its outline index is
|
||||
/// set when it is added). `None` for the missing-glyph shape.
|
||||
pub(crate) fn text_glyph(g: &GlyphOutline, c: char, x: f64, h: f64, wf: f64, shear: f64) -> Option<crate::TextGlyph> {
|
||||
let gid = u16::try_from(g.gid).ok().filter(|_| g.mapped)?;
|
||||
let face = g.face.clone()?;
|
||||
// Font units at 1000 per em scale by h / cap.
|
||||
let em = 1000.0 * h / g.cap;
|
||||
(em.is_finite() && em > 0.0).then(|| crate::TextGlyph {
|
||||
face,
|
||||
gid,
|
||||
ch: c,
|
||||
outline: 0,
|
||||
origin: Vec2::new(x, 0.0),
|
||||
x: Vec2::new(em * wf, 0.0),
|
||||
y: Vec2::new(em * shear, em),
|
||||
})
|
||||
}
|
||||
|
||||
/// Contours of a glyph scaled so its font's cap height is `h`, at pen position `x`.
|
||||
pub(crate) fn place_glyph(g: &GlyphOutline, x: f64, h: f64, wf: f64, shear: f64) -> Vec<Vec<Vec2>> {
|
||||
let scale = h / g.cap;
|
||||
@@ -762,7 +791,7 @@ pub fn shape(face: &Face, s: &str, height: f64, width_factor: f64, oblique: f64)
|
||||
if !c.is_whitespace() {
|
||||
let contours = place_glyph(&g, x, h, wf, shear);
|
||||
if !contours.is_empty() {
|
||||
out.glyphs.push(contours);
|
||||
out.push_glyph(contours, text_glyph(&g, c, x, h, wf, shear));
|
||||
}
|
||||
}
|
||||
let adv = g.advance * h / g.cap * wf;
|
||||
|
||||
@@ -15,6 +15,7 @@ cadcraft-doc = { workspace = true }
|
||||
cadcraft-dxf = { workspace = true }
|
||||
cadcraft-dwg = { workspace = true }
|
||||
cadcraft-render = { workspace = true }
|
||||
cadcraft-fonts = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
+83
-10
@@ -21,6 +21,8 @@ use serde_json::Value;
|
||||
|
||||
use crate::{IoError, Result};
|
||||
|
||||
mod fonts;
|
||||
|
||||
/// PDF points per millimetre.
|
||||
const PT_PER_MM: f64 = 72.0 / 25.4;
|
||||
/// Largest sheet side we accept, in millimetres (PDF viewers cap pages at 200 inches anyway).
|
||||
@@ -64,6 +66,10 @@ pub struct PdfOptions {
|
||||
/// Hyperlinks: an entity's handle and its link target (a URL). The area the entity covers
|
||||
/// on the sheet becomes a link annotation.
|
||||
pub links: Vec<(Handle, String)>,
|
||||
/// Write text in installed TrueType fonts as filled outlines instead of as text in an
|
||||
/// embedded font subset (like AutoCAD's PDFSHX for its own fonts). Stroke-font text is
|
||||
/// always geometry.
|
||||
pub text_as_geometry: bool,
|
||||
}
|
||||
|
||||
impl Default for PdfOptions {
|
||||
@@ -82,6 +88,7 @@ impl Default for PdfOptions {
|
||||
layers: true,
|
||||
include_off_layers: false,
|
||||
links: Vec::new(),
|
||||
text_as_geometry: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +103,9 @@ impl PdfOptions {
|
||||
_ => None,
|
||||
};
|
||||
PdfOptions {
|
||||
// `layers?` (default on), `offLayers?`, `links?: [{handle, url}]`.
|
||||
// `layers?` (default on), `offLayers?`, `links?: [{handle, url}]`, `text?: "text" |
|
||||
// "geometry"`.
|
||||
text_as_geometry: v.get("text").and_then(Value::as_str).is_some_and(|t| t.eq_ignore_ascii_case("geometry")),
|
||||
layers: v.get("layers").and_then(Value::as_bool).unwrap_or(true),
|
||||
include_off_layers: v.get("offLayers").and_then(Value::as_bool).unwrap_or(false),
|
||||
links: links_json(v.get("links")),
|
||||
@@ -262,14 +271,18 @@ pub fn pdf_sheets(d: &Drawing, spaces: &[Space], o: &PdfOptions) -> Result<Vec<u
|
||||
};
|
||||
let pages = spaces.iter().map(|s| sheet(drawn, s, o)).collect::<Result<Vec<_>>>()?;
|
||||
let title = if o.title.is_empty() { pages.first().map(|p| p.label.clone()).unwrap_or_default() } else { o.title.clone() };
|
||||
Ok(assemble(d, &pages, o, &title))
|
||||
let fonts = if o.text_as_geometry { fonts::Fonts::default() } else { fonts::Fonts::collect(pages.iter().map(|p| &p.list)) };
|
||||
Ok(assemble(d, &pages, &fonts, o, &title))
|
||||
}
|
||||
|
||||
/// One plotted sheet, ready to be written as a page.
|
||||
struct SheetPage {
|
||||
/// Page size in points.
|
||||
media: Vec2,
|
||||
content: String,
|
||||
/// What it draws, the mapping to points and the plot area (points).
|
||||
list: cadcraft_render::DisplayList,
|
||||
map: Map,
|
||||
clip_pt: Bounds2,
|
||||
/// The drawing layers (indices in the layer table) of the content's `/L<i>` marks.
|
||||
layers: Vec<u32>,
|
||||
/// Link annotations: rectangle in points and target.
|
||||
@@ -326,9 +339,8 @@ fn sheet(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<SheetPage> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let content = content_stream(&list, &map, &clip_pt, &layers);
|
||||
let links = links(&list, &map, &clip_pt, &o.links);
|
||||
Ok(SheetPage { media, content, layers, links, label: sheet_name(space) })
|
||||
Ok(SheetPage { media, list, map, clip_pt, layers, links, label: sheet_name(space) })
|
||||
}
|
||||
|
||||
/// Link annotation rectangles: the area (in points) each linked entity covers on the sheet,
|
||||
@@ -414,7 +426,16 @@ fn color(out: &mut String, c: Rgb, op: &str) {
|
||||
|
||||
/// The page's marks; those of primitives on `layers` are marked as optional content
|
||||
/// (`/OC /L<layer> BDC` … `EMC`).
|
||||
fn content_stream(list: &cadcraft_render::DisplayList, map: &Map, clip_pt: &Bounds2, layers: &[u32]) -> String {
|
||||
///
|
||||
/// Primitives in `texts` (filled glyph outlines) are written as text instead: the glyph in its
|
||||
/// embedded font (`/F<font>`), placed by a text matrix.
|
||||
fn content_stream(
|
||||
list: &cadcraft_render::DisplayList,
|
||||
map: &Map,
|
||||
clip_pt: &Bounds2,
|
||||
layers: &[u32],
|
||||
texts: &std::collections::HashMap<u32, (usize, &cadcraft_render::PlotText)>,
|
||||
) -> String {
|
||||
let marked: std::collections::HashSet<u32> = layers.iter().copied().collect();
|
||||
let white = Rgb(255, 255, 255);
|
||||
let mut c = String::new();
|
||||
@@ -438,8 +459,15 @@ fn content_stream(list: &cadcraft_render::DisplayList, map: &Map, clip_pt: &Boun
|
||||
};
|
||||
// The PDF layer (optional content) the marks are in: `/OC /L<i> BDC` … `EMC`.
|
||||
let mut layer: Option<u32> = None;
|
||||
// Inside a text object (`BT` … `ET`), with this font set.
|
||||
let mut text_font: Option<Option<usize>> = None;
|
||||
for (i, p) in list.prims.iter().enumerate() {
|
||||
let want = list.prim_layers.get(i).copied().filter(|l| marked.contains(l));
|
||||
let text = u32::try_from(i).ok().and_then(|i| texts.get(&i));
|
||||
if text_font.is_some() && (text.is_none() || want != layer) {
|
||||
c.push_str("ET\n");
|
||||
text_font = None;
|
||||
}
|
||||
if want != layer {
|
||||
flush(&mut c, &mut pending);
|
||||
if layer.is_some() {
|
||||
@@ -502,6 +530,10 @@ fn content_stream(list: &cadcraft_render::DisplayList, map: &Map, clip_pt: &Boun
|
||||
color(&mut c, rgb, "rg");
|
||||
fill = Some(rgb);
|
||||
}
|
||||
if let Some((font, t)) = text {
|
||||
glyph(&mut c, &mut text_font, *font, t, map);
|
||||
continue;
|
||||
}
|
||||
let mut any = false;
|
||||
for t in raw.as_chunks::<3>().0 {
|
||||
for (i, q) in t.iter().enumerate() {
|
||||
@@ -533,6 +565,9 @@ fn content_stream(list: &cadcraft_render::DisplayList, map: &Map, clip_pt: &Boun
|
||||
}
|
||||
}
|
||||
flush(&mut c, &mut pending);
|
||||
if text_font.is_some() {
|
||||
c.push_str("ET\n");
|
||||
}
|
||||
if layer.is_some() {
|
||||
c.push_str("EMC\n");
|
||||
}
|
||||
@@ -540,6 +575,27 @@ fn content_stream(list: &cadcraft_render::DisplayList, map: &Map, clip_pt: &Boun
|
||||
c
|
||||
}
|
||||
|
||||
/// Write one glyph as text: opens a text object and sets the font when needed, then places the
|
||||
/// glyph with a text matrix (font size 1: the matrix axes are one em).
|
||||
fn glyph(c: &mut String, text_font: &mut Option<Option<usize>>, font: usize, t: &cadcraft_render::PlotText, map: &Map) {
|
||||
if text_font.is_none() {
|
||||
c.push_str("BT\n");
|
||||
*text_font = Some(None);
|
||||
}
|
||||
if *text_font != Some(Some(font)) {
|
||||
let _ = writeln!(c, "/F{font} 1 Tf");
|
||||
*text_font = Some(Some(font));
|
||||
}
|
||||
let s = map.s * map.k;
|
||||
let g = &t.glyph;
|
||||
for v in [g.x.x * s, g.x.y * s, g.y.x * s, g.y.y * s] {
|
||||
num(c, v);
|
||||
c.push(' ');
|
||||
}
|
||||
pt(c, map.pt(g.origin));
|
||||
let _ = writeln!(c, " Tm <{:04X}> Tj", g.gid);
|
||||
}
|
||||
|
||||
/// A PDF text string: a literal with `(`, `)` and `\` escaped when it is printable ASCII,
|
||||
/// else UTF-16BE with a byte order mark (hex). At most 1000 characters.
|
||||
fn pdf_string(s: &str) -> String {
|
||||
@@ -629,7 +685,7 @@ fn refs(ids: impl IntoIterator<Item = usize>) -> String {
|
||||
/// Assemble the file: catalog, page tree, pages with their content streams and link
|
||||
/// annotations, the PDF layers (one optional content group per drawing layer used, named
|
||||
/// like it), page labels, document information, cross-reference table and trailer.
|
||||
fn assemble(d: &Drawing, pages: &[SheetPage], o: &PdfOptions, title: &str) -> Vec<u8> {
|
||||
fn assemble(d: &Drawing, pages: &[SheetPage], fonts: &fonts::Fonts, o: &PdfOptions, title: &str) -> Vec<u8> {
|
||||
let mut objs = Objects::default();
|
||||
let catalog = objs.reserve();
|
||||
let tree = objs.reserve();
|
||||
@@ -637,9 +693,15 @@ fn assemble(d: &Drawing, pages: &[SheetPage], o: &PdfOptions, title: &str) -> Ve
|
||||
let mut ocg_ids: std::collections::HashMap<u32, usize> = std::collections::HashMap::new();
|
||||
let mut ocgs: Vec<(u32, usize)> = Vec::new();
|
||||
let mut kids = Vec::new();
|
||||
// Page dictionaries are completed once the fonts are written: (page, its fonts, the rest).
|
||||
let mut page_dicts: Vec<(usize, Vec<usize>, String, String)> = Vec::new();
|
||||
for p in pages {
|
||||
let page = objs.reserve();
|
||||
let content = objs.add(stream(p.content.as_bytes(), o.compress, ""));
|
||||
let texts = fonts.texts(&p.list);
|
||||
let content = objs.add(stream(content_stream(&p.list, &p.map, &p.clip_pt, &p.layers, &texts).as_bytes(), o.compress, ""));
|
||||
let mut used: Vec<usize> = texts.values().map(|(f, _)| *f).collect();
|
||||
used.sort_unstable();
|
||||
used.dedup();
|
||||
let mut props = String::new();
|
||||
for l in &p.layers {
|
||||
let Some(layer) = d.layers.get(*l as usize) else { continue };
|
||||
@@ -665,11 +727,22 @@ fn assemble(d: &Drawing, pages: &[SheetPage], o: &PdfOptions, title: &str) -> Ve
|
||||
}
|
||||
let mut mb = String::new();
|
||||
pt(&mut mb, p.media);
|
||||
let resources = if props.is_empty() { "<< >>".to_string() } else { format!("<< /Properties <<{props} >> >>") };
|
||||
let annots = if annots.is_empty() { String::new() } else { format!(" /Annots [{}]", refs(annots)) };
|
||||
objs.set(page, format!("<< /Type /Page /Parent {tree} 0 R /MediaBox [0 0 {mb}] /Resources {resources} /Contents {content} 0 R{annots} >>"));
|
||||
page_dicts.push((page, used, props, format!("/MediaBox [0 0 {mb}] /Contents {content} 0 R{annots}")));
|
||||
kids.push(page);
|
||||
}
|
||||
let font_ids = if fonts.is_empty() { Vec::new() } else { fonts.write(&mut objs, o.compress) };
|
||||
for (page, used, props, rest) in page_dicts {
|
||||
let mut res = String::new();
|
||||
if !props.is_empty() {
|
||||
let _ = write!(res, " /Properties <<{props} >>");
|
||||
}
|
||||
let f: Vec<String> = used.iter().filter_map(|i| Some(format!("/F{i} {} 0 R", font_ids.get(*i)?))).collect();
|
||||
if !f.is_empty() {
|
||||
let _ = write!(res, " /Font << {} >>", f.join(" "));
|
||||
}
|
||||
objs.set(page, format!("<< /Type /Page /Parent {tree} 0 R /Resources <<{res} >> {rest} >>"));
|
||||
}
|
||||
objs.set(tree, format!("<< /Type /Pages /Kids [{}] /Count {} >>", refs(kids.iter().copied()), kids.len()));
|
||||
let mut cat = format!("<< /Type /Catalog /Pages {tree} 0 R");
|
||||
if !ocgs.is_empty() {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
//! Real text in PDF plots: the outline-font glyphs a plot draws ([`DisplayList::texts`]) are
|
||||
//! written as text in embedded TrueType subsets, so the text can be selected, searched and
|
||||
//! copied. Each font is a `Type0` font with `Identity-H` encoding over a `CIDFontType2`
|
||||
//! descendant whose CIDs are the glyph ids (`/CIDToGIDMap /Identity`), with a `ToUnicode`
|
||||
//! map for copying and searching.
|
||||
//!
|
||||
//! Fonts whose licence bits forbid embedding a subset, fonts without TrueType outlines, and
|
||||
//! anything past the caps on fonts and font data stay as filled outlines.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use cadcraft_fonts::subset::{self, FontInfo};
|
||||
use cadcraft_fonts::ttf::Face;
|
||||
use cadcraft_render::{DisplayList, PlotText};
|
||||
|
||||
use super::{Objects, num, refs, stream};
|
||||
|
||||
/// Most embedded fonts in one file.
|
||||
const MAX_FONTS: usize = 64;
|
||||
/// Most embedded font data in one file (uncompressed bytes).
|
||||
const MAX_FONT_BYTES: usize = 64 << 20;
|
||||
|
||||
/// A face's identity: its loaded file and face index.
|
||||
type FaceKey = (usize, usize, u32);
|
||||
|
||||
fn key(f: &Face) -> FaceKey {
|
||||
(f.bytes.as_ptr() as usize, f.bytes.len(), f.index)
|
||||
}
|
||||
|
||||
/// An embedded font.
|
||||
struct Font {
|
||||
face: Face,
|
||||
/// Glyphs used, and the character each shows.
|
||||
chars: BTreeMap<u16, char>,
|
||||
/// The subset file.
|
||||
data: Vec<u8>,
|
||||
info: FontInfo,
|
||||
}
|
||||
|
||||
/// The fonts of a document: those that can be embedded, by face.
|
||||
#[derive(Default)]
|
||||
pub(super) struct Fonts {
|
||||
fonts: Vec<Font>,
|
||||
by_face: HashMap<FaceKey, Option<usize>>,
|
||||
}
|
||||
|
||||
impl Fonts {
|
||||
/// Collect the glyphs of every plotted text glyph and subset their fonts.
|
||||
pub(super) fn collect<'a>(lists: impl IntoIterator<Item = &'a DisplayList>) -> Fonts {
|
||||
let mut used: Vec<(Face, BTreeMap<u16, char>)> = Vec::new();
|
||||
let mut index: HashMap<FaceKey, usize> = HashMap::new();
|
||||
for list in lists {
|
||||
for t in usable(list) {
|
||||
let k = key(&t.glyph.face);
|
||||
let i = match index.get(&k) {
|
||||
Some(i) => *i,
|
||||
None => {
|
||||
index.insert(k, used.len());
|
||||
used.push((t.glyph.face.clone(), BTreeMap::new()));
|
||||
used.len() - 1
|
||||
}
|
||||
};
|
||||
if let Some((_, chars)) = used.get_mut(i) {
|
||||
chars.entry(t.glyph.gid).or_insert(t.glyph.ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = Fonts::default();
|
||||
let mut total = 0usize;
|
||||
for (face, chars) in used {
|
||||
let k = key(&face);
|
||||
let font = (out.fonts.len() < MAX_FONTS)
|
||||
.then(|| subset::subset(&face, &chars.keys().copied().collect::<BTreeSet<u16>>()).ok())
|
||||
.flatten()
|
||||
.filter(|data| total.saturating_add(data.len()) <= MAX_FONT_BYTES)
|
||||
.and_then(|data| Some(Font { info: subset::info(&face)?, face, chars, data }));
|
||||
match font {
|
||||
Some(f) => {
|
||||
total = total.saturating_add(f.data.len());
|
||||
out.by_face.insert(k, Some(out.fonts.len()));
|
||||
out.fonts.push(f);
|
||||
}
|
||||
None => {
|
||||
out.by_face.insert(k, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.fonts.is_empty()
|
||||
}
|
||||
|
||||
/// The embedded font (index) a glyph is written in; `None`: draw its outline.
|
||||
fn font_of(&self, t: &PlotText) -> Option<usize> {
|
||||
self.by_face.get(&key(&t.glyph.face)).copied().flatten()
|
||||
}
|
||||
|
||||
/// The plotted glyphs of `list` written as text: outline primitive → (font, glyph).
|
||||
pub(super) fn texts<'a>(&self, list: &'a DisplayList) -> HashMap<u32, (usize, &'a PlotText)> {
|
||||
usable(list).filter_map(|t| Some((t.prim, (self.font_of(t)?, t)))).collect()
|
||||
}
|
||||
|
||||
/// Write the font objects; returns each font's `Type0` object, by font index.
|
||||
pub(super) fn write(&self, objs: &mut Objects, compress: bool) -> Vec<usize> {
|
||||
self.fonts.iter().map(|f| write_font(objs, f, compress)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The text glyphs of a list that stand for a primitive of it.
|
||||
fn usable(list: &DisplayList) -> impl Iterator<Item = &PlotText> {
|
||||
list.texts.iter().filter(|t| (t.prim as usize) < list.prims.len() && t.glyph.gid != 0)
|
||||
}
|
||||
|
||||
/// Six capital letters naming a subset (PDF subset fonts are `ABCDEF+Name`), from its glyphs.
|
||||
fn subset_tag(f: &Font) -> String {
|
||||
let mut h: u64 = 0xCBF2_9CE4_8422_2325;
|
||||
for b in f.info.postscript_name.bytes().chain(f.chars.keys().flat_map(|g| g.to_be_bytes())) {
|
||||
h = (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01B3);
|
||||
}
|
||||
(0..6).map(|i| char::from(b'A' + ((h >> (i * 8)) % 26) as u8)).collect()
|
||||
}
|
||||
|
||||
fn write_font(objs: &mut Objects, f: &Font, compress: bool) -> usize {
|
||||
let name = format!("{}+{}", subset_tag(f), f.info.postscript_name);
|
||||
let file = objs.add(stream(&f.data, compress, &format!(" /Length1 {}", f.data.len())));
|
||||
let mut bbox = String::new();
|
||||
for (i, v) in f.info.bbox.iter().enumerate() {
|
||||
if i > 0 {
|
||||
bbox.push(' ');
|
||||
}
|
||||
num(&mut bbox, v.round());
|
||||
}
|
||||
let mut metrics = String::new();
|
||||
for (k, v) in [("Ascent", f.info.ascent), ("Descent", f.info.descent), ("CapHeight", f.info.cap_height), ("ItalicAngle", f.info.italic_angle)] {
|
||||
let _ = write!(metrics, " /{k} ");
|
||||
num(&mut metrics, v.round());
|
||||
}
|
||||
let descriptor =
|
||||
objs.add(format!("<< /Type /FontDescriptor /FontName /{name} /Flags 4 /FontBBox [{bbox}]{metrics} /StemV 80 /FontFile2 {file} 0 R >>"));
|
||||
let mut widths = String::new();
|
||||
for g in f.chars.keys() {
|
||||
let _ = write!(widths, "{g} [");
|
||||
num(&mut widths, subset::advance(&f.face, *g).round());
|
||||
widths.push_str("] ");
|
||||
}
|
||||
let cid = objs.add(format!(
|
||||
"<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{name} /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor {descriptor} 0 R /CIDToGIDMap /Identity /DW 1000 /W [{}] >>",
|
||||
widths.trim_end()
|
||||
));
|
||||
let to_unicode = objs.add(stream(to_unicode(&f.chars).as_bytes(), compress, ""));
|
||||
objs.add(format!(
|
||||
"<< /Type /Font /Subtype /Type0 /BaseFont /{name} /Encoding /Identity-H /DescendantFonts [{}] /ToUnicode {to_unicode} 0 R >>",
|
||||
refs([cid])
|
||||
))
|
||||
}
|
||||
|
||||
/// A `ToUnicode` CMap from glyph ids (2-byte codes) to the characters they show.
|
||||
fn to_unicode(chars: &BTreeMap<u16, char>) -> String {
|
||||
let mut s = String::from(
|
||||
"/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n",
|
||||
);
|
||||
let entries: Vec<(&u16, &char)> = chars.iter().collect();
|
||||
// At most 100 entries per block.
|
||||
for block in entries.chunks(100) {
|
||||
let _ = writeln!(s, "{} beginbfchar", block.len());
|
||||
for (g, c) in block {
|
||||
let _ = write!(s, "<{g:04X}> <");
|
||||
for u in c.encode_utf16(&mut [0; 2]) {
|
||||
let _ = write!(s, "{u:04X}");
|
||||
}
|
||||
s.push_str(">\n");
|
||||
}
|
||||
s.push_str("endbfchar\n");
|
||||
}
|
||||
s.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
|
||||
s
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
//! Real text in PDF plots (issue #560): text in TrueType fonts is written as text in an
|
||||
//! embedded font subset (selectable and searchable through a ToUnicode map), placed with its
|
||||
//! rotation, width factor and obliquing; fonts whose licence bits forbid embedding, the stroke
|
||||
//! font and the `text: "geometry"` option keep filled outlines.
|
||||
//!
|
||||
//! The fonts are tiny synthetic TrueType files built here (no font files in the repository).
|
||||
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, Space, Text, TextStyle, Viewport};
|
||||
use cadcraft_geom::{Vec2, Vec3};
|
||||
use cadcraft_io::plot;
|
||||
use serde_json::json;
|
||||
|
||||
// ---------- a minimal TrueType font: .notdef, "A" (triangle), "B" (square) ----------
|
||||
|
||||
fn be16(v: &mut Vec<u8>, x: u16) {
|
||||
v.extend_from_slice(&x.to_be_bytes());
|
||||
}
|
||||
fn be32(v: &mut Vec<u8>, x: u32) {
|
||||
v.extend_from_slice(&x.to_be_bytes());
|
||||
}
|
||||
|
||||
/// A simple glyph: one closed contour of on-curve points.
|
||||
fn simple_glyph(pts: &[(i16, i16)]) -> Vec<u8> {
|
||||
let mut g = Vec::new();
|
||||
let xs: Vec<i16> = pts.iter().map(|p| p.0).collect();
|
||||
let ys: Vec<i16> = pts.iter().map(|p| p.1).collect();
|
||||
be16(&mut g, 1);
|
||||
for v in [*xs.iter().min().unwrap(), *ys.iter().min().unwrap(), *xs.iter().max().unwrap(), *ys.iter().max().unwrap()] {
|
||||
be16(&mut g, v as u16);
|
||||
}
|
||||
be16(&mut g, pts.len() as u16 - 1);
|
||||
be16(&mut g, 0);
|
||||
g.extend(std::iter::repeat_n(1u8, pts.len()));
|
||||
let (mut px, mut py) = (0i16, 0i16);
|
||||
for (x, _) in pts {
|
||||
be16(&mut g, (x - px) as u16);
|
||||
px = *x;
|
||||
}
|
||||
for (_, y) in pts {
|
||||
be16(&mut g, (y - py) as u16);
|
||||
py = *y;
|
||||
}
|
||||
while g.len() % 4 != 0 {
|
||||
g.push(0);
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// A TrueType font with glyphs .notdef (empty), A and B, and OS/2 `fsType`.
|
||||
fn test_font(fs_type: u16) -> Vec<u8> {
|
||||
let glyphs = [Vec::new(), simple_glyph(&[(50, 0), (550, 0), (300, 700)]), simple_glyph(&[(50, 0), (550, 0), (550, 700), (50, 700)])];
|
||||
let mut glyf = Vec::new();
|
||||
let mut loca = Vec::new();
|
||||
for g in &glyphs {
|
||||
be32(&mut loca, glyf.len() as u32);
|
||||
glyf.extend_from_slice(g);
|
||||
}
|
||||
be32(&mut loca, glyf.len() as u32);
|
||||
let mut head = Vec::new();
|
||||
be32(&mut head, 0x0001_0000);
|
||||
be32(&mut head, 0x0001_0000);
|
||||
be32(&mut head, 0);
|
||||
be32(&mut head, 0x5F0F_3CF5);
|
||||
be16(&mut head, 0);
|
||||
be16(&mut head, 1000);
|
||||
head.extend_from_slice(&[0; 16]);
|
||||
for v in [0i16, 0, 600, 700] {
|
||||
be16(&mut head, v as u16);
|
||||
}
|
||||
for v in [0u16, 8, 2, 1, 0] {
|
||||
be16(&mut head, v);
|
||||
}
|
||||
let mut hhea = Vec::new();
|
||||
be32(&mut hhea, 0x0001_0000);
|
||||
for v in [800i16, -200, 0, 600, 0, 0, 600, 1, 0, 0, 0, 0, 0, 0, 0] {
|
||||
be16(&mut hhea, v as u16);
|
||||
}
|
||||
be16(&mut hhea, 3);
|
||||
let mut maxp = Vec::new();
|
||||
be32(&mut maxp, 0x0001_0000);
|
||||
for v in [3u16, 4, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0] {
|
||||
be16(&mut maxp, v);
|
||||
}
|
||||
let mut hmtx = Vec::new();
|
||||
for _ in 0..3 {
|
||||
be16(&mut hmtx, 600);
|
||||
be16(&mut hmtx, 50);
|
||||
}
|
||||
// cmap: format 4, 'A'..'B' → 1..2.
|
||||
let mut sub = Vec::new();
|
||||
let segs: [(u16, u16, u16); 2] = [(0x41, 0x42, 1u16.wrapping_sub(0x41)), (0xFFFF, 0xFFFF, 1)];
|
||||
be16(&mut sub, 4);
|
||||
be16(&mut sub, (16 + segs.len() * 8) as u16);
|
||||
be16(&mut sub, 0);
|
||||
be16(&mut sub, (segs.len() * 2) as u16);
|
||||
for v in [4u16, 1, 0] {
|
||||
be16(&mut sub, v);
|
||||
}
|
||||
for s in segs {
|
||||
be16(&mut sub, s.1);
|
||||
}
|
||||
be16(&mut sub, 0);
|
||||
for s in segs {
|
||||
be16(&mut sub, s.0);
|
||||
}
|
||||
for s in segs {
|
||||
be16(&mut sub, s.2);
|
||||
}
|
||||
for _ in segs {
|
||||
be16(&mut sub, 0);
|
||||
}
|
||||
let mut cmap = Vec::new();
|
||||
for v in [0u16, 1, 3, 1] {
|
||||
be16(&mut cmap, v);
|
||||
}
|
||||
be32(&mut cmap, 12);
|
||||
cmap.extend_from_slice(&sub);
|
||||
let mut os2 = vec![0u8; 78];
|
||||
os2[8..10].copy_from_slice(&fs_type.to_be_bytes());
|
||||
let mut tables: Vec<(&[u8; 4], Vec<u8>)> =
|
||||
vec![(b"OS/2", os2), (b"cmap", cmap), (b"glyf", glyf), (b"head", head), (b"hhea", hhea), (b"hmtx", hmtx), (b"loca", loca), (b"maxp", maxp)];
|
||||
tables.sort_by_key(|t| *t.0);
|
||||
let mut out = Vec::new();
|
||||
be32(&mut out, 0x0001_0000);
|
||||
for v in [tables.len() as u16, 128, 3, tables.len() as u16 * 16 - 128] {
|
||||
be16(&mut out, v);
|
||||
}
|
||||
let mut off = 12 + 16 * tables.len();
|
||||
for (t, d) in &tables {
|
||||
out.extend_from_slice(*t);
|
||||
be32(&mut out, 0);
|
||||
be32(&mut out, off as u32);
|
||||
be32(&mut out, d.len() as u32);
|
||||
off += d.len().div_ceil(4) * 4;
|
||||
}
|
||||
for (_, d) in &tables {
|
||||
out.extend_from_slice(d);
|
||||
while out.len() % 4 != 0 {
|
||||
out.push(0);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Register the test fonts once: an embeddable one and one with a restricted licence.
|
||||
fn fonts() {
|
||||
static ONCE: std::sync::Once = std::sync::Once::new();
|
||||
ONCE.call_once(|| {
|
||||
cadcraft_fonts::ttf::register("cadcraft-pdf-test.ttf", test_font(0));
|
||||
cadcraft_fonts::ttf::register("cadcraft-pdf-restricted.ttf", test_font(0x0002));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- drawings and PDF reading ----------
|
||||
|
||||
fn style(d: &mut Drawing, name: &str, font: &str) {
|
||||
d.text_styles.push(TextStyle { name: name.into(), font: font.into(), ..TextStyle::default() });
|
||||
}
|
||||
|
||||
fn text(d: &mut Drawing, space: &Space, style: &str, at: (f64, f64), value: &str, rotation: f64, width_factor: f64, oblique: f64) {
|
||||
let t = Text {
|
||||
insert: Vec3::new(at.0, at.1, 0.0),
|
||||
align_pt: None,
|
||||
height: 10.0,
|
||||
value: value.into(),
|
||||
rotation,
|
||||
width_factor,
|
||||
oblique,
|
||||
style: style.into(),
|
||||
halign: Default::default(),
|
||||
valign: Default::default(),
|
||||
};
|
||||
d.add(space, Common::default(), EntityKind::Text(t)).unwrap();
|
||||
}
|
||||
|
||||
fn drawing() -> Drawing {
|
||||
fonts();
|
||||
let mut d = Drawing::new_metric();
|
||||
style(&mut d, "Real", "cadcraft-pdf-test.ttf");
|
||||
style(&mut d, "Locked", "cadcraft-pdf-restricted.ttf");
|
||||
d
|
||||
}
|
||||
|
||||
/// The uncompressed PDF as text (offsets kept: non-ASCII bytes become `?`).
|
||||
fn pdf(d: &Drawing, space: &Space, extra: serde_json::Value) -> String {
|
||||
let mut o = json!({"compress": false});
|
||||
if let (Some(m), Some(e)) = (o.as_object_mut(), extra.as_object()) {
|
||||
m.extend(e.clone());
|
||||
}
|
||||
let bytes = plot(d, space, &o).unwrap();
|
||||
check_xref(&bytes);
|
||||
bytes.iter().map(|b| if b.is_ascii() { char::from(*b) } else { '?' }).collect()
|
||||
}
|
||||
|
||||
/// The cross-reference table points at every object.
|
||||
fn check_xref(bytes: &[u8]) {
|
||||
let text: String = bytes.iter().map(|b| if b.is_ascii() { char::from(*b) } else { '?' }).collect();
|
||||
let sx = text.rfind("startxref\n").unwrap();
|
||||
let xref: usize = text[sx + 10..].lines().next().unwrap().trim().parse().unwrap();
|
||||
let mut lines = text[xref..].lines().skip(1);
|
||||
let count: usize = lines.next().unwrap().split_whitespace().nth(1).unwrap().parse().unwrap();
|
||||
for (i, e) in lines.take(count).enumerate().skip(1) {
|
||||
let off: usize = e[..10].parse().unwrap();
|
||||
assert!(text[off..].starts_with(&format!("{i} 0 obj\n")), "object {i}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The text matrices (`a b c d e f Tm`) of the content, in order.
|
||||
fn matrices(pdf: &str) -> Vec<[f64; 6]> {
|
||||
pdf.lines()
|
||||
.filter(|l| l.ends_with(" Tj") && l.contains(" Tm "))
|
||||
.map(|l| {
|
||||
let v: Vec<f64> = l.split(" Tm ").next().unwrap().split_whitespace().map(|x| x.parse().unwrap()).collect();
|
||||
[v[0], v[1], v[2], v[3], v[4], v[5]]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The embedded font file (FontFile2 stream data).
|
||||
fn font_file(pdf: &str, bytes_of: &[u8]) -> Vec<u8> {
|
||||
let i = pdf.find("/Length1 ").expect("a FontFile2 stream");
|
||||
let head = &pdf[..i];
|
||||
let len_at = head.rfind("/Length ").unwrap() + 8;
|
||||
let len: usize = pdf[len_at..].split(|c: char| !c.is_ascii_digit()).next().unwrap().parse().unwrap();
|
||||
let start = pdf[i..].find("stream\n").unwrap() + i + 7;
|
||||
bytes_of[start..start + len].to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truetype_text_is_embedded_as_text() {
|
||||
let mut d = drawing();
|
||||
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "ABBA", 0.0, 1.0, 0.0);
|
||||
let p = pdf(&d, &Space::Model, json!({}));
|
||||
// A Type0 font over a CIDFontType2 subset with a ToUnicode map.
|
||||
assert!(p.contains("/Subtype /Type0") && p.contains("/Encoding /Identity-H") && p.contains("/Subtype /CIDFontType2"), "{p}");
|
||||
assert!(p.contains("/CIDToGIDMap /Identity") && p.contains("/FontFile2 ") && p.contains("/ToUnicode "));
|
||||
let base = p.split("/BaseFont /").nth(1).unwrap().split_whitespace().next().unwrap();
|
||||
assert!(base.len() == 11 && base.as_bytes()[6] == b'+' && base[..6].chars().all(|c| c.is_ascii_uppercase()), "{base}");
|
||||
assert!(p.contains("<0001> <0041>") && p.contains("<0002> <0042>"), "ToUnicode maps glyphs to A and B");
|
||||
assert!(p.contains("/W [1 [600] 2 [600]]"), "widths");
|
||||
// The page uses the font; four glyphs written as text, in a text object.
|
||||
assert!(p.contains("/Font << /F0 "));
|
||||
let tj: Vec<&str> = p.lines().filter(|l| l.ends_with(" Tj")).map(|l| l.rsplit(' ').nth(1).unwrap()).collect();
|
||||
assert_eq!(tj, ["<0001>", "<0002>", "<0002>", "<0001>"]);
|
||||
assert!(p.contains("BT\n/F0 1 Tf\n") && p.contains("ET\n"));
|
||||
assert_eq!(p.matches("BT\n").count(), p.matches("ET\n").count());
|
||||
// No filled outlines left for them.
|
||||
assert!(!p.contains("h\nf\n"), "glyph outlines are not drawn: {p}");
|
||||
// The glyphs advance along the baseline by 600/1000 em, scaled like the outline would be.
|
||||
let m = matrices(&p);
|
||||
assert!(m.iter().all(|m| m[1].abs() < 1e-3 && m[2].abs() < 1e-3 && (m[0] - m[3]).abs() < 1e-3 && m[0] > 0.0));
|
||||
let step = m[1][4] - m[0][4];
|
||||
assert!((step - 0.6 * m[0][0]).abs() < 0.01, "advance {step} for em {}", m[0][0]);
|
||||
assert!(m.windows(2).all(|w| (w[0][5] - w[1][5]).abs() < 1e-3), "one baseline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_subset_is_a_valid_truetype_font() {
|
||||
let mut d = drawing();
|
||||
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "A", 0.0, 1.0, 0.0);
|
||||
let bytes = plot(&d, &Space::Model, &json!({"compress": false})).unwrap();
|
||||
let p: String = bytes.iter().map(|b| if b.is_ascii() { char::from(*b) } else { '?' }).collect();
|
||||
let file = font_file(&p, &bytes);
|
||||
assert!(p.contains(&format!("/Length1 {}", file.len())));
|
||||
let face = cadcraft_fonts::ttf::Face::new(std::sync::Arc::new(file.clone()), 0).expect("the subset parses");
|
||||
assert_eq!(cadcraft_fonts::subset::embedding(&face), cadcraft_fonts::subset::Embedding::Allowed);
|
||||
// Glyph A kept, B (unused) emptied: the subset is smaller than the font.
|
||||
assert!(file.len() < test_font(0).len());
|
||||
// Whole-file checksum adjusted.
|
||||
let sum = file.chunks(4).fold(0u32, |s, c| {
|
||||
let mut w = [0u8; 4];
|
||||
w[..c.len()].copy_from_slice(c);
|
||||
s.wrapping_add(u32::from_be_bytes(w))
|
||||
});
|
||||
assert_eq!(sum, 0xB1B0_AFBA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotation_width_factor_and_obliquing_go_into_the_text_matrix() {
|
||||
let mut d = drawing();
|
||||
let quarter = std::f64::consts::FRAC_PI_2;
|
||||
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "A", quarter, 1.0, 0.0);
|
||||
text(&mut d, &Space::Model, "Real", (0.0, 50.0), "A", 0.0, 2.0, 0.0);
|
||||
text(&mut d, &Space::Model, "Real", (0.0, 100.0), "A", 0.0, 1.0, 15f64.to_radians());
|
||||
let m = matrices(&pdf(&d, &Space::Model, json!({})));
|
||||
assert_eq!(m.len(), 3);
|
||||
// Rotated a quarter turn: the baseline points up, "up" points left.
|
||||
let [a, b, c, dd, _, _] = m[0];
|
||||
assert!(a.abs() < 1e-3 && b > 0.0 && c < 0.0 && dd.abs() < 1e-3, "{:?}", m[0]);
|
||||
// Width factor 2: twice as wide as high.
|
||||
assert!((m[1][0] - 2.0 * m[1][3]).abs() < 1e-2 && m[1][1].abs() < 1e-3, "{:?}", m[1]);
|
||||
// Oblique 15°: the up axis leans right by tan 15°.
|
||||
assert!((m[2][2] - m[2][3] * 15f64.to_radians().tan()).abs() < 1e-2, "{:?}", m[2]);
|
||||
// The em is sized so the cap height (here 700/1000 em) is the text height: 10 / 0.7.
|
||||
let em_pt = m[1][3];
|
||||
let em_mm = em_pt * 25.4 / 72.0;
|
||||
assert!(em_mm > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fonts_that_forbid_embedding_and_the_geometry_option_draw_outlines() {
|
||||
let mut d = drawing();
|
||||
text(&mut d, &Space::Model, "Locked", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
|
||||
let p = pdf(&d, &Space::Model, json!({}));
|
||||
assert!(!p.contains("/FontFile2") && !p.contains("BT\n") && p.contains("h\nf\n"), "restricted licence: outlines");
|
||||
let mut d = drawing();
|
||||
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
|
||||
let p = pdf(&d, &Space::Model, json!({"text": "geometry"}));
|
||||
assert!(!p.contains("/FontFile2") && !p.contains("BT\n") && p.contains("h\nf\n"), "text as geometry");
|
||||
// The stroke font is always geometry.
|
||||
let mut d = drawing();
|
||||
text(&mut d, &Space::Model, "Standard", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
|
||||
let p = pdf(&d, &Space::Model, json!({}));
|
||||
assert!(!p.contains("BT\n") && p.contains(" S\n") || p.contains("\nS\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_seen_through_a_viewport_and_on_layers() {
|
||||
let mut d = drawing();
|
||||
let paper = Space::Paper("Layout1".into());
|
||||
// Model: one word well inside the viewport, one cut by its right edge.
|
||||
text(&mut d, &Space::Model, "Real", (0.0, 0.0), "AB", 0.0, 1.0, 0.0);
|
||||
text(&mut d, &Space::Model, "Real", (88.0, 0.0), "BA", 0.0, 1.0, 0.0);
|
||||
let vp = Viewport {
|
||||
center: Vec3::new(150.0, 100.0, 0.0),
|
||||
width: 100.0,
|
||||
height: 100.0,
|
||||
view_center: Vec2::new(50.0, 5.0),
|
||||
view_height: 100.0,
|
||||
id: 2,
|
||||
locked: false,
|
||||
frozen_layers: Vec::new(),
|
||||
layer_colors: Vec::new(),
|
||||
clip: None,
|
||||
on: true,
|
||||
};
|
||||
d.add(&paper, Common::default(), EntityKind::Viewport(vp)).unwrap();
|
||||
text(&mut d, &paper, "Real", (20.0, 20.0), "B", 0.0, 1.0, 0.0);
|
||||
let p = pdf(&d, &paper, json!({}));
|
||||
let tj = p.lines().filter(|l| l.ends_with(" Tj")).count();
|
||||
// "AB" inside, the paper-space "B", and the glyph of "BA" left of the edge: real text. The
|
||||
// glyph the edge cuts stays a (clipped) outline.
|
||||
assert_eq!(tj, 4, "{p}");
|
||||
assert!(p.contains("h\nf\n"), "the cut glyph is an outline");
|
||||
// Text sits in its layer's marked content.
|
||||
let first_bt = p.find("BT\n").unwrap();
|
||||
assert!(p[..first_bt].contains("/OC /L0 BDC"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_text_never_panics() {
|
||||
let mut d = drawing();
|
||||
for (v, h, r) in
|
||||
[("A".repeat(5000), 10.0, 0.0), ("AB".into(), 1e300, 0.0), ("AB".into(), 1e-300, 1e300), ("\u{0}A\u{FFFF}B".into(), 5.0, f64::NAN)]
|
||||
{
|
||||
let t = Text {
|
||||
insert: Vec3::new(0.0, 0.0, 0.0),
|
||||
align_pt: None,
|
||||
height: h,
|
||||
value: v,
|
||||
rotation: r,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: "Real".into(),
|
||||
halign: Default::default(),
|
||||
valign: Default::default(),
|
||||
};
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Text(t)).unwrap();
|
||||
}
|
||||
pdf(&d, &Space::Model, json!({}));
|
||||
pdf(&d, &Space::Model, json!({"compress": true}));
|
||||
}
|
||||
@@ -36,6 +36,8 @@ pub struct DimGeometry {
|
||||
pub text: Vec<Vec<Vec2>>,
|
||||
/// Text glyph outlines (TrueType), grouped per glyph.
|
||||
pub text_glyphs: Vec<Vec<Vec<Vec2>>>,
|
||||
/// The font glyphs of `text_glyphs` (outline indices into it).
|
||||
pub text_runs: Vec<cadcraft_fonts::TextGlyph>,
|
||||
pub text_pos: Vec2,
|
||||
pub text_angle: f64,
|
||||
pub text_height: f64,
|
||||
@@ -948,6 +950,8 @@ pub fn dimension_geometry_with(d: &Dimension, st: &DimStyle, dimscale: f64, font
|
||||
if !g.mtext.is_empty() {
|
||||
let l = cadcraft_fonts::layout_mtext_with(&g.mtext, ¶ms(text_mid, text_angle));
|
||||
for p in l.pieces {
|
||||
let base = g.text_glyphs.len();
|
||||
g.text_runs.extend(p.shaped.text.into_iter().map(|t| cadcraft_fonts::TextGlyph { outline: t.outline + base, ..t }));
|
||||
g.text.extend(p.shaped.strokes);
|
||||
g.text_glyphs.extend(p.shaped.glyphs);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ pub struct DisplayList {
|
||||
/// it is drawn on (block contents on layer 0: the block reference's; [`NO_LAYER`] when the
|
||||
/// layer isn't in the table), for PDF layers. Empty for other lists.
|
||||
pub prim_layers: Vec<u32>,
|
||||
/// Plots: outline-font glyphs that a plotter can write as text instead of drawing the
|
||||
/// filled outline primitive they stand for ([`PlotText::prim`]), in drawing order.
|
||||
pub texts: Vec<PlotText>,
|
||||
pub bounds: Bounds2,
|
||||
/// The paper sheet when the list was built for a layout (paper space); the UI draws the
|
||||
/// white sheet, its shadow and the printable-area outline from it.
|
||||
@@ -91,6 +94,28 @@ pub struct DisplayList {
|
||||
pub pens: Vec<Pen>,
|
||||
}
|
||||
|
||||
/// A glyph of plotted text: the font glyph and, in world coordinates, where it sits. It is
|
||||
/// drawn by primitive [`PlotText::prim`] (a filled outline in the primitive's colour); output
|
||||
/// with real text writes the glyph instead of that primitive.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PlotText {
|
||||
/// The filled outline primitive (index in [`DisplayList::prims`]); [`PlotText::NONE`] when
|
||||
/// the glyph has none in the list (clipped by a viewport).
|
||||
pub prim: u32,
|
||||
/// Triangle vertices of the outline when it was drawn, and their bounding box (a clipped
|
||||
/// outline has others).
|
||||
pub verts: u32,
|
||||
pub bounds: Bounds2,
|
||||
/// The glyph; its origin and em axes are in world coordinates.
|
||||
pub glyph: cadcraft_fonts::TextGlyph,
|
||||
}
|
||||
|
||||
impl PlotText {
|
||||
pub const NONE: u32 = u32::MAX;
|
||||
/// [`PlotText::prim`] of a glyph cut by a viewport edge.
|
||||
pub const CLIPPED: u32 = u32::MAX - 1;
|
||||
}
|
||||
|
||||
impl DPrim {
|
||||
/// The colour to draw with on `background`.
|
||||
pub fn display_rgb(&self, background: Rgb) -> Rgb {
|
||||
@@ -272,10 +297,23 @@ impl Builder<'_> {
|
||||
return;
|
||||
}
|
||||
let fill = self.opts.fill && ctx.d.header.i64("TEXTFILL", 1) != 0;
|
||||
for g in &sh.glyphs {
|
||||
// Plots note which filled outlines are font glyphs (see `DisplayList::texts`).
|
||||
let mut text = if self.plotting && fill { sh.text.iter().peekable() } else { [].iter().peekable() };
|
||||
for (i, g) in sh.glyphs.iter().enumerate() {
|
||||
if fill {
|
||||
let tris = fill::triangulate_evenodd(g);
|
||||
let at = self.list.prims.len();
|
||||
self.tris(ctx, color, &tris);
|
||||
while text.next_if(|t| t.outline < i).is_some() {}
|
||||
if let Some(t) = text.next_if(|t| t.outline == i)
|
||||
&& self.list.prims.len() > at
|
||||
&& let (Ok(prim), Ok(verts)) = (u32::try_from(at), u32::try_from(tris.len()))
|
||||
{
|
||||
let xf = &ctx.xf;
|
||||
let glyph = cadcraft_fonts::TextGlyph { origin: xf.apply(t.origin), x: xf.apply_vec(t.x), y: xf.apply_vec(t.y), ..t.clone() };
|
||||
let bounds = self.list.prims.get(at).map(|p| Bounds2::from_points(self.list.points(p).iter().copied())).unwrap_or(Bounds2::EMPTY);
|
||||
self.list.texts.push(PlotText { prim, verts, bounds, glyph });
|
||||
}
|
||||
} else {
|
||||
for c in g {
|
||||
self.polyline(ctx, color, lw, c);
|
||||
@@ -441,6 +479,17 @@ fn viewport(b: &mut Builder, d: &Drawing, e: &Entity, vp: &cadcraft_doc::Viewpor
|
||||
continue;
|
||||
}
|
||||
entity(&mut sub, &Ctx { lt_factor, ..top_ctx(d, xf, VIEWPORT_CONTENT, &vp.frozen_layers, &vp.layer_colors) }, me);
|
||||
// Plotted text glyphs move to the layout's list; their outlines carry the glyph's
|
||||
// index there in their handle's high half until clipped (see `take_viewport_layers`).
|
||||
for t in std::mem::take(&mut sub.list.texts) {
|
||||
let k = b.list.texts.len() as u64 + 1;
|
||||
if let Some(p) = sub.list.prims.get_mut(t.prim as usize)
|
||||
&& k < u64::from(u32::MAX)
|
||||
{
|
||||
p.handle = Handle(p.handle.0 | (k << 32));
|
||||
b.list.texts.push(PlotText { prim: PlotText::NONE, ..t });
|
||||
}
|
||||
}
|
||||
}
|
||||
// The contents are in paper units now: draw their pens before clipping.
|
||||
pen::apply(&mut sub.list, paper::paper_unit_mm(d));
|
||||
@@ -570,11 +619,29 @@ fn entity_layer(ctx: &Ctx, e: &Entity) -> u32 {
|
||||
/// After a viewport is appended to a plot: the layers its contents carry in their handles
|
||||
/// (see [`entity`]) go to [`DisplayList::prim_layers`], and the handles back to
|
||||
/// [`VIEWPORT_CONTENT`]. The viewport's border was tagged when it was drawn.
|
||||
///
|
||||
/// Text glyphs seen through the viewport (marked in the handles' high half) keep their outline
|
||||
/// primitive when it came through the clip whole; a glyph cut by the viewport edge stays
|
||||
/// geometry.
|
||||
fn take_viewport_layers(list: &mut DisplayList) {
|
||||
let start = list.prim_layers.len();
|
||||
for p in list.prims.iter_mut().skip(start) {
|
||||
list.prim_layers.push(u32::try_from(p.handle.0).unwrap_or(NO_LAYER));
|
||||
p.handle = VIEWPORT_CONTENT;
|
||||
for i in start..list.prims.len() {
|
||||
let Some(p) = list.prims.get(i).copied() else { break };
|
||||
list.prim_layers.push(u32::try_from(p.handle.0 & 0xFFFF_FFFF).unwrap_or(NO_LAYER));
|
||||
let bounds = Bounds2::from_points(list.points(&p).iter().copied());
|
||||
if let Some(t) = (p.handle.0 >> 32).checked_sub(1).and_then(|k| list.texts.get_mut(k as usize)) {
|
||||
let same = |a: Vec2, b: Vec2| a.dist(b) <= 1e-9 * (1.0 + a.len());
|
||||
let whole = t.prim == PlotText::NONE
|
||||
&& p.kind == Kind::Tris
|
||||
&& p.len == t.verts
|
||||
&& same(bounds.min, t.bounds.min)
|
||||
&& same(bounds.max, t.bounds.max);
|
||||
// A second piece (or a cut one) means the clip changed the glyph.
|
||||
t.prim = if whole { u32::try_from(i).unwrap_or(PlotText::NONE) } else { PlotText::CLIPPED };
|
||||
}
|
||||
if let Some(p) = list.prims.get_mut(i) {
|
||||
p.handle = VIEWPORT_CONTENT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1044,7 +1111,7 @@ fn dimension(b: &mut Builder, ctx: &Ctx, e: &Entity, dm: &cadcraft_doc::Dimensio
|
||||
b.tris(ctx, dim_rgb, t);
|
||||
}
|
||||
if b.opts.text {
|
||||
let sh = cadcraft_fonts::Shaped { strokes: g.text.clone(), glyphs: g.text_glyphs.clone(), width: 0.0 };
|
||||
let sh = cadcraft_fonts::Shaped { strokes: g.text.clone(), glyphs: g.text_glyphs.clone(), width: 0.0, text: g.text_runs.clone() };
|
||||
b.shaped(ctx, txt_rgb, lw, &sh);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user