1use crate::error::PdfError;
4use crate::Result;
5use std::collections::HashMap;
6
7use super::cmap_utils::parse_cmap_format_12_filtered;
8use super::{FontDescriptor, FontFlags, FontMetrics};
9
10#[derive(Debug, Clone, Default)]
12pub struct GlyphMapping {
13 char_to_glyph: HashMap<u32, u16>,
15 glyph_to_char: HashMap<u16, u32>,
17 glyph_widths: HashMap<u16, u16>,
19 cmap_unparsed: bool,
23}
24
25impl GlyphMapping {
26 pub fn char_to_glyph(&self, ch: char) -> Option<u16> {
28 self.char_to_glyph.get(&(ch as u32)).copied()
29 }
30
31 pub(crate) fn coverage_known(&self) -> bool {
35 !self.char_to_glyph.is_empty() && !self.cmap_unparsed
36 }
37
38 pub fn glyph_to_char(&self, glyph: u16) -> Option<char> {
40 self.glyph_to_char
41 .get(&glyph)
42 .and_then(|&cp| char::from_u32(cp))
43 }
44
45 pub fn add_mapping(&mut self, ch: char, glyph: u16) {
47 let code_point = ch as u32;
48 self.char_to_glyph.insert(code_point, glyph);
49 self.glyph_to_char.insert(glyph, code_point);
50 }
51
52 pub fn set_glyph_width(&mut self, glyph: u16, width: u16) {
54 self.glyph_widths.insert(glyph, width);
55 }
56
57 pub fn get_glyph_width(&self, glyph: u16) -> Option<u16> {
59 self.glyph_widths.get(&glyph).copied()
60 }
61
62 pub fn get_char_width(&self, ch: char) -> Option<u16> {
64 self.char_to_glyph(ch)
65 .and_then(|glyph| self.get_glyph_width(glyph))
66 }
67
68 pub fn char_widths_iter(&self) -> impl Iterator<Item = (char, u16)> + '_ {
72 self.char_to_glyph
73 .iter()
74 .filter_map(move |(&code_point, &glyph_id)| {
75 let ch = char::from_u32(code_point)?;
76 let width = self.glyph_widths.get(&glyph_id).copied()?;
77 Some((ch, width))
78 })
79 }
80}
81
82#[derive(Debug, Clone)]
84struct TableRecord {
85 #[allow(dead_code)]
86 tag: [u8; 4],
87 #[allow(dead_code)]
88 checksum: u32,
89 offset: u32,
90 length: u32,
91}
92
93pub struct TtfParser<'a> {
95 data: &'a [u8],
96 tables: HashMap<String, TableRecord>,
97}
98
99impl<'a> TtfParser<'a> {
100 pub fn new(data: &'a [u8]) -> Result<Self> {
102 let mut parser = TtfParser {
103 data,
104 tables: HashMap::new(),
105 };
106 parser.parse_table_directory()?;
107 Ok(parser)
108 }
109
110 fn parse_table_directory(&mut self) -> Result<()> {
112 if self.data.len() < 12 {
113 return Err(PdfError::FontError("TTF header too small".into()));
114 }
115
116 let num_tables = u16::from_be_bytes([self.data[4], self.data[5]]);
118
119 let mut offset = 12;
121 for _ in 0..num_tables {
122 if offset + 16 > self.data.len() {
123 return Err(PdfError::FontError("Invalid table directory".into()));
124 }
125
126 let tag = [
127 self.data[offset],
128 self.data[offset + 1],
129 self.data[offset + 2],
130 self.data[offset + 3],
131 ];
132 let checksum = u32::from_be_bytes([
133 self.data[offset + 4],
134 self.data[offset + 5],
135 self.data[offset + 6],
136 self.data[offset + 7],
137 ]);
138 let table_offset = u32::from_be_bytes([
139 self.data[offset + 8],
140 self.data[offset + 9],
141 self.data[offset + 10],
142 self.data[offset + 11],
143 ]);
144 let length = u32::from_be_bytes([
145 self.data[offset + 12],
146 self.data[offset + 13],
147 self.data[offset + 14],
148 self.data[offset + 15],
149 ]);
150
151 let tag_str = String::from_utf8_lossy(&tag).to_string();
152 self.tables.insert(
153 tag_str,
154 TableRecord {
155 tag,
156 checksum,
157 offset: table_offset,
158 length,
159 },
160 );
161
162 offset += 16;
163 }
164
165 Ok(())
166 }
167
168 fn get_table(&self, tag: &str) -> Option<&[u8]> {
170 self.tables.get(tag).and_then(|record| {
171 let start = record.offset as usize;
172 let end = start + record.length as usize;
173 if end <= self.data.len() {
174 Some(&self.data[start..end])
175 } else {
176 None
177 }
178 })
179 }
180
181 pub fn extract_metrics(&self) -> Result<FontMetrics> {
183 let head_table = self
185 .get_table("head")
186 .ok_or_else(|| PdfError::FontError("Missing head table".into()))?;
187
188 if head_table.len() < 54 {
189 return Err(PdfError::FontError("Invalid head table".into()));
190 }
191
192 let units_per_em = u16::from_be_bytes([head_table[18], head_table[19]]);
193
194 let hhea_table = self
196 .get_table("hhea")
197 .ok_or_else(|| PdfError::FontError("Missing hhea table".into()))?;
198
199 if hhea_table.len() < 36 {
200 return Err(PdfError::FontError("Invalid hhea table".into()));
201 }
202
203 let ascent = i16::from_be_bytes([hhea_table[4], hhea_table[5]]);
204 let descent = i16::from_be_bytes([hhea_table[6], hhea_table[7]]);
205 let line_gap = i16::from_be_bytes([hhea_table[8], hhea_table[9]]);
206
207 Ok(FontMetrics {
208 units_per_em,
209 ascent,
210 descent,
211 line_gap,
212 cap_height: ascent * 7 / 10, x_height: ascent / 2, })
215 }
216
217 pub fn create_descriptor(&self) -> Result<FontDescriptor> {
219 let font_name = self.extract_font_name()?;
221
222 let metrics = self.extract_metrics()?;
224
225 let flags = self.extract_font_flags()?;
227
228 let head_table = self.get_table("head").ok_or_else(|| {
230 PdfError::InvalidFormat("Missing required 'head' table in TTF font".to_string())
231 })?;
232 let x_min = i16::from_be_bytes([head_table[36], head_table[37]]);
233 let y_min = i16::from_be_bytes([head_table[38], head_table[39]]);
234 let x_max = i16::from_be_bytes([head_table[40], head_table[41]]);
235 let y_max = i16::from_be_bytes([head_table[42], head_table[43]]);
236
237 Ok(FontDescriptor {
238 font_name: font_name.clone(),
239 font_family: font_name,
240 flags,
241 font_bbox: [x_min as f32, y_min as f32, x_max as f32, y_max as f32],
242 italic_angle: self.extract_italic_angle()?,
243 ascent: metrics.ascent as f32,
244 descent: metrics.descent as f32,
245 cap_height: metrics.cap_height as f32,
246 stem_v: 80.0, missing_width: 250.0, })
249 }
250
251 fn extract_font_name(&self) -> Result<String> {
253 let name_table = self
254 .get_table("name")
255 .ok_or_else(|| PdfError::FontError("Missing name table".into()))?;
256
257 if name_table.len() < 6 {
258 return Err(PdfError::FontError("Invalid name table".into()));
259 }
260
261 let name_data = name_table;
263
264 if name_data.len() < 6 {
265 return Ok("CustomFont".to_string());
266 }
267
268 let num_records = u16::from_be_bytes([name_data[2], name_data[3]]);
270 let string_offset = u16::from_be_bytes([name_data[4], name_data[5]]) as usize;
271
272 if name_data.len() < 6 + (num_records as usize * 12) {
273 return Ok("CustomFont".to_string());
274 }
275
276 for i in 0..num_records {
278 let record_offset = 6 + (i as usize * 12);
279 if record_offset + 12 > name_data.len() {
280 break;
281 }
282
283 let platform_id =
284 u16::from_be_bytes([name_data[record_offset], name_data[record_offset + 1]]);
285 let name_id =
286 u16::from_be_bytes([name_data[record_offset + 6], name_data[record_offset + 7]]);
287 let length =
288 u16::from_be_bytes([name_data[record_offset + 8], name_data[record_offset + 9]])
289 as usize;
290 let offset =
291 u16::from_be_bytes([name_data[record_offset + 10], name_data[record_offset + 11]])
292 as usize;
293
294 if (name_id == 6 || name_id == 1) && (platform_id == 1 || platform_id == 3) {
296 let str_start = string_offset + offset;
297 let str_end = str_start + length;
298
299 if str_end <= name_data.len() {
300 let name_bytes = &name_data[str_start..str_end];
301
302 if platform_id == 1 {
304 let name = String::from_utf8_lossy(name_bytes).to_string();
306 if !name.trim().is_empty() {
307 return Ok(name.trim().to_string());
308 }
309 } else if platform_id == 3 {
310 if name_bytes.len() % 2 == 0 {
312 let utf16_chars: Vec<u16> = name_bytes
313 .chunks_exact(2)
314 .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
315 .collect();
316 if let Ok(name) = String::from_utf16(&utf16_chars) {
317 if !name.trim().is_empty() {
318 return Ok(name.trim().to_string());
319 }
320 }
321 }
322 }
323 }
324 }
325 }
326
327 Ok("CustomFont".to_string())
328 }
329
330 fn extract_font_flags(&self) -> Result<FontFlags> {
332 let mut flags = FontFlags::empty();
333
334 if let Some(post_table) = self.get_table("post") {
336 if post_table.len() >= 12 {
337 let is_fixed_pitch = u32::from_be_bytes([
338 post_table[8],
339 post_table[9],
340 post_table[10],
341 post_table[11],
342 ]) != 0;
343 if is_fixed_pitch {
344 flags |= FontFlags::FIXED_PITCH;
345 }
346 }
347 }
348
349 flags |= FontFlags::NONSYMBOLIC;
351
352 Ok(flags)
353 }
354
355 pub fn extract_glyph_mapping(&self) -> Result<GlyphMapping> {
357 let mut mapping = GlyphMapping::default();
358
359 let cmap_table = self
361 .get_table("cmap")
362 .ok_or_else(|| PdfError::FontError("Missing cmap table".into()))?;
363
364 if cmap_table.len() < 4 {
365 return Err(PdfError::FontError("Invalid cmap table".into()));
366 }
367
368 let cmap_data = cmap_table;
370
371 if self.parse_cmap_table(cmap_data, &mut mapping).is_err() {
372 mapping.cmap_unparsed = true;
376 for ch in 0x20..=0x7E {
377 mapping.add_mapping(char::from(ch), ch as u16);
378 }
379 }
380
381 self.extract_glyph_widths(&mut mapping)?;
383
384 Ok(mapping)
385 }
386
387 fn extract_glyph_widths(&self, mapping: &mut GlyphMapping) -> Result<()> {
389 let hhea_table = self
391 .get_table("hhea")
392 .ok_or_else(|| PdfError::FontError("Missing hhea table".into()))?;
393
394 if hhea_table.len() < 36 {
395 return Err(PdfError::FontError("Invalid hhea table".into()));
396 }
397
398 let num_h_metrics = u16::from_be_bytes([hhea_table[34], hhea_table[35]]);
399
400 let hmtx_table = self
402 .get_table("hmtx")
403 .ok_or_else(|| PdfError::FontError("Missing hmtx table".into()))?;
404
405 let mut offset = 0;
407 for glyph_id in 0..num_h_metrics {
408 if offset + 4 > hmtx_table.len() {
409 break;
410 }
411
412 let advance_width = u16::from_be_bytes([hmtx_table[offset], hmtx_table[offset + 1]]);
413 mapping.set_glyph_width(glyph_id, advance_width);
414
415 offset += 4; }
417
418 if num_h_metrics > 0 {
420 let last_width = mapping.get_glyph_width(num_h_metrics - 1).unwrap_or(1000);
421 for glyph_id in num_h_metrics..256 {
423 mapping.set_glyph_width(glyph_id, last_width);
424 }
425 }
426
427 Ok(())
428 }
429
430 fn parse_cmap_table(&self, cmap_data: &[u8], mapping: &mut GlyphMapping) -> Result<()> {
432 if cmap_data.len() < 4 {
433 return Err(PdfError::FontError("Invalid cmap table header".into()));
434 }
435
436 let num_tables = u16::from_be_bytes([cmap_data[2], cmap_data[3]]) as usize;
437
438 if cmap_data.len() < 4 + num_tables * 8 {
439 return Err(PdfError::FontError("Incomplete cmap table".into()));
440 }
441
442 let mut best_offset = None;
444 for i in 0..num_tables {
445 let record_offset = 4 + i * 8;
446 let platform_id =
447 u16::from_be_bytes([cmap_data[record_offset], cmap_data[record_offset + 1]]);
448 let encoding_id =
449 u16::from_be_bytes([cmap_data[record_offset + 2], cmap_data[record_offset + 3]]);
450 let subtable_offset = u32::from_be_bytes([
451 cmap_data[record_offset + 4],
452 cmap_data[record_offset + 5],
453 cmap_data[record_offset + 6],
454 cmap_data[record_offset + 7],
455 ]) as usize;
456
457 if (platform_id == 3 && (encoding_id == 1 || encoding_id == 10)) || platform_id == 0 {
459 best_offset = Some(subtable_offset);
460 break;
461 }
462 else if platform_id == 1 && encoding_id == 0 && best_offset.is_none() {
464 best_offset = Some(subtable_offset);
465 }
466 }
467
468 if let Some(offset) = best_offset {
469 self.parse_cmap_subtable(cmap_data, offset, mapping)?;
470 }
471
472 Ok(())
473 }
474
475 fn parse_cmap_subtable(
477 &self,
478 cmap_data: &[u8],
479 offset: usize,
480 mapping: &mut GlyphMapping,
481 ) -> Result<()> {
482 if offset + 6 > cmap_data.len() {
483 return Err(PdfError::FontError("Invalid cmap subtable offset".into()));
484 }
485
486 let format = u16::from_be_bytes([cmap_data[offset], cmap_data[offset + 1]]);
487
488 match format {
489 0 => self.parse_cmap_format_0(cmap_data, offset, mapping),
490 4 => self.parse_cmap_format_4(cmap_data, offset, mapping),
491 12 => self.parse_cmap_format_12(cmap_data, offset, mapping),
492 _ => {
493 for ch in 0x20..=0x7E {
495 mapping.add_mapping(char::from(ch), ch as u16);
496 }
497 Ok(())
498 }
499 }
500 }
501
502 fn parse_cmap_format_0(
504 &self,
505 cmap_data: &[u8],
506 offset: usize,
507 mapping: &mut GlyphMapping,
508 ) -> Result<()> {
509 if offset + 262 > cmap_data.len() {
510 return Err(PdfError::FontError("Incomplete cmap format 0".into()));
511 }
512
513 for i in 0..256 {
515 let glyph_id = cmap_data[offset + 6 + i] as u16;
516 if glyph_id != 0 {
517 mapping.add_mapping(char::from(i as u8), glyph_id);
518 }
519 }
520
521 Ok(())
522 }
523
524 fn parse_cmap_format_4(
526 &self,
527 cmap_data: &[u8],
528 offset: usize,
529 mapping: &mut GlyphMapping,
530 ) -> Result<()> {
531 if offset + 14 > cmap_data.len() {
532 return Err(PdfError::FontError(
533 "Incomplete cmap format 4 header".into(),
534 ));
535 }
536
537 let seg_count_x2 = u16::from_be_bytes([cmap_data[offset + 6], cmap_data[offset + 7]]);
538 let seg_count = seg_count_x2 / 2;
539
540 let expected_length = 16 + seg_count as usize * 8;
541 if offset + expected_length > cmap_data.len() {
542 for ch in 0x20..=0x7E {
544 mapping.add_mapping(char::from(ch), ch as u16);
545 }
546 return Ok(());
547 }
548
549 let end_codes_offset = offset + 14;
551 let start_codes_offset = end_codes_offset + seg_count as usize * 2 + 2; let id_delta_offset = start_codes_offset + seg_count as usize * 2;
553 let id_range_offset_start = id_delta_offset + seg_count as usize * 2;
554
555 for i in 0..seg_count {
556 let i = i as usize;
557
558 if start_codes_offset + i * 2 + 1 >= cmap_data.len()
559 || end_codes_offset + i * 2 + 1 >= cmap_data.len()
560 || id_delta_offset + i * 2 + 1 >= cmap_data.len()
561 || id_range_offset_start + i * 2 + 1 >= cmap_data.len()
562 {
563 break;
564 }
565
566 let end_code = u16::from_be_bytes([
567 cmap_data[end_codes_offset + i * 2],
568 cmap_data[end_codes_offset + i * 2 + 1],
569 ]);
570 let start_code = u16::from_be_bytes([
571 cmap_data[start_codes_offset + i * 2],
572 cmap_data[start_codes_offset + i * 2 + 1],
573 ]);
574 let id_delta = i16::from_be_bytes([
575 cmap_data[id_delta_offset + i * 2],
576 cmap_data[id_delta_offset + i * 2 + 1],
577 ]);
578 let id_range_offset = u16::from_be_bytes([
579 cmap_data[id_range_offset_start + i * 2],
580 cmap_data[id_range_offset_start + i * 2 + 1],
581 ]) as usize;
582
583 for code in start_code..=end_code {
584 let glyph_id = if id_range_offset == 0 {
585 ((code as i32 + id_delta as i32) & 0xFFFF) as u16
587 } else {
588 let range_offset_pos = id_range_offset_start + i * 2;
592 let glyph_idx =
593 range_offset_pos + id_range_offset + (code - start_code) as usize * 2;
594 if glyph_idx + 1 >= cmap_data.len() {
595 continue;
596 }
597 let raw_gid =
598 u16::from_be_bytes([cmap_data[glyph_idx], cmap_data[glyph_idx + 1]]);
599 if raw_gid == 0 {
600 0
601 } else {
602 ((raw_gid as i32 + id_delta as i32) & 0xFFFF) as u16
603 }
604 };
605
606 if glyph_id != 0 {
607 if let Some(ch) = char::from_u32(code as u32) {
608 mapping.add_mapping(ch, glyph_id);
609 }
610 }
611 }
612 }
613
614 Ok(())
615 }
616
617 fn parse_cmap_format_12(
622 &self,
623 cmap_data: &[u8],
624 offset: usize,
625 mapping: &mut GlyphMapping,
626 ) -> Result<()> {
627 let code_to_gid = parse_cmap_format_12_filtered(cmap_data, offset, None)?;
628 for (char_code, glyph_id) in code_to_gid {
629 if let Some(ch) = char::from_u32(char_code) {
630 mapping.add_mapping(ch, glyph_id);
631 }
632 }
633 Ok(())
634 }
635
636 fn extract_italic_angle(&self) -> Result<f32> {
638 let head_table = self
639 .get_table("head")
640 .ok_or_else(|| PdfError::FontError("Missing head table".to_string()))?;
641
642 if head_table.len() < 46 {
643 return Ok(0.0); }
645
646 let mac_style = u16::from_be_bytes([head_table[44], head_table[45]]);
648
649 if mac_style & 0x02 != 0 {
651 Ok(-12.0) } else {
653 Ok(0.0)
654 }
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn test_glyph_mapping() {
664 let mut mapping = GlyphMapping::default();
665 mapping.add_mapping('A', 65);
666 mapping.add_mapping('B', 66);
667
668 assert_eq!(mapping.char_to_glyph('A'), Some(65));
669 assert_eq!(mapping.char_to_glyph('B'), Some(66));
670 assert_eq!(mapping.char_to_glyph('C'), None);
671
672 assert_eq!(mapping.glyph_to_char(65), Some('A'));
673 assert_eq!(mapping.glyph_to_char(66), Some('B'));
674 assert_eq!(mapping.glyph_to_char(67), None);
675 }
676
677 #[test]
678 fn test_char_widths_iter_yields_all_mapped_chars() {
679 let mut m = GlyphMapping::default();
680 m.add_mapping('A', 1);
681 m.set_glyph_width(1, 700);
682 m.add_mapping('B', 2);
683 m.set_glyph_width(2, 600);
684
685 let mut pairs: Vec<(char, u16)> = m.char_widths_iter().collect();
686 pairs.sort_by_key(|&(ch, _)| ch);
687 assert_eq!(pairs, vec![('A', 700), ('B', 600)]);
688 }
689
690 #[test]
691 fn test_char_widths_iter_skips_chars_without_width() {
692 let mut m = GlyphMapping::default();
693 m.add_mapping('C', 3); m.add_mapping('D', 4);
695 m.set_glyph_width(4, 500); let pairs: Vec<(char, u16)> = m.char_widths_iter().collect();
698 assert_eq!(pairs.len(), 1);
699 assert_eq!(pairs[0], ('D', 500));
700 }
701
702 #[test]
703 fn test_char_widths_iter_empty_on_empty_mapping() {
704 let m = GlyphMapping::default();
705 assert_eq!(m.char_widths_iter().count(), 0);
706 }
707}