F1: exact standard-14 metrics

pdfcraft_fonts::helvetica_width was a fifteen-line guess by character class,
and seven crates generated appearance streams through it: annot, edit, forms,
ocr, redact, sign and xfa. Times was Helvetica x 0.9, bold was x 1.05, Courier
a flat 0.6. Against the published metrics a Times capital A is 722, so that
estimate said 600 -- a 17% error on a letter, compounding across a line.

Std14 carries the real advance widths for all fourteen faces, indexed by
character code: WinAnsiEncoding for the twelve text fonts, which is what we
write into the appearance streams we generate, and their own built-in
encodings for Symbol and ZapfDingbats. The tables are generated by joining the
published encodings with the per-glyph widths.

helvetica_width now delegates, so every call site gained exact Helvetica with
no change. The three places that faked another face by scaling it -- Family
in edit::added, the paragraph measure in edit::text, and the redaction overlay
fonts -- now select the right face instead. So does Metrics::width in
fonts::pdf, which measures existing document text when a font carries no
/Widths; that path feeds redaction's glyph removal, where a wrong width means
cutting the wrong glyphs.

Tests spot-check the table against values typed in from the specification by
hand, so it is verified against the spec rather than against whatever produced
it; plus Courier being fixed pitch everywhere, full printable-ASCII and Latin-1
coverage, Symbol and ZapfDingbats not being WinAnsi, /BaseFont aliases and
subset prefixes resolving, and hostile text staying finite.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Tanish MankuandClaude Opus 5 committed 2026-10-12 13:57:43 -07:00
1 parent 681548a47f
commit d0d1ad8192
6 files changed
+581 -70

No files matched your search

