use super::cmap_table::{lookup_cid, PredefinedCmap, PREDEFINED_CMAPS};
pub fn decode(encoding_name: &str, registry: &str, ordering: &str, bytes: &[u8]) -> Option<String> {
if bytes.is_empty() {
return None;
}
let base = base_cmap_name(encoding_name);
if is_unicode_cmap(base) {
return decode_utf16be(bytes);
}
let cmap = find_table(ordering, base)?;
decode_with_table(bytes, cmap, registry, ordering)
}
fn base_cmap_name(name: &str) -> &str {
match name {
"H" | "V" => "H",
_ => name
.strip_suffix("-H")
.or_else(|| name.strip_suffix("-V"))
.unwrap_or(name),
}
}
fn is_unicode_cmap(base: &str) -> bool {
base.starts_with("Uni") && (base.contains("UCS2") || base.contains("UTF16"))
}
fn collection_of(ordering: &str) -> Option<&'static str> {
match ordering {
o if o.starts_with("Korea1") => Some("KOREA1"),
o if o.starts_with("Japan1") => Some("JAPAN1"),
o if o.starts_with("GB1") => Some("GB1"),
o if o.starts_with("CNS1") => Some("CNS1"),
_ => None,
}
}
fn find_table(ordering: &str, base: &str) -> Option<&'static PredefinedCmap> {
let collection = collection_of(ordering)?;
PREDEFINED_CMAPS
.iter()
.find(|cmap| cmap.collection == collection && cmap.column == base)
}
fn decode_utf16be(bytes: &[u8]) -> Option<String> {
if bytes.len() < 2 {
return None;
}
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
String::from_utf16(&units).ok().filter(|s| !s.is_empty())
}
fn decode_with_table(
bytes: &[u8],
cmap: &PredefinedCmap,
registry: &str,
ordering: &str,
) -> Option<String> {
let mut result = String::new();
let mut any_mapped = false;
let mut i = 0;
while i < bytes.len() {
let width = code_width(cmap, &bytes[i..]);
let code = if width == 2 {
u16::from_be_bytes([bytes[i], bytes[i + 1]])
} else {
bytes[i] as u16
};
i += width;
let mapped = cmap
.codes
.binary_search_by_key(&code, |&(c, _)| c)
.ok()
.and_then(|idx| lookup_cid(registry, ordering, cmap.codes[idx].1 as u32))
.or_else(|| ascii_fallback(code, width));
if let Some(ch) = mapped {
result.push(ch);
any_mapped = true;
}
}
if any_mapped {
Some(result)
} else {
None
}
}
fn code_width(cmap: &PredefinedCmap, rest: &[u8]) -> usize {
let byte = rest[0];
if byte < 0x80 || rest.len() < 2 {
return 1;
}
if cmap.lead_bytes.contains(&byte) || !contains_code(cmap, byte as u16) {
return 2;
}
1
}
fn contains_code(cmap: &PredefinedCmap, code: u16) -> bool {
cmap.codes.binary_search_by_key(&code, |&(c, _)| c).is_ok()
}
fn ascii_fallback(code: u16, width: usize) -> Option<char> {
match (width, code) {
(1, 0x20..=0x7E) => Some(code as u8 as char),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base_name_strips_writing_mode() {
assert_eq!(base_cmap_name("KSC-EUC-H"), "KSC-EUC");
assert_eq!(base_cmap_name("KSC-EUC-V"), "KSC-EUC");
assert_eq!(base_cmap_name("UniJIS-UCS2-HW-V"), "UniJIS-UCS2-HW");
assert_eq!(base_cmap_name("H"), "H");
assert_eq!(base_cmap_name("V"), "H");
assert_eq!(base_cmap_name("Identity-H"), "Identity");
}
#[test]
fn decodes_euc_kr() {
let decoded = decode("KSC-EUC-H", "Adobe", "Korea1", &[0xC7, 0xD1, 0xB1, 0xDB]);
assert_eq!(decoded.as_deref(), Some("한글"));
}
#[test]
fn decodes_euc_kr_mixed_with_ascii() {
let decoded = decode("KSC-EUC-V", "Adobe", "Korea1-2", &[0x41, 0xC7, 0xD1, 0x42]);
assert_eq!(decoded.as_deref(), Some("A한B"));
}
#[test]
fn decodes_shift_jis() {
let decoded = decode("90ms-RKSJ-H", "Adobe", "Japan1", &[0x82, 0xA0, 0x82, 0xA2]);
assert_eq!(decoded.as_deref(), Some("あい"));
}
#[test]
fn decodes_gbk() {
let decoded = decode("GBK-EUC-H", "Adobe", "GB1", &[0xD6, 0xD0, 0xCE, 0xC4]);
assert_eq!(decoded.as_deref(), Some("中文"));
}
#[test]
fn decodes_big5() {
let decoded = decode("ETen-B5-H", "Adobe", "CNS1", &[0xA4, 0xA4, 0xA4, 0xE5]);
assert_eq!(decoded.as_deref(), Some("中文"));
}
#[test]
fn decodes_unicode_cmap_without_table() {
let decoded = decode("UniKS-UCS2-H", "Adobe", "Korea1", &[0xD5, 0x5C, 0xAE, 0x00]);
assert_eq!(decoded.as_deref(), Some("한글"));
}
#[test]
fn unmapped_lead_byte_does_not_desync() {
let decoded = decode(
"90ms-RKSJ-H",
"Adobe",
"Japan1",
&[0xF0, 0x40, 0x82, 0xA0, 0xF0, 0x41],
);
assert_eq!(decoded.as_deref(), Some("あ"));
}
#[test]
fn unsupported_cmap_yields_none() {
assert_eq!(
decode("KSC-Johab-H", "Adobe", "Korea1", &[0xC7, 0xD1]),
None
);
assert_eq!(decode("KSC-EUC-H", "Adobe", "Unknown", &[0xC7, 0xD1]), None);
assert_eq!(decode("KSC-EUC-H", "Adobe", "Korea1", &[]), None);
}
}