use crate::fonts::FontInfo;
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct DecodePolicy {
pub preserve_unmapped: bool,
pub decompose_ligatures: bool,
pub question_mark_for_invalid: bool,
}
pub(crate) fn fallback_char_to_unicode(char_code: u32) -> String {
match char_code {
0x2014 => "—".to_string(), 0x2013 => "–".to_string(), 0x2018 => "\u{2018}".to_string(), 0x2019 => "\u{2019}".to_string(), 0x201C => "\u{201C}".to_string(), 0x201D => "\u{201D}".to_string(), 0x2022 => "•".to_string(), 0x2026 => "…".to_string(), 0x00B0 => "°".to_string(),
0x00B1 => "±".to_string(), 0x00D7 => "×".to_string(), 0x00F7 => "÷".to_string(), 0x2202 => "∂".to_string(), 0x2207 => "∇".to_string(), 0x220F => "∏".to_string(), 0x2211 => "∑".to_string(), 0x221A => "√".to_string(), 0x221E => "∞".to_string(), 0x2260 => "≠".to_string(), 0x2261 => "≡".to_string(), 0x2264 => "≤".to_string(), 0x2265 => "≥".to_string(), 0x222B => "∫".to_string(), 0x2248 => "≈".to_string(), 0x2282 => "⊂".to_string(), 0x2283 => "⊃".to_string(), 0x2286 => "⊆".to_string(), 0x2287 => "⊇".to_string(), 0x2208 => "∈".to_string(), 0x2209 => "∉".to_string(), 0x2200 => "∀".to_string(), 0x2203 => "∃".to_string(), 0x2205 => "∅".to_string(), 0x2227 => "∧".to_string(), 0x2228 => "∨".to_string(), 0x00AC => "¬".to_string(), 0x2192 => "→".to_string(), 0x2190 => "←".to_string(), 0x2194 => "↔".to_string(), 0x21D2 => "⇒".to_string(), 0x21D4 => "⇔".to_string(),
0x03B1 => "α".to_string(), 0x03B2 => "β".to_string(), 0x03B3 => "γ".to_string(), 0x03B4 => "δ".to_string(), 0x03B5 => "ε".to_string(), 0x03B6 => "ζ".to_string(), 0x03B7 => "η".to_string(), 0x03B8 => "θ".to_string(), 0x03B9 => "ι".to_string(), 0x03BA => "κ".to_string(), 0x03BB => "λ".to_string(), 0x03BC => "μ".to_string(), 0x03BD => "ν".to_string(), 0x03BE => "ξ".to_string(), 0x03BF => "ο".to_string(), 0x03C0 => "π".to_string(), 0x03C1 => "ρ".to_string(), 0x03C2 => "ς".to_string(), 0x03C3 => "σ".to_string(), 0x03C4 => "τ".to_string(), 0x03C5 => "υ".to_string(), 0x03C6 => "φ".to_string(), 0x03C7 => "χ".to_string(), 0x03C8 => "ψ".to_string(), 0x03C9 => "ω".to_string(),
0x0391 => "Α".to_string(), 0x0392 => "Β".to_string(), 0x0393 => "Γ".to_string(), 0x0394 => "Δ".to_string(), 0x0395 => "Ε".to_string(), 0x0396 => "Ζ".to_string(), 0x0397 => "Η".to_string(), 0x0398 => "Θ".to_string(), 0x0399 => "Ι".to_string(), 0x039A => "Κ".to_string(), 0x039B => "Λ".to_string(), 0x039C => "Μ".to_string(), 0x039D => "Ν".to_string(), 0x039E => "Ξ".to_string(), 0x039F => "Ο".to_string(), 0x03A0 => "Π".to_string(), 0x03A1 => "Ρ".to_string(), 0x03A3 => "Σ".to_string(), 0x03A4 => "Τ".to_string(), 0x03A5 => "Υ".to_string(), 0x03A6 => "Φ".to_string(), 0x03A7 => "Χ".to_string(), 0x03A8 => "Ψ".to_string(), 0x03A9 => "Ω".to_string(),
0x20AC => "€".to_string(), 0x00A3 => "£".to_string(), 0x00A5 => "¥".to_string(), 0x00A2 => "¢".to_string(), 0x20A3 => "₣".to_string(), 0x20A4 => "₤".to_string(), 0x20A9 => "₩".to_string(), 0x20AA => "₪".to_string(), 0x20AB => "₫".to_string(), 0x20B9 => "₹".to_string(),
code => {
if let Some(ch) = char::from_u32(code) {
if (0xE000..=0xF8FF).contains(&code) {
log::debug!("Private Use Area character: U+{:04X}", code);
}
ch.to_string()
} else {
log::warn!("Character code 0x{:04X} is not a valid Unicode code point", code);
"?".to_string()
}
},
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ByteMode {
OneByte,
TwoByte,
ShiftJIS,
}
pub(crate) fn font_has_utf8_cmap(font: &FontInfo) -> bool {
if font.subtype != "Type0" {
return false;
}
if let crate::fonts::Encoding::Standard(name) = &font.encoding {
let lower = name.to_ascii_lowercase();
lower.contains("utf8") || lower.contains("utf-8")
} else {
false
}
}
pub(crate) fn get_byte_mode(font: Option<&FontInfo>) -> ByteMode {
if let Some(font) = font {
if font.subtype == "Type0" {
if let Some(ref lazy_cmap) = font.to_unicode {
if lazy_cmap.code_width() == 2 {
return ByteMode::TwoByte;
}
}
match &font.encoding {
crate::fonts::Encoding::Identity => ByteMode::TwoByte,
crate::fonts::Encoding::Standard(name) => {
if (name.contains("Identity") && !name.contains("OneByteIdentity"))
|| name.contains("UCS2")
|| name.contains("UTF16")
|| name == "H"
|| name == "V"
{
ByteMode::TwoByte
} else if name.contains("RKSJ") {
ByteMode::ShiftJIS
} else if name.contains("EUC")
|| name.contains("GBK")
|| name.contains("GBpc")
|| name.contains("GB-")
|| name.contains("CNS")
|| name.contains("B5")
|| name.contains("KSC")
|| name.contains("KSCms")
{
ByteMode::TwoByte
} else {
ByteMode::OneByte
}
},
_ => ByteMode::OneByte,
}
} else {
ByteMode::OneByte
}
} else {
ByteMode::OneByte
}
}
pub(crate) struct TextCharIter<'a> {
bytes: &'a [u8],
byte_mode: ByteMode,
index: usize,
}
impl<'a> TextCharIter<'a> {
pub(crate) fn new(bytes: &'a [u8], font: Option<&FontInfo>) -> Self {
Self {
bytes,
byte_mode: get_byte_mode(font),
index: 0,
}
}
}
impl<'a> Iterator for TextCharIter<'a> {
type Item = (u16, usize);
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.bytes.len() {
return None;
}
let (char_code, bytes_consumed) = match self.byte_mode {
ByteMode::TwoByte if self.index + 1 < self.bytes.len() => {
(((self.bytes[self.index] as u16) << 8) | (self.bytes[self.index + 1] as u16), 2)
},
ByteMode::ShiftJIS => {
let b = self.bytes[self.index];
let is_lead = (0x81..=0x9F).contains(&b) || (0xE0..=0xFC).contains(&b);
if is_lead && self.index + 1 < self.bytes.len() {
(((b as u16) << 8) | (self.bytes[self.index + 1] as u16), 2)
} else {
(b as u16, 1)
}
},
_ => (self.bytes[self.index] as u16, 1),
};
self.index += bytes_consumed;
Some((char_code, bytes_consumed))
}
}
fn utf8_codes(bytes: &[u8]) -> impl Iterator<Item = u32> + '_ {
let mut i = 0;
std::iter::from_fn(move || {
if i >= bytes.len() {
return None;
}
let width = match bytes[i] {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xF7 => 4,
_ => 1,
}
.min(bytes.len() - i);
let mut code: u32 = 0;
for &b in &bytes[i..i + width] {
code = (code << 8) | b as u32;
}
i += width;
Some(code)
})
}
#[cfg_attr(not(feature = "rendering"), allow(dead_code))]
pub(crate) fn char_codes_with_widths(bytes: &[u8], font: &FontInfo) -> Vec<(u32, usize)> {
if font_has_utf8_cmap(font) {
let mut i = 0;
utf8_codes(bytes)
.map(|code| {
let width = match bytes[i] {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xF7 => 4,
_ => 1,
}
.min(bytes.len() - i);
i += width;
(code, width)
})
.collect()
} else {
TextCharIter::new(bytes, Some(font))
.map(|(code, width)| (code as u32, width))
.collect()
}
}
fn resolve_char(font: &FontInfo, code: u32, policy: DecodePolicy) -> String {
if let Some(char_str) = font.char_to_unicode(code) {
return char_str;
}
if char::from_u32(code).is_none() && !policy.question_mark_for_invalid {
return "\u{FFFD}".to_string();
}
fallback_char_to_unicode(code)
}
pub(crate) fn decode_text_to_unicode(
bytes: &[u8],
font: Option<&FontInfo>,
policy: DecodePolicy,
) -> String {
let raw_result = if let Some(font) = font {
let mut result = String::new();
if font.subtype != "Type0" {
let table = font.get_byte_to_char_table();
for &byte in bytes {
let c = table[byte as usize];
if c != '\0' {
result.push(c);
} else {
let char_str = resolve_char(font, byte as u32, policy);
if char_str != "\u{FFFD}" || policy.preserve_unmapped {
result.push_str(&char_str);
}
}
}
} else if font_has_utf8_cmap(font) {
for code in utf8_codes(bytes) {
let char_str = resolve_char(font, code, policy);
if char_str != "\u{FFFD}" || policy.preserve_unmapped {
result.push_str(&char_str);
}
}
} else {
for (char_code, _) in TextCharIter::new(bytes, Some(font)) {
let char_str = resolve_char(font, char_code as u32, policy);
if char_str != "\u{FFFD}" || policy.preserve_unmapped {
result.push_str(&char_str);
}
}
}
result
} else {
log::warn!(
"⚠️ No font provided for {} bytes, using Latin-1 fallback (PDF spec compliant)",
bytes.len()
);
bytes.iter().map(|&b| char::from(b)).collect()
};
let mut filtered = String::with_capacity(raw_result.len());
for c in raw_result.chars() {
if c < '\x20' && c != '\t' && c != '\n' && c != '\r' {
continue;
}
if policy.decompose_ligatures {
if let Some(components) = crate::text::ligature_processor::get_ligature_components(c) {
filtered.push_str(components);
continue;
}
}
filtered.push(c);
}
filtered
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fonts::{CIDToGIDMap, Encoding, FontInfo, VerticalMetrics};
use std::collections::HashMap;
fn utf8_cmap_font() -> FontInfo {
FontInfo {
base_font: "TestUtf8CMap".to_string(),
subtype: "Type0".to_string(),
encoding: Encoding::Standard("UniFull-UTF8-H".to_string()),
to_unicode: None,
font_weight: None,
flags: None,
stem_v: None,
ascent: 0.95,
descent: -0.35,
embedded_font_data: None,
truetype_cmap: std::sync::OnceLock::new(),
embedded_glyph_names: std::sync::OnceLock::new(),
is_truetype_font: false,
widths: None,
first_char: None,
last_char: None,
font_matrix_a: 0.001,
default_width: 1000.0,
cid_to_gid_map: Some(CIDToGIDMap::Identity),
cid_system_info: None,
cid_font_type: Some("CIDFontType2".to_string()),
cid_widths: None,
cid_default_width: 1000.0,
has_explicit_dw: false,
cff_gid_map: None,
cff_cid_to_gid: None,
multi_char_map: HashMap::new(),
byte_to_char_table: std::sync::OnceLock::new(),
type0_unicode_memo: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
byte_to_width_table: std::sync::OnceLock::new(),
weight_memo: std::sync::OnceLock::new(),
italic_memo: std::sync::OnceLock::new(),
std14_memo: std::sync::OnceLock::new(),
diff_glyph_names: HashMap::new(),
wmode: 0,
cid_vertical_metrics: None,
cid_default_vertical_metrics: VerticalMetrics::SPEC_DEFAULT,
cjk_substitution: None,
}
}
#[test]
fn utf8_codes_segments_by_lead_byte_width() {
let bytes = [
0x41, 0xC3, 0xA9, 0xE4, 0xB8, 0xAD, 0xF0, 0x9F, 0x98, 0x80, 0x80, 0xE4, 0xB8, ];
let codes: Vec<u32> = utf8_codes(&bytes).collect();
assert_eq!(codes, vec![0x41, 0xC3A9, 0xE4B8AD, 0xF09F_9880, 0x80, 0xE4B8]);
}
#[test]
fn char_codes_with_widths_uses_decode_segmentation_for_utf8_cmaps() {
let font = utf8_cmap_font();
assert!(font_has_utf8_cmap(&font), "fixture font must select the UTF-8 route");
let bytes = [0x41, 0xC3, 0xA9, 0xE4, 0xB8, 0xAD];
assert_eq!(
char_codes_with_widths(&bytes, &font),
vec![(0x41, 1), (0xC3A9, 2), (0xE4B8AD, 3)]
);
}
}