use lopdf::content::Operation;
use lopdf::{dictionary, Dictionary, Document, Object, ObjectId, Stream};
use std::collections::{BTreeMap, HashMap};
use std::fs::File;
use std::io::Read;
use std::path::Path;
use ttf_parser::{Face, GlyphId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StandardFont {
TimesRoman,
TimesBold,
TimesItalic,
TimesBoldItalic,
Helvetica,
HelveticaBold,
HelveticaOblique,
HelveticaBoldOblique,
Courier,
CourierBold,
CourierOblique,
CourierBoldOblique,
Symbol,
ZapfDingbats,
}
impl StandardFont {
pub fn postscript_name(&self) -> &'static str {
match self {
StandardFont::TimesRoman => "Times-Roman",
StandardFont::TimesBold => "Times-Bold",
StandardFont::TimesItalic => "Times-Italic",
StandardFont::TimesBoldItalic => "Times-BoldItalic",
StandardFont::Helvetica => "Helvetica",
StandardFont::HelveticaBold => "Helvetica-Bold",
StandardFont::HelveticaOblique => "Helvetica-Oblique",
StandardFont::HelveticaBoldOblique => "Helvetica-BoldOblique",
StandardFont::Courier => "Courier",
StandardFont::CourierBold => "Courier-Bold",
StandardFont::CourierOblique => "Courier-Oblique",
StandardFont::CourierBoldOblique => "Courier-BoldOblique",
StandardFont::Symbol => "Symbol",
StandardFont::ZapfDingbats => "ZapfDingbats",
}
}
pub fn family(&self) -> &'static str {
match self {
StandardFont::TimesRoman
| StandardFont::TimesBold
| StandardFont::TimesItalic
| StandardFont::TimesBoldItalic => "Times",
StandardFont::Helvetica
| StandardFont::HelveticaBold
| StandardFont::HelveticaOblique
| StandardFont::HelveticaBoldOblique => "Helvetica",
StandardFont::Courier
| StandardFont::CourierBold
| StandardFont::CourierOblique
| StandardFont::CourierBoldOblique => "Courier",
StandardFont::Symbol => "Symbol",
StandardFont::ZapfDingbats => "ZapfDingbats",
}
}
pub fn is_bold(&self) -> bool {
matches!(
self,
StandardFont::TimesBold
| StandardFont::TimesBoldItalic
| StandardFont::HelveticaBold
| StandardFont::HelveticaBoldOblique
| StandardFont::CourierBold
| StandardFont::CourierBoldOblique
)
}
pub fn is_italic(&self) -> bool {
matches!(
self,
StandardFont::TimesItalic
| StandardFont::TimesBoldItalic
| StandardFont::HelveticaOblique
| StandardFont::HelveticaBoldOblique
| StandardFont::CourierOblique
| StandardFont::CourierBoldOblique
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FontType {
Standard(StandardFont),
TrueType { data: Vec<u8> },
OpenType { data: Vec<u8> },
}
#[derive(Debug, Clone)]
pub struct FontMetadata {
pub family: String,
pub postscript_name: String,
pub weight: u16,
pub italic: bool,
pub encoding: String,
pub unicode: bool,
}
#[derive(Debug, Clone)]
pub struct FontMetrics {
pub units_per_em: u16,
pub ascender: i32,
pub descender: i32,
pub cap_height: i32,
pub italic_angle: f32,
pub bbox: [i32; 4],
pub default_width: u16,
}
#[derive(Debug, Clone)]
pub struct Font {
pub font_type: FontType,
pub metadata: FontMetadata,
pub source_path: Option<String>,
pub glyph_mapping: HashMap<u32, u16>,
pub glyph_widths: HashMap<u16, u16>,
pub missing_glyph_id: u16,
pub metrics: Option<FontMetrics>,
}
#[derive(Debug)]
struct ParsedFontGeometry {
glyph_mapping: HashMap<u32, u16>,
glyph_widths: HashMap<u16, u16>,
missing_glyph_id: u16,
metrics: FontMetrics,
}
impl Font {
pub fn standard(font: StandardFont) -> Self {
let metadata = FontMetadata {
family: font.family().to_string(),
postscript_name: font.postscript_name().to_string(),
weight: if font.is_bold() { 700 } else { 400 },
italic: font.is_italic(),
encoding: "WinAnsiEncoding".to_string(),
unicode: false,
};
Font {
font_type: FontType::Standard(font),
metadata,
source_path: None,
glyph_mapping: HashMap::new(),
glyph_widths: HashMap::new(),
missing_glyph_id: 0,
metrics: None,
}
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, Box<dyn std::error::Error>> {
let path = path.as_ref();
let mut file = File::open(path)?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
let source_path = Some(path.to_string_lossy().to_string());
Self::from_bytes(data, source_path)
}
pub fn from_bytes(
data: Vec<u8>,
source_path: Option<String>,
) -> Result<Self, Box<dyn std::error::Error>> {
let font_type = if data.len() >= 4 {
let tag = &data[0..4];
if tag == b"\x00\x01\x00\x00" || tag == b"true" {
FontType::TrueType { data: data.clone() }
} else if tag == b"OTTO" {
FontType::OpenType { data: data.clone() }
} else {
return Err("Unknown font format".into());
}
} else {
return Err("Invalid font data".into());
};
let face = Face::parse(&data, 0).map_err(|e| format!("Failed to parse font: {:?}", e))?;
let geometry = Self::extract_font_geometry(&face)?;
let metadata = Self::extract_metadata_from_face(&face)?;
Ok(Font {
font_type,
metadata,
source_path,
glyph_mapping: geometry.glyph_mapping,
glyph_widths: geometry.glyph_widths,
missing_glyph_id: geometry.missing_glyph_id,
metrics: Some(geometry.metrics),
})
}
fn extract_font_geometry(
face: &Face,
) -> Result<ParsedFontGeometry, Box<dyn std::error::Error>> {
let mut units_per_em = face.units_per_em();
if units_per_em == 0 {
units_per_em = 1000;
}
let mut glyph_mapping = HashMap::new();
if let Some(cmap) = face.tables().cmap {
for subtable in cmap.subtables {
if !subtable.is_unicode() {
continue;
}
subtable.codepoints(|codepoint| {
if let Some(glyph) = subtable.glyph_index(codepoint) {
glyph_mapping.entry(codepoint).or_insert(glyph.0);
}
});
}
}
if glyph_mapping.is_empty() {
return Err("Font cmap table did not produce any glyph mappings".into());
}
let mut glyph_widths = HashMap::new();
for glyph_id in 0..face.number_of_glyphs() {
let gid = GlyphId(glyph_id);
if let Some(advance) = face.glyph_hor_advance(gid) {
let width = Self::scale_width_to_pdf_units(advance, units_per_em);
glyph_widths.insert(glyph_id, width);
}
}
let missing_glyph_id = face.glyph_index('?').map(|g| g.0).unwrap_or(0);
let italic_angle = face.italic_angle().unwrap_or(0.0);
let ascender = Self::scale_metric_to_pdf_units(face.ascender(), units_per_em);
let descender = Self::scale_metric_to_pdf_units(face.descender(), units_per_em);
let cap_height = ascender;
let rect = face.global_bounding_box();
let bbox = [
Self::scale_metric_to_pdf_units(rect.x_min, units_per_em),
Self::scale_metric_to_pdf_units(rect.y_min, units_per_em),
Self::scale_metric_to_pdf_units(rect.x_max, units_per_em),
Self::scale_metric_to_pdf_units(rect.y_max, units_per_em),
];
let default_width = {
let space_gid = glyph_mapping
.get(&(b' ' as u32))
.copied()
.unwrap_or(missing_glyph_id);
glyph_widths
.get(&space_gid)
.copied()
.or_else(|| {
if glyph_widths.is_empty() {
None
} else {
let sum: u32 = glyph_widths.values().map(|&w| w as u32).sum();
Some((sum / glyph_widths.len() as u32) as u16)
}
})
.unwrap_or(1000)
};
let metrics = FontMetrics {
units_per_em,
ascender,
descender,
cap_height,
italic_angle,
bbox,
default_width,
};
glyph_widths
.entry(missing_glyph_id)
.or_insert(default_width);
Ok(ParsedFontGeometry {
glyph_mapping,
glyph_widths,
missing_glyph_id,
metrics,
})
}
fn scale_width_to_pdf_units(width: u16, units_per_em: u16) -> u16 {
if units_per_em == 0 {
return width;
}
let scaled = (width as f32 * 1000.0) / units_per_em as f32;
scaled.round().clamp(0.0, 65535.0) as u16
}
fn scale_metric_to_pdf_units(value: i16, units_per_em: u16) -> i32 {
if units_per_em == 0 {
return value as i32;
}
let scaled = (value as f32 * 1000.0) / units_per_em as f32;
scaled.round() as i32
}
fn extract_metadata_from_face(face: &Face) -> Result<FontMetadata, Box<dyn std::error::Error>> {
let family = face
.names()
.into_iter()
.filter(|name| name.name_id == 16 || name.name_id == 1) .filter(|name| name.is_unicode())
.find_map(|name| name.to_string())
.unwrap_or_else(|| "Unknown Font".to_string());
let raw_postscript = face
.names()
.into_iter()
.filter(|name| name.name_id == 6) .filter(|name| name.is_unicode())
.find_map(|name| name.to_string());
let postscript_name = raw_postscript
.filter(|name| !name.is_empty())
.map(|name| Self::sanitize_postscript_name(&name))
.unwrap_or_else(|| Self::sanitize_postscript_name(&family));
let weight = face.weight().to_number();
let italic = face.is_italic();
Ok(FontMetadata {
family,
postscript_name,
weight,
italic,
encoding: "Identity-H".to_string(),
unicode: true,
})
}
fn sanitize_postscript_name(name: &str) -> String {
let mut sanitized = String::with_capacity(name.len());
let mut last_was_hyphen = false;
for ch in name.chars() {
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '+') {
sanitized.push(ch);
last_was_hyphen = false;
} else if ch.is_whitespace() && !sanitized.is_empty() && !last_was_hyphen {
sanitized.push('-');
last_was_hyphen = true;
}
}
let sanitized = sanitized.trim_matches('-').to_string();
let mut sanitized = if sanitized.is_empty() {
"EmbeddedFont".to_string()
} else {
sanitized
};
if sanitized.len() > 127 {
sanitized.truncate(127);
}
sanitized
}
pub fn family(&self) -> &str {
&self.metadata.family
}
pub fn is_bold(&self) -> bool {
self.metadata.weight >= 600
}
pub fn is_italic(&self) -> bool {
self.metadata.italic
}
pub fn text_width(&self, text: &str, size: f32) -> f32 {
match &self.font_type {
FontType::Standard(_) => text.len() as f32 * size * 0.5,
FontType::TrueType { .. } | FontType::OpenType { .. } => {
let default_width = self
.metrics
.as_ref()
.map(|m| m.default_width)
.unwrap_or(1000);
let total_width: u32 = text
.chars()
.map(|ch| {
let glyph_id = self
.glyph_mapping
.get(&(ch as u32))
.copied()
.unwrap_or(self.missing_glyph_id);
self.glyph_widths
.get(&glyph_id)
.copied()
.unwrap_or(default_width) as u32
})
.sum();
(total_width as f32 * size) / 1000.0
}
}
}
pub fn char_width(&self, ch: char, size: f32) -> f32 {
match &self.font_type {
FontType::Standard(_) => size * 0.5,
FontType::TrueType { .. } | FontType::OpenType { .. } => {
let default_width = self
.metrics
.as_ref()
.map(|m| m.default_width)
.unwrap_or(1000);
let glyph_id = self
.glyph_mapping
.get(&(ch as u32))
.copied()
.unwrap_or(self.missing_glyph_id);
let width = self
.glyph_widths
.get(&glyph_id)
.copied()
.unwrap_or(default_width);
(width as f32 * size) / 1000.0
}
}
}
pub fn encode_text(&self, text: &str) -> Vec<u8> {
match &self.font_type {
FontType::Standard(_) => {
text.as_bytes().to_vec()
}
FontType::TrueType { .. } | FontType::OpenType { .. } => {
let mut encoded = Vec::with_capacity(text.len() * 2);
for ch in text.chars() {
let codepoint = ch as u32;
let glyph_id = self
.glyph_mapping
.get(&codepoint)
.copied()
.unwrap_or(self.missing_glyph_id);
encoded.push((glyph_id >> 8) as u8);
encoded.push((glyph_id & 0xFF) as u8);
}
encoded
}
}
}
pub fn needs_utf16_encoding(&self) -> bool {
matches!(
self.font_type,
FontType::TrueType { .. } | FontType::OpenType { .. }
)
}
}
pub struct FontManager {
fonts: Vec<(Font, ObjectId, String)>, name_counter: usize,
standard_fonts: HashMap<StandardFont, ObjectId>,
}
impl Default for FontManager {
fn default() -> Self {
Self::new()
}
}
impl FontManager {
pub fn new() -> Self {
FontManager {
fonts: Vec::new(),
name_counter: 0,
standard_fonts: HashMap::new(),
}
}
pub fn embed_font(
&mut self,
doc: &mut Document,
font: Font,
) -> Result<(ObjectId, String), Box<dyn std::error::Error>> {
for (cached_font, obj_id, resource_name) in &self.fonts {
if let (Some(path1), Some(path2)) = (&cached_font.source_path, &font.source_path) {
if path1 == path2 {
return Ok((*obj_id, resource_name.clone()));
}
}
}
let font_id = match &font.font_type {
FontType::Standard(std_font) => {
if let Some(&cached_id) = self.standard_fonts.get(std_font) {
cached_id
} else {
let id = self.embed_standard_font(doc, *std_font)?;
self.standard_fonts.insert(*std_font, id);
id
}
}
FontType::TrueType { .. } | FontType::OpenType { .. } => {
self.embed_truetype_font(doc, &font)?
}
};
let resource_name = format!("F{}", self.name_counter);
self.name_counter += 1;
self.fonts.push((font, font_id, resource_name.clone()));
Ok((font_id, resource_name))
}
fn embed_standard_font(
&self,
doc: &mut Document,
font: StandardFont,
) -> Result<ObjectId, Box<dyn std::error::Error>> {
let font_dict = dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => Object::Name(font.postscript_name().as_bytes().to_vec()),
"Encoding" => "WinAnsiEncoding",
};
Ok(doc.add_object(font_dict))
}
fn embed_truetype_font(
&self,
doc: &mut Document,
font: &Font,
) -> Result<ObjectId, Box<dyn std::error::Error>> {
let font_data = match &font.font_type {
FontType::TrueType { data } | FontType::OpenType { data } => data,
_ => return Err("Not a TrueType/OpenType font".into()),
};
let face = ttf_parser::Face::parse(font_data, 0)
.map_err(|e| format!("Failed to parse font for GID map: {:?}", e))?;
let num_glyphs = face.number_of_glyphs();
let mut gid_map_data = Vec::with_capacity(num_glyphs as usize * 2);
for i in 0..num_glyphs {
gid_map_data.push((i >> 8) as u8); gid_map_data.push((i & 0xFF) as u8); }
let cid_to_gid_map_stream = Stream::new(dictionary!{}, gid_map_data);
let cid_to_gid_map_id = doc.add_object(cid_to_gid_map_stream);
let font_file = Stream::new(
dictionary! {
"Length1" => font_data.len() as i64,
},
font_data.clone(),
);
let font_file_id = doc.add_object(font_file);
let metrics = font.metrics.as_ref();
let bbox = metrics.map(|m| m.bbox).unwrap_or([-500, -200, 1000, 900]);
let ascender = metrics.map(|m| m.ascender).unwrap_or(900);
let descender = metrics.map(|m| m.descender).unwrap_or(-200);
let cap_height = metrics.map(|m| m.cap_height).unwrap_or(700);
let italic_angle = metrics.map(|m| m.italic_angle as f64).unwrap_or_else(|| {
if font.metadata.italic {
-12.0
} else {
0.0
}
});
let default_width = metrics.map(|m| m.default_width as i64).unwrap_or(1000);
let font_descriptor = dictionary! {
"Type" => "FontDescriptor",
"FontName" => Object::Name(font.metadata.postscript_name.as_bytes().to_vec()),
"FontFamily" => Object::string_literal(font.metadata.family.as_str()),
"Flags" => 32, "FontBBox" => vec![
bbox[0].into(),
bbox[1].into(),
bbox[2].into(),
bbox[3].into(),
],
"ItalicAngle" => italic_angle,
"Ascent" => ascender,
"Descent" => descender,
"CapHeight" => cap_height,
"StemV" => if font.is_bold() { 120 } else { 80 },
"FontFile2" => Object::Reference(font_file_id),
};
let descriptor_id = doc.add_object(font_descriptor);
let mut cid_font = dictionary! {
"Type" => "Font",
"Subtype" => "CIDFontType2",
"BaseFont" => Object::Name(font.metadata.postscript_name.as_bytes().to_vec()),
"CIDSystemInfo" => dictionary! {
"Registry" => Object::string_literal("Adobe"),
"Ordering" => Object::string_literal("Identity"),
"Supplement" => 0,
},
"FontDescriptor" => Object::Reference(descriptor_id),
"DW" => default_width,
"CIDToGIDMap" => Object::Reference(cid_to_gid_map_id),
};
if let Some(width_array) = Self::build_width_array(&font.glyph_widths) {
cid_font.set("W", width_array);
}
let cid_font_id = doc.add_object(cid_font);
let to_unicode = Self::create_to_unicode_cmap(font);
let to_unicode_id = doc.add_object(to_unicode);
let type0_font = dictionary! {
"Type" => "Font",
"Subtype" => "Type0",
"BaseFont" => Object::Name(font.metadata.postscript_name.as_bytes().to_vec()),
"Encoding" => "Identity-H",
"DescendantFonts" => vec![Object::Reference(cid_font_id)],
"ToUnicode" => Object::Reference(to_unicode_id),
};
Ok(doc.add_object(type0_font))
}
fn build_width_array(widths: &HashMap<u16, u16>) -> Option<Object> {
if widths.is_empty() {
return None;
}
let mut glyph_ids: Vec<u16> = widths.keys().copied().collect();
glyph_ids.sort_unstable();
let mut entries: Vec<Object> = Vec::new();
if glyph_ids.is_empty() {
return Some(Object::Array(entries));
}
let mut iter = glyph_ids.into_iter();
let mut start_glyph = iter.next().unwrap();
let mut last_glyph = start_glyph;
let mut range_widths = vec![Object::Integer(
widths.get(&start_glyph).copied().unwrap_or(1000) as i64,
)];
for glyph_id in iter {
if glyph_id == last_glyph + 1 {
range_widths.push(Object::Integer(
widths.get(&glyph_id).copied().unwrap_or(1000) as i64,
));
last_glyph = glyph_id;
} else {
entries.push(Object::Integer(start_glyph as i64));
entries.push(Object::Array(range_widths));
start_glyph = glyph_id;
last_glyph = glyph_id;
range_widths = vec![Object::Integer(
widths.get(&glyph_id).copied().unwrap_or(1000) as i64,
)];
}
}
if !range_widths.is_empty() {
entries.push(Object::Integer(start_glyph as i64));
entries.push(Object::Array(range_widths));
}
Some(Object::Array(entries))
}
fn create_to_unicode_cmap(font: &Font) -> Stream {
let mut glyph_to_unicode: BTreeMap<u16, u32> = BTreeMap::new();
for (codepoint, glyph) in &font.glyph_mapping {
glyph_to_unicode.entry(*glyph).or_insert(*codepoint);
}
let mut cmap = String::from("/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo\n<< /Registry (Adobe)\n/Ordering (UCS)\n/Supplement 0\n>> def\n/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n");
let entries: Vec<(u16, u32)> = glyph_to_unicode.into_iter().collect();
for chunk in entries.chunks(100) {
cmap.push_str(&format!("{} beginbfchar\n", chunk.len()));
for (glyph_id, codepoint) in chunk {
if let Some(ch) = char::from_u32(*codepoint) {
let mut buffer = [0u16; 2];
let encoded = ch.encode_utf16(&mut buffer);
let mut unicode_hex = String::new();
for unit in encoded {
unicode_hex.push_str(&format!("{:04X}", unit));
}
cmap.push_str(&format!("<{:04X}> <{}>\n", glyph_id, unicode_hex));
}
}
cmap.push_str("endbfchar\n");
}
cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend");
Stream::new(dictionary! {}, cmap.into_bytes())
}
pub fn add_to_resources(
&self,
resources: &mut Dictionary,
font_id: ObjectId,
resource_name: &str,
) {
let font_dict = if let Ok(Object::Dictionary(dict)) = resources.get(b"Font") {
dict.clone()
} else {
Dictionary::new()
};
let mut font_dict = font_dict;
font_dict.set(resource_name, Object::Reference(font_id));
resources.set("Font", font_dict);
}
pub fn count(&self) -> usize {
self.fonts.len()
}
pub fn fonts(&self) -> impl Iterator<Item = &(Font, ObjectId, String)> {
self.fonts.iter()
}
pub fn clear(&mut self) {
self.fonts.clear();
self.standard_fonts.clear();
self.name_counter = 0;
}
}
pub struct TextBuilder {
operations: Vec<Operation>,
}
impl Default for TextBuilder {
fn default() -> Self {
Self::new()
}
}
impl TextBuilder {
pub fn new() -> Self {
TextBuilder {
operations: Vec::new(),
}
}
pub fn begin_text(mut self) -> Self {
self.operations.push(TextOperations::begin_text());
self
}
pub fn set_font(mut self, resource_name: &str, size: f32) -> Self {
self.operations
.push(TextOperations::set_font(resource_name, size));
self
}
pub fn position(mut self, x: f32, y: f32) -> Self {
self.operations.push(TextOperations::position(x, y));
self
}
pub fn show(mut self, text: &str) -> Self {
self.operations.push(TextOperations::show(text));
self
}
pub fn show_encoded(mut self, encoded_text: Vec<u8>) -> Self {
self.operations
.push(TextOperations::show_encoded(encoded_text));
self
}
pub fn next_line(mut self, x: f32, y: f32) -> Self {
self.operations.push(TextOperations::next_line(x, y));
self
}
pub fn set_leading(mut self, leading: f32) -> Self {
self.operations.push(TextOperations::set_leading(leading));
self
}
pub fn set_char_spacing(mut self, spacing: f32) -> Self {
self.operations
.push(TextOperations::set_char_spacing(spacing));
self
}
pub fn set_word_spacing(mut self, spacing: f32) -> Self {
self.operations
.push(TextOperations::set_word_spacing(spacing));
self
}
pub fn set_horizontal_scaling(mut self, scale: f32) -> Self {
self.operations
.push(TextOperations::set_horizontal_scaling(scale));
self
}
pub fn set_rendering_mode(mut self, mode: TextRenderingMode) -> Self {
self.operations
.push(TextOperations::set_rendering_mode(mode));
self
}
pub fn set_rise(mut self, rise: f32) -> Self {
self.operations.push(TextOperations::set_rise(rise));
self
}
pub fn set_matrix(mut self, a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self {
self.operations
.push(TextOperations::set_matrix(a, b, c, d, e, f));
self
}
pub fn set_fill_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.operations
.push(TextOperations::set_fill_color_rgb(r, g, b));
self
}
pub fn set_stroke_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.operations
.push(TextOperations::set_stroke_color_rgb(r, g, b));
self
}
pub fn end_text(mut self) -> Self {
self.operations.push(TextOperations::end_text());
self
}
pub fn add_operation(mut self, op: Operation) -> Self {
self.operations.push(op);
self
}
pub fn build(self) -> Vec<Operation> {
self.operations
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextRenderingMode {
Fill = 0,
Stroke = 1,
FillThenStroke = 2,
Invisible = 3,
FillAndClip = 4,
StrokeAndClip = 5,
FillStrokeAndClip = 6,
Clip = 7,
}
pub struct TextOperations;
impl TextOperations {
pub fn begin_text() -> Operation {
Operation::new("BT", vec![])
}
pub fn end_text() -> Operation {
Operation::new("ET", vec![])
}
pub fn set_font(resource_name: &str, size: f32) -> Operation {
Operation::new(
"Tf",
vec![Object::Name(resource_name.as_bytes().to_vec()), size.into()],
)
}
pub fn position(x: f32, y: f32) -> Operation {
Operation::new("Td", vec![x.into(), y.into()])
}
pub fn show(text: &str) -> Operation {
Operation::new("Tj", vec![Object::string_literal(text)])
}
pub fn show_encoded(encoded_text: Vec<u8>) -> Operation {
Operation::new(
"Tj",
vec![Object::String(
encoded_text,
lopdf::StringFormat::Hexadecimal,
)],
)
}
pub fn next_line(x: f32, y: f32) -> Operation {
Operation::new("Td", vec![x.into(), y.into()])
}
pub fn set_leading(leading: f32) -> Operation {
Operation::new("TL", vec![leading.into()])
}
pub fn set_char_spacing(spacing: f32) -> Operation {
Operation::new("Tc", vec![spacing.into()])
}
pub fn set_word_spacing(spacing: f32) -> Operation {
Operation::new("Tw", vec![spacing.into()])
}
pub fn set_horizontal_scaling(scale: f32) -> Operation {
Operation::new("Tz", vec![scale.into()])
}
pub fn set_rendering_mode(mode: TextRenderingMode) -> Operation {
Operation::new("Tr", vec![(mode as i32).into()])
}
pub fn set_rise(rise: f32) -> Operation {
Operation::new("Ts", vec![rise.into()])
}
pub fn set_matrix(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Operation {
Operation::new(
"Tm",
vec![a.into(), b.into(), c.into(), d.into(), e.into(), f.into()],
)
}
pub fn set_fill_color_rgb(r: f32, g: f32, b: f32) -> Operation {
Operation::new("rg", vec![r.into(), g.into(), b.into()])
}
pub fn set_stroke_color_rgb(r: f32, g: f32, b: f32) -> Operation {
Operation::new("RG", vec![r.into(), g.into(), b.into()])
}
}
pub mod utils {
use super::*;
#[derive(Debug, Clone)]
pub struct TextLine {
pub text: String,
pub x: f32,
pub y: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WrapStrategy {
Word,
Character,
Hybrid,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlign {
Left,
Center,
Right,
}
pub fn wrap_text(
font: &Font,
text: &str,
max_width: f32,
font_size: f32,
strategy: WrapStrategy,
) -> Vec<String> {
if text.is_empty() {
return vec![];
}
match strategy {
WrapStrategy::Word => wrap_by_words(font, text, max_width, font_size),
WrapStrategy::Character => wrap_by_characters(font, text, max_width, font_size),
WrapStrategy::Hybrid => {
let lines = wrap_by_words(font, text, max_width, font_size);
let mut result = Vec::new();
for line in lines {
let line_width = font.text_width(&line, font_size);
if line_width > max_width {
result.extend(wrap_by_characters(font, &line, max_width, font_size));
} else {
result.push(line);
}
}
result
}
}
}
fn wrap_by_words(font: &Font, text: &str, max_width: f32, font_size: f32) -> Vec<String> {
let mut lines = Vec::new();
let words: Vec<&str> = text.split_whitespace().collect();
if words.is_empty() {
return lines;
}
let mut current_line = String::new();
for word in words {
let test_line = if current_line.is_empty() {
word.to_string()
} else {
format!("{} {}", current_line, word)
};
let width = font.text_width(&test_line, font_size);
if width > max_width && !current_line.is_empty() {
lines.push(current_line.clone());
current_line = word.to_string();
} else {
current_line = test_line;
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}
fn wrap_by_characters(font: &Font, text: &str, max_width: f32, font_size: f32) -> Vec<String> {
let mut lines = Vec::new();
let mut current_line = String::new();
let mut current_width = 0.0;
for ch in text.chars() {
let char_width = font.char_width(ch, font_size);
if current_width + char_width > max_width && !current_line.is_empty() {
lines.push(current_line.clone());
current_line.clear();
current_width = 0.0;
}
current_line.push(ch);
current_width += char_width;
}
if !current_line.is_empty() {
lines.push(current_line);
}
lines
}
pub fn create_text_block(
font_resource: &str,
font: &Font,
text: &str,
x: f32,
y: f32,
font_size: f32,
max_width: Option<f32>,
max_height: Option<f32>,
line_height: f32,
align: TextAlign,
wrap_strategy: WrapStrategy,
) -> Vec<Operation> {
let mut operations = Vec::new();
let paragraphs: Vec<&str> = text.split('\n').collect();
let mut all_lines = Vec::new();
for paragraph in paragraphs {
if paragraph.is_empty() {
all_lines.push(String::new());
} else if let Some(width) = max_width {
let wrapped = wrap_text(font, paragraph, width, font_size, wrap_strategy);
all_lines.extend(wrapped);
} else {
all_lines.push(paragraph.to_string());
}
}
if let Some(max_h) = max_height {
let max_lines = (max_h / line_height).floor() as usize;
if all_lines.len() > max_lines {
all_lines.truncate(max_lines);
}
}
operations.push(TextOperations::begin_text());
operations.push(TextOperations::set_font(font_resource, font_size));
let mut current_y = y;
for line in all_lines {
let line_width = font.text_width(&line, font_size);
let line_x = match align {
TextAlign::Left => x,
TextAlign::Center => {
x + (max_width.unwrap_or(line_width) - line_width) / 2.0
}
TextAlign::Right => {
x + max_width.unwrap_or(line_width) - line_width
}
};
operations.push(TextOperations::position(line_x, current_y));
if font.needs_utf16_encoding() {
operations.push(TextOperations::show_encoded(font.encode_text(&line)));
} else {
operations.push(TextOperations::show(&line));
}
current_y -= line_height;
}
operations.push(TextOperations::end_text());
operations
}
pub fn create_paragraph(
font_resource: &str,
font: &Font,
text: &str,
x: f32,
y: f32,
font_size: f32,
max_width: f32,
line_height: f32,
) -> Vec<Operation> {
let mut operations = Vec::new();
let words: Vec<&str> = text.split_whitespace().collect();
let mut current_line = String::new();
let mut current_y = y;
operations.push(TextOperations::begin_text());
operations.push(TextOperations::set_font(font_resource, font_size));
for word in words {
let test_line = if current_line.is_empty() {
word.to_string()
} else {
format!("{} {}", current_line, word)
};
let width = font.text_width(&test_line, font_size);
if width > max_width && !current_line.is_empty() {
operations.push(TextOperations::position(x, current_y));
if font.needs_utf16_encoding() {
operations.push(TextOperations::show_encoded(
font.encode_text(¤t_line),
));
} else {
operations.push(TextOperations::show(¤t_line));
}
current_y -= line_height;
current_line = word.to_string();
} else {
current_line = test_line;
}
}
if !current_line.is_empty() {
operations.push(TextOperations::position(x, current_y));
if font.needs_utf16_encoding() {
operations.push(TextOperations::show_encoded(
font.encode_text(¤t_line),
));
} else {
operations.push(TextOperations::show(¤t_line));
}
}
operations.push(TextOperations::end_text());
operations
}
pub fn create_centered_text(
font_resource: &str,
font: &Font,
text: &str,
center_x: f32,
y: f32,
font_size: f32,
) -> Vec<Operation> {
let width = font.text_width(text, font_size);
let x = center_x - (width / 2.0);
vec![
TextOperations::begin_text(),
TextOperations::set_font(font_resource, font_size),
TextOperations::position(x, y),
if font.needs_utf16_encoding() {
TextOperations::show_encoded(font.encode_text(text))
} else {
TextOperations::show(text)
},
TextOperations::end_text(),
]
}
pub fn create_right_aligned_text(
font_resource: &str,
font: &Font,
text: &str,
right_x: f32,
y: f32,
font_size: f32,
) -> Vec<Operation> {
let width = font.text_width(text, font_size);
let x = right_x - width;
vec![
TextOperations::begin_text(),
TextOperations::set_font(font_resource, font_size),
TextOperations::position(x, y),
if font.needs_utf16_encoding() {
TextOperations::show_encoded(font.encode_text(text))
} else {
TextOperations::show(text)
},
TextOperations::end_text(),
]
}
}