diff --git a/crates/edit/src/added.rs b/crates/edit/src/added.rs index 42f786716..66047c559 100644 --- a/crates/edit/src/added.rs +++ b/crates/edit/src/added.rs @@ -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, + } } } diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index 027032282..cb8b93edf 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -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())); diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 19d0d1daf..622c609f6 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -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 diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index e3de687ff..b62110d18 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -19,14 +19,6 @@ enum Codes { Ranges(Vec<(usize, Vec, Vec)>), } -#[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, + /// The standard-14 face to measure with when the dictionary carries no `/Widths`. + std14: Option, /// 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, } } diff --git a/crates/fonts/src/std14.rs b/crates/fonts/src/std14.rs new file mode 100644 index 000000000..0aca38a12 --- /dev/null +++ b/crates/fonts/src/std14.rs @@ -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 { + 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::() * 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()); + } + } +} diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index 916d605ed..7ce4cfd16 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -222,12 +222,13 @@ fn overlay_content(marks: &[&Mark]) -> Vec { // 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]);