mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 05:22:58 +00:00
109 lines
4.8 KiB
Rust
109 lines
4.8 KiB
Rust
//! The font a text document is set in: standard Helvetica, or, for text Helvetica can't draw, the
|
|
//! bundled Inter face (`pdfcraft_fonts::document_latin_fonts`), which every build has and which
|
|
//! carries the Latin, Greek and Cyrillic letters WinAnsiEncoding lacks. Text Helvetica draws is
|
|
//! set in Helvetica exactly as before.
|
|
|
|
use pdfcraft_cos::{Dict, Document, ObjRef, Object};
|
|
use pdfcraft_fonts::pdf::Metrics;
|
|
use pdfcraft_fonts::{helvetica_width, literal, win_ansi};
|
|
|
|
use crate::CreateError;
|
|
|
|
/// The bundled face, Regular.
|
|
fn face() -> Option<&'static pdfcraft_fonts::CraftFont> {
|
|
pdfcraft_fonts::document_latin_fonts(false).into_iter().next()
|
|
}
|
|
|
|
/// Line breaks, tabs and form feeds lay the text out; they are not drawn.
|
|
fn layout(c: char) -> bool {
|
|
matches!(c, '\n' | '\r' | '\t' | '\u{c}')
|
|
}
|
|
|
|
/// The character the bundled face draws for `c`: a soft hyphen as a hyphen, as Helvetica draws it
|
|
/// (Inter has no glyph for it).
|
|
pub(crate) fn drawn_as(c: char) -> char {
|
|
if c == '\u{ad}' { '-' } else { c }
|
|
}
|
|
|
|
/// The line (1-based, lines split at `\n`) and character of the first one the bundled face can't
|
|
/// draw, or `None` when it draws all of `text`.
|
|
pub(crate) fn first_missing(text: &str) -> Option<(usize, char)> {
|
|
let face = face();
|
|
let draws = |s: &str| !s.chars().any(char::is_control) && face.is_some_and(|f| pdfcraft_fonts::embedding::covers(f.bytes, s));
|
|
let mut buf = [0u8; 4];
|
|
text.split('\n').enumerate().find_map(|(i, line)| {
|
|
// A whole line at once; the character only for a line the face can't draw.
|
|
let drawn: String = line.chars().filter(|c| !layout(*c)).map(drawn_as).collect();
|
|
if draws(&drawn) {
|
|
return None;
|
|
}
|
|
line.chars().find(|&c| !layout(c) && !draws(drawn_as(c).encode_utf8(&mut buf))).map(|c| (i + 1, c))
|
|
})
|
|
}
|
|
|
|
/// The refusal for text with `c` on `line`, which neither Helvetica nor the bundled face can draw.
|
|
pub(crate) fn undrawable_message(line: usize, c: char) -> String {
|
|
format!(
|
|
"neither Helvetica nor the bundled Inter face can draw \"{}\" (U+{:04X}) in the text (line {line}); only Latin, Greek and Cyrillic letters can be written there for now",
|
|
c.escape_debug(),
|
|
u32::from(c)
|
|
)
|
|
}
|
|
|
|
/// The font a text document is set in.
|
|
pub(crate) struct TextFont {
|
|
font: ObjRef,
|
|
/// The bundled face's metrics; `None` for Helvetica.
|
|
inter: Option<Metrics>,
|
|
}
|
|
|
|
impl TextFont {
|
|
/// Standard Helvetica in WinAnsiEncoding, added to `doc`.
|
|
pub(crate) fn helvetica(doc: &mut Document) -> Self {
|
|
let mut font = Dict::new();
|
|
font.set(b"Type".to_vec(), Object::name("Font"));
|
|
font.set(b"Subtype".to_vec(), Object::name("Type1"));
|
|
font.set(b"BaseFont".to_vec(), Object::name("Helvetica"));
|
|
font.set(b"Encoding".to_vec(), Object::name("WinAnsiEncoding"));
|
|
Self { font: doc.add(font), inter: None }
|
|
}
|
|
|
|
/// The bundled face, embedded in `doc` once for every page.
|
|
pub(crate) fn inter(doc: &mut Document) -> Result<Self, CreateError> {
|
|
let face = face().ok_or_else(|| CreateError::Invalid("this build has no bundled Latin, Greek and Cyrillic face".into()))?;
|
|
let mut fonts = Dict::new();
|
|
let name = pdfcraft_fonts::embedding::embed(doc, &mut fonts, face.bytes, face.family).map_err(CreateError::Invalid)?;
|
|
let font = fonts.reference(name.as_bytes()).ok_or_else(|| CreateError::Invalid("the bundled face could not be added".into()))?;
|
|
let dict = doc.dict(&Object::Ref(font)).ok_or_else(|| CreateError::Invalid("the bundled face could not be read".into()))?;
|
|
Ok(Self { font, inter: Some(Metrics::from_dict(doc, &dict)) })
|
|
}
|
|
|
|
/// The font object every page names.
|
|
pub(crate) fn font(&self) -> ObjRef {
|
|
self.font
|
|
}
|
|
|
|
/// The advance of `s` at `size`; characters the face lacks count as nothing ([`Self::show`]
|
|
/// refuses them).
|
|
pub(crate) fn width(&self, s: &str, size: f64) -> f64 {
|
|
match &self.inter {
|
|
None => helvetica_width(s, size),
|
|
Some(m) => m.encode(s).map_or(0.0, |codes| m.codes(&codes).iter().map(|(code, _)| m.width(*code)).sum::<f64>() * size),
|
|
}
|
|
}
|
|
|
|
/// The string operand that shows `line`. Helvetica writes what WinAnsiEncoding can't hold as
|
|
/// `?`, so its callers check first; the bundled face refuses a character it lacks.
|
|
pub(crate) fn show(&self, line: &str) -> Result<Vec<u8>, CreateError> {
|
|
let Some(m) = &self.inter else { return Ok(literal(&win_ansi(line))) };
|
|
let codes = m.encode(line).ok_or_else(|| CreateError::Invalid(format!("the bundled Inter face can't draw \"{}\"", line.escape_debug())))?;
|
|
let mut out = Vec::with_capacity(codes.len().saturating_mul(2).saturating_add(2));
|
|
out.push(b'<');
|
|
for b in codes {
|
|
out.extend_from_slice(format!("{b:02X}").as_bytes());
|
|
}
|
|
out.push(b'>');
|
|
Ok(out)
|
|
}
|
|
}
|