+21 -9
View File
@@ -12,7 +12,7 @@ use std::collections::{HashMap, HashSet};
use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream};
use pdfcraft_fonts::{
GlyphError, GlyphOutline, ShapedCluster, ShapedScript, helvetica_width, literal, shape_arabic, shape_thai, shaped_glyph, win_ansi,
GlyphError, GlyphOutline, ShapedCluster, ShapedScript, Std14, literal, shape_arabic, shape_thai, shaped_glyph, win_ansi,
};
use unicode_bidi::{Level, ParagraphBidiInfo};
@@ -67,15 +67,27 @@ impl Family {
(family, s.contains("Bold"), s.contains("Italic") || s.contains("Oblique"))
}
/// Approximate advance width of `s` (standard-14 metrics are not bundled: Helvetica widths,
/// scaled for Times; Courier is monospaced).
/// Advance width of `s` at `size` points, from the published standard-14 metrics.
pub fn width(self, s: &str, size: f64, bold: bool) -> f64 {
let w = match self {
Family::Courier => s.chars().count() as f64 * 0.6 * size,
Family::Times => helvetica_width(s, size) * 0.9,
Family::Helvetica => helvetica_width(s, size),
};
if bold { w * 1.05 } else { w }
self.std14(bold, false).text_width(s, size)
}
/// The standard-14 face this family means at the given style.
pub fn std14(self, bold: bool, italic: bool) -> Std14 {
match (self, bold, italic) {
(Family::Courier, false, false) => Std14::Courier,
(Family::Courier, true, false) => Std14::CourierBold,
(Family::Courier, false, true) => Std14::CourierOblique,
(Family::Courier, true, true) => Std14::CourierBoldOblique,
(Family::Times, false, false) => Std14::TimesRoman,
(Family::Times, true, false) => Std14::TimesBold,
(Family::Times, false, true) => Std14::TimesItalic,
(Family::Times, true, true) => Std14::TimesBoldItalic,
(Family::Helvetica, false, false) => Std14::Helvetica,
(Family::Helvetica, true, false) => Std14::HelveticaBold,
(Family::Helvetica, false, true) => Std14::HelveticaOblique,
(Family::Helvetica, true, true) => Std14::HelveticaBoldOblique,
}
}
}
+3 -7
View File
@@ -1724,13 +1724,9 @@ pub(crate) fn rewrite_block_with_limit(
// The chosen style also determines the real Japanese fallback outlines and advances.
let (family, bold, italic) = style.family.unwrap_or((source_family(&b.base_font), b.bold, b.italic));
let bold = style.bold.unwrap_or(bold);
let std_width = move |s: &str, size: f64| -> f64 {
match family {
crate::added::Family::Courier => s.chars().count() as f64 * 0.6 * size,
crate::added::Family::Times => pdfcraft_fonts::helvetica_width(s, size) * 0.92,
crate::added::Family::Helvetica => pdfcraft_fonts::helvetica_width(s, size) * if bold { 1.05 } else { 1.0 },
}
};
// The face is chosen after `bold` settles, so a bold request measures with the bold metrics.
let face = family.std14(bold, italic);
let std_width = move |s: &str, size: f64| -> f64 { face.text_width(s, size) };
let [dx, dy] = style.offset.unwrap_or([0.0, 0.0]);
if !(dx.is_finite() && dy.is_finite()) {
return Err(EditError::Invalid("the paragraph can't be moved that far".into()));
+11 -21
View File
@@ -1,7 +1,9 @@
//! pdfcraft-fonts — font metrics and encodings for generated appearances (L2).
//!
//! See the README: the metrics are approximations by character class (no vendor metrics files
//! are bundled). The full font subsystem lands in M2.2/M7.
//! Standard-14 advance widths are exact (`Std14`, ISO 32000-2 Annex D). What remains approximate
//! is everything that needs a font *program*: this crate reads font dictionaries, never embedded
//! programs, so it has no glyph outlines for document fonts and cannot subset or re-embed. That
//! is phase F 4.2 in docs/plan/execution-plan.md.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
@@ -12,6 +14,7 @@ pub mod pdf;
mod script;
mod shaping;
pub mod source_shaping;
mod std14;
pub use craft::{
CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_hebrew_font, document_japanese_font, document_japanese_font_for_style,
document_japanese_fonts_for_style, document_thai_font, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts, ui_japanese_fonts,
@@ -21,27 +24,14 @@ pub use script::{
GlyphError, GlyphOutline, INTER_REGULAR, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_missing, script_outline,
};
pub use shaping::{ShapedCluster, ShapedScript, arabic_glyph, arabic_has, shape_arabic, shape_thai, shaped_glyph, thai_has};
pub use std14::Std14;
/// Approximate advance of `s` in Helvetica (or Arial) at `size` points.
/// Advance of `s` in Helvetica at `size` points, using the exact standard-14 metrics.
///
/// Kept as a free function because generated appearances overwhelmingly use Helvetica; reach for
/// [`Std14`] directly for any other face, and never for a scaled approximation of this one.
pub fn helvetica_width(s: &str, size: f64) -> f64 {
let units: f64 = s
.chars()
.map(|c| match c {
' ' | 'i' | 'j' | 'l' | '\'' | '!' | '|' | '.' | ',' | ':' | ';' | 'I' => 260.0,
'f' | 't' | 'r' | '(' | ')' | '[' | ']' | '/' | '-' | '"' => 333.0,
'm' => 833.0,
'w' => 722.0,
'M' => 833.0,
'W' => 944.0,
'J' | 'c' | 'k' | 's' | 'v' | 'x' | 'y' | 'z' => 500.0,
'0'..='9' | 'a'..='z' | '$' | '#' | '?' | '_' => 556.0,
'A'..='Z' => 680.0,
'@' => 1015.0,
_ if c.is_whitespace() => 260.0,
_ => 584.0,
})
.sum();
units * size / 1000.0
Std14::Helvetica.text_width(s, size)
}
/// Approximate advance of `s` at `size` points in a CJK font: full-width characters take one
+14 -28
View File
@@ -19,14 +19,6 @@ enum Codes {
Ranges(Vec<(usize, Vec<u8>, Vec<u8>)>),
}
#[derive(Clone, Debug, PartialEq)]
enum Std14 {
Helvetica,
Times,
Courier,
Symbolic,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Metrics {
codes: Codes,
@@ -39,7 +31,8 @@ pub struct Metrics {
/// Code → CID for embedded non-identity CMaps (`cidrange` / `cidchar`).
cid_map: Vec<(u32, u32, u32)>,
default: f64,
std14: Option<Std14>,
/// The standard-14 face to measure with when the dictionary carries no `/Widths`.
std14: Option<crate::Std14>,
/// Glyph units → text space (0.001, or `/FontMatrix[0]` for Type 3).
pub scale: f64,
/// Glyph box in text space per unit of font size.
@@ -526,7 +519,7 @@ impl Metrics {
cid_ranges: Vec::new(),
cid_map: Vec::new(),
default: 500.0,
std14: Some(Std14::Helvetica),
std14: Some(crate::Std14::Helvetica),
scale: 0.001,
ascent: 0.9,
descent: -0.25,
@@ -655,16 +648,10 @@ impl Metrics {
}
}
} else if m.widths.is_empty() {
let base = String::from_utf8_lossy(font.name(b"BaseFont").unwrap_or(b"")).to_ascii_lowercase();
m.std14 = Some(if base.contains("courier") {
Std14::Courier
} else if base.contains("times") {
Std14::Times
} else if base.contains("symbol") || base.contains("dingbats") {
Std14::Symbolic
} else {
Std14::Helvetica
});
let base = String::from_utf8_lossy(font.name(b"BaseFont").unwrap_or(b"")).into_owned();
// The real face, bold and italic included: the published metrics differ by style,
// so Times-Bold is not Times-Roman and neither is Helvetica scaled.
m.std14 = crate::Std14::from_base_font(&base).or(Some(crate::Std14::Helvetica));
}
}
if let Some(d) = descriptor {
@@ -726,14 +713,13 @@ impl Metrics {
if let Some(w) = code.checked_sub(self.first).and_then(|i| self.widths.get(i as usize)) {
return w * self.scale;
}
match &self.std14 {
Some(Std14::Courier) => 0.6,
Some(Std14::Symbolic) => 0.75,
Some(f) => {
let c = char::from_u32(code).filter(|c| !c.is_control()).unwrap_or('n');
let w = crate::helvetica_width(&c.to_string(), 1.0);
if *f == Std14::Times { w * 0.92 } else { w }
}
match self.std14 {
// A font with no /Widths is one of the standard 14; use its published metrics.
// Codes the font's encoding leaves undefined fall back to /MissingWidth.
Some(f) => match u8::try_from(code).map(|c| f.width(c)).unwrap_or(0.0) {
0.0 => self.default * self.scale,
w => w / 1000.0,
},
None => self.default * self.scale,
}
}
+526
View File
@@ -0,0 +1,526 @@
//! Standard-14 font metrics (ISO 32000-2 Annex D; the Adobe Core14 AFM metrics), as advance
//! widths in glyph space — thousandths of an em.
//!
//! A PDF may name any of the fourteen standard fonts without embedding a program, and the viewer
//! has to supply the metrics. These are those metrics, exactly, replacing the character-class
//! estimate that generated appearances used before: that estimate had Times at 0.9 x Helvetica,
//! which makes a capital A 600 where the specification says 722.
//!
//! **Generated.** The tables are data, not typefaces, and are the item AGENTS.md §1.1 permits as
//! Adobe-authored non-visual technical data; `ATTRIBUTION.toml` carries the entry with
//! `adobe_data = true`. Each row is indexed by character **code**, not by Unicode: the twelve text
//! fonts are indexed by WinAnsiEncoding, which is what PdfCraft writes into the appearance streams
//! it generates, and Symbol and ZapfDingbats by their own built-in encodings. A zero means the
//! encoding leaves that code undefined.
/// One of the fourteen standard fonts (ISO 32000-2 §9.6.2.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Std14 {
Helvetica,
HelveticaBold,
HelveticaOblique,
HelveticaBoldOblique,
TimesRoman,
TimesBold,
TimesItalic,
TimesBoldItalic,
Courier,
CourierBold,
CourierOblique,
CourierBoldOblique,
Symbol,
ZapfDingbats,
}
impl Std14 {
/// The font a `/BaseFont` name selects, by the spec's names and the common aliases viewers
/// accept (`Arial` for `Helvetica`, `TimesNewRoman` for `Times`, `CourierNew` for `Courier`).
/// A subset prefix such as `ABCDEF+Helvetica` is ignored.
pub fn from_base_font(base: &str) -> Option<Self> {
let base = base.rsplit('+').next().unwrap_or(base);
let flat: String = base.chars().filter(|c| c.is_ascii_alphanumeric()).map(|c| c.to_ascii_lowercase()).collect();
let has = |w: &str| flat.contains(w);
let (bold, italic) = (has("bold"), has("italic") || has("oblique"));
if has("symbol") {
return Some(Self::Symbol);
}
if has("zapf") || has("dingbat") {
return Some(Self::ZapfDingbats);
}
if has("courier") || has("mono") {
return Some(match (bold, italic) {
(false, false) => Self::Courier,
(true, false) => Self::CourierBold,
(false, true) => Self::CourierOblique,
(true, true) => Self::CourierBoldOblique,
});
}
if has("times") || has("serif") || has("roman") {
return Some(match (bold, italic) {
(false, false) => Self::TimesRoman,
(true, false) => Self::TimesBold,
(false, true) => Self::TimesItalic,
(true, true) => Self::TimesBoldItalic,
});
}
if has("helvetica") || has("arial") {
return Some(match (bold, italic) {
(false, false) => Self::Helvetica,
(true, false) => Self::HelveticaBold,
(false, true) => Self::HelveticaOblique,
(true, true) => Self::HelveticaBoldOblique,
});
}
None
}
/// The advance of one character `code`, in thousandths of an em. Zero where the font's
/// encoding leaves the code undefined.
pub fn width(self, code: u8) -> f64 {
f64::from(WIDTHS[self as usize][code as usize])
}
/// The advance of `code` at `size` points.
pub fn advance(self, code: u8, size: f64) -> f64 {
self.width(code) * size / 1000.0
}
/// The advance of `text` at `size` points, encoded as the twelve text fonts are written:
/// WinAnsiEncoding, with characters it cannot represent replaced as `win_ansi` replaces them.
///
/// Symbol and ZapfDingbats have their own built-in encodings and are not WinAnsi, so this
/// measures them as if they were; pass codes to [`Std14::width`] for those.
pub fn text_width(self, text: &str, size: f64) -> f64 {
crate::win_ansi(text).iter().map(|&c| self.width(c)).sum::<f64>() * size / 1000.0
}
/// Courier is the only fixed-pitch family of the fourteen.
pub fn is_fixed_pitch(self) -> bool {
matches!(self, Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique)
}
/// The `/BaseFont` name for this font, as it is written into a PDF.
pub fn base_font(self) -> &'static str {
BASE_FONTS[self as usize]
}
}
static BASE_FONTS: [&str; 14] = [
"Helvetica",
"Helvetica-Bold",
"Helvetica-Oblique",
"Helvetica-BoldOblique",
"Times-Roman",
"Times-Bold",
"Times-Italic",
"Times-BoldItalic",
"Courier",
"Courier-Bold",
"Courier-Oblique",
"Courier-BoldOblique",
"Symbol",
"ZapfDingbats",
];
/// Advance widths by code, one row per [`Std14`] variant in declaration order.
#[rustfmt::skip]
static WIDTHS: [[u16; 256]; 14] = [
// Helvetica
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
],
// HelveticaBold
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
],
// HelveticaOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
556, 0, 222, 556, 333, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 222, 222, 333, 333, 350, 556, 1000, 333, 1000, 500, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500,
],
// HelveticaBoldOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278,
556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
556, 0, 278, 556, 500, 1000, 556, 556, 333, 1000, 667, 333, 1000, 0, 611, 0,
0, 278, 278, 500, 500, 350, 556, 1000, 333, 1000, 556, 333, 944, 0, 500, 667,
278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556,
],
// TimesRoman
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 408, 500, 500, 833, 778, 180, 333, 333, 500, 564, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444,
921, 722, 667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722,
556, 722, 667, 556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500,
333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500,
500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541, 0,
500, 0, 333, 500, 444, 1000, 500, 500, 333, 1000, 556, 333, 889, 0, 611, 0,
0, 333, 333, 444, 444, 350, 500, 1000, 333, 980, 389, 333, 722, 0, 444, 722,
250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333,
400, 564, 300, 300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444,
722, 722, 722, 722, 722, 722, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722, 722, 722, 722, 556, 500,
444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500, 500,
],
// TimesBold
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 555, 500, 500, 1000, 833, 278, 333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
930, 722, 667, 722, 722, 667, 611, 778, 778, 389, 500, 778, 667, 944, 722, 778,
611, 778, 722, 556, 667, 722, 722, 1000, 722, 722, 667, 333, 278, 333, 581, 500,
333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556, 278, 833, 556, 500,
556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520, 0,
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 1000, 0, 667, 0,
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 444, 722,
250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333,
400, 570, 300, 300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500,
722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 389, 389, 389, 389,
722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722, 722, 722, 722, 611, 556,
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556, 500,
],
// TimesItalic
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 420, 500, 500, 833, 778, 214, 333, 333, 500, 675, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500,
920, 611, 611, 667, 722, 611, 611, 722, 722, 333, 444, 667, 556, 833, 667, 722,
611, 722, 611, 500, 556, 722, 611, 833, 611, 556, 556, 389, 278, 389, 422, 500,
333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444, 278, 722, 500, 500,
500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541, 0,
500, 0, 333, 500, 556, 889, 500, 500, 333, 1000, 500, 333, 944, 0, 556, 0,
0, 333, 333, 556, 556, 350, 500, 889, 333, 980, 389, 333, 667, 0, 389, 556,
250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333,
400, 675, 300, 300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500,
611, 611, 611, 611, 611, 611, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722, 722, 722, 556, 611, 500,
500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500, 444,
],
// TimesBoldItalic
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 389, 555, 500, 500, 833, 778, 278, 333, 333, 500, 570, 250, 333, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
832, 667, 667, 667, 722, 667, 667, 722, 778, 389, 500, 667, 611, 889, 722, 722,
611, 722, 667, 556, 611, 722, 667, 889, 667, 611, 611, 333, 278, 333, 570, 500,
333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500, 278, 778, 556, 500,
500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570, 0,
500, 0, 333, 500, 500, 1000, 500, 500, 333, 1000, 556, 333, 944, 0, 611, 0,
0, 333, 333, 500, 500, 350, 500, 1000, 333, 1000, 389, 333, 722, 0, 389, 611,
250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 606, 333, 747, 333,
400, 570, 300, 300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500,
667, 667, 667, 667, 667, 667, 944, 667, 667, 667, 667, 667, 389, 389, 389, 389,
722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722, 722, 722, 611, 611, 500,
500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500, 444,
],
// Courier
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// CourierBold
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// CourierOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// CourierBoldOblique
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
600, 0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 0,
0, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
],
// Symbol
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
250, 333, 713, 500, 549, 833, 778, 439, 333, 333, 500, 549, 250, 549, 250, 278,
500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 549, 549, 549, 444,
549, 722, 667, 722, 612, 611, 763, 603, 722, 333, 631, 722, 686, 889, 722, 722,
768, 741, 556, 592, 611, 690, 439, 768, 645, 795, 611, 333, 863, 333, 658, 500,
500, 631, 549, 549, 494, 439, 521, 411, 603, 329, 603, 549, 549, 576, 521, 549,
549, 521, 549, 603, 439, 576, 713, 686, 493, 686, 494, 480, 200, 480, 549, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
750, 620, 247, 549, 167, 713, 500, 753, 753, 753, 753, 1042, 987, 603, 987, 603,
400, 549, 411, 549, 549, 713, 494, 460, 549, 549, 549, 549, 1000, 603, 1000, 658,
823, 686, 795, 987, 768, 768, 823, 768, 768, 713, 713, 713, 713, 713, 713, 713,
768, 713, 790, 790, 890, 823, 549, 250, 713, 603, 603, 1042, 987, 603, 987, 603,
494, 329, 790, 790, 786, 713, 384, 384, 384, 384, 384, 384, 494, 494, 494, 494,
0, 329, 274, 686, 686, 686, 384, 384, 384, 384, 384, 384, 494, 494, 494, 0,
],
// ZapfDingbats
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
278, 974, 961, 974, 980, 719, 789, 790, 791, 690, 960, 939, 549, 855, 911, 933,
911, 945, 974, 755, 846, 762, 761, 571, 677, 763, 760, 759, 754, 494, 552, 537,
577, 692, 786, 788, 788, 790, 793, 794, 816, 823, 789, 841, 823, 833, 816, 831,
923, 744, 723, 749, 790, 792, 695, 776, 768, 792, 759, 707, 708, 682, 701, 826,
815, 789, 789, 707, 687, 696, 689, 786, 787, 713, 791, 785, 791, 873, 761, 762,
762, 759, 759, 892, 892, 788, 784, 438, 138, 277, 415, 392, 392, 668, 668, 0,
390, 390, 317, 317, 276, 276, 509, 509, 410, 410, 234, 234, 334, 334, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 732, 544, 544, 910, 667, 760, 760, 776, 595, 694, 626, 788, 788, 788, 788,
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788, 788,
788, 788, 788, 788, 894, 838, 1016, 458, 748, 924, 748, 918, 927, 928, 928, 834,
873, 828, 924, 924, 917, 930, 931, 463, 883, 836, 836, 867, 867, 696, 696, 874,
0, 874, 760, 946, 771, 865, 771, 888, 967, 888, 831, 873, 927, 970, 918, 0,
],
];
#[cfg(test)]
mod tests {
use super::*;
/// Widths from the published Core14 metrics, typed in here by hand so the table is checked
/// against the specification rather than against whatever produced it.
#[test]
fn widths_match_the_published_metrics() {
let cases: &[(Std14, char, u16)] = &[
// Helvetica
(Std14::Helvetica, ' ', 278),
(Std14::Helvetica, 'A', 667),
(Std14::Helvetica, 'W', 944),
(Std14::Helvetica, 'i', 222),
(Std14::Helvetica, 'm', 833),
(Std14::Helvetica, '0', 556),
(Std14::Helvetica, '@', 1015),
(Std14::HelveticaBold, 'A', 722),
(Std14::HelveticaBold, 'i', 278),
// Times: not Helvetica scaled by 0.9, which is what the old estimate assumed.
(Std14::TimesRoman, ' ', 250),
(Std14::TimesRoman, 'A', 722),
(Std14::TimesRoman, 'W', 944),
(Std14::TimesRoman, 'i', 278),
(Std14::TimesRoman, 'm', 778),
(Std14::TimesBold, 'A', 722),
(Std14::TimesItalic, 'A', 611),
// Courier is fixed pitch at 600 for every glyph.
(Std14::Courier, ' ', 600),
(Std14::Courier, 'A', 600),
(Std14::Courier, 'm', 600),
(Std14::CourierBoldOblique, 'W', 600),
];
for &(font, ch, want) in cases {
let code = crate::win_ansi(&ch.to_string());
assert_eq!(code.len(), 1, "{ch:?} is one WinAnsi byte");
assert_eq!(font.width(code[0]) as u16, want, "{font:?} {ch:?}");
}
}
#[test]
fn courier_is_fixed_pitch_everywhere_it_is_defined() {
for font in [Std14::Courier, Std14::CourierBold, Std14::CourierOblique, Std14::CourierBoldOblique] {
assert!(font.is_fixed_pitch());
for code in 0..=255u8 {
let w = font.width(code);
assert!(w == 0.0 || w == 600.0, "{font:?} code {code} has width {w}");
}
}
assert!(!Std14::Helvetica.is_fixed_pitch());
}
#[test]
fn every_text_font_covers_printable_ascii_and_latin1() {
for font in [Std14::Helvetica, Std14::TimesRoman, Std14::Courier, Std14::TimesBoldItalic] {
for code in 0x20..=0x7Eu8 {
assert!(font.width(code) > 0.0, "{font:?} has no width for ASCII {code}");
}
for code in 0xA0..=0xFFu8 {
assert!(font.width(code) > 0.0, "{font:?} has no width for Latin-1 {code}");
}
}
}
#[test]
fn symbol_and_dingbats_have_their_own_encodings() {
// Symbol's code 0x61 is alpha, not 'a'; ZapfDingbats' is a scissors glyph.
assert!(Std14::Symbol.width(0x61) > 0.0);
assert!(Std14::ZapfDingbats.width(0x61) > 0.0);
// Neither defines the Latin-1 upper half the text fonts do.
assert_eq!(Std14::Symbol.width(0xFF), 0.0);
}
#[test]
fn base_font_names_round_trip() {
for i in 0..14usize {
let font = [
Std14::Helvetica,
Std14::HelveticaBold,
Std14::HelveticaOblique,
Std14::HelveticaBoldOblique,
Std14::TimesRoman,
Std14::TimesBold,
Std14::TimesItalic,
Std14::TimesBoldItalic,
Std14::Courier,
Std14::CourierBold,
Std14::CourierOblique,
Std14::CourierBoldOblique,
Std14::Symbol,
Std14::ZapfDingbats,
][i];
assert_eq!(Std14::from_base_font(font.base_font()), Some(font), "{}", font.base_font());
}
}
#[test]
fn base_font_aliases_and_subset_prefixes_resolve() {
assert_eq!(Std14::from_base_font("ABCDEF+Helvetica-Bold"), Some(Std14::HelveticaBold));
assert_eq!(Std14::from_base_font("Arial"), Some(Std14::Helvetica));
assert_eq!(Std14::from_base_font("Arial,BoldItalic"), Some(Std14::HelveticaBoldOblique));
assert_eq!(Std14::from_base_font("TimesNewRoman,Bold"), Some(Std14::TimesBold));
assert_eq!(Std14::from_base_font("CourierNew-Oblique"), Some(Std14::CourierOblique));
assert_eq!(Std14::from_base_font("ZapfDingbats"), Some(Std14::ZapfDingbats));
assert_eq!(Std14::from_base_font("Wingdings"), None);
assert_eq!(Std14::from_base_font(""), None);
}
#[test]
fn text_width_sums_the_glyphs_and_scales_with_size() {
let h = Std14::Helvetica;
// "AW" = 667 + 944 = 1611 thousandths of an em.
assert!((h.text_width("AW", 1000.0) - 1611.0).abs() < 1e-9);
assert!((h.text_width("AW", 12.0) - 1611.0 * 12.0 / 1000.0).abs() < 1e-9);
assert_eq!(h.text_width("", 12.0), 0.0);
// Courier: every character is 600, so the width is purely a count.
assert!((Std14::Courier.text_width("hello", 10.0) - 5.0 * 6.0).abs() < 1e-9);
}
#[test]
fn hostile_text_does_not_panic_and_stays_finite() {
for font in [Std14::Helvetica, Std14::Symbol, Std14::Courier] {
for s in ["", "\0", "\u{202E}", "💥", &"a".repeat(10_000), "\u{FFFD}"] {
let w = font.text_width(s, 12.0);
assert!(w.is_finite() && w >= 0.0, "{font:?} {s:?} -> {w}");
}
assert!(font.advance(0, 12.0).is_finite());
assert!(font.advance(255, 0.0).is_finite());
}
}
}
+6 -5
View File
@@ -222,12 +222,13 @@ fn overlay_content(marks: &[&Mark]) -> Vec<u8> {
// The overlay text: its font, colour and alignment; auto-sized to fit unless a size is
// set; repeated to fill the area when asked.
let look = &m.look;
let (res, width): (&str, fn(&str, f64) -> f64) = match look.font {
pdfcraft_annot::OverlayFont::Helvetica => ("PCHelv", pdfcraft_fonts::helvetica_width),
// Approximations of the standard metrics (no font program is bundled).
pdfcraft_annot::OverlayFont::Times => ("PCTimes", |s, size| pdfcraft_fonts::helvetica_width(s, size) * 0.9),
pdfcraft_annot::OverlayFont::Courier => ("PCCour", |s, size| s.chars().count() as f64 * size * 0.6),
// The published standard-14 metrics, so overlay text is measured as it will be drawn.
let (res, face) = match look.font {
pdfcraft_annot::OverlayFont::Helvetica => ("PCHelv", pdfcraft_fonts::Std14::Helvetica),
pdfcraft_annot::OverlayFont::Times => ("PCTimes", pdfcraft_fonts::Std14::TimesRoman),
pdfcraft_annot::OverlayFont::Courier => ("PCCour", pdfcraft_fonts::Std14::Courier),
};
let width = move |s: &str, size: f64| face.text_width(s, size);
let [cr, cg, cb] = look.color.map(|v| v.clamp(0.0, 1.0));
for r in &m.rects {
let (w, h) = (r[2] - r[0], r[3] - r[1]);